some more account authentication stuff and title bar fixes

This commit is contained in:
Owen LeJeune
2022-06-28 15:27:02 -04:00
parent 58b740cfb0
commit 24f75f0211
14 changed files with 256 additions and 120 deletions

View File

@@ -4,12 +4,15 @@ import android.os.Bundle
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.compose.animation.rememberSplineBasedDecay import androidx.compose.animation.rememberSplineBasedDecay
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Scaffold import androidx.compose.material.Scaffold
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
@@ -27,6 +30,7 @@ import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument import androidx.navigation.navArgument
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import com.kieronquinn.monetcompat.app.MonetCompatActivity import com.kieronquinn.monetcompat.app.MonetCompatActivity
import com.owenlejeune.tvtime.extensions.WindowSizeClass import com.owenlejeune.tvtime.extensions.WindowSizeClass
import com.owenlejeune.tvtime.extensions.rememberWindowSizeClass import com.owenlejeune.tvtime.extensions.rememberWindowSizeClass
@@ -171,6 +175,9 @@ class MainActivity : MonetCompatActivity() {
NavigationBar { NavigationBar {
BottomNavItem.Items.forEach { item -> BottomNavItem.Items.forEach { item ->
NavigationBarItem( NavigationBarItem(
modifier = Modifier
.padding(4.dp)
.clip(RoundedCornerShape(24.dp)),
icon = { Icon(painter = painterResource(id = item.icon), contentDescription = null) }, icon = { Icon(painter = painterResource(id = item.icon), contentDescription = null) },
label = { Text(item.name) }, label = { Text(item.name) },
selected = currentRoute == item.route, selected = currentRoute == item.route,
@@ -191,8 +198,8 @@ class MainActivity : MonetCompatActivity() {
appBarTitle: MutableState<String>, appBarTitle: MutableState<String>,
item: BottomNavItem item: BottomNavItem
) { ) {
appBarTitle.value = item.name
navigateToRoute(navController, item.route) navigateToRoute(navController, item.route)
appBarTitle.value = item.name
} }
private fun navigateToRoute(navController: NavController, route: String) { private fun navigateToRoute(navController: NavController, route: String) {

View File

@@ -25,6 +25,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.google.accompanist.pager.* import com.google.accompanist.pager.*
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import com.kieronquinn.monetcompat.app.MonetCompatActivity import com.kieronquinn.monetcompat.app.MonetCompatActivity
import com.owenlejeune.tvtime.extensions.launchActivity import com.owenlejeune.tvtime.extensions.launchActivity
import com.owenlejeune.tvtime.preferences.AppPreferences import com.owenlejeune.tvtime.preferences.AppPreferences

View File

@@ -99,6 +99,9 @@ class TmdbClient: KoinComponent {
sessionIdParam = QueryParam("session_id", SessionManager.currentSession!!.sessionId) sessionIdParam = QueryParam("session_id", SessionManager.currentSession!!.sessionId)
} else if (preferences.authorizedSessionId.isNotEmpty()) { } else if (preferences.authorizedSessionId.isNotEmpty()) {
sessionIdParam = QueryParam("session_id", preferences.authorizedSessionId) sessionIdParam = QueryParam("session_id", preferences.authorizedSessionId)
} else if (preferences.authorizedSessionValues != null) {
val sessionId = preferences.authorizedSessionValues!!.sessionId
sessionIdParam = QueryParam("session_id", sessionId)
} }
} }
return sessionIdParam return sessionIdParam
@@ -113,15 +116,14 @@ class TmdbClient: KoinComponent {
builder.header("Authorization", "Bearer ${BuildConfig.TMDB_Api_v4Key}") builder.header("Authorization", "Bearer ${BuildConfig.TMDB_Api_v4Key}")
} else { } else {
builder.header("Authorization", "Bearer ${SessionManager.currentSession!!.accessToken}") builder.header("Authorization", "Bearer ${SessionManager.currentSession!!.accessToken}")
}
}
val locale = Locale.current val locale = Locale.current
val languageCode = "${locale.language}-${locale.region}" val languageCode = "${locale.language}-${locale.region}"
val languageParam = QueryParam("language", languageCode) val languageParam = QueryParam("language", languageCode)
val url = chain.request().url.newBuilder().addQueryParams(languageParam).build() val newUrl = url.newBuilder().addQueryParams(languageParam).build()
builder.url(url) builder.url(newUrl)
}
}
return chain.proceed(builder.build()) return chain.proceed(builder.build())
} }

View File

