fix list button on details

This commit is contained in:
Owen LeJeune
2023-07-08 20:31:24 -04:00
parent 859ab14d95
commit 2712561a31
10 changed files with 237 additions and 162 deletions

View File

@@ -104,15 +104,13 @@ dependencies {
// retrofit
def retrofit = "2.9.0"
def stetho = "1.6.0"
def gson = "2.10.1"
def profiler = "1.0.8"
def logging = "4.6.0"
implementation "com.squareup.retrofit2:retrofit:$retrofit"
implementation "com.squareup.retrofit2:converter-gson:$retrofit"
implementation "com.google.code.gson:gson:$gson"
implementation "com.facebook.stetho:stetho:$stetho"
implementation "com.facebook.stetho:stetho-okhttp3:$stetho"
implementation "com.localebro:okhttpprofiler:$profiler"
implementation "com.squareup.okhttp3:logging-interceptor:$logging"
// koin
def koin = "3.3.0"

View File

@@ -1,7 +1,6 @@
package com.owenlejeune.tvtime
import android.app.Application
import com.facebook.stetho.Stetho
import com.kieronquinn.monetcompat.core.MonetCompat
import com.owenlejeune.tvtime.di.modules.appModule
import com.owenlejeune.tvtime.di.modules.networkModule
@@ -41,10 +40,6 @@ class TvTimeApplication: Application() {
val userPickedColor = preferences.selectedColor
it?.firstOrNull { color -> color == userPickedColor } ?: it?.firstOrNull()
}
if (BuildConfig.DEBUG) {
Stetho.initializeWithDefaults(this)
}
}
}

View File

@@ -1,12 +1,14 @@
package com.owenlejeune.tvtime.api
import com.facebook.stetho.okhttp3.StethoInterceptor
import com.localebro.okhttpprofiler.OkHttpProfilerInterceptor
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
class DebugHttpClient: HttpClient {
override val httpClient: OkHttpClient = OkHttpClient.Builder()
.addInterceptor(OkHttpProfilerInterceptor())
.addNetworkInterceptor(StethoInterceptor())
.addInterceptor(getLoggingInterceptor())
.build()
private fun getLoggingInterceptor() = HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
}
}

View File

