diff --git a/.idea/gradle.xml b/.idea/gradle.xml index 526b4c2..a2d7c21 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -13,7 +13,6 @@ - diff --git a/app/build.gradle b/app/build.gradle index 32f3a7c..5405db6 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -17,14 +17,14 @@ android { keyPassword signingProperties.getProperty("signing.release.password") } } - compileSdk 31 + compileSdk 33 defaultConfig { applicationId "de.mr_pine.xkcdfeed" minSdk 22 targetSdk 31 - versionCode 3 - versionName "1.1.1" + versionCode 4 + versionName "1.2.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { @@ -45,13 +45,12 @@ android { } kotlinOptions { jvmTarget = '1.8' - useIR = true } buildFeatures { compose true } composeOptions { - kotlinCompilerExtensionVersion compose_version + kotlinCompilerExtensionVersion "1.3.1" } packagingOptions { resources { @@ -62,17 +61,17 @@ android { dependencies { - implementation 'androidx.core:core-ktx:1.7.0' - implementation 'androidx.appcompat:appcompat:1.4.1' - implementation 'com.google.android.material:material:1.5.0' + implementation 'androidx.core:core-ktx:1.8.0' + implementation 'androidx.appcompat:appcompat:1.5.0' + implementation 'com.google.android.material:material:1.6.1' implementation "androidx.compose.ui:ui:$compose_version" implementation "androidx.compose.ui:ui-util:$compose_version" implementation "androidx.compose.material:material:$compose_version" implementation "androidx.compose.ui:ui-tooling-preview:$compose_version" - implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.1' + implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.5.1' - implementation 'androidx.activity:activity-compose:1.4.0' + implementation 'androidx.activity:activity-compose:1.5.1' implementation platform('com.google.firebase:firebase-bom:29.0.3') implementation 'com.google.firebase:firebase-auth-ktx' @@ -80,7 +79,7 @@ dependencies { implementation 'com.google.firebase:firebase-analytics-ktx' implementation 'com.google.firebase:firebase-firestore-ktx' - implementation 'com.google.android.gms:play-services-auth:20.1.0' + implementation 'com.google.android.gms:play-services-auth:20.3.0' testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' @@ -90,7 +89,7 @@ dependencies { implementation "com.google.accompanist:accompanist-pager:$accompanist_version" implementation "com.google.accompanist:accompanist-pager-indicators:$accompanist_version" implementation "com.google.accompanist:accompanist-placeholder-material:$accompanist_version" - implementation 'androidx.navigation:navigation-compose:2.5.0-alpha03' + implementation 'androidx.navigation:navigation-compose:2.5.1' implementation "androidx.compose.runtime:runtime-livedata:$compose_version" implementation "com.android.volley:volley:1.2.1" implementation "androidx.datastore:datastore-preferences:1.0.0" @@ -98,7 +97,7 @@ dependencies { implementation 'com.google.android.gms:play-services-oss-licenses:17.0.0' - implementation "io.coil-kt:coil-compose:2.0.0-rc02" + implementation "io.coil-kt:coil-compose:2.2.0" implementation 'de.mr-pine.utils:zoomables:1.1.1' } diff --git a/app/src/main/java/de/mr_pine/xkcdfeed/MainActivity.kt b/app/src/main/java/de/mr_pine/xkcdfeed/MainActivity.kt index e572eb0..bd0924e 100644 --- a/app/src/main/java/de/mr_pine/xkcdfeed/MainActivity.kt +++ b/app/src/main/java/de/mr_pine/xkcdfeed/MainActivity.kt @@ -10,9 +10,16 @@ import android.text.format.DateFormat import android.util.Log import androidx.activity.ComponentActivity import androidx.activity.compose.setContent +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.padding +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.Scaffold import androidx.compose.material.Text +import androidx.compose.material.rememberScaffoldState import androidx.compose.runtime.* +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.intPreferencesKey @@ -35,6 +42,7 @@ import de.mr_pine.xkcdfeed.composables.settings.Theme import de.mr_pine.xkcdfeed.composables.settings.settingsDataStore import de.mr_pine.xkcdfeed.composables.single.SingleViewContentStateful import de.mr_pine.xkcdfeed.ui.theme.XKCDFeedTheme +import kotlinx.coroutines.ObsoleteCoroutinesApi import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch @@ -52,13 +60,12 @@ class MainActivity : ComponentActivity() { private lateinit var navController: NavHostController - @OptIn( - ExperimentalPagerApi::class, - androidx.compose.ui.ExperimentalComposeUiApi::class, - androidx.compose.foundation.ExperimentalFoundationApi::class, - androidx.compose.material.ExperimentalMaterialApi::class, - kotlinx.coroutines.ObsoleteCoroutinesApi::class - ) + + @ExperimentalPagerApi + @ExperimentalComposeUiApi + @ExperimentalFoundationApi + @ExperimentalMaterialApi + @ObsoleteCoroutinesApi override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) Log.d(TAG, "onCreate: ${intent.data}") @@ -91,6 +98,8 @@ class MainActivity : ComponentActivity() { val singleComicViewModel: SingleComicViewModel = viewModel() + val scaffoldState = rememberScaffoldState() + val mainViewModel: MainViewModel = ViewModelProvider( this, MainViewModelFactory( @@ -100,6 +109,7 @@ class MainActivity : ComponentActivity() { navController::navigate, this.baseContext, loginViewModel, + scaffoldState.snackbarHostState, singleComicViewModel::addToComicCache, singleComicViewModel::setComicCacheImageLoaded ) @@ -124,82 +134,88 @@ class MainActivity : ComponentActivity() { } ) { - val rootUri = "xkcd.com" - NavHost(navController = navController, startDestination = "mainView") { - composable( - "mainView", deepLinks = listOf( - navDeepLink { uriPattern = rootUri }, - navDeepLink { uriPattern = "$rootUri/" }, - navDeepLink { uriPattern = "www.$rootUri" }, - navDeepLink { uriPattern = "www.$rootUri/" }, - ) - ) { - MainContent(mainViewModel) { - navController.navigate("singleView/${it.id}") - } - lastDestination = "mainView" - } - composable("test") { Text("hello") } - composable( - route = "singleView/{number}", - arguments = listOf(navArgument("number") { type = NavType.IntType }), - deepLinks = listOf( - navDeepLink { uriPattern = "$rootUri/{number}/" }, - navDeepLink { uriPattern = "$rootUri/{number}" }, - navDeepLink { uriPattern = "www.$rootUri/{number}/" }, - navDeepLink { uriPattern = "www.$rootUri/{number}" }, - ) - ) { backStackEntry -> - val comicNumber = backStackEntry.arguments?.getInt("number") - - if (lastDestination != "singleView") singleComicViewModel.currentComic = - comicNumber?.let { mainViewModel.loadComic(it) } - - SingleViewContentStateful( - mainViewModel = mainViewModel, - singleViewModel = singleComicViewModel, - navigateHome = { - Log.d(TAG, "onCreate: navigating home $backStackEntry, $navController") - navController.navigateUp() + Scaffold(scaffoldState = scaffoldState) { paddingValues -> + val rootUri = "xkcd.com" + NavHost(navController = navController, startDestination = "mainView", modifier = Modifier.padding(paddingValues)) { + composable( + "mainView", deepLinks = listOf( + navDeepLink { uriPattern = rootUri }, + navDeepLink { uriPattern = "$rootUri/" }, + navDeepLink { uriPattern = "www.$rootUri" }, + navDeepLink { uriPattern = "www.$rootUri/" }, + ) + ) { + MainContent(mainViewModel) { + navController.navigate("singleView/${it.id}") } - ) - lastDestination = "singleView" - } - composable( - route = "singleView" - ) { - if (singleComicViewModel.currentComic == null) { - singleComicViewModel.currentComic = - mainViewModel.loadComic(mainViewModel.latestComicNumber) + lastDestination = "mainView" } - SingleViewContentStateful( - mainViewModel = mainViewModel, - singleViewModel = singleComicViewModel, - navigateHome = navController::navigateUp - ) - - lastDestination = "singleView" - } - composable(route = "settings") { - SettingsComposable( - navigateBack = { navController.navigateUp() }, - loginViewModel = loginViewModel, - mainViewModel = mainViewModel, - context = this@MainActivity.baseContext, - onLoginChanged = { - scope.launch { - mainViewModel.initFavoriteList( - this@MainActivity, - true, - if (loginViewModel.signedIn) MainViewModel.ClearType.FIREBASE else MainViewModel.ClearType.LOCAL + composable("test") { Text("hello") } + composable( + route = "singleView/{number}", + arguments = listOf(navArgument("number") { type = NavType.IntType }), + deepLinks = listOf( + navDeepLink { uriPattern = "$rootUri/{number}/" }, + navDeepLink { uriPattern = "$rootUri/{number}" }, + navDeepLink { uriPattern = "www.$rootUri/{number}/" }, + navDeepLink { uriPattern = "www.$rootUri/{number}" }, + ) + ) { backStackEntry -> + val comicNumber = backStackEntry.arguments?.getInt("number") + + if (lastDestination != "singleView") singleComicViewModel.currentComic = + comicNumber?.let { mainViewModel.loadComic(it) } + + SingleViewContentStateful( + mainViewModel = mainViewModel, + singleViewModel = singleComicViewModel, + navigateHome = { + Log.d( + TAG, + "onCreate: navigating home $backStackEntry, $navController" ) + navController.navigateUp() } + ) + lastDestination = "singleView" + } + composable( + route = "singleView" + ) { + if (singleComicViewModel.currentComic == null) { + singleComicViewModel.currentComic = + mainViewModel.loadComic(mainViewModel.latestComicNumber) } - ) + SingleViewContentStateful( + mainViewModel = mainViewModel, + singleViewModel = singleComicViewModel, + navigateHome = navController::navigateUp + ) + + lastDestination = "singleView" + } + composable(route = "settings") { + SettingsComposable( + navigateBack = { navController.navigateUp() }, + loginViewModel = loginViewModel, + mainViewModel = mainViewModel, + context = this@MainActivity.baseContext, + onLoginChanged = { + scope.launch { + mainViewModel.initFavoriteList( + this@MainActivity, + true, + if (loginViewModel.signedIn) MainViewModel.ClearType.FIREBASE else MainViewModel.ClearType.LOCAL + ) + } + } + ) - lastDestination = "settings" + lastDestination = "settings" + } } } + } } } diff --git a/app/src/main/java/de/mr_pine/xkcdfeed/MainViewModel.kt b/app/src/main/java/de/mr_pine/xkcdfeed/MainViewModel.kt index 57c2095..404ca2f 100644 --- a/app/src/main/java/de/mr_pine/xkcdfeed/MainViewModel.kt +++ b/app/src/main/java/de/mr_pine/xkcdfeed/MainViewModel.kt @@ -4,9 +4,7 @@ import android.content.Context import android.content.Intent import android.util.Log import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material.ModalBottomSheetState -import androidx.compose.material.ModalBottomSheetValue +import androidx.compose.material.* import androidx.compose.runtime.* import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences @@ -33,6 +31,7 @@ class MainViewModel( val navigateTo: (String) -> Unit, private val context: Context, private val loginViewModel: LoginViewModel, + private val snackbarHostState: SnackbarHostState, private val addToComicCache: (XKCDComic, Boolean) -> Unit, private val setComicCacheImageLoaded: (Int, Boolean) -> Unit ) : ViewModel() { @@ -48,30 +47,36 @@ class MainViewModel( init { //implementation of http://www.graficaobscura.com/matrix/index.html //RGB invert - matrix = matrix.matrixMultiply(arrayOf( - floatArrayOf(-1f, 0f, 0f, 0f), - floatArrayOf(0f, -1f, 0f, 0f), - floatArrayOf(0f, 0f, -1f, 0f), - floatArrayOf(0f, 0f, 0f, -1f), - )) - matrix = matrix.matrixAdd(arrayOf( - floatArrayOf(0f, 0f, 0f, 0f), - floatArrayOf(0f, 0f, 0f, 0f), - floatArrayOf(0f, 0f, 0f, 0f), - floatArrayOf(255f, 255f, 255f, 0f), - )) + matrix = matrix.matrixMultiply( + arrayOf( + floatArrayOf(-1f, 0f, 0f, 0f), + floatArrayOf(0f, -1f, 0f, 0f), + floatArrayOf(0f, 0f, -1f, 0f), + floatArrayOf(0f, 0f, 0f, -1f), + ) + ) + matrix = matrix.matrixAdd( + arrayOf( + floatArrayOf(0f, 0f, 0f, 0f), + floatArrayOf(0f, 0f, 0f, 0f), + floatArrayOf(0f, 0f, 0f, 0f), + floatArrayOf(255f, 255f, 255f, 0f), + ) + ) //HSV 180 rotation - matrix = matrix.matrixMultiply(xRotation(cos = 1/ sqrt(2f), sin = 1/ sqrt(2f))) - matrix = matrix.matrixMultiply(yRotation(cos = sqrt(2/3f), sin = -sqrt(1/3f))) - val transformedWeights = arrayOf(floatArrayOf(wR, wG, wB)).matrixMultiply(matrix.cutTo(3,3)).matrixMultiply(matrix.cutTo(3,3)) - val shearX = (transformedWeights[0][0]/transformedWeights[0][2]) - val shearY = (transformedWeights[0][1]/transformedWeights[0][2]) + matrix = matrix.matrixMultiply(xRotation(cos = 1 / sqrt(2f), sin = 1 / sqrt(2f))) + matrix = matrix.matrixMultiply(yRotation(cos = sqrt(2 / 3f), sin = -sqrt(1 / 3f))) + val transformedWeights = + arrayOf(floatArrayOf(wR, wG, wB)).matrixMultiply(matrix.cutTo(3, 3)) + .matrixMultiply(matrix.cutTo(3, 3)) + val shearX = (transformedWeights[0][0] / transformedWeights[0][2]) + val shearY = (transformedWeights[0][1] / transformedWeights[0][2]) matrix = matrix.matrixMultiply(shearZ(shearX, shearY)) matrix = matrix.matrixMultiply(zRotation(PI.toFloat())) matrix = matrix.matrixMultiply(shearZ(-shearX, -shearY)) - matrix = matrix.matrixMultiply(yRotation(cos = sqrt(2/3f), sin = sqrt(1/3f))) - matrix = matrix.matrixMultiply(xRotation(cos = 1/ sqrt(2f), sin = -1/ sqrt(2f))) + matrix = matrix.matrixMultiply(yRotation(cos = sqrt(2 / 3f), sin = sqrt(1 / 3f))) + matrix = matrix.matrixMultiply(xRotation(cos = 1 / sqrt(2f), sin = -1 / sqrt(2f))) } @@ -106,13 +111,14 @@ class MainViewModel( var favoriteListInitialized = false var lastClearType = ClearType.UNDEFINED var favoriteList = mutableStateListOf() + var hideFavoritesList = mutableStateListOf() var cacheList = ConcurrentLinkedQueue() fun loadComic(id: Int): XKCDComic { val match = cacheList.indexOfFirst { it.id == id } return if (match == -1) { - val newComic = XKCDComic(id, viewModelScope, context) {} + val newComic = XKCDComic(id, viewModelScope, context) cacheList.add(newComic) newComic } else { @@ -131,14 +137,14 @@ class MainViewModel( if (clearType != null) lastClearType = clearType viewModelScope.launch { if (!loginViewModel.signedIn) { - userDataStore.data.first { preferences -> - // No type safety. - Log.d(TAG, "initFavoriteList: hi :)") - val stringList = preferences[favoriteListKey] ?: "[]" - favoriteList.addAll(generateListFromJSON(stringList)) - addFromFavoritesList(context, clear) - false - } + val preferences = userDataStore.data.first() + + // No type safety. + Log.d(TAG, "initFavoriteList: hi :)") + val stringList = preferences[favoriteListKey] ?: "[]" + favoriteList.addAll(generateListFromJSON(stringList)) + addFromFavoritesList(context, clear) + } else { val favoritesReference = db.collection("Users/${loginViewModel.user?.uid}/Favorites") @@ -230,9 +236,17 @@ class MainViewModel( } fun removeFavorite(xkcdComic: XKCDComic) { + hideFavoritesList.add(xkcdComic.id) viewModelScope.launch { - removeFromFavoriteList(xkcdComic.id) - removeFromFavoriteComicList(xkcdComic) + if (snackbarHostState.showSnackbar( + "Comic removed from favorites", + "UNDO" + ) != SnackbarResult.ActionPerformed + ) { + removeFromFavoriteList(xkcdComic.id) + removeFromFavoriteComicList(xkcdComic) + } + hideFavoritesList.remove(xkcdComic.id) } } // @@ -287,22 +301,6 @@ class MainViewModel( } else { addToFavoriteComicList(comic) } - /*XKCDComic.getComic( - number = number, - coroutineScope = viewModelScope, - context = context, - onImageLoaded = { - (if (to == Tab.LATEST) latestImagesLoadedMap else favoriteImagesLoadedMap)[number] = - true - setComicCacheImageLoaded(number, true) - }) { - if (to == Tab.LATEST) { - addToLatestComicList(it) - } else { - addToFavoriteComicList(it) - } - addToComicCache(it, false) - }*/ } // @@ -344,6 +342,7 @@ class MainViewModelFactory( private val navigateTo: (String) -> Unit, private val context: Context, private val loginViewModel: LoginViewModel, + private val snackbarHostState: SnackbarHostState, private val addToComicCache: (XKCDComic, Boolean) -> Unit, private val setComicCacheImageLoaded: (Int, Boolean) -> Unit ) : ViewModelProvider.Factory { @@ -358,6 +357,7 @@ class MainViewModelFactory( navigateTo, context, loginViewModel, + snackbarHostState, addToComicCache, setComicCacheImageLoaded ) as T diff --git a/app/src/main/java/de/mr_pine/xkcdfeed/XKCDComic.kt b/app/src/main/java/de/mr_pine/xkcdfeed/XKCDComic.kt index b7a7d14..f544abb 100644 --- a/app/src/main/java/de/mr_pine/xkcdfeed/XKCDComic.kt +++ b/app/src/main/java/de/mr_pine/xkcdfeed/XKCDComic.kt @@ -26,16 +26,13 @@ private const val TAG = "XKCDComic" class XKCDComic( val id: Int, - private val coroutineScope: CoroutineScope, - context: Context, - onImageLoaded: () -> Unit + coroutineScope: CoroutineScope, + context: Context ) { var title: String? by mutableStateOf(null) var pubDate: Calendar? by mutableStateOf(null) var imageURL by mutableStateOf("") var description: String? by mutableStateOf(null) - var bitmapLight: Bitmap? by mutableStateOf(null) - var bitmapDark: Bitmap? by mutableStateOf(null) private val TAG = "XKCDComic" diff --git a/app/src/main/java/de/mr_pine/xkcdfeed/composables/main/Card.kt b/app/src/main/java/de/mr_pine/xkcdfeed/composables/main/Card.kt index 0b15eff..d744184 100644 --- a/app/src/main/java/de/mr_pine/xkcdfeed/composables/main/Card.kt +++ b/app/src/main/java/de/mr_pine/xkcdfeed/composables/main/Card.kt @@ -1,5 +1,8 @@ package de.mr_pine.xkcdfeed.composables.main +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.expandVertically +import androidx.compose.animation.shrinkVertically import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image import androidx.compose.foundation.clickable @@ -30,6 +33,7 @@ import coil.size.Size import com.google.accompanist.placeholder.PlaceholderHighlight import com.google.accompanist.placeholder.material.placeholder import com.google.accompanist.placeholder.material.shimmer +import de.mr_pine.xkcdfeed.MainViewModel import de.mr_pine.xkcdfeed.XKCDComic import de.mr_pine.xkcdfeed.toColorMatrix import de.mr_pine.xkcdfeed.ui.theme.Amber500 @@ -70,12 +74,13 @@ val matrix = ColorMatrix(floatArrayOf( 0f, 0f, 0f, 1f, 0f ))*/ -@OptIn(ExperimentalMaterialApi::class, ExperimentalFoundationApi::class) +@OptIn(ExperimentalFoundationApi::class) @Composable fun ComicCard( xkcdComic: XKCDComic, dateFormat: DateFormat, favoriteList: List, + tab: MainViewModel.Tab, setFavorite: (XKCDComic) -> Unit, removeFavorite: (XKCDComic) -> Unit, invertMatrix: Array, @@ -85,100 +90,102 @@ fun ComicCard( val colorFilter = ColorFilter.colorMatrix(ColorMatrix(invertMatrix.toColorMatrix())) val scope = rememberCoroutineScope() - Card( - elevation = 5.dp, - backgroundColor = if (MaterialTheme.colors.isLight) Color.White else Color.Black, - shape = MaterialTheme.shapes.medium, - modifier = Modifier - .fillMaxWidth() - .clip(MaterialTheme.shapes.medium) - .combinedClickable( - onLongClick = { scope.launch { onLongPress(xkcdComic) } }, - onClick = { showSingle(xkcdComic) } - ) - ) { - MaterialTheme.colors.primarySurface - Column(modifier = Modifier.padding(top = 4.dp, start = 8.dp, end = 8.dp, bottom = 8.dp)) { - Row(verticalAlignment = Alignment.CenterVertically) { - Row(verticalAlignment = Alignment.Bottom, modifier = Modifier.padding(top = 3.dp)) { - Text( - text = xkcdComic.title ?: "I am a title :)", - fontWeight = FontWeight.Bold, - modifier = Modifier.placeholder(xkcdComic.title == null) - ) - Text( - text = "(${xkcdComic.id})", - modifier = Modifier - .padding(start = 4.dp, bottom = 1.5.dp) - .placeholder(xkcdComic.title == null), - fontStyle = FontStyle.Italic, - style = MaterialTheme.typography.caption - ) - } + AnimatedVisibility(visible = tab == MainViewModel.Tab.LATEST || favoriteList.contains(xkcdComic.id), enter = expandVertically(), exit = shrinkVertically()) { + Card( + elevation = 5.dp, + backgroundColor = if (MaterialTheme.colors.isLight) Color.White else Color.Black, + shape = MaterialTheme.shapes.medium, + modifier = Modifier + .fillMaxWidth() + .clip(MaterialTheme.shapes.medium) + .combinedClickable( + onLongClick = { scope.launch { onLongPress(xkcdComic) } }, + onClick = { showSingle(xkcdComic) } + ) + ) { + MaterialTheme.colors.primarySurface + Column(modifier = Modifier.padding(top = 4.dp, start = 8.dp, end = 8.dp, bottom = 8.dp)) { + Row(verticalAlignment = Alignment.CenterVertically) { + Row(verticalAlignment = Alignment.Bottom, modifier = Modifier.padding(top = 3.dp)) { + Text( + text = xkcdComic.title ?: "I am a title :)", + fontWeight = FontWeight.Bold, + modifier = Modifier.placeholder(xkcdComic.title == null) + ) + Text( + text = "(${xkcdComic.id})", + modifier = Modifier + .padding(start = 4.dp, bottom = 1.5.dp) + .placeholder(xkcdComic.title == null), + fontStyle = FontStyle.Italic, + style = MaterialTheme.typography.caption + ) + } - Column( - horizontalAlignment = Alignment.End, - modifier = Modifier.fillMaxWidth() - ) { - var icon = Icons.Outlined.StarOutline - var tint = Gray400 + Column( + horizontalAlignment = Alignment.End, + modifier = Modifier.fillMaxWidth() + ) { + var icon = Icons.Outlined.StarOutline + var tint = Gray400 - if (favoriteList.contains(xkcdComic.id)) { - icon = Icons.Filled.Star - tint = Amber500 + if (favoriteList.contains(xkcdComic.id)) { + icon = Icons.Filled.Star + tint = Amber500 + } + Icon( + icon, + "Star", + tint = tint, + modifier = Modifier.clip(MaterialTheme.shapes.small).clickable { + (if (!favoriteList.contains(xkcdComic.id)) setFavorite else removeFavorite)( + xkcdComic + ) + }) } - Icon( - icon, - "Star", - tint = tint, - modifier = Modifier.clickable { - (if (!favoriteList.contains(xkcdComic.id)) setFavorite else removeFavorite)( - xkcdComic - ) - }) } - } - Text( - text = xkcdComic.pubDate.let { if (it != null) dateFormat.format(it.time) else "00/00/0000" }, - fontStyle = FontStyle.Italic, - fontSize = 12.sp, - modifier = Modifier - .padding(bottom = 8.dp) - .placeholder(xkcdComic.pubDate == null) - ) - val painter = rememberAsyncImagePainter( - model = ImageRequest.Builder(LocalContext.current) - .data(xkcdComic.imageURL) - .size(Size.ORIGINAL) // Set the target size to load the image at. - .build() - ) - - Image( - painter = painter, - //bitmap = bitmap.asImageBitmap(), - contentDescription = "Image of the comic", - contentScale = ContentScale.Crop, - modifier = Modifier - .fillMaxWidth() - .then( - if (painter.state is AsyncImagePainter.State.Success) Modifier else Modifier.sizeIn( - minHeight = 200.dp + Text( + text = xkcdComic.pubDate.let { if (it != null) dateFormat.format(it.time) else "00/00/0000" }, + fontStyle = FontStyle.Italic, + fontSize = 12.sp, + modifier = Modifier + .padding(bottom = 8.dp) + .placeholder(xkcdComic.pubDate == null) + ) + val painter = rememberAsyncImagePainter( + model = ImageRequest.Builder(LocalContext.current) + .data(xkcdComic.imageURL) + .size(Size.ORIGINAL) // Set the target size to load the image at. + .build() + ) + + Image( + painter = painter, + //bitmap = bitmap.asImageBitmap(), + contentDescription = "Image of the comic", + contentScale = ContentScale.Crop, + modifier = Modifier + .fillMaxWidth() + .then( + if (painter.state is AsyncImagePainter.State.Success) Modifier else Modifier.sizeIn( + minHeight = 200.dp + ) ) - ) - .placeholder( - painter.state !is AsyncImagePainter.State.Success, - highlight = PlaceholderHighlight.shimmer() - ), - colorFilter = if (MaterialTheme.colors.isLight) null else colorFilter - ) - Text( - text = xkcdComic.description - ?: "I am a description text. If you see me, something isn't working as intended. Written at 2:36 a.m.", - style = MaterialTheme.typography.caption, - modifier = Modifier - .padding(top = 8.dp) - .placeholder(xkcdComic.description == null) - ) + .placeholder( + painter.state !is AsyncImagePainter.State.Success, + highlight = PlaceholderHighlight.shimmer() + ), + colorFilter = if (MaterialTheme.colors.isLight) null else colorFilter + ) + Text( + text = xkcdComic.description + ?: "I am a description text. If you see me, something isn't working as intended. Written at 2:36 a.m.", + style = MaterialTheme.typography.caption, + modifier = Modifier + .padding(top = 8.dp) + .placeholder(xkcdComic.description == null) + ) + } } } } diff --git a/app/src/main/java/de/mr_pine/xkcdfeed/composables/main/MainComposables.kt b/app/src/main/java/de/mr_pine/xkcdfeed/composables/main/MainComposables.kt index 534bf77..fa0944e 100644 --- a/app/src/main/java/de/mr_pine/xkcdfeed/composables/main/MainComposables.kt +++ b/app/src/main/java/de/mr_pine/xkcdfeed/composables/main/MainComposables.kt @@ -167,8 +167,8 @@ fun sheetContent( @Composable fun MainScaffold(viewModel: MainViewModel, showSingleComic: (XKCDComic) -> Unit) { - Scaffold(topBar = { TopAppBar { viewModel.navigateTo(it) } }) { - TabbedContent(viewModel, showSingleComic) + Scaffold(topBar = { TopAppBar { viewModel.navigateTo(it) } }) { paddingValues -> + TabbedContent(viewModel, showSingleComic, paddingValues) } } @@ -193,12 +193,12 @@ fun TopAppBar(navigate: (String) -> Unit) { // @ExperimentalPagerApi @Composable -fun TabbedContent(viewModel: MainViewModel, showSingleComic: (XKCDComic) -> Unit) { +fun TabbedContent(viewModel: MainViewModel, showSingleComic: (XKCDComic) -> Unit, paddingValues: PaddingValues) { val tabPagerState = rememberPagerState(0) val scope = rememberCoroutineScope() - Column() { + Column(modifier = Modifier.padding(paddingValues)) { TabRow( selectedTabIndex = tabPagerState.currentPage, indicator = { tabPositions -> @@ -271,11 +271,13 @@ fun Tab1(viewModel: MainViewModel, showSingleComic: (XKCDComic) -> Unit) { }) { Icon(Icons.Default.History, "History") } - }) { + }) { paddingValues -> ComicList( list = viewModel.latestComicsList, + currentTab = MainViewModel.Tab.LATEST, viewModel = viewModel, - showSingleComic = showSingleComic + showSingleComic = showSingleComic, + paddingValues = paddingValues ) } @@ -293,12 +295,14 @@ fun Tab2(viewModel: MainViewModel, scope: CoroutineScope, showSingleComic: (XKCD }) { Icon(Icons.Default.Shuffle, "Shuffle") } - }) { + }) { paddingValues -> ComicList( list = viewModel.favoriteComicsList, + currentTab = MainViewModel.Tab.FAVORITES, viewModel = viewModel, showSingleComic = showSingleComic, - state = viewModel.favListState + state = viewModel.favListState, + paddingValues = paddingValues ) } } @@ -308,11 +312,14 @@ fun Tab2(viewModel: MainViewModel, scope: CoroutineScope, showSingleComic: (XKCD fun ComicList( list: List, viewModel: MainViewModel, + currentTab: MainViewModel.Tab, showSingleComic: (XKCDComic) -> Unit, - state: LazyListState? = null + state: LazyListState? = null, + paddingValues: PaddingValues ) { LazyColumn( Modifier + .padding(paddingValues) .fillMaxSize() .background(MaterialTheme.colors.surface), contentPadding = PaddingValues(8.dp), @@ -329,7 +336,8 @@ fun ComicList( ComicCard( item, viewModel.dateFormat, - viewModel.favoriteList, + viewModel.favoriteList - viewModel.hideFavoritesList, + currentTab, viewModel::addFavorite, viewModel::removeFavorite, viewModel.matrix, diff --git a/app/src/main/java/de/mr_pine/xkcdfeed/composables/single/Single.kt b/app/src/main/java/de/mr_pine/xkcdfeed/composables/single/Single.kt index e05567b..c0632c8 100644 --- a/app/src/main/java/de/mr_pine/xkcdfeed/composables/single/Single.kt +++ b/app/src/main/java/de/mr_pine/xkcdfeed/composables/single/Single.kt @@ -221,221 +221,6 @@ fun SingleViewContent( } } -/*@ExperimentalFoundationApi -@Composable -fun ZoomableImage( - bitmap: ImageBitmap, - onSwipeLeft: (Int) -> Unit, - onSwipeRight: (Int) -> Unit, - getCurrentNumber: () -> Int -) { - val scope = rememberCoroutineScope() - - var scale by remember { mutableStateOf(1f) } - var rotation by remember { mutableStateOf(0f) } - var offset by remember { mutableStateOf(Offset.Zero) } - val state = rememberTransformableState { zoomChange, offsetChange, rotationChange -> - scale *= zoomChange - rotation += rotationChange - offset += offsetChange - } - - var dragOffset by remember { mutableStateOf(Offset.Zero) } - var imageCenter by remember { mutableStateOf(Offset.Zero) } - var transformOffset by remember { mutableStateOf(Offset.Zero) } - - fun onTransformGesture( - centroid: Offset, - pan: Offset, - zoom: Float, - transformRotation: Float - ) { - offset += pan - scale *= zoom - rotation += transformRotation - - val x0 = centroid.x - imageCenter.x - val y0 = centroid.y - imageCenter.y - - val hyp0 = sqrt(x0 * x0 + y0 * y0) - val hyp1 = zoom * hyp0 * (if (x0 > 0) { - 1f - } else { - -1f - }) - - val alpha0 = atan(y0 / x0) - - val alpha1 = alpha0 + (transformRotation * ((2 * PI) / 360)) - - val x1 = cos(alpha1) * hyp1 - val y1 = sin(alpha1) * hyp1 - - transformOffset = - centroid - (imageCenter - offset) - Offset(x1.toFloat(), y1.toFloat()) - offset = transformOffset - } - - Box( - Modifier - .pointerInput(Unit) { - detectTapGestures( - onDoubleTap = { - if (scale != 1f) { - scope.launch { - state.animateZoomBy(1 / scale) - } - offset = Offset.Zero - rotation = 0f - } else { - scope.launch { - state.animateZoomBy(2f) - } - } - } - ) - } - .pointerInput(Unit) { - val panZoomLock = true - forEachGesture { - awaitPointerEventScope { - var transformRotation = 0f - var zoom = 1f - var pan = Offset.Zero - var pastTouchSlop = false - val touchSlop = viewConfiguration.touchSlop - var lockedToPanZoom = false - var drag: PointerInputChange? - var overSlop = Offset.Zero - - val down = awaitFirstDown(requireUnconsumed = false) - - - var transformEventCounter = 0 - do { - val event = awaitPointerEvent() - val canceled = event.changes.fastAny { it.positionChangeConsumed() } - var relevant = true - if (event.changes.size > 1) { - if (!canceled) { - val zoomChange = event.calculateZoom() - val rotationChange = event.calculateRotation() - val panChange = event.calculatePan() - - if (!pastTouchSlop) { - zoom *= zoomChange - transformRotation += rotationChange - pan += panChange - - val centroidSize = - event.calculateCentroidSize(useCurrent = false) - val zoomMotion = abs(1 - zoom) * centroidSize - val rotationMotion = - abs(transformRotation * PI.toFloat() * centroidSize / 180f) - val panMotion = pan.getDistance() - - if (zoomMotion > touchSlop || - rotationMotion > touchSlop || - panMotion > touchSlop - ) { - pastTouchSlop = true - lockedToPanZoom = - panZoomLock && rotationMotion < touchSlop - } - } - - if (pastTouchSlop) { - val eventCentroid = - event.calculateCentroid(useCurrent = false) - val effectiveRotation = - if (lockedToPanZoom) 0f else rotationChange - if (effectiveRotation != 0f || - zoomChange != 1f || - panChange != Offset.Zero - ) { - onTransformGesture( - eventCentroid, - panChange, - zoomChange, - effectiveRotation - ) - } - event.changes.fastForEach { - if (it.positionChanged()) { - it.consumeAllChanges() - } - } - } - } - } else if (transformEventCounter > 3) relevant = false - transformEventCounter++ - } while (!canceled && event.changes.fastAny { it.pressed } && relevant) - - do { - val event = awaitPointerEvent() - drag = awaitTouchSlopOrCancellation(down.id) { change, over -> - change.consumePositionChange() - overSlop = over - } - } while (drag != null && !drag.positionChangeConsumed()) - if (drag != null) { - dragOffset = Offset.Zero - if (scale !in 0.92f..1.08f) { - offset += overSlop - } else { - dragOffset += overSlop - } - if (drag(drag.id) { - if (scale !in 0.92f..1.08f) { - offset += it.positionChange() - } else { - dragOffset += it.positionChange() - } - it.consumePositionChange() - } - ) { - if (scale in 0.92f..1.08f) { - val offsetX = dragOffset.x - if (offsetX > 300) { - onSwipeRight(getCurrentNumber()) - - } else if (offsetX < -300) { - onSwipeLeft(getCurrentNumber()) - } - } - } - } - } - } - } - ) { - Image( - bitmap = bitmap, - contentDescription = "Comic Image", - modifier = Modifier - .fillMaxSize() - .clip(RectangleShape) - .offset { IntOffset(offset.x.roundToInt(), offset.y.roundToInt()) } - .graphicsLayer( - scaleX = scale - 0.02f, - scaleY = scale - 0.02f, - rotationZ = rotation - ) - .onGloballyPositioned { coordinates -> - val localOffset = - Offset( - coordinates.size.width.toFloat() / 2, - coordinates.size.height.toFloat() / 2 - ) - val windowOffset = coordinates.localToWindow(localOffset) - imageCenter = coordinates.parentLayoutCoordinates?.windowToLocal(windowOffset) - ?: Offset.Zero - }, - contentScale = ContentScale.Fit - ) - } -}*/ - @Composable fun bottomSheetContent( @@ -515,7 +300,7 @@ fun bottomSheetContent( icon, "Star", tint = tint, - modifier = Modifier.clickable { + modifier = Modifier.clip(MaterialTheme.shapes.small).clickable { (if (!isFavorite) setFavorite else removeFavorite)( comic ) @@ -591,7 +376,7 @@ fun SingleViewContentStateful( if (currentComic != null) { SingleViewContent( comic = currentComic, - isFavorite = favoriteList.contains(currentComic.id), + isFavorite = favoriteList.contains(currentComic.id) && !mainViewModel.hideFavoritesList.contains(currentComic.id), dateFormat = mainViewModel.dateFormat, setFavorite = mainViewModel::addFavorite, removeFavorite = mainViewModel::removeFavorite, diff --git a/build.gradle b/build.gradle index 9958ac9..3c69164 100644 --- a/build.gradle +++ b/build.gradle @@ -1,17 +1,17 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { ext { - compose_version = '1.1.1' - accompanist_version = "0.23.1" + compose_version = '1.2.1' + accompanist_version = "0.25.1" } repositories { google() mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:7.1.2' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.10" - classpath 'com.google.gms:google-services:4.3.10' + classpath 'com.android.tools.build:gradle:7.2.2' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.7.10" + classpath 'com.google.gms:google-services:4.3.13' classpath 'com.google.android.gms:oss-licenses-plugin:0.10.5' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index cb5889b..227b9b2 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Mon Sep 27 22:34:46 CEST 2021 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME