diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index e409325f..acf81103 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -34,10 +34,6 @@
-
>
@Query(
"SELECT * FROM film_entity " +
- "WHERE updateDate BETWEEN :startAt AND :endAt "
+ "WHERE updateDate BETWEEN :startAt AND :endAt ",
)
suspend fun loadFilm(startAt: Int, endAt: Int): List
+ @Query(
+ "SELECT * FROM film_entity " +
+ "WHERE updateDate BETWEEN :startAt AND :endAt " +
+ "ORDER BY updateDate ASC LIMIT :count OFFSET :page",
+ )
+ suspend fun loadPagedFilm(startAt: Int, endAt: Int, page: Int, count: Int = 10): List
+
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAll(filmEntityList: List)
diff --git a/app/src/main/java/com/boostcamp/dailyfilm/data/calendar/CalendarDataSource.kt b/app/src/main/java/com/boostcamp/dailyfilm/data/calendar/CalendarDataSource.kt
index d1caa7b3..f2fdfe72 100644
--- a/app/src/main/java/com/boostcamp/dailyfilm/data/calendar/CalendarDataSource.kt
+++ b/app/src/main/java/com/boostcamp/dailyfilm/data/calendar/CalendarDataSource.kt
@@ -14,6 +14,8 @@ interface CalendarDataSource {
suspend fun loadFilm(startAt: Int, endAt: Int): List
+ suspend fun loadPagedFilm(startAt: Int, endAt: Int, page: Int): List
+
suspend fun insertFilm(film: FilmEntity)
suspend fun insertAllFilm(filmList: List)
@@ -22,7 +24,7 @@ interface CalendarDataSource {
}
class CalendarLocalDataSource(
- private val calendarDao: CalendarDao
+ private val calendarDao: CalendarDao,
) : CalendarDataSource {
override fun loadFilmFlow(startAt: Int, endAt: Int): Flow> =
@@ -31,6 +33,9 @@ class CalendarLocalDataSource(
override suspend fun loadFilm(startAt: Int, endAt: Int): List =
calendarDao.loadFilm(startAt, endAt)
+ override suspend fun loadPagedFilm(startAt: Int, endAt: Int, page: Int): List =
+ calendarDao.loadPagedFilm(startAt, endAt, page)
+
override suspend fun insertFilm(film: FilmEntity) {
calendarDao.insert(film)
}
diff --git a/app/src/main/java/com/boostcamp/dailyfilm/data/calendar/CalendarPagingSource.kt b/app/src/main/java/com/boostcamp/dailyfilm/data/calendar/CalendarPagingSource.kt
new file mode 100644
index 00000000..642f880b
--- /dev/null
+++ b/app/src/main/java/com/boostcamp/dailyfilm/data/calendar/CalendarPagingSource.kt
@@ -0,0 +1,39 @@
+package com.boostcamp.dailyfilm.data.calendar
+
+import androidx.paging.PagingSource
+import androidx.paging.PagingState
+import com.boostcamp.dailyfilm.data.model.DailyFilmItem
+
+class CalendarPagingSource(
+ private val startAt: Int,
+ private val endAt: Int,
+ private val calendarDataSource: CalendarDataSource,
+) : PagingSource() {
+ override suspend fun load(params: LoadParams): LoadResult {
+ runCatching {
+ val page = params.key ?: 0
+ val data = calendarDataSource.loadPagedFilm(startAt, endAt, page).filterNotNull().map { it.mapToDailyFilmItem() }
+ return LoadResult.Page(
+ data = data,
+ prevKey = if (page == 0) null else shouldPositive(page - PAGING_SIZE),
+ nextKey = if (data.isEmpty()) null else page + data.size,
+ )
+ }.onFailure {
+ return LoadResult.Error(it)
+ }
+ return LoadResult.Error(Exception("Unknown Error in CalendarPagingSource"))
+ }
+
+ override fun getRefreshKey(state: PagingState): Int? {
+ return state.anchorPosition?.let { anchorPosition ->
+ state.closestPageToPosition(anchorPosition)?.prevKey?.plus(PAGING_SIZE)
+ ?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(PAGING_SIZE)
+ }
+ }
+
+ private fun shouldPositive(num: Int) = if (num < 0) 0 else num
+
+ companion object {
+ const val PAGING_SIZE = 10
+ }
+}
diff --git a/app/src/main/java/com/boostcamp/dailyfilm/data/calendar/CalendarRepository.kt b/app/src/main/java/com/boostcamp/dailyfilm/data/calendar/CalendarRepository.kt
index f76b9ee0..f5eab19d 100644
--- a/app/src/main/java/com/boostcamp/dailyfilm/data/calendar/CalendarRepository.kt
+++ b/app/src/main/java/com/boostcamp/dailyfilm/data/calendar/CalendarRepository.kt
@@ -1,5 +1,9 @@
package com.boostcamp.dailyfilm.data.calendar
+import androidx.paging.Pager
+import androidx.paging.PagingConfig
+import androidx.paging.PagingData
+import com.boostcamp.dailyfilm.data.calendar.CalendarPagingSource.Companion.PAGING_SIZE
import com.boostcamp.dailyfilm.data.model.DailyFilmItem
import com.boostcamp.dailyfilm.data.model.Result
import kotlinx.coroutines.flow.Flow
@@ -10,11 +14,13 @@ interface CalendarRepository {
suspend fun loadFilm(startAt: String, endAt: String): List
+ suspend fun loadPagedFilm(startAt: String, endAt: String): Flow>
+
suspend fun deleteAllData(): Result
}
class CalendarRepositoryImpl(
- private val calendarLocalDataSource: CalendarDataSource
+ private val calendarLocalDataSource: CalendarDataSource,
) : CalendarRepository {
override fun loadFilmInfo(startAt: String, endAt: String): Flow> =
@@ -29,6 +35,11 @@ class CalendarRepositoryImpl(
filmEntity?.mapToDailyFilmItem()
}
+ override suspend fun loadPagedFilm(startAt: String, endAt: String): Flow> =
+ Pager(config = PagingConfig(pageSize = PAGING_SIZE)) {
+ CalendarPagingSource(startAt.toInt(), endAt.toInt(), calendarLocalDataSource)
+ }.flow
+
override suspend fun deleteAllData(): Result =
calendarLocalDataSource.deleteAllData()
}
diff --git a/app/src/main/java/com/boostcamp/dailyfilm/presentation/searchfilm/SearchFilmActivity.kt b/app/src/main/java/com/boostcamp/dailyfilm/presentation/searchfilm/SearchFilmActivity.kt
deleted file mode 100644
index bd80eab3..00000000
--- a/app/src/main/java/com/boostcamp/dailyfilm/presentation/searchfilm/SearchFilmActivity.kt
+++ /dev/null
@@ -1,146 +0,0 @@
-package com.boostcamp.dailyfilm.presentation.searchfilm
-
-import android.content.Context
-import android.content.Intent
-import android.util.Log
-import android.view.inputmethod.InputMethodManager
-import android.widget.SearchView
-import androidx.activity.result.ActivityResult
-import androidx.activity.result.ActivityResultLauncher
-import androidx.activity.result.contract.ActivityResultContracts
-import androidx.activity.viewModels
-import androidx.core.util.Pair
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.lifecycleScope
-import androidx.lifecycle.repeatOnLifecycle
-import com.boostcamp.dailyfilm.R
-import com.boostcamp.dailyfilm.databinding.ActivitySearchFilmBinding
-import com.boostcamp.dailyfilm.presentation.BaseActivity
-import com.boostcamp.dailyfilm.presentation.calendar.CalendarActivity
-import com.boostcamp.dailyfilm.presentation.calendar.DateFragment
-import com.boostcamp.dailyfilm.presentation.playfilm.PlayFilmActivity
-import com.google.android.material.datepicker.MaterialDatePicker
-import dagger.hilt.android.AndroidEntryPoint
-import kotlinx.coroutines.flow.collectLatest
-import kotlinx.coroutines.launch
-import java.util.ArrayList
-
-@AndroidEntryPoint
-class SearchFilmActivity : BaseActivity(R.layout.activity_search_film) {
-
- private val viewModel: SearchFilmViewModel by viewModels()
-
- private val startForResult: ActivityResultLauncher =
- registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
- /*if (result.resultCode == Activity.RESULT_OK && result.data != null) {
- val calendarIndex = result.data?.getIntExtra(DateFragment.KEY_CALENDAR_INDEX, -1)
- ?: return@registerForActivityResult
- val dateModel = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
- result.data?.getParcelableExtra(PlayFilmFragment.KEY_DATE_MODEL, DateModel::class.java)
- } else {
- result.data?.getParcelableExtra(PlayFilmFragment.KEY_DATE_MODEL)
- }
- dateModel ?: return@registerForActivityResult
- viewModel.setVideo(calendarIndex, dateModel)
- reloadItem(calendarIndex, dateModel)
- }*/
- }
-
- override fun initView() {
- binding.viewModel = viewModel
-
- initClickEvent()
- observeEvent()
- handleSearchQuery()
- }
-
- private fun initClickEvent() {
- binding.barSearch.apply {
- setNavigationOnClickListener { finish() }
- setOnMenuItemClickListener { menuItem ->
- when (menuItem.itemId) {
- R.id.item_search -> true
- else -> false
- }
- }
- }
-
- binding.tvSearchRange.setOnClickListener {
- val datePicker = MaterialDatePicker.Builder.dateRangePicker()
- .setTitleText("Select Date Range")
- .apply {
- if (viewModel.startAt != null && viewModel.endAt != null) {
- setSelection(Pair(viewModel.startAt, viewModel.endAt))
- }
- }
- .build()
-
- datePicker.apply {
- addOnPositiveButtonClickListener { selection ->
- viewModel.searchDateRange(selection.first, selection.second)
- }
- show(supportFragmentManager, TAG_DATE_PICKER)
- }
- }
- }
-
- private fun observeEvent() {
- lifecycleScope.launch {
- repeatOnLifecycle(Lifecycle.State.STARTED) {
- viewModel.eventFlow.collectLatest { event ->
- when (event) {
- is SearchEvent.ItemClickEvent -> {
- startForResult.launch(
- Intent(this@SearchFilmActivity, PlayFilmActivity::class.java).apply {
- putExtra(
- DateFragment.KEY_CALENDAR_INDEX,
- 0
- )
- putExtra(
- DateFragment.KEY_DATE_MODEL_INDEX,
- event.index
- )
- putParcelableArrayListExtra(
- CalendarActivity.KEY_FILM_ARRAY,
- ArrayList(viewModel.itemListFlow.value.map { it?.toDateModel() })
- )
- }
- )
- }
- }
- }
- }
- }
- }
-
- private fun handleSearchQuery() {
- val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
-
- (binding.barSearch.menu.findItem(R.id.item_search).actionView as SearchView).apply {
- setOnQueryTextListener(object : SearchView.OnQueryTextListener {
- override fun onQueryTextSubmit(query: String?): Boolean {
- Log.d("handleIntent", "onQueryTextSubmit: $query")
-
- viewModel.searchKeyword(query ?: "")
- return false
- }
-
- override fun onQueryTextChange(newText: String?): Boolean {
- Log.d("handleIntent", "onQueryTextChange: $newText")
- return false
- }
- })
-
- setOnCloseListener {
- viewModel.searchKeyword("")
- imm.hideSoftInputFromWindow(windowToken, 0)
- clearFocus()
- false
- }
- }
- }
-
- companion object {
- const val TAG_DATE_PICKER = "datePicker"
- }
-}
diff --git a/app/src/main/java/com/boostcamp/dailyfilm/presentation/searchfilm/SearchFilmBindingAdapter.kt b/app/src/main/java/com/boostcamp/dailyfilm/presentation/searchfilm/SearchFilmBindingAdapter.kt
deleted file mode 100644
index a2058802..00000000
--- a/app/src/main/java/com/boostcamp/dailyfilm/presentation/searchfilm/SearchFilmBindingAdapter.kt
+++ /dev/null
@@ -1,42 +0,0 @@
-package com.boostcamp.dailyfilm.presentation.searchfilm
-
-import android.annotation.SuppressLint
-import android.widget.TextView
-import androidx.databinding.BindingAdapter
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.findViewTreeLifecycleOwner
-import androidx.lifecycle.lifecycleScope
-import androidx.lifecycle.repeatOnLifecycle
-import androidx.recyclerview.widget.RecyclerView
-import com.boostcamp.dailyfilm.data.model.DailyFilmItem
-import com.boostcamp.dailyfilm.presentation.searchfilm.adapter.SearchFilmAdapter
-import kotlinx.coroutines.launch
-
-@BindingAdapter("itemList", "viewModel", requireAll = true)
-fun RecyclerView.updateAdapter(itemList: List, viewModel: SearchFilmViewModel) {
- if (adapter == null) {
- adapter = SearchFilmAdapter { index ->
- viewModel.onClickItem(index)
- }
- }
-
- findViewTreeLifecycleOwner()?.lifecycleScope?.launch {
- findViewTreeLifecycleOwner()?.repeatOnLifecycle(Lifecycle.State.STARTED) {
- (adapter as SearchFilmAdapter).submitList(itemList)
- }
- }
-}
-
-@SuppressLint("SetTextI18n")
-@BindingAdapter("startDate", "endDate", requireAll = true)
-fun TextView.setSearchRange(startDate: String?, endDate: String?) {
- if (startDate != null && endDate != null) {
- text = "$startDate ~ $endDate"
- }
-}
-
-@SuppressLint("SetTextI18n")
-@BindingAdapter("setDate", requireAll = true)
-fun TextView.setDate(date: String) {
- text = "${date.substring(0, 4)}년 ${date.substring(4, 6)}월 ${date.substring(6)}일"
-}
diff --git a/app/src/main/java/com/boostcamp/dailyfilm/presentation/searchfilm/SearchFilmComposeActivity.kt b/app/src/main/java/com/boostcamp/dailyfilm/presentation/searchfilm/SearchFilmComposeActivity.kt
index f9fa7247..872001f3 100644
--- a/app/src/main/java/com/boostcamp/dailyfilm/presentation/searchfilm/SearchFilmComposeActivity.kt
+++ b/app/src/main/java/com/boostcamp/dailyfilm/presentation/searchfilm/SearchFilmComposeActivity.kt
@@ -7,86 +7,19 @@ import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.background
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.aspectRatio
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.lazy.grid.GridCells
-import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
-import androidx.compose.foundation.lazy.grid.itemsIndexed
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.foundation.text.KeyboardActions
-import androidx.compose.foundation.text.KeyboardOptions
-import androidx.compose.material.Card
-import androidx.compose.material.ExperimentalMaterialApi
-import androidx.compose.material.Icon
-import androidx.compose.material.IconButton
-import androidx.compose.material.Scaffold
-import androidx.compose.material.Text
-import androidx.compose.material.TextField
-import androidx.compose.material.TextFieldDefaults
-import androidx.compose.material.TopAppBar
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.ArrowBack
-import androidx.compose.material.icons.filled.Close
-import androidx.compose.material.icons.filled.DateRange
-import androidx.compose.material.icons.filled.Search
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.saveable.rememberSaveable
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.focus.FocusManager
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.layout.ContentScale
-import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.platform.LocalFocusManager
-import androidx.compose.ui.res.colorResource
-import androidx.compose.ui.text.TextStyle
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.text.input.ImeAction
-import androidx.compose.ui.text.style.TextAlign
-import androidx.compose.ui.text.style.TextOverflow
-import androidx.compose.ui.tooling.preview.Preview
-import androidx.compose.ui.unit.TextUnit
-import androidx.compose.ui.unit.TextUnitType
-import androidx.compose.ui.unit.dp
import androidx.core.util.Pair
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
-import com.boostcamp.dailyfilm.R
-import com.boostcamp.dailyfilm.data.model.DailyFilmItem
import com.boostcamp.dailyfilm.presentation.calendar.CalendarActivity
import com.boostcamp.dailyfilm.presentation.calendar.DateFragment
import com.boostcamp.dailyfilm.presentation.playfilm.PlayFilmActivity
-import com.boostcamp.dailyfilm.presentation.ui.theme.DailyFilmTheme
-import com.boostcamp.dailyfilm.presentation.ui.theme.lightGray
-import com.bumptech.glide.Glide
-import com.bumptech.glide.RequestManager
-import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi
-import com.bumptech.glide.integration.compose.GlideImage
-import com.bumptech.glide.load.resource.bitmap.CenterCrop
-import com.bumptech.glide.load.resource.bitmap.RoundedCorners
-import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
-import com.bumptech.glide.request.transition.DrawableCrossFadeFactory
import com.google.android.material.datepicker.MaterialDatePicker
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
-import java.text.SimpleDateFormat
import java.util.ArrayList
-import java.util.Locale
@AndroidEntryPoint
class SearchFilmComposeActivity : FragmentActivity() {
@@ -95,24 +28,26 @@ class SearchFilmComposeActivity : FragmentActivity() {
private val startForResult: ActivityResultLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
- /*if (result.resultCode == Activity.RESULT_OK && result.data != null) {
- val calendarIndex = result.data?.getIntExtra(DateFragment.KEY_CALENDAR_INDEX, -1)
- ?: return@registerForActivityResult
+ /*
+ if (result.resultCode == Activity.RESULT_OK && result.data != null) {
val dateModel = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
result.data?.getParcelableExtra(PlayFilmFragment.KEY_DATE_MODEL, DateModel::class.java)
} else {
result.data?.getParcelableExtra(PlayFilmFragment.KEY_DATE_MODEL)
}
dateModel ?: return@registerForActivityResult
+ val calendarIndex = result.data?.getIntExtra(DateFragment.KEY_CALENDAR_INDEX, -1)
+ ?: return@registerForActivityResult
viewModel.setVideo(calendarIndex, dateModel)
reloadItem(calendarIndex, dateModel)
- }*/
+ }
+ */
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
- SearchView(viewModel)
+ SearchScreen(viewModel)
observeEvent()
}
}
@@ -140,226 +75,34 @@ class SearchFilmComposeActivity : FragmentActivity() {
},
)
}
- }
- }
- }
- }
- }
-}
-
-@OptIn(
- ExperimentalFoundationApi::class,
-)
-@Composable
-fun SearchView(viewModel: SearchFilmViewModel) {
- val activity = LocalContext.current as FragmentActivity
- val focusManager = LocalFocusManager.current
- val requestManager = Glide.with(activity)
- val factory = DrawableCrossFadeFactory.Builder().setCrossFadeEnabled(true).build()
- val dottedDateFormat = SimpleDateFormat("yyyy.MM.dd", Locale.getDefault())
- val list: List by viewModel.itemListFlow.collectAsStateWithLifecycle(minActiveState = Lifecycle.State.STARTED)
-
- var titleVisibility by rememberSaveable { mutableStateOf(true) }
- var searchText by rememberSaveable { mutableStateOf("") }
- var dateRange by rememberSaveable { mutableStateOf("검색 범위를 설정하세요") }
-
- DailyFilmTheme {
- Scaffold(
- modifier = Modifier.fillMaxSize(),
- topBar = {
- SearchAppBar(
- titleVisibility = titleVisibility,
- searchText = searchText,
- viewModel = viewModel,
- focusManager = focusManager,
- onVisibilityChange = { titleVisibility = it },
- onSearchTextChange = { searchText = it },
- onNavigationClick = { activity.finish() },
- )
- },
- ) {
- Column {
- SearchRangeTextBox(dateRange = dateRange) {
- val datePicker = MaterialDatePicker.Builder
- .dateRangePicker()
- .setTitleText("Select Date Range")
- .apply {
- if (viewModel.startAt != null && viewModel.endAt != null) {
- setSelection(Pair(viewModel.startAt, viewModel.endAt))
- }
+ is SearchEvent.DatePickerEvent -> {
+ MaterialDatePicker.Builder
+ .dateRangePicker()
+ .apply {
+ if (viewModel.startAt != null && viewModel.endAt != null) {
+ setSelection(Pair(viewModel.startAt, viewModel.endAt))
+ }
+ }
+ .build()
+ .apply {
+ addOnPositiveButtonClickListener { selection ->
+ viewModel.searchDateRange(selection.first, selection.second)
+ }
+ show(supportFragmentManager, TAG_DATE_PICKER)
+ }
}
- .build()
- datePicker.apply {
- addOnPositiveButtonClickListener { selection ->
- viewModel.searchDateRange(selection.first, selection.second)
- dateRange = "${dottedDateFormat.format(selection.first)} ~ ${
- dottedDateFormat.format(selection.second)
- }"
- }
- show(activity.supportFragmentManager, SearchFilmActivity.TAG_DATE_PICKER)
- }
- }
- LazyVerticalGrid(columns = GridCells.Fixed(2), contentPadding = it) {
- itemsIndexed(
- items = list,
- key = { i, item -> item?.videoUrl ?: i },
- ) { index, item ->
- item?.let {
- Row(modifier = Modifier.animateItemPlacement()) {
- FilmCard(it, requestManager, factory) { viewModel.onClickItem(index) }
- }
+ is SearchEvent.FinishEvent -> {
+ this@SearchFilmComposeActivity.finish()
}
}
}
}
}
}
-}
-
-@Composable
-fun SearchAppBar(
- titleVisibility: Boolean,
- searchText: String,
- viewModel: SearchFilmViewModel,
- focusManager: FocusManager,
- onVisibilityChange: (Boolean) -> Unit,
- onSearchTextChange: (String) -> Unit,
- onNavigationClick: () -> Unit,
-) {
- TopAppBar(
- title = {
- if (titleVisibility) {
- Text("검색")
- } else {
- TextField(
- value = searchText,
- onValueChange = onSearchTextChange,
- singleLine = true,
- placeholder = { Icon(Icons.Filled.Search, null, tint = Color.Gray) },
- keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
- keyboardActions = KeyboardActions(onSearch = {
- viewModel.searchKeyword(searchText)
- focusManager.clearFocus()
- }),
- colors = TextFieldDefaults.textFieldColors(
- backgroundColor = colorResource(R.color.Background),
- focusedIndicatorColor = colorResource(R.color.Background),
- unfocusedIndicatorColor = colorResource(R.color.Background),
- ),
- )
- }
- },
- navigationIcon = {
- IconButton(onClick = onNavigationClick) { Icon(Icons.Filled.ArrowBack, null) }
- },
- actions = {
- if (titleVisibility) {
- IconButton(
- onClick = { onVisibilityChange(false) },
- ) { Icon(Icons.Filled.Search, null) }
- } else {
- IconButton(
- onClick = {
- onVisibilityChange(true)
- onSearchTextChange("")
- viewModel.searchKeyword("")
- },
- ) { Icon(Icons.Filled.Close, null) }
- }
- },
- backgroundColor = colorResource(R.color.Background),
- elevation = 4.dp,
- )
-}
-
-@Composable
-fun SearchRangeTextBox(dateRange: String, onClickTextBox: () -> Unit) {
- Box(
- modifier = Modifier
- .fillMaxWidth()
- .padding(8.dp)
- .background(lightGray, RoundedCornerShape(4.dp))
- .padding(0.dp, 4.dp)
- .clickable { onClickTextBox() },
- ) {
- Icon(Icons.Filled.DateRange, null, modifier = Modifier.padding(4.dp), tint = Color.Black)
- Text(
- text = dateRange,
- modifier = Modifier.align(Alignment.Center),
- color = Color.Black,
- maxLines = 1,
- style = TextStyle(
- textAlign = TextAlign.Center,
- fontSize = TextUnit(20F, TextUnitType.Sp),
- fontWeight = FontWeight.Bold,
- ),
- )
- }
-}
-@OptIn(
- ExperimentalGlideComposeApi::class,
- ExperimentalMaterialApi::class,
-)
-@Composable
-fun FilmCard(
- item: DailyFilmItem,
- requestManager: RequestManager,
- factory: DrawableCrossFadeFactory,
- onClickItem: () -> Unit,
-) {
- Card(
- modifier = Modifier
- .fillMaxWidth()
- .padding(8.dp),
- elevation = 4.dp,
- backgroundColor = lightGray,
- onClick = { onClickItem() },
- ) {
- Column(modifier = Modifier.padding(8.dp)) {
- GlideImage(
- model = item.videoUrl,
- contentDescription = "thumbnail",
- modifier = Modifier
- .fillMaxWidth()
- .aspectRatio(3 / 4F),
- contentScale = ContentScale.FillWidth,
- ) {
- it.thumbnail(
- requestManager
- .asDrawable()
- .load(item.videoUrl)
- .transition(DrawableTransitionOptions.withCrossFade(factory))
- .placeholder(R.color.gray)
- .transform(CenterCrop(), RoundedCorners(2)),
- )
- }
- Text(
- text = "${item.updateDate.substring(0, 4)}년 " +
- "${item.updateDate.substring(4, 6)}월 " +
- "${item.updateDate.substring(6)}일",
- modifier = Modifier
- .fillMaxWidth()
- .padding(0.dp, 8.dp, 0.dp, 0.dp),
- color = Color.Black,
- maxLines = 1,
- overflow = TextOverflow.Ellipsis,
- style = TextStyle(fontSize = TextUnit(16F, TextUnitType.Sp)),
- )
- Text(
- text = item.text,
- modifier = Modifier.fillMaxWidth(),
- color = Color.Gray,
- maxLines = 1,
- overflow = TextOverflow.Ellipsis,
- )
- }
+ companion object {
+ const val TAG_DATE_PICKER = "datePicker"
}
}
-
-@Preview(showBackground = true)
-@Composable
-fun DefaultPreview() {
-}
diff --git a/app/src/main/java/com/boostcamp/dailyfilm/presentation/searchfilm/SearchFilmViewModel.kt b/app/src/main/java/com/boostcamp/dailyfilm/presentation/searchfilm/SearchFilmViewModel.kt
index b2b25fd1..221186af 100644
--- a/app/src/main/java/com/boostcamp/dailyfilm/presentation/searchfilm/SearchFilmViewModel.kt
+++ b/app/src/main/java/com/boostcamp/dailyfilm/presentation/searchfilm/SearchFilmViewModel.kt
@@ -2,6 +2,9 @@ package com.boostcamp.dailyfilm.presentation.searchfilm
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
+import androidx.paging.PagingData
+import androidx.paging.cachedIn
+import androidx.paging.filter
import com.boostcamp.dailyfilm.data.calendar.CalendarRepository
import com.boostcamp.dailyfilm.data.model.DailyFilmItem
import com.boostcamp.dailyfilm.data.sync.SyncRepository
@@ -13,6 +16,8 @@ import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import java.text.SimpleDateFormat
import java.util.*
@@ -21,36 +26,34 @@ import javax.inject.Inject
@HiltViewModel
class SearchFilmViewModel @Inject constructor(
private val calendarRepository: CalendarRepository,
- private val syncRepository: SyncRepository
+ private val syncRepository: SyncRepository,
) : ViewModel() {
private val dateFormat = SimpleDateFormat("yyyyMMdd", Locale.getDefault())
private val dottedDateFormat = SimpleDateFormat("yyyy.MM.dd", Locale.getDefault())
+ private val userId = FirebaseAuth.getInstance().currentUser?.uid ?: error("Unknown User")
var startAt: Long? = null
private set
var endAt: Long? = null
private set
- private val userId = FirebaseAuth.getInstance().currentUser?.uid ?: error("Unknown User")
+
+ private val _dateRangeFlow = MutableStateFlow("검색 범위를 설정하세요")
+ val dateRangeFlow: StateFlow = _dateRangeFlow.asStateFlow()
private val _itemListFlow = MutableStateFlow>(emptyList())
val itemListFlow: StateFlow> = _itemListFlow.asStateFlow()
+ private val _itemPageFlow = MutableStateFlow>(PagingData.empty())
+ val itemPageFlow: StateFlow> = _itemPageFlow.asStateFlow()
+
private val _eventFlow = MutableSharedFlow()
val eventFlow: SharedFlow = _eventFlow.asSharedFlow()
- private val _startDateFlow = MutableStateFlow(null)
- val startDateFlow: StateFlow = _startDateFlow.asStateFlow()
-
- private val _endDateFlow = MutableStateFlow(null)
- val endDateFlow: StateFlow = _endDateFlow.asStateFlow()
-
fun searchDateRange(startAt: Long, endAt: Long) {
- this.startAt = startAt
- this.endAt = endAt
-
viewModelScope.launch {
- _startDateFlow.tryEmit(dottedDateFormat.format(startAt))
- _endDateFlow.tryEmit(dottedDateFormat.format(endAt))
+ this@SearchFilmViewModel.startAt = startAt
+ this@SearchFilmViewModel.endAt = endAt
+ _dateRangeFlow.emit("${dottedDateFormat.format(startAt)} ~ ${dottedDateFormat.format(endAt)}")
val start = dateFormat.format(startAt)
val end = dateFormat.format(endAt)
@@ -67,16 +70,23 @@ class SearchFilmViewModel @Inject constructor(
}
_itemListFlow.emit(calendarRepository.loadFilm(start, end))
+ calendarRepository.loadPagedFilm(start, end).cachedIn(viewModelScope)
+ .onEach { pagingData ->
+ _itemPageFlow.emit(pagingData)
+ }.launchIn(viewModelScope)
}
}
fun searchKeyword(query: String) {
viewModelScope.launch {
if (startAt != null && endAt != null) {
- _itemListFlow.emit(
- calendarRepository.loadFilm(dateFormat.format(startAt), dateFormat.format(endAt))
- .filter { it?.text?.contains(query) ?: false }
- )
+ val start = dateFormat.format(startAt)
+ val end = dateFormat.format(endAt)
+ _itemListFlow.emit(calendarRepository.loadFilm(start, end).filter { it?.text?.contains(query) ?: false })
+ calendarRepository.loadPagedFilm(start, end).cachedIn(viewModelScope)
+ .onEach { pagingData ->
+ _itemPageFlow.emit(pagingData.filter { it.text.contains(query) })
+ }.launchIn(viewModelScope)
}
}
}
@@ -85,6 +95,14 @@ class SearchFilmViewModel @Inject constructor(
event(SearchEvent.ItemClickEvent(index))
}
+ fun showDatePicker() {
+ event(SearchEvent.DatePickerEvent)
+ }
+
+ fun onNavigationClick() {
+ event(SearchEvent.FinishEvent)
+ }
+
private fun event(event: SearchEvent) {
viewModelScope.launch {
_eventFlow.emit(event)
@@ -94,4 +112,6 @@ class SearchFilmViewModel @Inject constructor(
sealed class SearchEvent {
data class ItemClickEvent(val index: Int) : SearchEvent()
+ object DatePickerEvent : SearchEvent()
+ object FinishEvent : SearchEvent()
}
diff --git a/app/src/main/java/com/boostcamp/dailyfilm/presentation/searchfilm/SearchScreen.kt b/app/src/main/java/com/boostcamp/dailyfilm/presentation/searchfilm/SearchScreen.kt
new file mode 100644
index 00000000..48808891
--- /dev/null
+++ b/app/src/main/java/com/boostcamp/dailyfilm/presentation/searchfilm/SearchScreen.kt
@@ -0,0 +1,268 @@
+package com.boostcamp.dailyfilm.presentation.searchfilm
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.grid.GridCells
+import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.text.KeyboardActions
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material.Card
+import androidx.compose.material.ExperimentalMaterialApi
+import androidx.compose.material.Icon
+import androidx.compose.material.IconButton
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Scaffold
+import androidx.compose.material.Text
+import androidx.compose.material.TextField
+import androidx.compose.material.TextFieldDefaults
+import androidx.compose.material.TopAppBar
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.ArrowBack
+import androidx.compose.material.icons.filled.Close
+import androidx.compose.material.icons.filled.DateRange
+import androidx.compose.material.icons.filled.Search
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.SideEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusManager
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.platform.LocalFocusManager
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.TextUnit
+import androidx.compose.ui.unit.TextUnitType
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import androidx.paging.compose.collectAsLazyPagingItems
+import com.boostcamp.dailyfilm.R
+import com.boostcamp.dailyfilm.data.model.DailyFilmItem
+import com.boostcamp.dailyfilm.presentation.ui.theme.DailyFilmTheme
+import com.boostcamp.dailyfilm.presentation.ui.theme.lightGray
+import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi
+import com.bumptech.glide.integration.compose.GlideImage
+
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+fun SearchScreen(
+ viewModel: SearchFilmViewModel,
+) {
+ val focusManager = LocalFocusManager.current
+ val focusRequester = remember { FocusRequester() }
+ val lazyPagingItems = viewModel.itemPageFlow.collectAsLazyPagingItems()
+ val dateRange: String by viewModel.dateRangeFlow.collectAsStateWithLifecycle()
+ var titleVisibility by rememberSaveable { mutableStateOf(true) }
+ var searchText by rememberSaveable { mutableStateOf("") }
+
+ DailyFilmTheme {
+ Scaffold(
+ modifier = Modifier.fillMaxSize(),
+ topBar = {
+ SearchAppBar(
+ titleVisibility = titleVisibility,
+ searchText = searchText,
+ viewModel = viewModel,
+ focusManager = focusManager,
+ focusRequester = focusRequester,
+ onVisibilityChange = { titleVisibility = it },
+ onSearchTextChange = { searchText = it },
+ onNavigationClick = { viewModel.onNavigationClick() },
+ )
+ },
+ ) {
+ Column {
+ SearchRangeTextBox(dateRange = dateRange) {
+ viewModel.showDatePicker()
+ }
+ LazyVerticalGrid(columns = GridCells.Fixed(2), contentPadding = it) {
+ items(
+ count = lazyPagingItems.itemCount,
+ key = { i -> lazyPagingItems[i]?.videoUrl ?: i },
+ ) { index ->
+ lazyPagingItems[index]?.let {
+ Row(modifier = Modifier.animateItemPlacement()) {
+ FilmCard(it) { viewModel.onClickItem(index) }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun SearchAppBar(
+ titleVisibility: Boolean,
+ searchText: String,
+ viewModel: SearchFilmViewModel,
+ focusManager: FocusManager,
+ focusRequester: FocusRequester,
+ onVisibilityChange: (Boolean) -> Unit,
+ onSearchTextChange: (String) -> Unit,
+ onNavigationClick: () -> Unit,
+) {
+ TopAppBar(
+ title = {
+ if (titleVisibility) {
+ Text("검색")
+ } else {
+ TextField(
+ value = searchText,
+ onValueChange = onSearchTextChange,
+ singleLine = true,
+ placeholder = { Icon(Icons.Filled.Search, null, tint = Color.Gray) },
+ keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
+ keyboardActions = KeyboardActions(onSearch = {
+ viewModel.searchKeyword(searchText)
+ focusManager.clearFocus()
+ }),
+ colors = TextFieldDefaults.textFieldColors(
+ backgroundColor = MaterialTheme.colors.background,
+ focusedIndicatorColor = MaterialTheme.colors.background,
+ unfocusedIndicatorColor = MaterialTheme.colors.background,
+ ),
+ modifier = Modifier.focusRequester(focusRequester),
+ )
+ }
+ },
+ navigationIcon = {
+ IconButton(onClick = onNavigationClick) { Icon(Icons.Filled.ArrowBack, null) }
+ },
+ actions = {
+ IconButton(
+ onClick = {
+ if (titleVisibility) {
+ onVisibilityChange(false)
+ } else {
+ onVisibilityChange(true)
+ onSearchTextChange("")
+ viewModel.searchKeyword("")
+ }
+ },
+ ) {
+ if (titleVisibility) {
+ Icon(Icons.Filled.Search, null)
+ } else {
+ Icon(Icons.Filled.Close, null)
+ }
+ }
+ },
+ backgroundColor = MaterialTheme.colors.background,
+ elevation = 4.dp,
+ )
+
+ if (titleVisibility.not()) {
+ SideEffect {
+ focusRequester.requestFocus()
+ }
+ }
+}
+
+@Composable
+private fun SearchRangeTextBox(dateRange: String, onClickTextBox: () -> Unit) {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(8.dp)
+ .background(lightGray, RoundedCornerShape(4.dp))
+ .padding(0.dp, 4.dp)
+ .clickable { onClickTextBox() },
+ ) {
+ Icon(Icons.Filled.DateRange, null, modifier = Modifier.padding(4.dp), tint = Color.Black)
+ Text(
+ text = dateRange,
+ modifier = Modifier.align(Alignment.Center),
+ color = Color.Black,
+ maxLines = 1,
+ style = TextStyle(
+ textAlign = TextAlign.Center,
+ fontSize = TextUnit(20F, TextUnitType.Sp),
+ fontWeight = FontWeight.Bold,
+ ),
+ )
+ }
+}
+
+@OptIn(
+ ExperimentalGlideComposeApi::class,
+ ExperimentalMaterialApi::class,
+)
+@Composable
+private fun FilmCard(
+ item: DailyFilmItem,
+ onClickItem: () -> Unit,
+) {
+ item.updateDate.take(4)
+
+ Card(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(8.dp),
+ elevation = 4.dp,
+ backgroundColor = lightGray,
+ onClick = { onClickItem() },
+ ) {
+ Column(modifier = Modifier.padding(8.dp)) {
+ GlideImage(
+ model = item.videoUrl,
+ contentDescription = "thumbnail",
+ modifier = Modifier
+ .fillMaxWidth()
+ .aspectRatio(3 / 4F),
+ contentScale = ContentScale.FillBounds,
+ )
+ Text(
+ text = item.updateDate.let {
+ stringResource(
+ R.string.card_date_format,
+ it.substring(0, 4),
+ it.substring(4, 6),
+ it.substring(6),
+ )
+ },
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(0.dp, 8.dp, 0.dp, 0.dp),
+ color = Color.Black,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ style = TextStyle(fontSize = TextUnit(16F, TextUnitType.Sp)),
+ )
+ Text(
+ text = item.text,
+ modifier = Modifier.fillMaxWidth(),
+ color = Color.Gray,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ )
+ }
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun DefaultPreview() {
+}
diff --git a/app/src/main/java/com/boostcamp/dailyfilm/presentation/searchfilm/adapter/SearchFilmAdapter.kt b/app/src/main/java/com/boostcamp/dailyfilm/presentation/searchfilm/adapter/SearchFilmAdapter.kt
deleted file mode 100644
index dd9312dc..00000000
--- a/app/src/main/java/com/boostcamp/dailyfilm/presentation/searchfilm/adapter/SearchFilmAdapter.kt
+++ /dev/null
@@ -1,55 +0,0 @@
-package com.boostcamp.dailyfilm.presentation.searchfilm.adapter
-
-import android.view.LayoutInflater
-import android.view.ViewGroup
-import androidx.databinding.DataBindingUtil
-import androidx.recyclerview.widget.DiffUtil
-import androidx.recyclerview.widget.ListAdapter
-import androidx.recyclerview.widget.RecyclerView
-import com.boostcamp.dailyfilm.R
-import com.boostcamp.dailyfilm.data.model.DailyFilmItem
-import com.boostcamp.dailyfilm.databinding.ItemSearchResultBinding
-import com.bumptech.glide.Glide
-
-class SearchFilmAdapter(private val onClick: (Int) -> Unit) : ListAdapter(diffUtil) {
-
- override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SearchFilmViewHolder {
- return SearchFilmViewHolder(
- DataBindingUtil.inflate(
- LayoutInflater.from(parent.context),
- R.layout.item_search_result,
- parent,
- false
- )
- )
- }
-
- override fun onBindViewHolder(holder: SearchFilmViewHolder, position: Int) {
- holder.bind(getItem(position))
- }
-
- inner class SearchFilmViewHolder(private val binding: ItemSearchResultBinding) : RecyclerView.ViewHolder(binding.root) {
- init {
- itemView.setOnClickListener {
- onClick(absoluteAdapterPosition)
- }
- }
-
- fun bind(item: DailyFilmItem) {
- binding.item = item
- binding.requestManager = Glide.with(itemView)
- }
- }
-
- companion object {
- private val diffUtil = object : DiffUtil.ItemCallback() {
- override fun areItemsTheSame(oldItem: DailyFilmItem, newItem: DailyFilmItem): Boolean {
- return oldItem == newItem
- }
-
- override fun areContentsTheSame(oldItem: DailyFilmItem, newItem: DailyFilmItem): Boolean {
- return oldItem.videoUrl == newItem.videoUrl
- }
- }
- }
-}
diff --git a/app/src/main/res/layout/activity_search_film.xml b/app/src/main/res/layout/activity_search_film.xml
deleted file mode 100644
index 3416d982..00000000
--- a/app/src/main/res/layout/activity_search_film.xml
+++ /dev/null
@@ -1,77 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_search_result.xml b/app/src/main/res/layout/item_search_result.xml
deleted file mode 100644
index a2928502..00000000
--- a/app/src/main/res/layout/item_search_result.xml
+++ /dev/null
@@ -1,74 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 9338514c..b4505143 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -54,6 +54,7 @@
설정 화면
TotalComposeActivity
Failed Google Login
+ %s년 %s월 %s일
sound_lottie.json
속도 조절
\ No newline at end of file