diff --git a/UIViews/src/main/java/com/programmersbox/uiviews/BaseMainActivity.kt b/UIViews/src/main/java/com/programmersbox/uiviews/BaseMainActivity.kt index 85abcdec8..da0ff4a48 100644 --- a/UIViews/src/main/java/com/programmersbox/uiviews/BaseMainActivity.kt +++ b/UIViews/src/main/java/com/programmersbox/uiviews/BaseMainActivity.kt @@ -61,6 +61,9 @@ import androidx.compose.material.icons.filled.Notifications import androidx.compose.material.icons.filled.Search import androidx.compose.material.icons.filled.Settings import androidx.compose.material.icons.filled.Star +import androidx.compose.material.icons.outlined.BrowseGallery +import androidx.compose.material.icons.outlined.History +import androidx.compose.material.icons.outlined.Settings import androidx.compose.material3.Badge import androidx.compose.material3.BadgedBox import androidx.compose.material3.BottomAppBarScrollBehavior @@ -458,19 +461,19 @@ abstract class BaseMainActivity : AppCompatActivity() { ScreenBottomItem( screen = Screen.RecentScreen, - icon = Icons.Default.History, + icon = if (currentDestination.isTopLevelDestinationInHierarchy(Screen.RecentScreen)) Icons.Default.History else Icons.Outlined.History, label = R.string.recent ) if (showAllItem) { ScreenBottomItem( screen = Screen.AllScreen, - icon = Icons.Default.BrowseGallery, + icon = if (currentDestination.isTopLevelDestinationInHierarchy(Screen.AllScreen)) Icons.Default.BrowseGallery else Icons.Outlined.BrowseGallery, label = R.string.all ) } ScreenBottomItem( screen = Screen.Settings, - icon = Icons.Default.Settings, + icon = if (currentDestination.isTopLevelDestinationInHierarchy(Screen.Settings)) Icons.Default.Settings else Icons.Outlined.Settings, label = R.string.settings, badge = { if (updateCheck()) Badge { Text("") } } ) @@ -526,19 +529,19 @@ abstract class BaseMainActivity : AppCompatActivity() { ScreenBottomItem( screen = Screen.RecentScreen, - icon = Icons.Default.History, + icon = if (currentDestination.isTopLevelDestinationInHierarchy(Screen.RecentScreen)) Icons.Default.History else Icons.Outlined.History, label = R.string.recent ) if (showAllItem) { ScreenBottomItem( screen = Screen.AllScreen, - icon = Icons.Default.BrowseGallery, + icon = if (currentDestination.isTopLevelDestinationInHierarchy(Screen.AllScreen)) Icons.Default.BrowseGallery else Icons.Outlined.BrowseGallery, label = R.string.all ) } ScreenBottomItem( screen = Screen.Settings, - icon = Icons.Default.Settings, + icon = if (currentDestination.isTopLevelDestinationInHierarchy(Screen.Settings)) Icons.Default.Settings else Icons.Outlined.Settings, label = R.string.settings, badge = { if (updateCheck()) Badge { Text("") } } ) diff --git a/UIViews/src/main/java/com/programmersbox/uiviews/details/DetailsUtils.kt b/UIViews/src/main/java/com/programmersbox/uiviews/details/DetailsUtils.kt index cf9bcf824..b4d722b35 100644 --- a/UIViews/src/main/java/com/programmersbox/uiviews/details/DetailsUtils.kt +++ b/UIViews/src/main/java/com/programmersbox/uiviews/details/DetailsUtils.kt @@ -14,6 +14,7 @@ import androidx.compose.foundation.layout.WindowInsets import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.PlaylistAdd import androidx.compose.material.icons.automirrored.filled.Sort +import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.BookmarkRemove import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.Close @@ -34,7 +35,10 @@ import androidx.compose.material3.DrawerState import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.ExtendedFloatingActionButton +import androidx.compose.material3.FloatingActionButtonMenu +import androidx.compose.material3.FloatingActionButtonMenuItem import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.LocalContentColor @@ -42,16 +46,27 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text +import androidx.compose.material3.ToggleFloatingActionButton +import androidx.compose.material3.ToggleFloatingActionButtonDefaults.animateIcon +import androidx.compose.material3.animateFloatingActionButton import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.stateDescription +import androidx.compose.ui.semantics.traversalIndex import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavController @@ -396,4 +411,117 @@ fun DetailBottomBar( windowInsets = windowInsets, modifier = modifier ) +} + +@OptIn(ExperimentalMaterial3ExpressiveApi::class) +@Composable +fun DetailFloatingActionButtonMenu( + navController: NavController, + isVisible: Boolean, + onShowLists: () -> Unit, + info: InfoModel, + removeFromSaved: () -> Unit, + isSaved: Boolean, + canNotify: Boolean, + notifyAction: () -> Unit, + modifier: Modifier = Modifier, + isFavorite: Boolean, + onFavoriteClick: (Boolean) -> Unit, +) { + var fabMenuExpanded by rememberSaveable { mutableStateOf(false) } + + BackHandler(fabMenuExpanded) { fabMenuExpanded = false } + + FloatingActionButtonMenu( + expanded = fabMenuExpanded, + button = { + ToggleFloatingActionButton( + modifier = Modifier + .semantics { + traversalIndex = -1f + stateDescription = if (fabMenuExpanded) "Expanded" else "Collapsed" + contentDescription = "Toggle menu" + } + .animateFloatingActionButton( + visible = isVisible || fabMenuExpanded, + alignment = Alignment.BottomEnd, + scaleAnimationSpec = MaterialTheme.motionScheme.defaultEffectsSpec(), + alphaAnimationSpec = MaterialTheme.motionScheme.defaultEffectsSpec(), + ), + checked = fabMenuExpanded, + onCheckedChange = { fabMenuExpanded = !fabMenuExpanded } + ) { + val imageVector by remember { + derivedStateOf { + if (checkedProgress > 0.5f) Icons.Filled.Close else Icons.Filled.Add + } + } + Icon( + painter = rememberVectorPainter(imageVector), + contentDescription = null, + modifier = Modifier.animateIcon({ checkedProgress }) + ) + } + }, + modifier = modifier, + ) { + if (isSaved) { + FloatingActionButtonMenuItem( + onClick = { + fabMenuExpanded = false + removeFromSaved() + }, + icon = { Icon(Icons.Default.BookmarkRemove, contentDescription = null) }, + text = { Text(text = "Remove from Saved") }, + ) + } + + FloatingActionButtonMenuItem( + onClick = { + fabMenuExpanded = false + onShowLists() + }, + icon = { Icon(Icons.AutoMirrored.Filled.PlaylistAdd, contentDescription = null) }, + text = { Text(text = "Add to List") }, + ) + + FloatingActionButtonMenuItem( + onClick = { + fabMenuExpanded = false + navController.navigate(Screen.GlobalSearchScreen(info.title)) + }, + icon = { Icon(Icons.Default.Search, contentDescription = null) }, + text = { Text(text = "Global Search by Name") }, + ) + + FloatingActionButtonMenuItem( + onClick = { + fabMenuExpanded = false + onFavoriteClick(isFavorite) + }, + icon = { + Icon( + if (isFavorite) Icons.Default.Favorite else Icons.Default.FavoriteBorder, + contentDescription = null, + ) + }, + text = { Text(stringResource(if (isFavorite) R.string.removeFromFavorites else R.string.addToFavorites)) }, + ) + + if (isFavorite && LocalContext.current.shouldCheckFlow.collectAsStateWithLifecycle(initialValue = true).value) { + FloatingActionButtonMenuItem( + onClick = { + fabMenuExpanded = false + notifyAction() + }, + icon = { + Icon( + if (canNotify) Icons.Default.NotificationsActive else Icons.Default.NotificationsOff, + null + ) + }, + text = { Text(if (canNotify) "Check for updates" else "Do not check for updates") }, + ) + } + } } \ No newline at end of file diff --git a/UIViews/src/main/java/com/programmersbox/uiviews/details/DetailsView.kt b/UIViews/src/main/java/com/programmersbox/uiviews/details/DetailsView.kt index 98dfdfb0b..86da1cc3e 100644 --- a/UIViews/src/main/java/com/programmersbox/uiviews/details/DetailsView.kt +++ b/UIViews/src/main/java/com/programmersbox/uiviews/details/DetailsView.kt @@ -22,10 +22,10 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.ArrowDropDownCircle -import androidx.compose.material3.BottomAppBarDefaults import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.DrawerValue import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme @@ -50,9 +50,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.drawWithCache import androidx.compose.ui.draw.rotate -import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.painter.BitmapPainter import androidx.compose.ui.graphics.toArgb @@ -82,9 +80,11 @@ import com.programmersbox.uiviews.utils.NotificationLogo import com.programmersbox.uiviews.utils.components.OtakuScaffold import com.programmersbox.uiviews.utils.components.ToolTipWrapper import com.programmersbox.uiviews.utils.components.minus +import com.programmersbox.uiviews.utils.isScrollingUp import dev.chrisbanes.haze.HazeState import dev.chrisbanes.haze.haze import dev.chrisbanes.haze.hazeChild +import dev.chrisbanes.haze.materials.HazeMaterials import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.launch @@ -93,6 +93,7 @@ import me.tatarka.compose.collapsable.rememberCollapsableTopBehavior import my.nanihadesuka.compose.InternalLazyColumnScrollbar import my.nanihadesuka.compose.ScrollbarSettings +@OptIn(ExperimentalMaterial3ExpressiveApi::class) @ExperimentalComposeUiApi @ExperimentalMaterial3Api @ExperimentalAnimationApi @@ -124,12 +125,15 @@ fun DetailsView( val settings = LocalSettingsHandling.current val showBlur by settings.rememberShowBlur() - val isAmoledMode by settings.rememberIsAmoledMode() val hostState = remember { SnackbarHostState() } + val notificationManager = LocalContext.current.notificationManager + val listState = rememberLazyListState() + val fabVisible = listState.isScrollingUp()//remember { derivedStateOf { listState.firstVisibleItemIndex == 0 } } + val listDao = LocalCustomListDao.current val scope = rememberCoroutineScope() @@ -150,7 +154,6 @@ fun DetailsView( } } - val bottomAppBarScrollBehavior = BottomAppBarDefaults.exitAlwaysScrollBehavior() //val bottomAppBarScrollBehavior = LocalBottomAppBarScrollBehavior.current val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior() @@ -187,9 +190,6 @@ fun DetailsView( } } ) { - val backgroundColor = MaterialTheme.colorScheme.background - - val surface = MaterialTheme.colorScheme.surface val collapsableBehavior = rememberCollapsableTopBehavior( enterAlways = false @@ -203,7 +203,7 @@ fun DetailsView( InsetSmallTopAppBar( modifier = Modifier .zIndex(2f) - .let { if (showBlur) it.hazeChild(hazeState) { this.backgroundColor = surface } else it }, + .let { if (showBlur) it.hazeChild(hazeState, HazeMaterials.thin()) else it }, colors = TopAppBarDefaults.topAppBarColors( containerColor = if (showBlur) Color.Transparent @@ -299,13 +299,12 @@ fun DetailsView( ) } }, - bottomBar = { - val notificationManager = LocalContext.current.notificationManager - DetailBottomBar( + floatingActionButton = { + DetailFloatingActionButtonMenu( navController = navController, + isVisible = fabVisible, onShowLists = { showLists = true }, info = info, - customActions = {}, removeFromSaved = { scope.launch(Dispatchers.IO) { dao.getNotificationItemFlow(info.url) @@ -319,27 +318,9 @@ fun DetailsView( isSaved = isSaved, canNotify = canNotify, notifyAction = notifyAction, - containerColor = when { - showBlur -> Color.Transparent - isAmoledMode -> MaterialTheme.colorScheme.surface - else -> BottomAppBarDefaults.containerColor - }, isFavorite = isFavorite, onFavoriteClick = onFavoriteClick, - bottomAppBarScrollBehavior = bottomAppBarScrollBehavior, - modifier = Modifier - .padding(LocalNavHostPadding.current) - .drawWithCache { - onDrawBehind { - drawLine( - backgroundColor, - Offset(0f, 8f), - Offset(size.width, 8f), - 4 * density - ) - } - } - .let { if (showBlur) it.hazeChild(hazeState) { this.backgroundColor = surface } else it } + modifier = Modifier.padding(LocalNavHostPadding.current) ) }, snackbarHost = { @@ -362,7 +343,6 @@ fun DetailsView( modifier = Modifier .nestedScroll(collapsableBehavior.nestedScrollConnection) .nestedScroll(scrollBehavior.nestedScrollConnection) - .nestedScroll(bottomAppBarScrollBehavior.nestedScrollConnection) ) { p -> val modifiedPaddingValues = p - LocalNavHostPadding.current var descriptionVisibility by remember { mutableStateOf(false) } diff --git a/UIViews/src/main/java/com/programmersbox/uiviews/lists/OtakuListScreen.kt b/UIViews/src/main/java/com/programmersbox/uiviews/lists/OtakuListScreen.kt index 6947e0383..b16ce6586 100644 --- a/UIViews/src/main/java/com/programmersbox/uiviews/lists/OtakuListScreen.kt +++ b/UIViews/src/main/java/com/programmersbox/uiviews/lists/OtakuListScreen.kt @@ -105,7 +105,7 @@ fun OtakuListScreen( onSearchBarActiveChange = { viewModel.searchBarActive = it }, navigateBack = { viewModel.customItem = null - state.navigateBack() + scope.launch { state.navigateBack() } }, addSecurityItem = { scope.launch { listDao.updateBiometric(it, true) } @@ -117,7 +117,7 @@ fun OtakuListScreen( ) BackHandler { viewModel.customItem = null - state.navigateBack() + scope.launch { state.navigateBack() } } } else { NoDetailSelected() @@ -127,10 +127,12 @@ fun OtakuListScreen( } val navigate = { - if (showListDetail) - state.navigateTo(ListDetailPaneScaffoldRole.Detail) - else - state.navigateTo(ListDetailPaneScaffoldRole.Extra) + scope.launch { + if (showListDetail) + state.navigateTo(ListDetailPaneScaffoldRole.Detail) + else + state.navigateTo(ListDetailPaneScaffoldRole.Extra) + } } val biometricPrompt = rememberBiometricPrompt( diff --git a/UIViews/src/main/java/com/programmersbox/uiviews/settings/ExtensionListScreen.kt b/UIViews/src/main/java/com/programmersbox/uiviews/settings/ExtensionListScreen.kt index 5ca39cc8f..937b5c106 100644 --- a/UIViews/src/main/java/com/programmersbox/uiviews/settings/ExtensionListScreen.kt +++ b/UIViews/src/main/java/com/programmersbox/uiviews/settings/ExtensionListScreen.kt @@ -191,7 +191,7 @@ fun ExtensionList( ) BackHandler(navigator.canNavigateBack()) { - navigator.navigateBack() + scope.launch { navigator.navigateBack() } } OtakuScaffold( @@ -214,7 +214,7 @@ fun ExtensionList( if (navigator.currentDestination?.contentKey == 1) { ToolTipWrapper(info = { Text("View Installed Extensions") }) { - IconButton(onClick = { navigator.navigateBack() }) { + IconButton(onClick = { scope.launch { navigator.navigateBack() } }) { Icon( Icons.Default.SendTimeExtension, null, modifier = Modifier.scale(scaleX = -1f, scaleY = 1f) @@ -223,7 +223,7 @@ fun ExtensionList( } } else { ToolTipWrapper(info = { Text("View Remote Extensions") }) { - IconButton(onClick = { navigator.navigateTo(ListDetailPaneScaffoldRole.Detail, 1) }) { + IconButton(onClick = { scope.launch { navigator.navigateTo(ListDetailPaneScaffoldRole.Detail, 1) } }) { Icon(Icons.Default.SendTimeExtension, null) } } diff --git a/UIViews/src/main/java/com/programmersbox/uiviews/utils/ComposableUtils.kt b/UIViews/src/main/java/com/programmersbox/uiviews/utils/ComposableUtils.kt index 790d7e144..ea42a1a62 100644 --- a/UIViews/src/main/java/com/programmersbox/uiviews/utils/ComposableUtils.kt +++ b/UIViews/src/main/java/com/programmersbox/uiviews/utils/ComposableUtils.kt @@ -31,9 +31,10 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material3.BottomAppBarScrollBehavior import androidx.compose.material3.CenterAlignedTopAppBar -import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ColorScheme +import androidx.compose.material3.ContainedLoadingIndicator import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.LargeTopAppBar @@ -423,6 +424,7 @@ fun InsetLargeTopAppBar( ) } +@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun LoadingDialog( showLoadingDialog: Boolean, @@ -440,7 +442,7 @@ fun LoadingDialog( .background(MaterialTheme.colorScheme.surface, shape = RoundedCornerShape(28.0.dp)) ) { Column { - CircularProgressIndicator( + ContainedLoadingIndicator( modifier = Modifier.align(Alignment.CenterHorizontally) ) Text(text = stringResource(id = R.string.loading), Modifier.align(Alignment.CenterHorizontally)) diff --git a/UIViews/src/main/java/com/programmersbox/uiviews/utils/OtakuMaterialTheme.kt b/UIViews/src/main/java/com/programmersbox/uiviews/utils/OtakuMaterialTheme.kt index 5f77b9127..daccddc7d 100644 --- a/UIViews/src/main/java/com/programmersbox/uiviews/utils/OtakuMaterialTheme.kt +++ b/UIViews/src/main/java/com/programmersbox/uiviews/utils/OtakuMaterialTheme.kt @@ -6,11 +6,12 @@ import androidx.activity.SystemBarStyle import androidx.activity.enableEdgeToEdge import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.ColorScheme -import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.MaterialExpressiveTheme import androidx.compose.material3.darkColorScheme import androidx.compose.material3.dynamicDarkColorScheme import androidx.compose.material3.dynamicLightColorScheme -import androidx.compose.material3.lightColorScheme +import androidx.compose.material3.expressiveLightColorScheme import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.DisposableEffect @@ -50,7 +51,7 @@ import org.koin.androidx.compose.KoinAndroidContext import org.koin.compose.koinInject import org.koin.core.annotation.KoinExperimentalAPI -@OptIn(KoinExperimentalAPI::class, ExperimentalKamelApi::class) +@OptIn(KoinExperimentalAPI::class, ExperimentalKamelApi::class, ExperimentalMaterial3ExpressiveApi::class) @Composable fun OtakuMaterialTheme( navController: NavHostController, @@ -94,9 +95,9 @@ fun OtakuMaterialTheme( secondary = Color(0xff90CAF9) ) - else -> lightColorScheme( - primary = Color(0xff2196F3), - secondary = Color(0xff90CAF9) + else -> expressiveLightColorScheme( + //primary = Color(0xff2196F3), + //secondary = Color(0xff90CAF9) ) }.let { if (isAmoledMode && darkTheme) { @@ -120,7 +121,7 @@ fun OtakuMaterialTheme( ) } - MaterialTheme(colorScheme.animate()) { + MaterialExpressiveTheme(colorScheme.animate()) { CompositionLocalProvider( LocalActivity provides remember { context.findActivity() }, LocalNavController provides navController, diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b25b3a8fe..507ea0885 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -40,7 +40,7 @@ composeVersion = "1.8.0-alpha01" compose3Version = "1.3.0" compose3CommonVersion = "1.0.0-alpha01" compose3AdaptiveVersion = "1.1.0-alpha02" -composeBomVersion = "2024.09.02" +composeBomVersion = "2024.09.03" materialKolor = "2.0.0" firebaseKtx = "0.2.0" diff --git a/mangaworld/src/main/java/com/programmersbox/mangaworld/reader/compose/ReaderCompose.kt b/mangaworld/src/main/java/com/programmersbox/mangaworld/reader/compose/ReaderCompose.kt index 03f477d51..8b8fd4a26 100644 --- a/mangaworld/src/main/java/com/programmersbox/mangaworld/reader/compose/ReaderCompose.kt +++ b/mangaworld/src/main/java/com/programmersbox/mangaworld/reader/compose/ReaderCompose.kt @@ -68,6 +68,7 @@ import com.programmersbox.uiviews.utils.components.OtakuPullToRefreshBox import dev.chrisbanes.haze.HazeState import dev.chrisbanes.haze.haze import dev.chrisbanes.haze.hazeChild +import dev.chrisbanes.haze.materials.HazeMaterials import eu.wewox.pagecurl.ExperimentalPageCurlApi import eu.wewox.pagecurl.config.rememberPageCurlConfig import eu.wewox.pagecurl.page.PageCurl @@ -263,7 +264,7 @@ fun ReadView( playingStartAction = startAction, playingMiddleAction = middleAction, showBlur = showBlur, - modifier = if (showBlur) Modifier.hazeChild(hazeState) { backgroundColor = background } else Modifier + modifier = if (showBlur) Modifier.hazeChild(hazeState, style = HazeMaterials.thin()) else Modifier ) } }, @@ -280,7 +281,7 @@ fun ReadView( vm = readVm, showBlur = showBlur, isAmoledMode = isAmoledMode, - modifier = if (showBlur) Modifier.hazeChild(hazeState) { backgroundColor = background } else Modifier + modifier = if (showBlur) Modifier.hazeChild(hazeState, style = HazeMaterials.thin()) else Modifier ) } },