diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v4/model/ListUpdateBody.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v4/model/ListUpdateBody.kt index a11bf20..259e1c8 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v4/model/ListUpdateBody.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v4/model/ListUpdateBody.kt @@ -3,8 +3,8 @@ package com.owenlejeune.tvtime.api.tmdb.api.v4.model import com.google.gson.annotations.SerializedName class ListUpdateBody( - @SerializedName("description") val description: String?, - @SerializedName("name") val name: String?, - @SerializedName("public") val isPublic: Boolean? -// @SerializedName("sort_by") + @SerializedName("name") val name: String, + @SerializedName("description") val description: String, + @SerializedName("public") val isPublic: Boolean, + @SerializedName("sort_by") val sortBy: SortOrder ) diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v4/model/MediaList.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v4/model/MediaList.kt index d0dc757..029bad5 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v4/model/MediaList.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v4/model/MediaList.kt @@ -1,6 +1,7 @@ package com.owenlejeune.tvtime.api.tmdb.api.v4.model import com.google.gson.annotations.SerializedName +import com.owenlejeune.tvtime.R class MediaList( @SerializedName("poster_path") val posterPath: String?, @@ -17,5 +18,52 @@ class MediaList( @SerializedName("average_rating") val averageRating: Float, @SerializedName("runtime") val runtime: Int, @SerializedName("name") val name: String, - @SerializedName("revenue") val revenue: Int -) \ No newline at end of file + @SerializedName("revenue") val revenue: Int, + @SerializedName("sort_by") val sortBy: SortOrder +) + +enum class SortOrder( + val stringKey: Int, + val sort: (List) -> List +) { + @SerializedName("original_order.asc") + ORIGINAL_ASCENDING( + R.string.sort_order_original_ascending, + { it } + ), + @SerializedName("original_order.desc") + ORIGINAL_DESCENDING( + R.string.sort_order_original_descending, + { ORIGINAL_ASCENDING.sort(it).reversed() } + ), + @SerializedName("vote_average.asc") + RATING_ASCENDING( + R.string.sort_order_rating_ascending, + { lin -> lin.sortedBy { it.voteAverage } } + ), + @SerializedName("vote_average.desc") + RATING_DESCENDING( + R.string.sort_order_rating_descending, + { lin -> RATING_ASCENDING.sort(lin).reversed() } + ), + @SerializedName("primary_release_date.asc") + RELEASE_DATE_ASCENDING( + R.string.sort_order_release_date_ascending, + { lin -> lin.sortedBy { it.releaseDate } } + ), + @SerializedName("primary_release_date.desc") + RELEASE_DATE_DESCENDING( + R.string.sort_order_release_date_descending, + { lin -> RELEASE_DATE_ASCENDING.sort(lin).reversed() } + ), + @SerializedName("title.asc") + TITLE_ASCENDING( + R.string.sort_order_title_ascending, + { lin -> lin.sortedBy { it.title } } + ), + @SerializedName("title.desc") + TITLE_DESCENDING( + R.string.sort_order_title_descending, + { lin -> TITLE_ASCENDING.sort(lin).reversed() } + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/components/Widgets.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/components/Widgets.kt index b38f308..f5b036e 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/ui/components/Widgets.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/components/Widgets.kt @@ -16,15 +16,13 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.Card -import androidx.compose.material.OutlinedTextField -import androidx.compose.material.TextFieldDefaults import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowDropDown +import androidx.compose.material.icons.filled.ArrowDropUp import androidx.compose.material.icons.filled.Error import androidx.compose.material.icons.filled.Search import androidx.compose.material.icons.filled.Visibility import androidx.compose.material.icons.filled.VisibilityOff -import androidx.compose.material.icons.outlined.ArrowDropDown import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable @@ -60,8 +58,6 @@ import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.* import androidx.compose.ui.viewinterop.AndroidView -import androidx.compose.ui.window.Dialog -import androidx.compose.ui.window.DialogProperties import androidx.core.text.HtmlCompat import androidx.navigation.NavHostController import coil.compose.AsyncImage @@ -73,9 +69,6 @@ import com.owenlejeune.tvtime.preferences.AppPreferences import com.owenlejeune.tvtime.ui.navigation.MainNavItem import com.owenlejeune.tvtime.ui.screens.main.MediaViewType import com.owenlejeune.tvtime.utils.TmdbUtils -import com.pierfrancescosoffritti.androidyoutubeplayer.core.player.YouTubePlayer -import com.pierfrancescosoffritti.androidyoutubeplayer.core.player.listeners.AbstractYouTubePlayerListener -import com.pierfrancescosoffritti.androidyoutubeplayer.core.player.views.YouTubePlayerView import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.intellij.markdown.flavours.commonmark.CommonMarkFlavourDescriptor @@ -83,6 +76,7 @@ import org.intellij.markdown.html.HtmlGenerator import org.intellij.markdown.parser.MarkdownParser import org.koin.java.KoinJavaComponent +@OptIn(ExperimentalMaterial3Api::class) @Composable fun TopLevelSwitch( text: String, @@ -94,14 +88,16 @@ fun TopLevelSwitch( modifier = Modifier .fillMaxWidth() .height(100.dp) - .padding(12.dp), - shape = RoundedCornerShape(30.dp), - backgroundColor = when { - isSystemInDarkTheme() && checkedState.value -> MaterialTheme.colorScheme.primary - isSystemInDarkTheme() && !checkedState.value -> MaterialTheme.colorScheme.secondary - checkedState.value -> MaterialTheme.colorScheme.primaryContainer - else -> MaterialTheme.colorScheme.secondaryContainer - } + .padding(12.dp) + .background( + color = when { + isSystemInDarkTheme() && checkedState.value -> MaterialTheme.colorScheme.primary + isSystemInDarkTheme() && !checkedState.value -> MaterialTheme.colorScheme.secondary + checkedState.value -> MaterialTheme.colorScheme.primaryContainer + else -> MaterialTheme.colorScheme.secondaryContainer + } + ), + shape = RoundedCornerShape(30.dp) ) { Row( modifier = Modifier.fillMaxSize(), @@ -962,17 +958,40 @@ fun Spinner( Box(modifier = modifier) { Column { - OutlinedTextField( - value = selected.first, - onValueChange = { }, - trailingIcon = { Icon(Icons.Outlined.ArrowDropDown, null) }, - readOnly = true, - modifier = Modifier.clickable( - onClick = { - expanded = true + Box( + modifier = Modifier + .fillMaxWidth() + .border( + width = 1.dp, + color = MaterialTheme.colorScheme.outline, + shape = RoundedCornerShape(4.dp) + ) + .clickable { + expanded = !expanded } - ) - ) + ) { + Row( + modifier = Modifier.padding(start = 16.dp, end = 8.dp, top = 16.dp, bottom = 16.dp), + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + modifier = Modifier.weight(1f), + text = selected.first, + fontSize = 14.sp, + color = MaterialTheme.colorScheme.onBackground + ) + Icon( + imageVector = if (expanded) Icons.Filled.ArrowDropUp else Icons.Filled.ArrowDropDown, + contentDescription = null, + modifier = Modifier.clickable( + onClick = { + expanded = !expanded + } + ) + ) + } + } DropdownMenu( expanded = expanded, onDismissRequest = { expanded = false } diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/screens/main/ListDetailView.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/main/ListDetailView.kt index 203096b..f425b8d 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/ui/screens/main/ListDetailView.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/main/ListDetailView.kt @@ -42,6 +42,8 @@ import com.owenlejeune.tvtime.api.tmdb.api.v4.model.* import com.owenlejeune.tvtime.extensions.WindowSizeClass import com.owenlejeune.tvtime.extensions.unlessEmpty import com.owenlejeune.tvtime.preferences.AppPreferences +import com.owenlejeune.tvtime.ui.components.Spinner +import com.owenlejeune.tvtime.ui.components.SwitchPreference import com.owenlejeune.tvtime.ui.navigation.MainNavItem import com.owenlejeune.tvtime.ui.theme.* import com.owenlejeune.tvtime.utils.SessionManager @@ -55,31 +57,6 @@ import kotlinx.coroutines.withContext import org.koin.java.KoinJavaComponent import kotlin.math.roundToInt -enum class SortOrder(val stringKey: Int) { - ORIGINAL(R.string.sort_order_original) { - override fun sort(listIn: List): List { - return listIn - } - }, - RATING(R.string.sort_order_rating) { - override fun sort(listIn: List): List { - return listIn.sortedBy { it.voteAverage }.reversed() - } - }, - RELEASE_DATE(R.string.sort_order_release_date) { - override fun sort(listIn: List): List { - return listIn.sortedBy { it.releaseDate }.reversed() - } - }, - TITLE(R.string.sort_order_title) { - override fun sort(listIn: List): List { - return listIn.sortedBy { it.title } - } - }; - - abstract fun sort(listIn: List): List -} - @OptIn(ExperimentalMaterial3Api::class) @Composable fun ListDetailView( @@ -136,10 +113,16 @@ fun ListDetailView( .verticalScroll(state = rememberScrollState()), verticalArrangement = Arrangement.spacedBy(12.dp) ) { - val selectedSortOrder = remember { mutableStateOf(SortOrder.ORIGINAL) } - ListHeader(list = mediaList, selectedSortOrder = selectedSortOrder) + val selectedSortOrder = remember { mutableStateOf(mediaList.sortBy) } + ListHeader( + list = mediaList, + selectedSortOrder = selectedSortOrder, + service = service, + parentList = parentList + ) - selectedSortOrder.value.sort(mediaList.results).forEach { listItem -> + val sortedResults = selectedSortOrder.value.sort(mediaList.results) + sortedResults.forEach { listItem -> ListItemView( appNavController = appNavController, listItem = listItem, @@ -152,11 +135,12 @@ fun ListDetailView( } } -@OptIn(ExperimentalMaterial3Api::class) @Composable private fun ListHeader( list: MediaList, - selectedSortOrder: MutableState + selectedSortOrder: MutableState, + service: ListV4Service, + parentList: MutableState ) { val context = LocalContext.current @@ -222,14 +206,14 @@ private fun ListHeader( } val showSortByOrderDialog = remember { mutableStateOf(false) } - + val showEditListDialog = remember { mutableStateOf(false) } Row( horizontalArrangement = Arrangement.spacedBy(8.dp) ) { Button( modifier = Modifier.weight(1f), shape = RoundedCornerShape(10.dp), - onClick = { Toast.makeText(context, "Edit", Toast.LENGTH_SHORT).show() } + onClick = { showEditListDialog.value = true } ) { Text(text = stringResource(R.string.action_edit)) } @@ -250,7 +234,20 @@ private fun ListHeader( } if (showSortByOrderDialog.value) { - SortOrderDialog(showSortByOrderDialog = showSortByOrderDialog, selectedSortOrder = selectedSortOrder) + SortOrderDialog( + showSortByOrderDialog = showSortByOrderDialog, + selectedSortOrder = selectedSortOrder + ) + } + + if (showEditListDialog.value) { + EditListDialog( + showEditListDialog = showEditListDialog, + list = list, + service = service, + parentList = parentList, + selectedSortOrder = selectedSortOrder + ) } } } @@ -262,13 +259,19 @@ private fun SortOrderDialog( selectedSortOrder: MutableState ) { AlertDialog( + modifier = Modifier.wrapContentSize(), onDismissRequest = { showSortByOrderDialog.value = false }, - confirmButton = {}, - title = { Text(text = "Sort By") }, - dismissButton = { Text(text = "Dismiss") }, + confirmButton = { + Button( + onClick = { showSortByOrderDialog.value = false } + ) { + Text(text = stringResource(id = R.string.action_dismiss)) + } + }, + title = { Text(text = stringResource(id = R.string.action_sort_by)) }, text = { Column( - verticalArrangement = Arrangement.spacedBy(12.dp) + verticalArrangement = Arrangement.spacedBy(16.dp) ) { SortOrder.values().forEach { Row( @@ -280,14 +283,14 @@ private fun SortOrderDialog( }, role = Role.RadioButton ), - horizontalArrangement = Arrangement.spacedBy(4.dp), + horizontalArrangement = Arrangement.spacedBy(6.dp), verticalAlignment = Alignment.CenterVertically ) { RadioButton( selected = selectedSortOrder.value == it, onClick = null ) - Text(text = stringResource(id = it.stringKey), fontSize = 20.sp) + Text(text = stringResource(id = it.stringKey), fontSize = 18.sp) } } } @@ -295,6 +298,86 @@ private fun SortOrderDialog( ) } +@Composable +private fun EditListDialog( + showEditListDialog: MutableState, + list: MediaList, + service: ListV4Service, + parentList: MutableState, + selectedSortOrder: MutableState +) { + val coroutineScope = rememberCoroutineScope() + + var listTitle by remember { mutableStateOf(list.name) } + var listDescription by remember { mutableStateOf(list.description) } + var isPublicList by remember { mutableStateOf(list.isPublic) } + var editSelectedSortOrder by remember { mutableStateOf(list.sortBy) } + + AlertDialog( + onDismissRequest = { }, + dismissButton = { + Button( + onClick = { showEditListDialog.value = false } + ) { + Text(text = stringResource(id = R.string.action_dismiss)) + } + }, + confirmButton = { + Button( + onClick = { + val listUpdateBody = ListUpdateBody(listTitle, listDescription, isPublicList, editSelectedSortOrder) + coroutineScope.launch { + val response = service.updateList(list.id, listUpdateBody) + if (response.isSuccessful) { + fetchList(list.id, service, parentList) + selectedSortOrder.value = editSelectedSortOrder + } + showEditListDialog.value = false + } + } + ) { + Text(text = stringResource(id = R.string.action_save)) + } + }, + text = { + Column( + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + OutlinedTextField( + value = listTitle, + onValueChange = { listTitle = it }, + singleLine = true, + label = { Text(text = stringResource(id = R.string.label_name)) } + ) + + OutlinedTextField( + value = listDescription, + onValueChange = { listDescription = it }, + modifier = Modifier.heightIn(min = 100.dp), + label = { Text(text = stringResource(id = R.string.label_description)) } + ) + + SwitchPreference( + titleText = stringResource(id = R.string.label_public_list), + checkState = isPublicList, + onCheckedChange = { isPublicList = it } + ) + + Text( + text = stringResource(id = R.string.action_sort_by), + fontSize = 16.sp + ) + Spinner( + list = SortOrder.values().map { stringResource(id = it.stringKey) to it }, + preselected = Pair(stringResource(id = editSelectedSortOrder.stringKey), editSelectedSortOrder), + onSelectionChanged = { editSelectedSortOrder = it.second } + ) + } + }, + title = { Text(text = stringResource(id = R.string.title_edit_list)) } + ) +} + @Composable private fun RowScope.OverviewStatCard( top: String, diff --git a/app/src/main/java/com/owenlejeune/tvtime/utils/TmdbUtils.kt b/app/src/main/java/com/owenlejeune/tvtime/utils/TmdbUtils.kt index 95ca9be..a2fa5c9 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/utils/TmdbUtils.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/utils/TmdbUtils.kt @@ -93,6 +93,9 @@ object TmdbUtils { } fun convertRuntimeToHoursMinutes(series: DetailedTv): String { + if (series.episodeRuntime.isEmpty()) { + return "" + } return convertRuntimeToHoursAndMinutes(series.episodeRuntime[0]) } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 59e9f34..075bd04 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -193,8 +193,18 @@ Sort By Share Remove from list - Original - Rating - Release Date - Title + Original (Ascending) + Original (Descending) + Rating (Ascending) + Rating (Descending) + Release Date (Ascending) + Release Date (Ascending) + Title (A-Z) + Title (Z-A) + Dismiss + Save + Name + Description + Public list? + Edit List \ No newline at end of file