@@ -1,7 +1,10 @@
package com.owenlejeune.tvtime.api.tmdb.api.v4
import android.content.Context
import android.util.Log
import android.widget.Toast
import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.mutableStateOf
import com.owenlejeune.tvtime.api.tmdb.api.v4.model.AddToListBody
import com.owenlejeune.tvtime.api.tmdb.api.v4.model.CreateListBody
import com.owenlejeune.tvtime.api.tmdb.api.v4.model.DeleteListItemsBody
@@ -18,6 +21,7 @@ class ListV4Service: KoinComponent {
}
private val service: ListV4Api by inject()
private val context: Context by inject()
val listMap = mutableStateMapOf<Int, MediaList>()
@@ -59,8 +63,21 @@ class ListV4Service: KoinComponent {
service.deleteList(listId)
}
suspend fun addItemsToList(listId: Int, body: AddToListBody) {//}: Response<AddToListResponse> {
service.addItemsToList(listId, body)
suspend fun addItemsToList(listId: Int, body: AddToListBody) {
val response = service.addItemsToList(listId, body)
if (response.isSuccessful) {
response.body()?.let {
if (it.isSuccess) {
Toast.makeText(context, "Successfully added to list", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(context, "Error: This item already exists in list", Toast.LENGTH_SHORT).show()
}
} ?: run {
Toast.makeText(context, "An error occurred", Toast.LENGTH_SHORT).show()
}
} else {
Toast.makeText(context, "An error occurred", Toast.LENGTH_SHORT).show()
}
}
suspend fun updateListItems(listId: Int, body: UpdateListItemBody) {//}: Response<AddToListResponse> {

View File

@@ -8,49 +8,34 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredWidthIn
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Bookmark
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material.icons.filled.List
import androidx.compose.material.icons.filled.Star
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.owenlejeune.tvtime.R
import com.owenlejeune.tvtime.ui.theme.FavoriteSelected
import com.owenlejeune.tvtime.ui.theme.RatingSelected
import com.owenlejeune.tvtime.ui.theme.WatchlistSelected
import com.owenlejeune.tvtime.ui.theme.actionButtonColor
import com.owenlejeune.tvtime.ui.viewmodel.AccountViewModel
import com.owenlejeune.tvtime.ui.viewmodel.MainViewModel
import com.owenlejeune.tvtime.utils.SessionManager
import com.owenlejeune.tvtime.utils.types.MediaViewType
import kotlinx.coroutines.launch
import java.text.DecimalFormat
enum class Actions {
RATE,
@@ -63,8 +48,8 @@ enum class Actions {
fun ActionsView(
itemId: Int,
type: MediaViewType,
actions: List<Actions> = listOf(Actions.RATE, Actions.WATCHLIST, Actions.LIST, Actions.FAVORITE),
modifier: Modifier = Modifier
modifier: Modifier = Modifier,
actions: List<Actions> = listOf(Actions.RATE, Actions.WATCHLIST, Actions.LIST, Actions.FAVORITE)
) {
val accountViewModel = viewModel<AccountViewModel>()
val mainViewModel = viewModel<MainViewModel>()
@@ -77,6 +62,12 @@ fun ActionsView(
modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
if (actions.contains(Actions.LIST)) {
ListButton(
itemId = itemId,
type = type
)
}
if (actions.contains(Actions.RATE)) {
RateButton(
itemId = itemId,
@@ -100,12 +91,6 @@ fun ActionsView(
mainViewModel = mainViewModel
)
}
if (actions.contains(Actions.LIST)) {
ListButton(
itemId = itemId,
type = type
)
}
}
}
@@ -215,15 +200,21 @@ fun ListButton(
type: MediaViewType,
modifier: Modifier = Modifier
) {
CircleBackgroundColorImage(
modifier = modifier.clickable(
onClick = {}
),
size = 40.dp,
backgroundColor = MaterialTheme.colorScheme.actionButtonColor,
image = Icons.Filled.List,
colorFilter = ColorFilter.tint(color = MaterialTheme.colorScheme.background),
contentDescription = ""
val showListDialog = remember { mutableStateOf(false) }
ActionButton(
imageVector = Icons.Filled.List,
contentDescription = "",
isSelected = false,
filledIconColor = MaterialTheme.colorScheme.background,
onClick = { showListDialog.value = true },
modifier = modifier
)
AddToListDialog(
showDialog = showListDialog,
itemId = itemId,
itemType = type
)
}

View File

@@ -6,14 +6,12 @@ import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter
@@ -24,8 +22,6 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.constraintlayout.compose.ConstraintLayout
import coil.compose.AsyncImage
import coil.compose.rememberAsyncImagePainter
import coil.request.CachePolicy
@@ -37,11 +33,9 @@ import com.google.accompanist.pager.rememberPagerState
import com.owenlejeune.tvtime.R
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.ExternalIds
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.ImageCollection
import com.owenlejeune.tvtime.ui.viewmodel.ConfigurationViewModel
import com.owenlejeune.tvtime.utils.TmdbUtils
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.koin.androidx.compose.koinViewModel
@OptIn(ExperimentalPagerApi::class)
@Composable
@@ -55,76 +49,7 @@ fun DetailHeader(
posterContentDescription: String? = null,
rating: Float? = null,
pagerState: PagerState? = null,
elevation: Dp = 20.dp
) {
ConstraintLayout(modifier = modifier
.fillMaxWidth()
.wrapContentHeight()
) {
val (
backdropImage, posterImage, ratingsView
) = createRefs()
if (imageCollection != null) {
BackdropGallery(
modifier = Modifier
.constrainAs(backdropImage) {
top.linkTo(parent.top)
start.linkTo(parent.start)
end.linkTo(parent.end)
}
.clickable {
showGalleryOverlay?.value = true
},
imageCollection = imageCollection,
state = pagerState
)
} else {
Backdrop(
imageUrl = backdropUrl,
contentDescription = backdropContentDescription
)
}
PosterItem(
modifier = Modifier
.constrainAs(posterImage) {
bottom.linkTo(backdropImage.bottom)
start.linkTo(parent.start, margin = 16.dp)
},
url = posterUrl,
title = posterContentDescription,
elevation = elevation,
overrideShowTitle = false,
enabled = false
)
rating?.let {
RatingView(
modifier = Modifier
.constrainAs(ratingsView) {
bottom.linkTo(parent.bottom)
start.linkTo(posterImage.end, margin = 20.dp)
},
progress = rating
)
}
}
}
@OptIn(ExperimentalPagerApi::class)
@Composable
fun DetailHeader2(
modifier: Modifier = Modifier,
showGalleryOverlay: MutableState<Boolean>? = null,
imageCollection: ImageCollection? = null,
backdropUrl: String? = null,
posterUrl: String? = null,
backdropContentDescription: String? = null,
posterContentDescription: String? = null,
rating: Float? = null,
pagerState: PagerState? = null,
elevation: Dp = 20.dp
elevation: Dp = 50.dp
) {
Box(
modifier = modifier.then(

View File

@@ -1,22 +1,54 @@
package com.owenlejeune.tvtime.ui.components
import android.util.Log
import android.widget.Toast
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.paging.compose.collectAsLazyPagingItems
import com.owenlejeune.tvtime.R
import com.owenlejeune.tvtime.api.tmdb.api.v4.ListV4Service
import com.owenlejeune.tvtime.api.tmdb.api.v4.model.AccountList
import com.owenlejeune.tvtime.extensions.lazyPagingItems
import com.owenlejeune.tvtime.ui.viewmodel.AccountViewModel
import com.owenlejeune.tvtime.utils.types.MediaViewType
import kotlinx.coroutines.launch
import java.text.DecimalFormat
private const val TAG = "Dialogs"
@Composable
fun RatingDialog(
showDialog: MutableState<Boolean>,
@@ -28,7 +60,7 @@ fun RatingDialog(
}
if (showDialog.value) {
var sliderPosition by remember { mutableStateOf(rating) }
var sliderPosition by remember { mutableFloatStateOf(rating) }
val formatted = formatPosition(sliderPosition).toFloat()
AlertDialog(
modifier = Modifier.wrapContentHeight(),
@@ -79,4 +111,96 @@ fun RatingDialog(
}
)
}
}
@Composable
fun AddToListDialog(
itemId: Int,
itemType: MediaViewType,
showDialog: MutableState<Boolean>
) {
if (showDialog.value) {
val scope = rememberCoroutineScope()
val accountViewModel = viewModel<AccountViewModel>()
val lists = accountViewModel.userLists.collectAsLazyPagingItems()
AlertDialog(
onDismissRequest = { showDialog.value = false },
title = {
Text(text = stringResource(id = R.string.add_to_list_action_label))
},
text = {
Column(
modifier = Modifier.height(300.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
val filter = remember { mutableStateOf("") }
RoundedTextField(
modifier = Modifier
.height(55.dp)
.padding(bottom = 12.dp),
value = filter.value,
onValueChange = { filter.value = it },
placeHolder = stringResource(id = R.string.search_placeholder, stringResource(id = R.string.lists)),
leadingIcon = {
Image(
imageVector = Icons.Filled.Search,
contentDescription = stringResource(R.string.search_icon_content_descriptor),
colorFilter = ColorFilter.tint(color = MaterialTheme.colorScheme.primary)
)
}
)
LazyColumn(
verticalArrangement = Arrangement.spacedBy(18.dp)
) {
if (lists.itemCount == 0) {
item {
Spacer(modifier = Modifier.weight(1f))
Text(stringResource(id = R.string.no_lists_message))
Spacer(modifier = Modifier.weight(1f))
}
}
lazyPagingItems(lists) { list ->
(list as AccountList).apply {
if (list.name.contains(filter.value)) {
Box(
modifier = Modifier
.clip(RoundedCornerShape(10.dp))
.clickable {
scope.launch {
accountViewModel.addToList(
listId = list.id,
itemId = itemId,
itemType = itemType
)
}
}
) {
Text(
text = stringResource(id = R.string.list_count_label, list.name, list.numberOfItems),
fontSize = 16.sp,
modifier = Modifier
.padding(horizontal = 12.dp, vertical = 4.dp)
.fillMaxWidth()
)
}
}
}
}
}
}
},
confirmButton = {
Button(
modifier = Modifier.height(40.dp),
onClick = {
showDialog.value = false
}
) {
Text(stringResource(id = R.string.action_dismiss))
}
}
)
}
}

View File

@@ -167,7 +167,7 @@ private fun MediaViewContent(
.verticalScroll(state = rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
DetailHeader2(
DetailHeader(
posterUrl = TmdbUtils.getFullPosterPath(mediaItem?.posterPath),
posterContentDescription = mediaItem?.title,
backdropUrl = TmdbUtils.getFullBackdropPath(mediaItem?.backdropPath),

View File

@@ -8,6 +8,8 @@ import com.owenlejeune.tvtime.api.tmdb.api.v3.model.MarkAsFavoriteBody
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.WatchlistBody
import com.owenlejeune.tvtime.api.tmdb.api.v4.AccountV4Service
import com.owenlejeune.tvtime.api.tmdb.api.v4.ListV4Service
import com.owenlejeune.tvtime.api.tmdb.api.v4.model.AddToListBody
import com.owenlejeune.tvtime.api.tmdb.api.v4.model.AddToListBodyItem
import com.owenlejeune.tvtime.api.tmdb.api.v4.model.DeleteListItemsBody
import com.owenlejeune.tvtime.api.tmdb.api.v4.model.DeleteListItemsItem
import com.owenlejeune.tvtime.api.tmdb.api.v4.model.ListUpdateBody
@@ -30,44 +32,53 @@ class AccountViewModel: ViewModel(), KoinComponent {
val listMap = listService.listMap
val ratedTv: Flow<PagingData<Any>> = createPagingFlow(
fetcher = { p -> accountV4Service.getRatedTvShows(accountId, p) },
processor = { it.results }
)
val favoriteTv: Flow<PagingData<Any>> = createPagingFlow(
fetcher = { p -> accountV4Service.getFavoriteTvShows(accountId, p) },
processor = { it.results }
)
val watchlistTv: Flow<PagingData<Any>> = createPagingFlow(
fetcher = { p -> accountV4Service.getTvShowWatchlist(accountId, p) },
processor = { it.results }
)
val recommendedTv: Flow<PagingData<Any>> = createPagingFlow(
fetcher = { p -> accountV4Service.getRecommendedTvSeries(accountId, p) },
processor = { it.results }
)
val ratedTv: Flow<PagingData<Any>>
get() = createPagingFlow(
fetcher = { p -> accountV4Service.getRatedTvShows(accountId, p) },
processor = { it.results }
)
val favoriteTv: Flow<PagingData<Any>>
get() = createPagingFlow(
fetcher = { p -> accountV4Service.getFavoriteTvShows(accountId, p) },
processor = { it.results }
)
val watchlistTv: Flow<PagingData<Any>>
get() = createPagingFlow(
fetcher = { p -> accountV4Service.getTvShowWatchlist(accountId, p) },
processor = { it.results }
)
val recommendedTv: Flow<PagingData<Any>>
get() = createPagingFlow(
fetcher = { p -> accountV4Service.getRecommendedTvSeries(accountId, p) },
processor = { it.results }
)
val ratedMovies: Flow<PagingData<Any>> = createPagingFlow(
fetcher = { p -> accountV4Service.getRatedMovies(accountId, p) },
processor = { it.results }
)
val favoriteMovies: Flow<PagingData<Any>> = createPagingFlow(
fetcher = { p -> accountV4Service.getFavoriteMovies(accountId, p) },
processor = { it.results }
)
val watchlistMovies: Flow<PagingData<Any>> = createPagingFlow(
fetcher = { p -> accountV4Service.getMovieWatchlist(accountId, p) },
processor = { it.results }
)
val recommendedMovies: Flow<PagingData<Any>> = createPagingFlow(
fetcher = { p -> accountV4Service.getRecommendedMovies(accountId, p) },
processor = { it.results }
)
val ratedMovies: Flow<PagingData<Any>>
get() = createPagingFlow(
fetcher = { p -> accountV4Service.getRatedMovies(accountId, p) },
processor = { it.results }
)
val favoriteMovies: Flow<PagingData<Any>>
get() = createPagingFlow(
fetcher = { p -> accountV4Service.getFavoriteMovies(accountId, p) },
processor = { it.results }
)
val watchlistMovies: Flow<PagingData<Any>>
get() = createPagingFlow(
fetcher = { p -> accountV4Service.getMovieWatchlist(accountId, p) },
processor = { it.results }
)
val recommendedMovies: Flow<PagingData<Any>>
get() = createPagingFlow(
fetcher = { p -> accountV4Service.getRecommendedMovies(accountId, p) },
processor = { it.results }
)
val userLists: Flow<PagingData<Any>> = createPagingFlow(
fetcher = { p -> accountV4Service.getLists(accountId, p) },
processor = { it.results }
)
val userLists: Flow<PagingData<Any>>
get() = createPagingFlow(
fetcher = { p -> accountV4Service.getLists(accountId, p) },
processor = { it.results }
)
fun getPagingFlowFor(type: MediaViewType, accountTabType: AccountTabNavItem.AccountTabType): Flow<PagingData<Any>> {
return when (accountTabType) {
@@ -124,6 +135,15 @@ class AccountViewModel: ViewModel(), KoinComponent {
listService.updateList(listId, body)
}
suspend fun addToList(listId: Int, itemId: Int, itemType: MediaViewType) {
val body = AddToListBody(listOf(AddToListBodyItem(itemType, itemId)))
addToList(listId, body)
}
suspend fun addToList(listId: Int, body: AddToListBody) {
listService.addItemsToList(listId, body)
}
suspend fun addToFavourites(type: MediaViewType, itemId: Int, favourited: Boolean) {
val accountId = SessionManager.currentSession.value?.accountDetails?.value?.id ?: throw Exception("Session must not be null")
accountService.markAsFavorite(accountId, MarkAsFavoriteBody(type, itemId, favourited))

View File

@@ -244,4 +244,7 @@
<string name="network_disconnected_message">Network Disconnected</string>
<string name="network_api_rate_limit_reached">API Rate limit reached</string>
<string name="network_error_occurred">%1$d: An error occurred</string>
<string name="lists">lists</string>
<string name="no_lists_message">You don\'t have any lists yet</string>
<string name="list_count_label">%1$s (%2$d items)</string>
</resources>