@@ -12,28 +12,28 @@ import retrofit2.http.Path
interface AccountV4Api { interface AccountV4Api {
@GET("/account/{account_id}/lists") @GET("account/{account_id}/lists")
suspend fun getLists(@Path("account_id") accountId: String, page: Int = 1): Response<V4AccountResponse<V4AccountList>> suspend fun getLists(@Path("account_id") accountId: String, page: Int = 1): Response<V4AccountResponse<V4AccountList>>
@GET("/account/{account_id}/movie/favorites") @GET("account/{account_id}/movie/favorites")
suspend fun getFavoriteMovies(@Path("account_id") accountId: String, page: Int = 1): Response<V4AccountResponse<FavoriteMovie>> suspend fun getFavoriteMovies(@Path("account_id") accountId: String, page: Int = 1): Response<V4AccountResponse<FavoriteMovie>>
@GET("/account/{account_id}/tv/favorites") @GET("account/{account_id}/tv/favorites")
suspend fun getFavoriteTvShows(@Path("account_id") accountId: String, page: Int = 1): Response<V4AccountResponse<FavoriteTvSeries>> suspend fun getFavoriteTvShows(@Path("account_id") accountId: String, page: Int = 1): Response<V4AccountResponse<FavoriteTvSeries>>
@GET("/account/{account_id}/movie/recommendations") @GET("account/{account_id}/movie/recommendations")
suspend fun getMovieRecommendations(@Path("account_id") accountId: String, page: Int = 1): Response<V4AccountResponse<FavoriteMovie>> suspend fun getMovieRecommendations(@Path("account_id") accountId: String, page: Int = 1): Response<V4AccountResponse<FavoriteMovie>>
@GET("/account/{account_id}/tv/recommendations") @GET("account/{account_id}/tv/recommendations")
suspend fun getTvShowRecommendations(@Path("account_id") accountId: String, page: Int = 1): Response<V4AccountResponse<FavoriteTvSeries>> suspend fun getTvShowRecommendations(@Path("account_id") accountId: String, page: Int = 1): Response<V4AccountResponse<FavoriteTvSeries>>
@GET("/account/{account_id}/movie/watchlist") @GET("account/{account_id}/movie/watchlist")
suspend fun getMovieWatchlist(@Path("account_id") accountId: String, page: Int = 1): Response<V4AccountResponse<FavoriteMovie>> suspend fun getMovieWatchlist(@Path("account_id") accountId: String, page: Int = 1): Response<V4AccountResponse<FavoriteMovie>>
@GET("/account/{account_id}/tv/watchlist") @GET("account/{account_id}/tv/watchlist")
suspend fun getTvShowWatchlist(@Path("account_id") accountId: String, page: Int = 1): Response<V4AccountResponse<FavoriteTvSeries>> suspend fun getTvShowWatchlist(@Path("account_id") accountId: String, page: Int = 1): Response<V4AccountResponse<FavoriteTvSeries>>
@GET("/account/{account_id}/movie/rated") @GET("account/{account_id}/movie/rated")
suspend fun getRatedMovies(@Path("account_id") accountId: String, page: Int = 1): Response<V4AccountResponse<V4RatedMovie>> suspend fun getRatedMovies(@Path("account_id") accountId: String, page: Int = 1): Response<V4AccountResponse<V4RatedMovie>>
@GET("account/{account_id}/tv/rated") @GET("account/{account_id}/tv/rated")

View File

@@ -3,18 +3,21 @@ package com.owenlejeune.tvtime.api.tmdb.api.v4
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.StatusResponse import com.owenlejeune.tvtime.api.tmdb.api.v3.model.StatusResponse
import com.owenlejeune.tvtime.api.tmdb.api.v4.model.* import com.owenlejeune.tvtime.api.tmdb.api.v4.model.*
import retrofit2.Response import retrofit2.Response
import retrofit2.http.Body
import retrofit2.http.DELETE import retrofit2.http.DELETE
import retrofit2.http.HTTP
import retrofit2.http.POST import retrofit2.http.POST
interface AuthenticationV4Api { interface AuthenticationV4Api {
@POST("auth/request_token") @POST("auth/request_token")
suspend fun createRequestToken(body: AuthRequestBody): Response<AuthResponse> suspend fun createRequestToken(@Body body: AuthRequestBody): Response<AuthResponse>
@POST("auth/access_token") @POST("auth/access_token")
suspend fun createAccessToken(body: AuthAccessBody): Response<AccessResponse> suspend fun createAccessToken(@Body body: AuthAccessBody): Response<AccessResponse>
@DELETE("auth/access_token") // @DELETE("auth/access_token")
suspend fun deleteAccessToken(body: AuthDeleteBody): Response<StatusResponse> @HTTP(method = "DELETE", path = "auth/access_token", hasBody = true)
suspend fun deleteAccessToken(@Body body: AuthDeleteBody): Response<StatusResponse>
} }

View File

@@ -2,8 +2,10 @@ package com.owenlejeune.tvtime.preferences
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import com.google.gson.Gson
import com.kieronquinn.monetcompat.core.MonetCompat import com.kieronquinn.monetcompat.core.MonetCompat
import com.owenlejeune.tvtime.BuildConfig import com.owenlejeune.tvtime.BuildConfig
import com.owenlejeune.tvtime.utils.SessionManager
class AppPreferences(context: Context) { class AppPreferences(context: Context) {
@@ -15,11 +17,13 @@ class AppPreferences(context: Context) {
private val HIDE_TITLE = "hide_title" private val HIDE_TITLE = "hide_title"
private val GUEST_SESSION = "guest_session_id" private val GUEST_SESSION = "guest_session_id"
private val AUTHORIZED_SESSION = "authorized_session_id" private val AUTHORIZED_SESSION = "authorized_session_id"
private val AUTHORIZED_SESSION_VALUES = "authorized_session_values"
private val FIRST_LAUNCH = "first_launch" private val FIRST_LAUNCH = "first_launch"
private val FIRST_LAUNCH_TESTING = "first_launch_testing" private val FIRST_LAUNCH_TESTING = "first_launch_testing"
private val CHROMA_MULTIPLIER = "chroma_multiplier" private val CHROMA_MULTIPLIER = "chroma_multiplier"
private val USE_SYSTEM_COLORS = "use_system_colors" private val USE_SYSTEM_COLORS = "use_system_colors"
private val SELECTED_COLOR = "selected_color" private val SELECTED_COLOR = "selected_color"
private val USE_V4_API = "use_v4_api"
} }
private val preferences: SharedPreferences = context.getSharedPreferences(PREF_FILE, Context.MODE_PRIVATE) private val preferences: SharedPreferences = context.getSharedPreferences(PREF_FILE, Context.MODE_PRIVATE)
@@ -36,6 +40,12 @@ class AppPreferences(context: Context) {
get() = preferences.getString(GUEST_SESSION, "") ?: "" get() = preferences.getString(GUEST_SESSION, "") ?: ""
set(value) { preferences.put(GUEST_SESSION, value) } set(value) { preferences.put(GUEST_SESSION, value) }
var authorizedSessionValues: SessionManager.AuthorizedSessionValues?
get() = preferences.getString(AUTHORIZED_SESSION_VALUES, null)?.let {
Gson().fromJson(it, SessionManager.AuthorizedSessionValues::class.java)
}
set(value) { preferences.putNullableString(AUTHORIZED_SESSION_VALUES, value?.let { Gson().toJson(value) }) }
var authorizedSessionId: String var authorizedSessionId: String
get() = preferences.getString(AUTHORIZED_SESSION, "") ?: "" get() = preferences.getString(AUTHORIZED_SESSION, "") ?: ""
set(value) { preferences.put(AUTHORIZED_SESSION, value) } set(value) { preferences.put(AUTHORIZED_SESSION, value) }
@@ -64,6 +74,10 @@ class AppPreferences(context: Context) {
get() = preferences.getInt(SELECTED_COLOR, Int.MAX_VALUE) get() = preferences.getInt(SELECTED_COLOR, Int.MAX_VALUE)
set(value) { preferences.put(SELECTED_COLOR, value) } set(value) { preferences.put(SELECTED_COLOR, value) }
var useV4Api: Boolean
get() = preferences.getBoolean(USE_V4_API, true)
set(value) { preferences.put(USE_V4_API, value) }
private fun SharedPreferences.put(key: String, value: Any?) { private fun SharedPreferences.put(key: String, value: Any?) {
edit().apply { edit().apply {
when (value) { when (value) {
@@ -79,25 +93,10 @@ class AppPreferences(context: Context) {
} }
} }
private fun SharedPreferences.putNullableString(key: String, value: String?) {
edit().putString(key, value).apply()
}
class UnsupportedTypeError: Exception() class UnsupportedTypeError: Exception()
// private fun <T> SharedPreferences.get(key: String, java: Class<T>): T {
//
// }
//
// inner class PrefsMutableState<T>(val key: String): MutableState<T> {
// override var value: T
// get() = preferences.get(key, T::class.java)
// set(value) { preferences.put(key, value) }
//
// override fun component1(): T {
// TODO("Not yet implemented")
// }
//
// override fun component2(): (T) -> Unit {
// TODO("Not yet implemented")
// }
//
// }
} }

View File

@@ -20,7 +20,7 @@ sealed class BottomNavItem(stringRes: Int, val icon: Int, val route: String): Ko
Movies.route -> Movies Movies.route -> Movies
TV.route -> TV TV.route -> TV
Account.route -> Account Account.route -> Account
Favourites.route -> Favourites // Favourites.route -> Favourites
Settings.route -> Settings Settings.route -> Settings
else -> null else -> null
} }
@@ -30,7 +30,7 @@ sealed class BottomNavItem(stringRes: Int, val icon: Int, val route: String): Ko
object Movies: BottomNavItem(R.string.nav_movies_title, R.drawable.ic_movie, "movies_route") object Movies: BottomNavItem(R.string.nav_movies_title, R.drawable.ic_movie, "movies_route")
object TV: BottomNavItem(R.string.nav_tv_title, R.drawable.ic_tv, "tv_route") object TV: BottomNavItem(R.string.nav_tv_title, R.drawable.ic_tv, "tv_route")
object Account: BottomNavItem(R.string.nav_account_title, R.drawable.ic_person, "account_route") object Account: BottomNavItem(R.string.nav_account_title, R.drawable.ic_person, "account_route")
object Favourites: BottomNavItem(R.string.nav_favourites_title, R.drawable.ic_favorite, "favourites_route") // object Favourites: BottomNavItem(R.string.nav_favourites_title, R.drawable.ic_favorite, "favourites_route")
object Settings: BottomNavItem(R.string.nav_settings_title, R.drawable.ic_settings, "settings_route") object Settings: BottomNavItem(R.string.nav_settings_title, R.drawable.ic_settings, "settings_route")
object People: BottomNavItem(R.string.nav_people_title, R.drawable.ic_face, "people_route") object People: BottomNavItem(R.string.nav_people_title, R.drawable.ic_face, "people_route")

View File

@@ -63,26 +63,26 @@ fun MainNavGraph(
NavHost(navController = navController, startDestination = startDestination) { NavHost(navController = navController, startDestination = startDestination) {
composable(BottomNavItem.Movies.route) { composable(BottomNavItem.Movies.route) {
appBarActions.value = {} appBarActions.value = {}
MediaTab(appNavController = appNavController, mediaType = MediaViewType.MOVIE) MediaTab(appBarTitle = appBarTitle, appNavController = appNavController, mediaType = MediaViewType.MOVIE)
} }
composable(BottomNavItem.TV.route) { composable(BottomNavItem.TV.route) {
appBarActions.value = {} appBarActions.value = {}
MediaTab(appNavController = appNavController, mediaType = MediaViewType.TV) MediaTab(appBarTitle = appBarTitle, appNavController = appNavController, mediaType = MediaViewType.TV)
} }
composable(BottomNavItem.Account.route) { composable(BottomNavItem.Account.route) {
AccountTab(appBarTitle = appBarTitle, appNavController = appNavController, appBarActions = appBarActions) AccountTab(appBarTitle = appBarTitle, appNavController = appNavController, appBarActions = appBarActions)
} }
composable(BottomNavItem.People.route) { composable(BottomNavItem.People.route) {
appBarActions.value = {} appBarActions.value = {}
PeopleTab(appBarTitle, appNavController = appNavController) PeopleTab(appBarTitle = appBarTitle, appNavController = appNavController)
}
composable(BottomNavItem.Favourites.route) {
appBarActions.value = {}
FavouritesTab()
} }
// composable(BottomNavItem.Favourites.route) {
// appBarActions.value = {}
// FavouritesTab()
// }
composable(BottomNavItem.Settings.route) { composable(BottomNavItem.Settings.route) {
appBarActions.value = {} appBarActions.value = {}
SettingsTab() SettingsTab(appBarTitle = appBarTitle)
} }
} }
} }

View File

@@ -8,10 +8,15 @@ import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@@ -24,7 +29,9 @@ import com.google.accompanist.pager.PagerState
import com.google.accompanist.pager.rememberPagerState import com.google.accompanist.pager.rememberPagerState
import com.owenlejeune.tvtime.R import com.owenlejeune.tvtime.R
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.* import com.owenlejeune.tvtime.api.tmdb.api.v3.model.*
import com.owenlejeune.tvtime.ui.components.* import com.owenlejeune.tvtime.preferences.AppPreferences
import com.owenlejeune.tvtime.ui.components.RoundedLetterImage
import com.owenlejeune.tvtime.ui.components.SignInDialog
import com.owenlejeune.tvtime.ui.navigation.AccountTabNavItem import com.owenlejeune.tvtime.ui.navigation.AccountTabNavItem
import com.owenlejeune.tvtime.ui.navigation.ListFetchFun import com.owenlejeune.tvtime.ui.navigation.ListFetchFun
import com.owenlejeune.tvtime.ui.navigation.MainNavItem import com.owenlejeune.tvtime.ui.navigation.MainNavItem
@@ -35,6 +42,8 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.koin.java.KoinJavaComponent
import org.koin.java.KoinJavaComponent.get
import kotlin.reflect.KClass import kotlin.reflect.KClass
private const val GUEST_SIGN_IN = "guest_sign_in" private const val GUEST_SIGN_IN = "guest_sign_in"
@@ -51,11 +60,12 @@ fun AccountTab(
) { ) {
val lastSelectedOption = remember { mutableStateOf("") } val lastSelectedOption = remember { mutableStateOf("") }
val lso = lastSelectedOption.value
if (SessionManager.isV4SignInInProgress) { if (SessionManager.isV4SignInInProgress) {
appBarTitle.value = stringResource(id = R.string.account_not_logged_in)
AccountLoadingView()
v4SignInPart2(lastSelectedOption) v4SignInPart2(lastSelectedOption)
} } else {
if (appBarTitle.value == stringResource(id = R.string.nav_account_title)) {
when (SessionManager.currentSession?.isAuthorized) { when (SessionManager.currentSession?.isAuthorized) {
false -> { false -> {
appBarTitle.value = appBarTitle.value =
@@ -75,12 +85,15 @@ fun AccountTab(
appBarTitle.value = stringResource(id = R.string.account_not_logged_in) appBarTitle.value = stringResource(id = R.string.account_not_logged_in)
} }
} }
}
appBarActions.value = { appBarActions.value = {
AccountDropdownMenu(session = SessionManager.currentSession, lastSelectedOption = lastSelectedOption) AccountDropdownMenu(
session = SessionManager.currentSession,
lastSelectedOption = lastSelectedOption
)
} }
if (!SessionManager.isV4SignInInProgress) {
SessionManager.currentSession?.let { session -> SessionManager.currentSession?.let { session ->
val tabs = if (session.isAuthorized) { val tabs = if (session.isAuthorized) {
AccountTabNavItem.AuthorizedItems AccountTabNavItem.AuthorizedItems
@@ -89,9 +102,13 @@ fun AccountTab(
} }
Column { Column {
when(session.isAuthorized) { when (session.isAuthorized) {
true -> { AuthorizedSessionIcon() } true -> {
false -> { GuestSessionIcon() } AuthorizedSessionIcon()
}
false -> {
GuestSessionIcon()
}
} }
val pagerState = rememberPagerState() val pagerState = rememberPagerState()
@@ -103,6 +120,20 @@ fun AccountTab(
) )
} }
} }
}
}
}
@Composable
private fun AccountLoadingView() {
Box(modifier = Modifier.fillMaxSize()) {
CircularProgressIndicator(
modifier = Modifier
.align(Alignment.Center)
.size(200.dp),
color = MaterialTheme.colorScheme.secondary
)
}
} }
private fun getAccountName(accountDetails: AccountDetails?): String { private fun getAccountName(accountDetails: AccountDetails?): String {
@@ -276,12 +307,17 @@ private fun AccountDropdownMenu(
@Composable @Composable
private fun AuthorizedSessionMenuItems( private fun AuthorizedSessionMenuItems(
expanded: MutableState<Boolean>, expanded: MutableState<Boolean>,
lastSelectedOption: MutableState<String> lastSelectedOption: MutableState<String>,
preferences: AppPreferences = get(AppPreferences::class.java)
) { ) {
DropdownMenuItem( DropdownMenuItem(
text = { Text(text = stringResource(id = R.string.action_sign_out)) }, text = { Text(text = stringResource(id = R.string.action_sign_out)) },
onClick = { onClick = {
if (preferences.useV4Api) {
signOutV4(lastSelectedOption)
} else {
signOut(lastSelectedOption) signOut(lastSelectedOption)
}
expanded.value = false expanded.value = false
} }
) )
@@ -320,7 +356,8 @@ private fun GuestSessionMenuItems(
@Composable @Composable
private fun NoSessionMenuItems( private fun NoSessionMenuItems(
expanded: MutableState<Boolean>, expanded: MutableState<Boolean>,
lastSelectedOption: MutableState<String> lastSelectedOption: MutableState<String>,
preferences: AppPreferences = get(AppPreferences::class.java)
) { ) {
val showSignInDialog = remember { mutableStateOf(false) } val showSignInDialog = remember { mutableStateOf(false) }
@@ -333,15 +370,18 @@ private fun NoSessionMenuItems(
} }
} }
if (!preferences.useV4Api) {
DropdownMenuItem( DropdownMenuItem(
text = { Text(text = stringResource(id = R.string.action_sign_in)) }, text = { Text(text = stringResource(id = R.string.action_sign_in)) },
onClick = { showSignInDialog.value = true } onClick = { showSignInDialog.value = true }
) )
// val context = LocalContext.current } else {
// DropdownMenuItem( val context = LocalContext.current
// text = { Text(text = stringResource(id = R.string.action_sign_in)) }, DropdownMenuItem(
// onClick = { v4SignInPart1(context) } text = { Text(text = stringResource(id = R.string.action_sign_in)) },
// ) onClick = { v4SignInPart1(context) }
)
}
DropdownMenuItem( DropdownMenuItem(
text = { Text(text = stringResource(id = R.string.action_sign_in_as_guest)) }, text = { Text(text = stringResource(id = R.string.action_sign_in_as_guest)) },
@@ -362,9 +402,11 @@ private fun v4SignInPart2(lastSelectedOption: MutableState<String>) {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
val signIn = SessionManager.signInWithV4Part2() val signIn = SessionManager.signInWithV4Part2()
if (signIn) { if (signIn) {
withContext(Dispatchers.Main) {
lastSelectedOption.value = NO_SESSION_SIGN_IN lastSelectedOption.value = NO_SESSION_SIGN_IN
} }
} }
}
} }
@Composable @Composable
@@ -419,11 +461,23 @@ private fun createGuestSession(lastSelectedOption: MutableState<String>) {
} }
private fun signOut(lastSelectedOption: MutableState<String>) { private fun signOut(lastSelectedOption: MutableState<String>) {
CoroutineScope(Dispatchers.IO).launch {
SessionManager.clearSession { isSuccessful -> SessionManager.clearSession { isSuccessful ->
if (isSuccessful) { if (isSuccessful) {
lastSelectedOption.value = SIGN_OUT lastSelectedOption.value = SIGN_OUT
} }
} }
}
}
private fun signOutV4(lastSelectedOption: MutableState<String>) {
CoroutineScope(Dispatchers.IO).launch {
SessionManager.clearSessionV4 { isSuccessful ->
if (isSuccessful) {
lastSelectedOption.value = SIGN_OUT
}
}
}
} }

View File

@@ -2,6 +2,8 @@ package com.owenlejeune.tvtime.ui.screens.main
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
@@ -9,6 +11,7 @@ import com.google.accompanist.pager.ExperimentalPagerApi
import com.google.accompanist.pager.HorizontalPager import com.google.accompanist.pager.HorizontalPager
import com.google.accompanist.pager.PagerState import com.google.accompanist.pager.PagerState
import com.google.accompanist.pager.rememberPagerState import com.google.accompanist.pager.rememberPagerState
import com.owenlejeune.tvtime.R
import com.owenlejeune.tvtime.api.tmdb.api.v3.HomePageService import com.owenlejeune.tvtime.api.tmdb.api.v3.HomePageService
import com.owenlejeune.tvtime.api.tmdb.api.v3.MoviesService import com.owenlejeune.tvtime.api.tmdb.api.v3.MoviesService
import com.owenlejeune.tvtime.api.tmdb.api.v3.TvService import com.owenlejeune.tvtime.api.tmdb.api.v3.TvService
@@ -24,7 +27,17 @@ import kotlinx.coroutines.withContext
@OptIn(ExperimentalPagerApi::class) @OptIn(ExperimentalPagerApi::class)
@Composable @Composable
fun MediaTab(appNavController: NavHostController, mediaType: MediaViewType) { fun MediaTab(
appBarTitle: MutableState<String>,
appNavController: NavHostController,
mediaType: MediaViewType
) {
appBarTitle.value = when (mediaType) {
MediaViewType.MOVIE -> stringResource(id = R.string.nav_movies_title)
MediaViewType.TV -> stringResource(id = R.string.nav_tv_title)
else -> ""
}
Column { Column {
val tabs = when (mediaType) { val tabs = when (mediaType) {
MediaViewType.MOVIE -> MediaTabNavItem.MovieItems MediaViewType.MOVIE -> MediaTabNavItem.MovieItems

View File

@@ -24,7 +24,7 @@ fun PeopleTab(
appBarTitle: MutableState<String>, appBarTitle: MutableState<String>,
appNavController: NavHostController appNavController: NavHostController
) { ) {
// appBarTitle.value = stringResource(id = R.string.nav_people_title) appBarTitle.value = stringResource(id = R.string.nav_people_title)
val service = PeopleService() val service = PeopleService()

View File

@@ -40,7 +40,12 @@ import kotlinx.coroutines.launch
import org.koin.java.KoinJavaComponent.get import org.koin.java.KoinJavaComponent.get
@Composable @Composable
fun SettingsTab(preferences: AppPreferences = get(AppPreferences::class.java)) { fun SettingsTab(
appBarTitle: MutableState<String>,
preferences: AppPreferences = get(AppPreferences::class.java)
) {
appBarTitle.value = stringResource(id = R.string.nav_settings_title)
val scrollState = rememberScrollState() val scrollState = rememberScrollState()
Column( Column(
@@ -238,6 +243,7 @@ private fun WallpaperPicker(
@Composable @Composable
private fun DebugOptions(preferences: AppPreferences = get(AppPreferences::class.java)) { private fun DebugOptions(preferences: AppPreferences = get(AppPreferences::class.java)) {
val context = LocalContext.current val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
val firstLaunchTesting = remember { mutableStateOf(preferences.firstLaunchTesting) } val firstLaunchTesting = remember { mutableStateOf(preferences.firstLaunchTesting) }
SwitchPreference( SwitchPreference(
@@ -285,12 +291,29 @@ private fun DebugOptions(preferences: AppPreferences = get(AppPreferences::class
.clickable( .clickable(
onClick = { onClick = {
preferences.guestSessionId = "" preferences.guestSessionId = ""
coroutineScope.launch {
SessionManager.clearSession { SessionManager.clearSession {
Toast Toast
.makeText(context, "Cleared session: $it", Toast.LENGTH_SHORT) .makeText(context, "Cleared session: $it", Toast.LENGTH_SHORT)
.show() .show()
} }
SessionManager.clearSessionV4 {
Toast
.makeText(context, "Cleared session v4: $it", Toast.LENGTH_SHORT)
.show()
}
}
} }
) )
) )
val useV4Api = remember { mutableStateOf(preferences.useV4Api) }
SwitchPreference(
titleText = "Use v4 API",
checkState = useV4Api.value,
onCheckedChange = { isChecked ->
useV4Api.value = isChecked
preferences.useV4Api = isChecked
}
)
} }

View File

@@ -6,6 +6,7 @@ import androidx.compose.material.MaterialTheme
import androidx.compose.material3.darkColorScheme import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import com.kieronquinn.monetcompat.core.MonetCompat import com.kieronquinn.monetcompat.core.MonetCompat
private val DarkColorPalette = darkColorScheme( private val DarkColorPalette = darkColorScheme(
@@ -127,7 +128,12 @@ fun TVTimeTheme(
isLight = !isDarkTheme isLight = !isDarkTheme
), ),
shapes = Shapes, shapes = Shapes,
content = content content = {
val systemUiController = rememberSystemUiController()
systemUiController.setSystemBarsColor(color = androidx.compose.material3.MaterialTheme.colorScheme.background)
content()
}
) )
} }
} }

View File

@@ -3,6 +3,8 @@ package com.owenlejeune.tvtime.utils
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.widget.Toast
import com.google.gson.annotations.SerializedName
import com.owenlejeune.tvtime.R import com.owenlejeune.tvtime.R
import com.owenlejeune.tvtime.api.tmdb.api.v3.AccountService import com.owenlejeune.tvtime.api.tmdb.api.v3.AccountService
import com.owenlejeune.tvtime.api.tmdb.api.v3.AuthenticationService import com.owenlejeune.tvtime.api.tmdb.api.v3.AuthenticationService
@@ -34,7 +36,13 @@ object SessionManager: KoinComponent {
private val authenticationService by lazy { TmdbClient().createAuthenticationService() } private val authenticationService by lazy { TmdbClient().createAuthenticationService() }
private val authenticationV4Service by lazy { TmdbClient().createV4AuthenticationService() } private val authenticationV4Service by lazy { TmdbClient().createV4AuthenticationService() }
fun clearSession(onResponse: (isSuccessful: Boolean) -> Unit) { class AuthorizedSessionValues(
@SerializedName("session_id") val sessionId: String,
@SerializedName("access_token") val accessToken: String,
@SerializedName("account_id") val accountId: String
)
suspend fun clearSession(onResponse: (isSuccessful: Boolean) -> Unit) {
currentSession?.let { session -> currentSession?.let { session ->
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
val deleteResponse = authenticationService.deleteSession(SessionBody(session.sessionId)) val deleteResponse = authenticationService.deleteSession(SessionBody(session.sessionId))
@@ -42,7 +50,8 @@ object SessionManager: KoinComponent {
if (deleteResponse.isSuccessful) { if (deleteResponse.isSuccessful) {
_currentSession = null _currentSession = null
preferences.guestSessionId = "" preferences.guestSessionId = ""
preferences.authorizedSessionId = "" preferences.authorizedSessionValues = null
// preferences.authorizedSessionId = ""
} }
onResponse(deleteResponse.isSuccessful) onResponse(deleteResponse.isSuccessful)
} }
@@ -53,12 +62,13 @@ object SessionManager: KoinComponent {
fun clearSessionV4(onResponse: (isSuccessful: Boolean) -> Unit) { fun clearSessionV4(onResponse: (isSuccessful: Boolean) -> Unit) {
currentSession?.let { session -> currentSession?.let { session ->
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
val deleteResponse = authenticationV4Service.deleteAccessToken(AuthDeleteBody(session.sessionId)) val deleteResponse = authenticationV4Service.deleteAccessToken(AuthDeleteBody(session.accessToken))
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
if (deleteResponse.isSuccessful) { if (deleteResponse.isSuccessful) {
_currentSession = null _currentSession = null
preferences.guestSessionId = "" preferences.guestSessionId = ""
preferences.authorizedSessionId = "" preferences.authorizedSessionValues = null
// preferences.authorizedSessionId = ""
} }
onResponse(deleteResponse.isSuccessful) onResponse(deleteResponse.isSuccessful)
} }
@@ -75,6 +85,16 @@ object SessionManager: KoinComponent {
val session = AuthorizedSession() val session = AuthorizedSession()
session.initialize() session.initialize()
_currentSession = session _currentSession = session
} else {
preferences.authorizedSessionValues?.let { values ->
val session = AuthorizedSession(
sessionId = values.sessionId,
accessToken = values.accessToken,
accountId = values.accountId
)
session.initialize()
_currentSession = session
}
} }
} }
@@ -104,6 +124,7 @@ object SessionManager: KoinComponent {
if (sr.isSuccess) { if (sr.isSuccess) {
preferences.authorizedSessionId = sr.sessionId preferences.authorizedSessionId = sr.sessionId
preferences.guestSessionId = "" preferences.guestSessionId = ""
preferences.authorizedSessionValues = null
_currentSession = AuthorizedSession() _currentSession = AuthorizedSession()
_currentSession?.initialize() _currentSession?.initialize()
return true return true
@@ -122,7 +143,7 @@ object SessionManager: KoinComponent {
isV4SignInInProgress = true isV4SignInInProgress = true
val service = AuthenticationV4Service() val service = AuthenticationV4Service()
val requestTokenResponse = service.createRequestToken(AuthRequestBody(redirect = "")) val requestTokenResponse = service.createRequestToken(AuthRequestBody(redirect = "app://tvtime.auth.return"))
if (requestTokenResponse.isSuccessful) { if (requestTokenResponse.isSuccessful) {
requestTokenResponse.body()?.let { ctr -> requestTokenResponse.body()?.let { ctr ->
_currentSession = InProgressSession(ctr.requestToken) _currentSession = InProgressSession(ctr.requestToken)
@@ -147,20 +168,23 @@ object SessionManager: KoinComponent {
val sessionResponse = authenticationService.createSessionFromV4Token(V4TokenBody(ar.accessToken)) val sessionResponse = authenticationService.createSessionFromV4Token(V4TokenBody(ar.accessToken))
if (sessionResponse.isSuccessful) { if (sessionResponse.isSuccessful) {
sessionResponse.body()?.let { sr -> sessionResponse.body()?.let { sr ->
preferences.authorizedSessionId = sr.sessionId preferences.authorizedSessionValues = AuthorizedSessionValues(
sessionId = sr.sessionId,
accountId = ar.accountId,
accessToken = ar.accessToken
)
preferences.authorizedSessionId = ""
preferences.guestSessionId = "" preferences.guestSessionId = ""
_currentSession = AuthorizedSession(accessToken = ar.accessToken, accountId = ar.accountId) _currentSession = AuthorizedSession(
sessionId = sr.sessionId,
accessToken = ar.accessToken,
accountId = ar.accountId
)
_currentSession?.initialize() _currentSession?.initialize()
isV4SignInInProgress = false isV4SignInInProgress = false
return true return true
} }
} }
// preferences.authorizedSessionId = ar.accessToken
// preferences.guestSessionId = ""
// _currentSession = AuthorizedSession()
// _currentSession?.initialize()
// isV4SignInInProgress = false
// return true
} }
} }
} }
@@ -275,7 +299,11 @@ object SessionManager: KoinComponent {
} }
private class AuthorizedSession(accessToken: String = "", accountId: String = ""): Session(preferences.authorizedSessionId, true, accessToken, accountId) { private class AuthorizedSession(
sessionId: String = preferences.authorizedSessionId,
accessToken: String = "",
accountId: String = ""
): Session(sessionId, true, accessToken, accountId) {
private val service by lazy { AccountService() } private val service by lazy { AccountService() }
override suspend fun initialize() { override suspend fun initialize() {
@@ -286,14 +314,14 @@ object SessionManager: KoinComponent {
if (changed.contains(Changed.AccountDetails)) { if (changed.contains(Changed.AccountDetails)) {
service.getAccountDetails().apply { service.getAccountDetails().apply {
if (isSuccessful) { if (isSuccessful) {
withContext(Dispatchers.Main) { // withContext(Dispatchers.Main) {
_accountDetails = body() ?: _accountDetails _accountDetails = body() ?: _accountDetails
accountDetails?.let { accountDetails?.let {
CoroutineScope(Dispatchers.IO).launch { // CoroutineScope(Dispatchers.IO).launch {
refreshWithAccountId(it.id, changed) refreshWithAccountId(it.id, changed)
// }
} }
} // }
}
} }
} }
} else if (accountDetails != null) { } else if (accountDetails != null) {