From 643e274b70e22ba1eb6bbbf5594bd31a47b5a6a4 Mon Sep 17 00:00:00 2001 From: Zakir Sheikh Date: Mon, 2 Dec 2024 10:53:39 +0530 Subject: [PATCH] [CHORE] Updated the translations of Spanish - Special thanks to @KeXxDumb for contributing the Spanish translations. [UPDATE] Updated the implementation of `Folders` screen. - It now utilizes a blur effect. [FIX] Fixed various places where ActionMenu had erroneous padding. - [FEAT] Added `Filters` to extensions.kt - [UPDATE] Updated the implementation of ListHeader - [FIX] The `lastModified` of Folder was returning time in seconds instead of milliseconds. - [FIX] When the view was in selection mode, the TopBar was becoming invisible; removed that logic. It now stays on. [REFACTOR] Renamed `MenuItem` to `Action` [FEAT] Properly mapped `es-r419` (Spanish Latin America) to `b+es+419` [UPDATE] Enhanced the layout of settings. - Replaced each settings summary and title with a single resource; this will make handling resources more robust. - Replaced the old settings layout with a new multi-form factor support layout. [CHORE] Copied translation of `Spanish` from `es-rES` to `es-r419` - This was because the translator mistakenly added his dialect in another one. [CHORE] Updated Gradle to `8.7.3` --- .idea/codeStyles/Project.xml | 1 + app/build.gradle.kts | 4 +- app/src/main/java/com/zs/gallery/Gallery.kt | 17 +- app/src/main/java/com/zs/gallery/bin/Trash.kt | 97 ++- .../main/java/com/zs/gallery/common/Common.kt | 38 + .../java/com/zs/gallery/common/extensions.kt | 111 ++- .../main/java/com/zs/gallery/files/Album.kt | 19 +- .../main/java/com/zs/gallery/files/Folder.kt | 21 +- .../java/com/zs/gallery/folders/Folders.kt | 335 +++++---- .../zs/gallery/folders/FoldersViewState.kt | 30 +- .../com/zs/gallery/impl/FoldersViewModel.kt | 76 +- .../java/com/zs/gallery/impl/Initializer.kt | 2 +- .../com/zs/gallery/impl/SettingsViewModel.kt | 131 +--- .../com/zs/gallery/impl/ViewerViewModel.kt | 18 +- .../java/com/zs/gallery/settings/Settings.kt | 683 ++++++++++++------ .../zs/gallery/settings/SettingsViewState.kt | 32 +- .../java/com/zs/gallery/viewer/Details.kt | 10 +- .../main/java/com/zs/gallery/viewer/Viewer.kt | 47 +- .../com/zs/gallery/viewer/ViewerViewState.kt | 10 +- app/src/main/res/values-b+es+419/strings.xml | 196 +++++ app/src/main/res/values-es-rES/strings.xml | 122 +++- app/src/main/res/values/strings.xml | 141 +++- crowdin.yml | 1 + .../com/zs/domain/store/MediaProviderImpl.kt | 2 +- .../main/java/com/zs/foundation/Headers.kt | 19 +- .../java/com/zs/foundation/menu/Action.kt | 71 ++ .../main/java/com/zs/foundation/menu/Menu.kt | 4 +- .../java/com/zs/foundation/menu/MenuItem.kt | 90 --- gradle/libs.versions.toml | 4 +- 29 files changed, 1432 insertions(+), 900 deletions(-) create mode 100644 app/src/main/java/com/zs/gallery/common/Common.kt create mode 100644 app/src/main/res/values-b+es+419/strings.xml create mode 100644 foundation/src/main/java/com/zs/foundation/menu/Action.kt delete mode 100644 foundation/src/main/java/com/zs/foundation/menu/MenuItem.kt diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml index 7643783..ab75be2 100644 --- a/.idea/codeStyles/Project.xml +++ b/.idea/codeStyles/Project.xml @@ -1,6 +1,7 @@ + diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f0ff47b..e5fee7d 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -14,8 +14,8 @@ android { applicationId = "com.googol.android.apps.photos" minSdk = 24 targetSdk = 35 - versionCode = 35 - versionName = "0.2.4-dev" + versionCode = 37 + versionName = "0.2.5-dev" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { diff --git a/app/src/main/java/com/zs/gallery/Gallery.kt b/app/src/main/java/com/zs/gallery/Gallery.kt index 6eb8c8f..ccc7723 100644 --- a/app/src/main/java/com/zs/gallery/Gallery.kt +++ b/app/src/main/java/com/zs/gallery/Gallery.kt @@ -64,6 +64,8 @@ import androidx.navigation.NavGraphBuilder import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.primex.core.MetroGreen +import com.primex.core.OrientRed import com.primex.core.plus import com.primex.core.textResource import com.primex.material2.Label @@ -134,7 +136,7 @@ private const val TAG = "Gallery" private val NAV_RAIL_MIN_WIDTH = 106.dp private val BOTTOM_NAV_MIN_HEIGHT = 56.dp -private val LightAccentColor = Color(0xFF514700) +private val LightAccentColor = Color.OrientRed private val DarkAccentColor = Color(0xFFD8A25E) /** @@ -314,8 +316,8 @@ private fun NavigationBar( val current by navController.current() val color = LocalContentColor.current val colors = NavigationItemDefaults.navigationItemColors( - selectedContentColor = color, - selectedBackgroundColor = color.copy(0.12f) + selectedContentColor = if (AppTheme.colors.isLight) AppTheme.colors.accent else color, + selectedBackgroundColor =if (AppTheme.colors.isLight) AppTheme.colors.accent.copy(alpha = 0.12f) else color.copy(alpha = 0.12f), ) val domain = current?.destination?.domain val facade = LocalSystemFacade.current @@ -546,10 +548,11 @@ fun Gallery( } ) + // Observe the state of the IMMERSE_VIEW setting // Observe the state of the IMMERSE_VIEW setting val immersiveView by activity.observeAsState(Settings.KEY_IMMERSIVE_VIEW) - val translucentBg by activity.observeAsState(Settings.KEY_TRANSPARENT_SYSTEM_BARS) - LaunchedEffect(immersiveView, style, isDark, translucentBg) { + val transparentSystemBars by activity.observeAsState(Settings.KEY_TRANSPARENT_SYSTEM_BARS) + LaunchedEffect(immersiveView, style, isDark, transparentSystemBars) { // Get the WindowInsetsController for managing system bars val window = activity.window val controller = WindowCompat.getInsetsController(window, window.decorView) @@ -570,11 +573,13 @@ fun Gallery( else -> !isDark // If not explicitly set, use the isDark setting } // Configure the system bars background color based on the current style settings + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) + return@LaunchedEffect // No supported from here. window.apply { val color = when (style.flagSystemBarBackground) { WindowStyle.FLAG_SYSTEM_BARS_BG_TRANSLUCENT -> Color(0x20000000).toArgb() // Translucent background WindowStyle.FLAG_SYSTEM_BARS_BG_TRANSPARENT -> Color.Transparent.toArgb() // Transparent background - else -> (if (translucentBg) Color(0x20000000) else Color.Transparent).toArgb()// automate using the setting + else -> (if (!transparentSystemBars) Color(0x20000000) else Color.Transparent).toArgb()// automate using the setting } // Set the status and navigation bar colors statusBarColor = color diff --git a/app/src/main/java/com/zs/gallery/bin/Trash.kt b/app/src/main/java/com/zs/gallery/bin/Trash.kt index d627d6c..70baa05 100644 --- a/app/src/main/java/com/zs/gallery/bin/Trash.kt +++ b/app/src/main/java/com/zs/gallery/bin/Trash.kt @@ -25,7 +25,6 @@ import android.annotation.SuppressLint import androidx.activity.compose.BackHandler import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.ExperimentalSharedTransitionApi -import androidx.compose.animation.animateContentSize import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.slideInVertically @@ -33,8 +32,10 @@ import androidx.compose.animation.slideOutVertically import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.lazy.grid.GridCells @@ -60,20 +61,19 @@ import com.primex.core.textResource import com.primex.material2.Button import com.primex.material2.IconButton import com.primex.material2.Label -import com.primex.material2.OutlinedButton -import com.primex.material2.TextButton import com.primex.material2.appbar.LargeTopAppBar import com.primex.material2.appbar.TopAppBarDefaults import com.primex.material2.appbar.TopAppBarScrollBehavior import com.zs.foundation.AppTheme +import com.zs.foundation.ContentPadding import com.zs.foundation.LocalWindowSize import com.zs.foundation.None import com.zs.foundation.VerticalDivider import com.zs.foundation.adaptive.TwoPane import com.zs.foundation.adaptive.VerticalTwoPaneStrategy import com.zs.foundation.adaptive.contentInsets -import com.zs.gallery.R import com.zs.foundation.menu.FloatingActionMenu +import com.zs.gallery.R import com.zs.gallery.common.LocalNavController import com.zs.gallery.common.emit import com.zs.gallery.common.items @@ -87,49 +87,44 @@ private fun TopAppBar( behavior: TopAppBarScrollBehavior? = null, insets: WindowInsets = WindowInsets.None ) { - AnimatedVisibility( - visible = !viewState.isInSelectionMode, - enter = slideInVertically() + fadeIn(), - exit = slideOutVertically() + fadeOut(), - modifier = Modifier.animateContentSize(), - content = { - LargeTopAppBar( - navigationIcon = { - val navController = LocalNavController.current - IconButton( - imageVector = Icons.AutoMirrored.Filled.ArrowBack, - onClick = navController::navigateUp - ) - }, - title = { Label(text = textResource(id = R.string.trash)) }, - scrollBehavior = behavior, - windowInsets = insets, - modifier = modifier, - style = TopAppBarDefaults.largeAppBarStyle( - containerColor = AppTheme.colors.background, - scrolledContainerColor = AppTheme.colors.background(elevation = 1.dp), - scrolledContentColor = AppTheme.colors.onBackground, - contentColor = AppTheme.colors.onBackground - ), - actions = { - val context = LocalContext.current - Button( - label = stringResource(R.string.restore), - onClick = { viewState.restoreAll(context.findActivity()) }, - colors = ButtonDefaults.buttonColors(backgroundColor = AppTheme.colors.background(2.dp)), - shape = CircleShape, - elevation = null, - modifier = Modifier.scale(0.9f) - ) - Button( - label = stringResource(R.string.empty_bin), - onClick = { viewState.empty(context.findActivity()) }, - colors = ButtonDefaults.buttonColors(backgroundColor = AppTheme.colors.background(2.dp)), - shape = CircleShape, - elevation = null, - modifier = Modifier.scale(0.9f) - ) - } + + LargeTopAppBar( + navigationIcon = { + val navController = LocalNavController.current + IconButton( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + onClick = navController::navigateUp + ) + }, + title = { Label(text = textResource(id = R.string.trash)) }, + scrollBehavior = behavior, + windowInsets = insets, + modifier = modifier, + style = TopAppBarDefaults.largeAppBarStyle( + containerColor = AppTheme.colors.background, + scrolledContainerColor = AppTheme.colors.background(elevation = 1.dp), + scrolledContentColor = AppTheme.colors.onBackground, + contentColor = AppTheme.colors.onBackground + ), + actions = { + val context = LocalContext.current + Button( + label = stringResource(R.string.restore), + onClick = { viewState.restoreAll(context.findActivity()) }, + colors = ButtonDefaults.buttonColors(backgroundColor = AppTheme.colors.background(2.dp)), + shape = CircleShape, + elevation = null, + modifier = Modifier.scale(0.9f), + enabled = !viewState.isInSelectionMode + ) + Button( + label = stringResource(R.string.empty_bin), + onClick = { viewState.empty(context.findActivity()) }, + colors = ButtonDefaults.buttonColors(backgroundColor = AppTheme.colors.background(2.dp)), + shape = CircleShape, + elevation = null, + modifier = Modifier.scale(0.9f), + enabled = !viewState.isInSelectionMode ) } ) @@ -157,7 +152,9 @@ fun Actions( val context = LocalContext.current IconButton( imageVector = Icons.Default.Restore, - onClick = { viewState.restore(context.findActivity()) }) + onClick = { viewState.restore(context.findActivity()) } + + ) IconButton( imageVector = Icons.Default.DeleteSweep, onClick = { viewState.delete(context.findActivity()) }) @@ -194,7 +191,7 @@ fun Trash(viewState: TrashViewState) { visible = viewState.isInSelectionMode, enter = fadeIn() + slideInVertically(), exit = fadeOut() + slideOutVertically(), - modifier = Modifier.padding(navInsets), + modifier = Modifier.padding(navInsets).navigationBarsPadding().padding(bottom = ContentPadding.medium), content = { Actions(viewState = viewState) } @@ -209,7 +206,7 @@ fun Trash(viewState: TrashViewState) { columns = GridCells.Adaptive(MIN_TILE_SIZE * multiplier), horizontalArrangement = GridItemsArrangement, verticalArrangement = GridItemsArrangement, - contentPadding = WindowInsets.contentInsets + navInsets, + contentPadding = WindowInsets.contentInsets + navInsets + PaddingValues(horizontal = ContentPadding.medium), content = { // emit the state; val data = emit(values) ?: return@LazyVerticalGrid diff --git a/app/src/main/java/com/zs/gallery/common/Common.kt b/app/src/main/java/com/zs/gallery/common/Common.kt new file mode 100644 index 0000000..3a85c73 --- /dev/null +++ b/app/src/main/java/com/zs/gallery/common/Common.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2024 Zakir Sheikh + * + * Created by Zakir Sheikh on 02-12-2024. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.zs.gallery.common + +import com.zs.foundation.menu.Action + +private const val TAG = "Common" + +/** + * Represents a sorting order and associated grouping or ordering action. + * + * @property first Specifies whether the sorting is ascending or descending. + * @property second Specifies the action to group by or order by. + */ +typealias Filter = Pair + +/** + * Represents a mapping from a string key to a list of items of type T. + * + * @param T The type of items in the list. + */ +typealias Mapped = Map> \ No newline at end of file diff --git a/app/src/main/java/com/zs/gallery/common/extensions.kt b/app/src/main/java/com/zs/gallery/common/extensions.kt index 0c0f5ed..ea1c185 100644 --- a/app/src/main/java/com/zs/gallery/common/extensions.kt +++ b/app/src/main/java/com/zs/gallery/common/extensions.kt @@ -16,26 +16,44 @@ * limitations under the License. */ +@file:OptIn(ExperimentalMaterialApi::class) + package com.zs.gallery.common import android.content.pm.PackageManager import android.os.Build import androidx.annotation.RawRes +import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.Image +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.grid.GridItemSpan import androidx.compose.foundation.lazy.grid.LazyGridItemSpanScope import androidx.compose.foundation.lazy.grid.LazyGridScope +import androidx.compose.material.Chip +import androidx.compose.material.ChipDefaults import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.FilterChip +import androidx.compose.material.Icon import androidx.compose.material.SelectableChipColors +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.outlined.Sort import androidx.compose.runtime.Composable import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.rotate import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp import androidx.lifecycle.SavedStateHandle import androidx.navigation.NavDestination import androidx.navigation.NavHostController import androidx.navigation.compose.currentBackStackEntryAsState import com.primex.core.composableOrNull +import com.primex.core.fadeEdge import com.primex.material2.Label import com.primex.material2.Text import com.zs.foundation.AppTheme @@ -43,6 +61,12 @@ import com.zs.foundation.adaptive.BottomNavItem import com.zs.foundation.adaptive.NavRailItem import com.zs.foundation.adaptive.NavigationItemDefaults import com.zs.foundation.lottieAnimationPainter +import com.zs.foundation.menu.Action + +import androidx.compose.foundation.layout.PaddingValues as Padding +import androidx.compose.foundation.rememberScrollState as ScrollState +import com.primex.core.textResource as stringResource +import com.zs.foundation.ContentPadding as CP private const val TAG = "extensions" @@ -184,4 +208,89 @@ inline fun NavItem( true -> NavRailItem(onClick, icon, label, modifier, checked, colors = colors) else -> BottomNavItem(onClick, icon, label, modifier, checked, colors = colors) } -} \ No newline at end of file +} + +private val ITEM_SPACING = Arrangement.spacedBy(CP.small) + + +/** + * Represents a [Row] of [Chip]s for ordering and filtering. + * + * @param current The currently selected filter. + * @param values The list of supported filter options. + * @param onRequest Callback function to be invoked when a filter option is selected. null + * represents ascending/descending toggle. + */ +// TODO - Migrate to LazyRow instead. +@Composable +fun Filters( + current: Filter, + values: List, + padding: Padding = AppTheme.padding.None, + onRequest: (order: Action?) -> Unit, + modifier: Modifier = Modifier +) { + // Early return if values are empty. + if (values.isEmpty()) return + // TODO - Migrate to LazyRow + val state = ScrollState() + Row( + modifier = modifier + .fillMaxWidth() + .padding(padding) + .fadeEdge(AppTheme.colors.background, state) + .horizontalScroll(state), + horizontalArrangement = ITEM_SPACING, + verticalAlignment = Alignment.CenterVertically, + content = { + // Chip for ascending/descending order + val (ascending, order) = current + val padding = Padding(vertical = 6.dp) + Chip( + onClick = { onRequest(null) }, + content = { + Icon( + Icons.AutoMirrored.Outlined.Sort, + contentDescription = "ascending", + modifier = Modifier.rotate(if (ascending) 0f else 180f) + ) + }, + colors = ChipDefaults.chipColors( + backgroundColor = AppTheme.colors.accent, + contentColor = AppTheme.colors.onAccent + ), + modifier = Modifier + .padding(end = CP.medium), + shape = AppTheme.shapes.compact + ) + // Rest of the chips for selecting filter options + val colors = ChipDefaults.filterChipColors( + backgroundColor = AppTheme.colors.background(0.5.dp), + selectedBackgroundColor = AppTheme.colors.background(2.dp), + selectedContentColor = AppTheme.colors.accent, + selectedLeadingIconColor = AppTheme.colors.accent + ) + + for (value in values) { + val selected = value == order + val label = stringResource(value.label) + FilterChip( + selected = selected, + onClick = { onRequest(value) }, + content = { + Label(label, modifier = Modifier.padding(padding)) + }, + leadingIcon = composableOrNull(value.icon != null){ + Icon(value.icon!!, contentDescription = label.toString()) + }, + colors = colors, + border = if (!selected) null else BorderStroke( + 0.5.dp, + AppTheme.colors.accent.copy(0.12f) + ), + shape = AppTheme.shapes.compact + ) + } + } + ) +} diff --git a/app/src/main/java/com/zs/gallery/files/Album.kt b/app/src/main/java/com/zs/gallery/files/Album.kt index 246d692..3beff20 100644 --- a/app/src/main/java/com/zs/gallery/files/Album.kt +++ b/app/src/main/java/com/zs/gallery/files/Album.kt @@ -32,6 +32,7 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.lazy.grid.GridCells @@ -147,18 +148,10 @@ fun Album(viewState: AlbumViewState) { modifier = Modifier .nestedScroll(behaviour.nestedScrollConnection), topBar = { - AnimatedVisibility( - visible = !viewState.isInSelectionMode, - enter = fadeIn() + slideInVertically(), - exit = fadeOut() + slideOutVertically(), - modifier = Modifier.animateContentSize(), - content = { - TopAppBar( - viewState, - behavior = behaviour, - insets = WindowInsets.statusBars, - ) - } + TopAppBar( + viewState, + behavior = behaviour, + insets = WindowInsets.statusBars, ) }, floatingActionButton = { @@ -166,7 +159,7 @@ fun Album(viewState: AlbumViewState) { visible = viewState.isInSelectionMode, enter = fadeIn() + slideInVertically(), exit = fadeOut() + slideOutVertically(), - modifier = Modifier.padding(navInsets), + modifier = Modifier.padding(navInsets).navigationBarsPadding(), content = { Actions(viewState) } diff --git a/app/src/main/java/com/zs/gallery/files/Folder.kt b/app/src/main/java/com/zs/gallery/files/Folder.kt index 6190aa2..09c90a6 100644 --- a/app/src/main/java/com/zs/gallery/files/Folder.kt +++ b/app/src/main/java/com/zs/gallery/files/Folder.kt @@ -34,6 +34,7 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.lazy.grid.GridCells @@ -65,6 +66,7 @@ import com.primex.material2.appbar.LargeTopAppBar import com.primex.material2.appbar.TopAppBarDefaults import com.primex.material2.appbar.TopAppBarScrollBehavior import com.zs.foundation.AppTheme +import com.zs.foundation.ContentPadding import com.zs.foundation.LocalWindowSize import com.zs.foundation.None import com.zs.foundation.VerticalDivider @@ -180,17 +182,10 @@ fun Folder( modifier = Modifier .nestedScroll(behaviour.nestedScrollConnection), topBar = { - AnimatedVisibility( - visible = !viewState.isInSelectionMode, - enter = fadeIn() + slideInVertically(), - exit = fadeOut() + slideOutVertically(), - content = { - TopAppBar( - viewState, - behavior = behaviour, - insets = WindowInsets.statusBars, - ) - } + TopAppBar( + viewState, + behavior = behaviour, + insets = WindowInsets.statusBars, ) }, floatingActionButton = { @@ -198,7 +193,7 @@ fun Folder( visible = viewState.isInSelectionMode, enter = fadeIn() + slideInVertically(), exit = fadeOut() + slideOutVertically(), - modifier = Modifier.padding(navInsets), + modifier = Modifier.padding(navInsets).navigationBarsPadding(), content = { Actions(state = viewState) } @@ -213,7 +208,7 @@ fun Folder( columns = GridCells.Adaptive(Settings.STANDARD_TILE_SIZE * multiplier), horizontalArrangement = GridItemsArrangement, verticalArrangement = GridItemsArrangement, - contentPadding = navInsets + PaddingValues(vertical = AppTheme.padding.normal), + contentPadding = navInsets + PaddingValues(vertical = AppTheme.padding.normal) + PaddingValues(horizontal = ContentPadding.medium), modifier = Modifier.padding(WindowInsets.contentInsets) ) { val data = emit(values) ?: return@LazyVerticalGrid diff --git a/app/src/main/java/com/zs/gallery/folders/Folders.kt b/app/src/main/java/com/zs/gallery/folders/Folders.kt index 5a0a7f6..2928b2b 100644 --- a/app/src/main/java/com/zs/gallery/folders/Folders.kt +++ b/app/src/main/java/com/zs/gallery/folders/Folders.kt @@ -21,196 +21,168 @@ package com.zs.gallery.folders import android.os.Build -import androidx.annotation.StringRes import androidx.compose.animation.ExperimentalSharedTransitionApi -import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.background +import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.RowScope -import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.sizeIn -import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.layout.statusBars +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyGridScope +import androidx.compose.foundation.lazy.grid.LazyGridState import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.items -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.Button -import androidx.compose.material.ButtonDefaults +import androidx.compose.foundation.lazy.grid.rememberLazyGridState +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Icon import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.outlined.Sort import androidx.compose.material.icons.filled.FolderCopy -import androidx.compose.material.icons.outlined.DateRange import androidx.compose.material.icons.outlined.HotelClass -import androidx.compose.material.icons.outlined.Memory import androidx.compose.material.icons.outlined.Recycling -import androidx.compose.material.icons.outlined.TextFields import androidx.compose.runtime.Composable +import androidx.compose.runtime.NonRestartableComposable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.rotate -import androidx.compose.ui.draw.scale -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.navigation.NavHostController -import com.primex.core.drawHorizontalDivider import com.primex.core.plus -import com.primex.core.textResource -import com.primex.material2.DropDownMenuItem -import com.primex.material2.IconButton import com.primex.material2.Label -import com.primex.material2.menu.DropDownMenu2 -import com.primex.material2.neumorphic.NeumorphicTopAppBar +import com.primex.material2.appbar.TopAppBarDefaults +import com.zs.domain.store.Folder import com.zs.foundation.AppTheme -import com.zs.foundation.ContentPadding +import com.zs.foundation.ListHeader +import com.zs.foundation.None import com.zs.foundation.adaptive.TwoPane import com.zs.foundation.adaptive.contentInsets -import com.zs.foundation.sharedElement +import com.zs.foundation.stickyHeader +import com.zs.foundation.thenIf import com.zs.gallery.R import com.zs.gallery.bin.RouteTrash +import com.zs.gallery.common.Filters import com.zs.gallery.common.LocalNavController +import com.zs.gallery.common.Mapped +import com.zs.gallery.common.Regular +import com.zs.gallery.common.dynamicBackdrop import com.zs.gallery.common.emit import com.zs.gallery.common.fullLineSpan import com.zs.gallery.common.preference import com.zs.gallery.files.RouteAlbum import com.zs.gallery.files.RouteFolder import com.zs.gallery.settings.Settings -import com.zs.gallery.viewer.RouteViewer +import dev.chrisbanes.haze.HazeState +import dev.chrisbanes.haze.HazeStyle +import androidx.compose.foundation.layout.PaddingValues as Padding +import androidx.compose.ui.graphics.Brush.Companion.verticalGradient as VerticalGradient +import com.zs.foundation.ContentPadding as CP +import com.zs.gallery.common.rememberHazeState as BackdropProvider +import dev.chrisbanes.haze.haze as observerBackdrop private const val TAG = "Folders" -@StringRes -fun fetchOrderTitle(order: Int): Int { - return when (order) { - FoldersViewState.ORDER_BY_NAME -> R.string.name - FoldersViewState.ORDER_BY_DATE_MODIFIED -> R.string.last_modified - FoldersViewState.ORDER_BY_SIZE -> R.string.size - else -> error("Oops invalid id passed $order") - } -} - -fun fetchOrderIcon(order: Int): ImageVector { - return when (order) { - FoldersViewState.ORDER_BY_NAME -> Icons.Outlined.TextFields - FoldersViewState.ORDER_BY_DATE_MODIFIED -> Icons.Outlined.DateRange - FoldersViewState.ORDER_BY_SIZE -> Icons.Outlined.Memory - else -> error("Oops invalid id passed $order") - } -} +private val FloatingTopBarShape = RoundedCornerShape(20) /** - * Constructs an order by menu. if [onRequestChange] == -1 then the menu is not shown. + * Represents a Top app bar for this screen. */ -context(RowScope) -@Suppress("NOTHING_TO_INLINE") @Composable -private inline fun Actions( - viewState: FoldersViewState +@NonRestartableComposable +private fun FloatingTopAppBar( + modifier: Modifier = Modifier, + backdropProvider: HazeState? = null, + insets: WindowInsets = WindowInsets.None, ) { - val ascending = viewState.ascending - val rotation by animateFloatAsState(targetValue = if (ascending) 180f else 0f) - IconButton( - imageVector = Icons.AutoMirrored.Outlined.Sort, - onClick = { viewState.ascending = !ascending }, - modifier = Modifier.rotate(rotation) - ) - // show order - var expanded by remember { mutableStateOf(false) } - Button( - onClick = { expanded = !expanded }, - contentPadding = PaddingValues(horizontal = 12.dp), - modifier = Modifier - .padding(end = AppTheme.padding.small) - .scale(0.90f), - shape = CircleShape, - elevation = null, - colors = ButtonDefaults.buttonColors( - backgroundColor = AppTheme.colors.background(elevation = 1.dp), - contentColor = AppTheme.colors.onBackground, - ), + val colors = AppTheme.colors + Box( + modifier = modifier + .fillMaxWidth() + .background( + VerticalGradient( + listOf( + colors.background(1.dp), + colors.background.copy(alpha = 0.5f), + Color.Transparent + ) + ) + ), content = { - val order = viewState.order - // icon - Icon( - painter = rememberVectorPainter(image = fetchOrderIcon(order = order)), - contentDescription = null, - modifier = Modifier.padding(end = ButtonDefaults.IconSpacing) - ) - - // label - Label(text = stringResource(id = fetchOrderTitle(order))) - - DropDownMenu2( - expanded = expanded, - onDismissRequest = { expanded = false }, - shape = AppTheme.shapes.compact, - modifier = Modifier.sizeIn(minWidth = 180.dp), - content = { - // Sort by size - repeat(3) { - DropDownMenuItem( - title = textResource(id = fetchOrderTitle(it)), - icon = rememberVectorPainter(image = fetchOrderIcon(it)), - onClick = { viewState.order = it; expanded = false }, - enabled = order != it - ) - } - } + com.primex.material2.appbar.TopAppBar( + navigationIcon = { + Icon( + imageVector = Icons.Default.FolderCopy, + contentDescription = null, + modifier = Modifier.padding(horizontal = CP.medium) + ) + }, + title = { + Label( + text = stringResource(id = R.string.folders), + style = AppTheme.typography.titleMedium, + fontWeight = FontWeight.Bold + ) + }, + windowInsets = WindowInsets.None, + style = TopAppBarDefaults.topAppBarStyle( + containerColor = Color.Transparent + ), + modifier = Modifier + .widthIn(max = 550.dp) + .windowInsetsPadding(insets) + .padding(horizontal = CP.xLarge, vertical = CP.small) + .clip(FloatingTopBarShape) + .border( + 0.5.dp, + VerticalGradient( + listOf( + if (colors.isLight) colors.background else Color.Gray.copy( + 0.24f + ), + if (colors.isLight) colors.background.copy(0.3f) else Color.Gray.copy( + 0.075f + ), + ) + ), + FloatingTopBarShape + ) + .dynamicBackdrop( + backdropProvider, + HazeStyle.Regular(colors.background), + colors.background, + colors.accent + ) ) - }, + } ) } -@Composable -private fun Toolbar( - viewState: FoldersViewState, - modifier: Modifier = Modifier +private val GRID_ITEM_SPACING = Arrangement.spacedBy(CP.small) +private fun LazyGridScope.content( + navController: NavHostController, + state: LazyGridState, + data: Mapped, ) { - NeumorphicTopAppBar( - title = { Label(text = textResource(id = R.string.folders)) }, - elevation = AppTheme.elevation.low, - shape = CircleShape, - modifier = modifier.padding(top = ContentPadding.medium), - lightShadowColor = AppTheme.colors.lightShadowColor, - darkShadowColor = AppTheme.colors.darkShadowColor, - navigationIcon = { - IconButton(imageVector = Icons.Default.FolderCopy, onClick = {}) - }, - actions = { Actions(viewState = viewState) } - ) -} - -/** - * The min size of the single cell in grid. - */ -private val MIN_TILE_SIZE = 100.dp -private val FolderContentPadding = - PaddingValues(vertical = AppTheme.padding.normal, horizontal = AppTheme.padding.medium) -private val GridItemsArrangement = Arrangement.spacedBy(6.dp) - -private fun LazyGridScope.shortcuts(navController: NavHostController) = + // First row represents shortcuts items(2, contentType = { "shortcut" }) { index -> when (index) { 0 -> Shortcut( Icons.Outlined.HotelClass, - "Favourites", + stringResource(R.string.favourites), onClick = { navController.navigate(RouteAlbum()) } ) 1 -> Shortcut( Icons.Outlined.Recycling, - "Recycle Bin", + stringResource(R.string.recycle_bin), onClick = { navController.navigate(RouteTrash()) }, // Only enable if R and above enabled = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R @@ -218,50 +190,101 @@ private fun LazyGridScope.shortcuts(navController: NavHostController) = } } + for ((header, items) in data) { + if (header.isNotBlank()) // only show this if non-blank. + stickyHeader( + state, + header, + contentType = "header", + content = { + Box( + modifier = Modifier + .animateItem() + .padding(horizontal = 6.dp), + content = { ListHeader(header) } + ) + } + ) + // rest of the items + items( + items, + key = Folder::path, + contentType = { "album" }, + itemContent = { + Folder( + value = it, + modifier = Modifier + .clickable() { navController.navigate(RouteFolder(it.path)) } + //.sharedElement(RouteViewer.buildSharedFrameKey(it.artworkID)) + .animateItem() + ) + } + ) + } +} + +/** + * The min size of the single cell in grid. + */ +private val MIN_TILE_SIZE = 100.dp +private val FolderContentPadding = + Padding(horizontal = AppTheme.padding.large, AppTheme.padding.normal) +private val GridItemsArrangement = Arrangement.spacedBy(6.dp) + @Composable fun Folders(viewState: FoldersViewState) { val navInsets = WindowInsets.contentInsets + val observer = when { + Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> BackdropProvider() + else -> null + } + + // Actual Content TwoPane( topBar = { - Toolbar( - viewState = viewState, - modifier = Modifier - .background(AppTheme.colors.background) - .statusBarsPadding() - .drawHorizontalDivider(color = AppTheme.colors.onBackground) - .padding(bottom = ContentPadding.medium), + FloatingTopAppBar( + insets = WindowInsets.statusBars, + backdropProvider = observer ) }, content = { val values by viewState.data.collectAsState() val multiplier by preference(key = Settings.KEY_GRID_ITEM_SIZE_MULTIPLIER) val navController = LocalNavController.current + val state = rememberLazyGridState() LazyVerticalGrid( + state = state, columns = GridCells.Adaptive((MIN_TILE_SIZE * multiplier.coerceAtLeast(1f))), contentPadding = FolderContentPadding + WindowInsets.contentInsets + navInsets, verticalArrangement = GridItemsArrangement, horizontalArrangement = GridItemsArrangement, + modifier = Modifier.thenIf(observer != null) { observerBackdrop(observer!!) }, content = { val data = emit(values) ?: return@LazyVerticalGrid + // only show other content; if data is avaiable. + // Filters: Display the filters section. + item( + "folder_filters", + contentType = "filters", + span = fullLineSpan, + content = { + Filters( + viewState.filter, + viewState.orders, + modifier = Modifier.padding(bottom = CP.medium), + onRequest = { + when { + it == null -> viewState.filter(!viewState.filter.first) + else -> viewState.filter(order = it) + } + } + ) + } + ) - // The standard shortcuts. - shortcuts(navController) - - item(span = fullLineSpan) { - Spacer(Modifier.padding(vertical = ContentPadding.medium)) - } - - // else emit the items. - items(data, key = { it.artworkID }) { - Folder( - value = it, - modifier = Modifier - .clickable() { navController.navigate(RouteFolder(it.path)) } - .sharedElement(RouteViewer.buildSharedFrameKey(it.artworkID)) - .animateItem() - ) - } - }, + // Rest of the content. + content(navController, state, data) + } ) } ) diff --git a/app/src/main/java/com/zs/gallery/folders/FoldersViewState.kt b/app/src/main/java/com/zs/gallery/folders/FoldersViewState.kt index b5e6410..353f4e7 100644 --- a/app/src/main/java/com/zs/gallery/folders/FoldersViewState.kt +++ b/app/src/main/java/com/zs/gallery/folders/FoldersViewState.kt @@ -19,6 +19,9 @@ package com.zs.gallery.folders import com.zs.domain.store.Folder +import com.zs.foundation.menu.Action +import com.zs.gallery.common.Filter +import com.zs.gallery.common.Mapped import com.zs.gallery.common.Route import kotlinx.coroutines.flow.StateFlow @@ -27,33 +30,28 @@ object RouteFolders : Route interface FoldersViewState { - companion object { - const val ORDER_BY_NAME = 0 - const val ORDER_BY_DATE_MODIFIED = 1 - const val ORDER_BY_SIZE = 2 - } - /** - * Gets or sets the grouping criterion of the list of folders. - * A value of [ORDER_BY_NAME] means the list is grouped by the folder name, while a value of [ORDER_BY_DATE_MODIFIED] means the list is grouped by the folder date modified, and a value of [ORDER_BY_SIZE] means the list is grouped by the folder size. - * The default value is [ORDER_BY_SIZE]. + * The list of supported orders. */ - var ascending: Boolean + val orders: List /** - * Gets or sets the grouping criterion of the list of folders. - * A value of [ORDER_BY_NAME] means the list is grouped by the folder name, while a value of [ORDER_BY_DATE_MODIFIED] means the list is grouped by the folder date modified, and a value of [ORDER_BY_SIZE] means the list is grouped by the folder size. - * The default value is [ORDER_BY_SIZE]. + * The filter criteria for the list. */ - var order: Int + val filter: Filter /** - * A state flow that emits the list of folders in the app. + * A state flow that emits the grid of folders in the app. * The list can be null, empty, or non-empty depending on the loading status and the availability of data. * A null value indicates that the folders are being loaded from the source and the UI should show a loading indicator. * An empty list indicates that there are no folders to display and the UI should show an empty state message. * A non-empty list indicates that the folders are successfully loaded and the UI should show them in a list view. * Any error that occurs during the loading process will be handled by a snackbar that shows the error message and a retry option. */ - val data: StateFlow?> + val data: StateFlow?> + + /** + * Updates the [filter] and triggers the update. + */ + fun filter(ascending: Boolean = this.filter.first, order: Action = this.filter.second) } \ No newline at end of file diff --git a/app/src/main/java/com/zs/gallery/impl/FoldersViewModel.kt b/app/src/main/java/com/zs/gallery/impl/FoldersViewModel.kt index f6a659f..34ced1f 100644 --- a/app/src/main/java/com/zs/gallery/impl/FoldersViewModel.kt +++ b/app/src/main/java/com/zs/gallery/impl/FoldersViewModel.kt @@ -18,68 +18,73 @@ package com.zs.gallery.impl +import android.text.format.DateUtils import android.util.Log import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.DateRange import androidx.compose.material.icons.outlined.NearbyError +import androidx.compose.material.icons.outlined.TextFields import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.graphics.Color import androidx.lifecycle.viewModelScope import com.primex.core.Rose +import com.primex.core.castTo +import com.zs.domain.store.Folder import com.zs.domain.store.MediaProvider +import com.zs.foundation.menu.Action import com.zs.foundation.toast.Toast import com.zs.gallery.R +import com.zs.gallery.common.Filter +import com.zs.gallery.common.Mapped import com.zs.gallery.folders.FoldersViewState -import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn +import java.util.Locale private const val TAG = "FoldersViewModel" +private val Folder.firstTitleChar + inline get() = name.uppercase(Locale.ROOT)[0].toString() + +private val ORDER_BY_DATE_MODIFIED = Action(R.string.last_modified, Icons.Outlined.DateRange) +private val ORDER_BY_NAME = Action(R.string.name, Icons.Outlined.TextFields) + class FoldersViewModel( - private val provider: MediaProvider + private val provider: MediaProvider, ) : KoinViewModel(), FoldersViewState { - // Trigger for refreshing the list - private val trigger = MutableStateFlow(false) - private var _ascending: Boolean by mutableStateOf(false) - private var _order: Int by mutableIntStateOf(FoldersViewState.ORDER_BY_DATE_MODIFIED) - private fun invalidate() { - trigger.value = !trigger.value + override val orders: List = listOf(ORDER_BY_DATE_MODIFIED, ORDER_BY_NAME) + + override fun filter(ascending: Boolean, order: Action) { + if (ascending == filter.first && order == filter.second) return + filter = ascending to order } - override var ascending: Boolean - get() = _ascending - set(value) { - _ascending = value - invalidate() - } + override var filter: Filter by mutableStateOf(true to ORDER_BY_NAME) - override var order: Int - get() = _order - set(value) { - _order = value - invalidate() - } + override val data: StateFlow?> = combine( + snapshotFlow(::filter), provider.observer(MediaProvider.EXTERNAL_CONTENT_URI) + ) { (ascending, order), _ -> + val folders = provider.fetchFolders() + val result = when (order) { + ORDER_BY_NAME -> folders.sortedBy { it.firstTitleChar } + .let { if (ascending) it else it.reversed() }.groupBy { it.firstTitleChar } + + ORDER_BY_DATE_MODIFIED -> folders.sortedBy { it.lastModified } + .let { if (ascending) it else it.reversed() } + .groupBy { DateUtils.getRelativeTimeSpanString(it.lastModified).toString() } - override val data = provider - // Observe the changes in the URI - .observer(MediaProvider.EXTERNAL_CONTENT_URI) - // Observe the changes in trigger also - .combine(trigger) { _, _ -> - val folders = provider.fetchFolders() - val result = when (_order) { - FoldersViewState.ORDER_BY_SIZE -> folders.sortedBy { it.size } - FoldersViewState.ORDER_BY_DATE_MODIFIED -> folders.sortedBy { it.lastModified } - FoldersViewState.ORDER_BY_NAME -> folders.sortedBy { it.name } - else -> error("Oops invalid id passed $_order") - } - if (ascending) result else result.reversed() + else -> error("Oops invalid id passed $order") } + // This should be safe + castTo(result) as Mapped + } // Catch any exceptions in upstream flow and emit using the snackbar. .catch { exception -> Log.e(TAG, "provider: ${exception.message}") @@ -96,4 +101,5 @@ class FoldersViewModel( } // Convert to state. .stateIn(viewModelScope, started = SharingStarted.Lazily, null) -} \ No newline at end of file +} + diff --git a/app/src/main/java/com/zs/gallery/impl/Initializer.kt b/app/src/main/java/com/zs/gallery/impl/Initializer.kt index e8a3614..c8e689b 100644 --- a/app/src/main/java/com/zs/gallery/impl/Initializer.kt +++ b/app/src/main/java/com/zs/gallery/impl/Initializer.kt @@ -54,7 +54,7 @@ private val appModules = module { // Initialize Preferences val preferences = com.primex.preferences.Preferences(get(), "shared_preferences") // Retrieve the current launch counter value, defaulting to 0 if not set - val counter = preferences.value(Settings.KEY_LAUNCH_COUNTER) ?: 0 + val counter = preferences.value(Settings.KEY_LAUNCH_COUNTER) // Increment the launch counter for cold starts preferences[Settings.KEY_LAUNCH_COUNTER] = counter + 1 Log.d(TAG, "Cold start counter: ${preferences.value(Settings.KEY_LAUNCH_COUNTER)}") diff --git a/app/src/main/java/com/zs/gallery/impl/SettingsViewModel.kt b/app/src/main/java/com/zs/gallery/impl/SettingsViewModel.kt index a71e88b..2eb2d84 100644 --- a/app/src/main/java/com/zs/gallery/impl/SettingsViewModel.kt +++ b/app/src/main/java/com/zs/gallery/impl/SettingsViewModel.kt @@ -18,139 +18,10 @@ package com.zs.gallery.impl -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.AutoAwesomeMotion -import androidx.compose.material.icons.outlined.Fullscreen -import androidx.compose.material.icons.outlined.GridView -import androidx.compose.material.icons.outlined.Lightbulb -import androidx.compose.material.icons.outlined.Recycling -import androidx.compose.material.icons.outlined.Security -import androidx.compose.material.icons.outlined.TextFields -import androidx.compose.material.icons.outlined.VisibilityOff -import androidx.compose.runtime.State -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope import com.primex.preferences.Key -import com.primex.preferences.Preferences -import com.zs.gallery.R -import com.zs.foundation.NightMode -import com.zs.gallery.settings.Preference -import com.zs.gallery.settings.Settings import com.zs.gallery.settings.SettingsViewState -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.runBlocking -context (ViewModel) @Suppress("NOTHING_TO_INLINE") -@Deprecated("find new solution.") -inline fun Flow.asComposeState(initial: T): State { - val state = mutableStateOf(initial) - onEach { state.value = it }.launchIn(viewModelScope) - return state -} - -context (Preferences, ViewModel) -private fun Flow.asComposeState(): State = asComposeState(runBlocking { first() }) class SettingsViewModel() : KoinViewModel(), SettingsViewState { - override val nightMode: Preference by with(preferences) { - preferences[Settings.KEY_NIGHT_MODE].map { - Preference( - value = it, - title = getText(R.string.pref_app_theme), - summery = getText(R.string.pref_app_theme_summery), - vector = Icons.Outlined.Lightbulb - ) - }.asComposeState() - } - override val trashCanEnabled: Preference by with(preferences) { - preferences[Settings.KEY_TRASH_CAN_ENABLED].map { - Preference( - value = it, - title = getText(R.string.pref_trash_enabled), - summery = getText(R.string.pref_trash_can_summery), - vector = Icons.Outlined.Recycling - ) - }.asComposeState() - } - override val gridItemSizeMultiplier by with(preferences) { - preferences[Settings.KEY_GRID_ITEM_SIZE_MULTIPLIER].map { - Preference( - value = it, - title = getText(R.string.pref_grid_item_size_multiplier), - summery = getText(R.string.pref_grid_item_size_multiplier_summery), - vector = Icons.Outlined.GridView - ) - }.asComposeState() - } - override val liveGallery by with(preferences) { - preferences[Settings.KEY_DYNAMIC_GALLERY].map { - Preference( - value = it, - title = getText(R.string.pref_live_gallery), - summery = getText(R.string.pref_live_gallery_summery), - vector = Icons.Outlined.AutoAwesomeMotion - ) - }.asComposeState() - } - override val applock: Preference by with(preferences) { - preferences[Settings.KEY_APP_LOCK_TIME_OUT].map { - Preference( - value = it, - title = getText(R.string.pref_app_lock), - summery = null, - vector = Icons.Outlined.Security - ) - }.asComposeState() - } - override val fontScale by with(preferences) { - preferences[Settings.KEY_FONT_SCALE].map { - Preference( - value = it, - title = getText(R.string.pref_font_scale), - summery = getText(R.string.pref_font_scale_summery), - vector = Icons.Outlined.TextFields - ) - }.asComposeState() - } - override val isSystemBarsTransparent by with(preferences) { - preferences[Settings.KEY_TRANSPARENT_SYSTEM_BARS].map { - Preference( - value = it, - title = getText(R.string.pref_transparent_system_bars), - summery = getText(R.string.pref_transparent_system_bars_summery), - vector = Icons.Outlined.VisibilityOff - ) - }.asComposeState() - } - override val immersiveView: Preference by with(preferences) { - preferences[Settings.KEY_IMMERSIVE_VIEW].map { - Preference( - value = it, - title = getText(R.string.pref_immersive_view), - summery = getText(R.string.pref_immersive_view_summery), - vector = Icons.Outlined.Fullscreen - ) - }.asComposeState() - } - override val secureMode: Preference by with(preferences) { - preferences[Settings.KEY_SECURE_MODE].map { - Preference( - value = it, - title = getText(R.string.pref_secure_mode), - summery = getText(R.string.pref_secure_mode_summery), - vector = Icons.Outlined.Security - ) - }.asComposeState() - } - - override fun set(key: Key, value: O) { - preferences[key] = value - } - + override fun set(key: Key, value: O) { preferences[key] = value } } \ No newline at end of file diff --git a/app/src/main/java/com/zs/gallery/impl/ViewerViewModel.kt b/app/src/main/java/com/zs/gallery/impl/ViewerViewModel.kt index 9a0a27b..afcf2c5 100644 --- a/app/src/main/java/com/zs/gallery/impl/ViewerViewModel.kt +++ b/app/src/main/java/com/zs/gallery/impl/ViewerViewModel.kt @@ -41,7 +41,7 @@ import com.zs.domain.store.MediaFile import com.zs.domain.store.MediaProvider import com.zs.domain.store.isImage import com.zs.domain.store.mediaUri -import com.zs.foundation.menu.MenuItem +import com.zs.foundation.menu.Action import com.zs.gallery.R import com.zs.gallery.common.get import com.zs.gallery.settings.Settings @@ -54,12 +54,12 @@ import kotlinx.coroutines.flow.onEach private const val TAG = "ViewerViewModel" -private val DELETE = MenuItem("action_delete", R.string.delete, Icons.Outlined.Delete) -private val SHARE = MenuItem("action_share", R.string.share, Icons.Outlined.Share) -private val USE_AS = MenuItem("action_use_as", R.string.set_as_wallpaper, Icons.Outlined.Wallpaper) -private val EDIT_IN = MenuItem("action_edit_in", R.string.edit_in, Icons.Outlined.Edit) -private val STAR = MenuItem("action_like", R.string.like, Icons.Outlined.StarOutline) -private val UN_STAR = MenuItem("action_unlike", R.string.unlike, Icons.Outlined.Star) +private val DELETE = Action(R.string.delete, Icons.Outlined.Delete) +private val SHARE = Action( R.string.share, Icons.Outlined.Share) +private val USE_AS = Action( R.string.set_as_wallpaper, Icons.Outlined.Wallpaper) +private val EDIT_IN = Action(R.string.edit_in, Icons.Outlined.Edit) +private val STAR = Action(R.string.like, Icons.Outlined.StarOutline) +private val UN_STAR = Action(R.string.unlike, Icons.Outlined.Star) /** * Creates an Intent to edit an image at the given URI. @@ -133,7 +133,7 @@ class ViewerViewModel( details = if (value) current else null } - override val actions: List by derivedStateOf { + override val actions: List by derivedStateOf { buildList { Log.d(TAG, "actions: changed ") this += if (favourite) UN_STAR else STAR @@ -147,7 +147,7 @@ class ViewerViewModel( } - override fun onAction(item: MenuItem, activity: Activity) { + override fun onAction(item: Action, activity: Activity) { when (item) { STAR, UN_STAR -> toggleLike() DELETE -> remove(activity) diff --git a/app/src/main/java/com/zs/gallery/settings/Settings.kt b/app/src/main/java/com/zs/gallery/settings/Settings.kt index eb48c83..320ff77 100644 --- a/app/src/main/java/com/zs/gallery/settings/Settings.kt +++ b/app/src/main/java/com/zs/gallery/settings/Settings.kt @@ -16,338 +16,581 @@ * limitations under the License. */ +@file:Suppress("NOTHING_TO_INLINE") + package com.zs.gallery.settings import android.annotation.SuppressLint import android.app.Activity import androidx.compose.foundation.background import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBars +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll -import androidx.compose.material.Scaffold +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.Icon import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ArrowBack -import androidx.compose.material.icons.filled.ErrorOutline -import androidx.compose.material.icons.filled.PrivacyTip -import androidx.compose.material.icons.outlined.TouchApp +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.LightMode +import androidx.compose.material.icons.filled.Recycling +import androidx.compose.material.icons.filled.Security +import androidx.compose.material.icons.outlined.AlternateEmail +import androidx.compose.material.icons.outlined.AutoAwesomeMotion +import androidx.compose.material.icons.outlined.BugReport +import androidx.compose.material.icons.outlined.Dashboard +import androidx.compose.material.icons.outlined.DataObject +import androidx.compose.material.icons.outlined.FormatSize +import androidx.compose.material.icons.outlined.NewReleases +import androidx.compose.material.icons.outlined.PrivacyTip +import androidx.compose.material.icons.outlined.Share +import androidx.compose.material.icons.outlined.Star +import androidx.compose.material.icons.outlined.Textsms import androidx.compose.runtime.Composable import androidx.compose.runtime.NonRestartableComposable import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Shape import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp +import com.primex.core.fadeEdge +import com.primex.core.plus import com.primex.core.textArrayResource -import com.primex.core.textResource +import com.primex.core.thenIf +import com.primex.material2.Button import com.primex.material2.DropDownPreference import com.primex.material2.IconButton import com.primex.material2.Label +import com.primex.material2.ListTile import com.primex.material2.Preference import com.primex.material2.SliderPreference import com.primex.material2.SwitchPreference +import com.primex.material2.Text +import com.primex.material2.TextButton import com.primex.material2.appbar.LargeTopAppBar import com.primex.material2.appbar.TopAppBarDefaults import com.primex.material2.appbar.TopAppBarScrollBehavior import com.zs.foundation.AppTheme import com.zs.foundation.Colors +import com.zs.foundation.Header +import com.zs.foundation.LocalWindowSize import com.zs.foundation.NightMode import com.zs.foundation.None +import com.zs.foundation.Range +import com.zs.foundation.adaptive.HorizontalTwoPaneStrategy +import com.zs.foundation.adaptive.TwoPane +import com.zs.foundation.adaptive.VerticalTwoPaneStrategy import com.zs.foundation.adaptive.contentInsets import com.zs.gallery.BuildConfig import com.zs.gallery.R import com.zs.gallery.common.LocalNavController import com.zs.gallery.common.LocalSystemFacade import com.zs.gallery.common.preference +import androidx.compose.foundation.layout.PaddingValues as Padding +import androidx.compose.ui.graphics.RectangleShape as Rectangle +import com.primex.core.rememberVectorPainter as painter +import com.primex.core.textResource as stringResource +import com.zs.foundation.ContentPadding as CP + +private const val TAG = "Settings" -////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// Constants -// Region: Preference Item Shapes - Used to style individual items within a preference section. -////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// The max width of the secondary pane +private val sPaneMaxWidth = 280.dp + +// Used to style individual items within a preference section. private val TopTileShape = RoundedCornerShape(24.dp, 24.dp, 0.dp, 0.dp) -private val CentreTileShape = RectangleShape +private val CentreTileShape = Rectangle private val BottomTileShape = RoundedCornerShape(0.dp, 0.dp, 24.dp, 24.dp) private val SingleTileShape = RoundedCornerShape(24.dp) private val Colors.tileBackgroundColor - @ReadOnlyComposable @Composable get() = - background(elevation = 1.dp) + @ReadOnlyComposable @Composable inline get() = background(elevation = 1.dp) + +// when topBar doesn't fill the screen; this is for that case. +private val RoundedTopBarShape = RoundedCornerShape(15) +/** + * Represents a Top app bar for this screen. + * + * Handles padding/margins based on shape to ensure proper layout. + * + * @param modifier [Modifier] to apply to this top app bar. + * @param shape [Shape] of the top app bar. Defaults to `null`. + * @param behaviour [TopAppBarScrollBehavior] for scroll behavior. + */ @Composable @NonRestartableComposable -private fun Toolbar( +private fun TopAppBar( modifier: Modifier = Modifier, - behavior: TopAppBarScrollBehavior? = null + insets: WindowInsets = WindowInsets.None, + shape: Shape? = null, + behaviour: TopAppBarScrollBehavior? = null ) { LargeTopAppBar( - title = { Label(text = textResource(id = R.string.settings)) }, - scrollBehavior = behavior, - modifier = modifier, + modifier = modifier.thenIf(shape != null) { + windowInsetsPadding(insets) + .padding(horizontal = CP.medium) + .clip(shape!!) + }, + title = { Label(text = stringResource(id = R.string.settings)) }, navigationIcon = { val navController = LocalNavController.current IconButton( - imageVector = Icons.Filled.ArrowBack, + imageVector = Icons.AutoMirrored.Filled.ArrowBack, onClick = navController::navigateUp ) }, + scrollBehavior = behaviour, + windowInsets = if (shape == null) insets else WindowInsets.None, style = TopAppBarDefaults.largeAppBarStyle( - scrolledContainerColor = AppTheme.colors.background(elevation = 1.dp), - containerColor = AppTheme.colors.background, + scrolledContainerColor = AppTheme.colors.background(2.dp), scrolledContentColor = AppTheme.colors.onBackground, + containerColor = AppTheme.colors.background, contentColor = AppTheme.colors.onBackground - ) + ), + actions = { + val facade = LocalSystemFacade.current + // Feedback + IconButton( + imageVector = Icons.Outlined.AlternateEmail, + onClick = { facade.launch(Settings.FeedbackIntent) }, + ) + // Star on Github + IconButton( + imageVector = Icons.Outlined.DataObject, + onClick = { facade.launch(Settings.GithubIntent) }, + ) + // Report Bugs on Github. + IconButton( + imageVector = Icons.Outlined.BugReport, + onClick = { facade.launch(Settings.GitHubIssuesPage) }, + ) + // Join our telegram channel + IconButton( + imageVector = Icons.Outlined.Textsms, + onClick = { facade.launch(Settings.TelegramIntent) }, + ) + } ) } -@Suppress("NOTHING_TO_INLINE") +private val HeaderPadding = PaddingValues(horizontal = CP.large, vertical = CP.xLarge) + +/** + * Represents the group header of [Preference]s + */ @Composable private inline fun GroupHeader( text: CharSequence, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, + paddingValues: Padding = HeaderPadding, ) { - com.primex.material2.Text( + Text( text = text, modifier = Modifier - .padding(AppTheme.padding.xLarge) + .padding(paddingValues) .then(modifier), color = AppTheme.colors.accent, style = AppTheme.typography.titleSmall ) } -context(ColumnScope) +private val APP_LOCK_VALUES = arrayOf(-1, 0, 1, 30) + +private const val CONTENT_TYPE_HEADER = "header" +private const val CONTENT_TYPE_ITEM = "item" + +/** + * Represents the settings of General + */ @SuppressLint("NewApi") -@Composable -private inline fun General( +private inline fun LazyListScope.General( viewState: SettingsViewState ) { - val prefLiveGallery = viewState.liveGallery - SwitchPreference( - title = prefLiveGallery.title, - checked = prefLiveGallery.value, - summery = prefLiveGallery.summery, - icon = prefLiveGallery.vector, - onCheckedChange = { viewState.set(Settings.KEY_DYNAMIC_GALLERY, it) }, - modifier = Modifier - .background(AppTheme.colors.tileBackgroundColor, TopTileShape) - ) - - val prefAppLock = viewState.applock - val facade = LocalSystemFacade.current - DropDownPreference( - title = prefAppLock.title, - defaultValue = prefAppLock.value, - icon = prefAppLock.vector, - entries = textArrayResource(R.array.pref_app_lock_options).let { - listOf( - it[0]. toString() to -1, - it[1]. toString() to 0, - it[2]. toString() to 1, - it[3]. toString() to 30 - ) - }, - modifier = Modifier - .background(AppTheme.colors.tileBackgroundColor, CentreTileShape), - onRequestChange = {value -> - // User wishes to enable app lock - if (!facade.canAuthenticate()) { - // If the user cannot authenticate, prompt them to enroll in biometric authentication - return@DropDownPreference facade.enroll() - } - // Securely make sure that app_lock is set. - facade.authenticate((facade as Activity).getString(R.string.auth_confirm_biometric)) { - viewState.set(Settings.KEY_APP_LOCK_TIME_OUT, value) - } - }, - ) + // Live Gallery + item(contentType = CONTENT_TYPE_ITEM) { + val prefLiveGallery by preference(Settings.KEY_DYNAMIC_GALLERY) + SwitchPreference( + text = stringResource(R.string.pref_live_gallery), + checked = prefLiveGallery, + icon = Icons.Outlined.AutoAwesomeMotion, + onCheckedChange = { viewState.set(Settings.KEY_DYNAMIC_GALLERY, it) }, + modifier = Modifier.background(AppTheme.colors.tileBackgroundColor, TopTileShape) + ) + } - val prefTrashCan = viewState.trashCanEnabled - SwitchPreference( - title = prefTrashCan.title, - checked = prefTrashCan.value, - summery = prefTrashCan.summery, - icon = prefTrashCan.vector, - onCheckedChange = { viewState.set(Settings.KEY_TRASH_CAN_ENABLED, it) }, - modifier = Modifier - .background(AppTheme.colors.tileBackgroundColor, CentreTileShape) - ) + // AppLock + item(contentType = CONTENT_TYPE_ITEM) { + val facade = LocalSystemFacade.current + // + val value by preference(Settings.KEY_APP_LOCK_TIME_OUT) + val entries = textArrayResource(R.array.pref_app_lock_options) + DropDownPreference( + text = stringResource( + R.string.pref_app_lock_s, + entries[APP_LOCK_VALUES.indexOf(value)] + ), + value = value, + icon = Icons.Default.LightMode, + entries = entries, + onRequestChange = { value -> + // User wishes to enable app lock + if (!facade.canAuthenticate()) { + // If the user cannot authenticate, prompt them to enroll in biometric authentication + return@DropDownPreference facade.enroll() + } + // Securely make sure that app_lock is set. + facade.authenticate((facade as Activity).getString(R.string.auth_confirm_biometric)) { + viewState.set(Settings.KEY_APP_LOCK_TIME_OUT, value) + } + }, + values = APP_LOCK_VALUES, + modifier = Modifier.background(AppTheme.colors.tileBackgroundColor, CentreTileShape), + ) + } - val prefGridSizeMultiplier = viewState.gridItemSizeMultiplier - SliderPreference( - title = prefGridSizeMultiplier.title, - summery = prefGridSizeMultiplier.summery, - icon = prefGridSizeMultiplier.vector, - valueRange = 0.5f..1.5f, - steps = 9, - defaultValue = prefGridSizeMultiplier.value, - onValueChange = { viewState.set(Settings.KEY_GRID_ITEM_SIZE_MULTIPLIER, it) }, - modifier = Modifier.background(AppTheme.colors.tileBackgroundColor, CentreTileShape), - preview = { - Label(text = stringResource(R.string.times_factor_x_f, prefGridSizeMultiplier.value)) - } - ) + // Recycle Bin + item(contentType = CONTENT_TYPE_ITEM) { + val value by preference(Settings.KEY_TRASH_CAN_ENABLED) + SwitchPreference( + text = stringResource(R.string.pref_enable_trash_can), + checked = value, + icon = Icons.Default.Recycling, + onCheckedChange = { viewState.set(Settings.KEY_TRASH_CAN_ENABLED, it) }, + modifier = Modifier + .background(AppTheme.colors.tileBackgroundColor, CentreTileShape) + ) + } - val prefSecureMode = viewState.secureMode - SwitchPreference( - title = prefSecureMode.title, - checked = prefSecureMode.value, - summery = prefSecureMode.summery, - icon = prefSecureMode.vector, - onCheckedChange = { viewState.set(Settings.KEY_SECURE_MODE, it) }, - modifier = Modifier - .background(AppTheme.colors.tileBackgroundColor, BottomTileShape) - ) + // Secure Mode + item(contentType = CONTENT_TYPE_ITEM) { + val value by preference(Settings.KEY_SECURE_MODE) + SwitchPreference( + text = stringResource(R.string.pref_secure_mode), + checked = value, + icon = Icons.Default.Security, + onCheckedChange = { viewState.set(Settings.KEY_SECURE_MODE, it) }, + modifier = Modifier.background(AppTheme.colors.tileBackgroundColor, BottomTileShape) + ) + } } -context(ColumnScope) -@Composable -private inline fun Appearance( +/** + * Represents items that are related to appearence of the App. + */ +private inline fun LazyListScope.Appearence( viewState: SettingsViewState ) { - val prefNightMode = viewState.nightMode - DropDownPreference( - title = prefNightMode.title, - defaultValue = prefNightMode.value, - icon = prefNightMode.vector, - onRequestChange = { viewState.set(Settings.KEY_NIGHT_MODE, it) }, - entries = listOf( - stringResource(R.string.dark) to NightMode.YES, - stringResource(R.string.light) to NightMode.NO, - stringResource(R.string.sync_with_system) to NightMode.FOLLOW_SYSTEM - ), - modifier = Modifier - .background(AppTheme.colors.tileBackgroundColor, TopTileShape) - ) + // Night Mode + item(contentType = CONTENT_TYPE_ITEM) { + // Night Mode Strategy + // The strategy to use for night mode. + val value by preference(Settings.KEY_NIGHT_MODE) + val entries = textArrayResource(R.array.pref_night_mode_entries) + DropDownPreference( + text = stringResource(R.string.pref_app_theme_s, entries[value.ordinal]), + value = value, + icon = Icons.Default.LightMode, + entries = entries, + onRequestChange = { + viewState.set(Settings.KEY_NIGHT_MODE, it) + }, + values = NightMode.values(), + modifier = Modifier.background(AppTheme.colors.tileBackgroundColor, TopTileShape) + ) + } - val isSystemBarsTransparent = viewState.isSystemBarsTransparent - SwitchPreference( - title = isSystemBarsTransparent.title, - checked = isSystemBarsTransparent.value, - summery = isSystemBarsTransparent.summery, - icon = isSystemBarsTransparent.vector, - onCheckedChange = { viewState.set(Settings.KEY_TRANSPARENT_SYSTEM_BARS, it) }, - modifier = Modifier - .background(AppTheme.colors.tileBackgroundColor, CentreTileShape) - ) + // Font Scale + item(contentType = CONTENT_TYPE_ITEM) { + // The font scale to use for the app if -1 is used, the system font scale is used. + val scale by preference(Settings.KEY_FONT_SCALE) + SliderPreference( + value = scale, + text = stringResource(R.string.pref_font_scale), + valueRange = 0.7f..2f, + steps = 13, // (2.0 - 0.7) / 0.1 = 13 steps + icon = Icons.Outlined.FormatSize, + preview = { + Label( + text = when { + it < 0.76f -> stringResource(R.string.system) + else -> stringResource(R.string.postfix_x_f, it) + }, + fontWeight = FontWeight.Bold + ) + }, + onRequestChange = { value: Float -> + val newValue = if (value < 0.76f) -1f else value + viewState.set(Settings.KEY_FONT_SCALE, newValue) + }, + modifier = Modifier + .background(AppTheme.colors.tileBackgroundColor, CentreTileShape) + ) + } - val prefImmersiveView = viewState.immersiveView - SwitchPreference( - title = prefImmersiveView.title, - checked = prefImmersiveView.value, - summery = prefImmersiveView.summery, - icon = prefImmersiveView.vector, - onCheckedChange = { viewState.set(Settings.KEY_IMMERSIVE_VIEW, it) }, - modifier = Modifier - .background(AppTheme.colors.tileBackgroundColor, CentreTileShape) - ) + // Trasnsparent System Bars + item(contentType = CONTENT_TYPE_ITEM) { + // Translucent System Bars + // Whether System Bars are rendered as translucent or Transparent. + val value by preference(Settings.KEY_TRANSPARENT_SYSTEM_BARS) + SwitchPreference( + checked = value, + text = stringResource(R.string.pref_transparent_system_bars), + onCheckedChange = { should: Boolean -> + viewState.set(Settings.KEY_TRANSPARENT_SYSTEM_BARS, should) + }, + modifier = Modifier + .background(AppTheme.colors.tileBackgroundColor, CentreTileShape) + ) + } - val prefFontScale = viewState.fontScale - SliderPreference( - title = prefFontScale.title, - summery = prefFontScale.summery, - icon = prefFontScale.vector, - // (2.0 - 0.7) / 0.1 = 13 steps - steps = 13, - valueRange = 0.7f..2.0f, - defaultValue = prefFontScale.value, - onValueChange = { value -> - val newValue = if (value < 0.76f) -1f else value - viewState.set(Settings.KEY_FONT_SCALE, newValue) - }, - modifier = Modifier - .background(AppTheme.colors.tileBackgroundColor, CentreTileShape), - preview = { - Label( - text = if (prefFontScale.value == -1f) "System" else stringResource( - R.string.times_factor_x_f, - prefFontScale.value - ), - fontWeight = FontWeight.Bold - ) - } - ) + // Use Accent in NavBar + item(contentType = CONTENT_TYPE_ITEM) { + val useAccent by preference(Settings.KEY_USE_ACCENT_IN_NAV_BAR) + SwitchPreference( + stringResource(R.string.pref_color_nav_bar), + checked = useAccent, + onCheckedChange = { viewState.set(Settings.KEY_USE_ACCENT_IN_NAV_BAR, it) }, + modifier = Modifier + .background(AppTheme.colors.tileBackgroundColor, CentreTileShape), + ) + } - val useAccent by preference(Settings.KEY_USE_ACCENT_IN_NAV_BAR) - SwitchPreference( - stringResource(R.string.pref_color_nav_bar), - checked = useAccent, - onCheckedChange = { viewState.set(Settings.KEY_USE_ACCENT_IN_NAV_BAR, it) }, - modifier = Modifier - .background(AppTheme.colors.tileBackgroundColor, BottomTileShape), - summery = stringResource(R.string.pref_color_nav_bar_summery) - ) + // Grid Size Multiplier + + item(contentType = CONTENT_TYPE_ITEM) { + // Grid Item Multiplier + // The multiplier increases/decreases the size of the grid item from 0.6 to 2f + val gridItemSizeMultiplier by preference(Settings.KEY_GRID_ITEM_SIZE_MULTIPLIER) + SliderPreference( + value = gridItemSizeMultiplier, + text = stringResource(R.string.pref_grid_item_size_multiplier), + valueRange = 0.6f..2f, + steps = 14, // (2.0 - 0.7) / 0.1 = 13 steps + icon = Icons.Outlined.Dashboard, + preview = { + Label( + text = stringResource(R.string.postfix_x_f, it), + fontWeight = FontWeight.Bold + ) + }, + onRequestChange = { value: Float -> + viewState.set(Settings.KEY_GRID_ITEM_SIZE_MULTIPLIER, value) + }, + modifier = Modifier.background(AppTheme.colors.tileBackgroundColor, CentreTileShape) + ) + } + + // Hide/Show SystemBars for Immersive View + item(contentType = CONTENT_TYPE_ITEM) { + // Whether System Bars are hidden for immersive view or not. + val immersiveView by preference(Settings.KEY_IMMERSIVE_VIEW) + SwitchPreference( + checked = immersiveView, + text = stringResource(R.string.pref_immersive_view), + onCheckedChange = { should: Boolean -> + viewState.set(Settings.KEY_IMMERSIVE_VIEW, should) + }, + modifier = Modifier + .background(AppTheme.colors.tileBackgroundColor, BottomTileShape) + ) + } } -context(ColumnScope) @Composable -private inline fun AboutUs( - viewState: SettingsViewState -) { +private inline fun ColumnScope.AboutUs() { // The app version and check for updates. val facade = LocalSystemFacade.current - Preference( - title = textResource(id = R.string.pref_app_version), - icon = Icons.Outlined.TouchApp, - summery = textResource(id = R.string.pref_app_version_summery, BuildConfig.VERSION_NAME), - modifier = Modifier - .clickable { facade.launchUpdateFlow(true) } - .background(AppTheme.colors.tileBackgroundColor, TopTileShape) + ListTile( + headline = { Label(stringResource(R.string.version), fontWeight = FontWeight.Bold) }, + subtitle = { + Label( + stringResource(R.string.version_info_s, BuildConfig.VERSION_NAME) + ) + }, + footer = { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(CP.medium) + ) { + TextButton( + stringResource(R.string.update_gallery), + onClick = { facade.launchUpdateFlow(true) }) + TextButton( + stringResource(R.string.join_the_beta), + onClick = { facade.launch(Settings.JoinBetaIntent) }, + enabled = false + ) + } + }, + leading = { Icon(imageVector = Icons.Outlined.NewReleases, contentDescription = null) }, ) + // Privacy Policy Preference( - title = textResource(id = R.string.pref_privacy_policy), - summery = textResource(id = R.string.pref_privacy_policy_summery), - icon = Icons.Default.PrivacyTip, + text = stringResource(R.string.pref_privacy_policy), + icon = Icons.Outlined.PrivacyTip, modifier = Modifier - .background(AppTheme.colors.tileBackgroundColor, CentreTileShape) + .clip(AppTheme.shapes.medium) + .clickable { facade.launch(Settings.PrivacyPolicyIntent) }, ) - Preference( - title = textResource(id = R.string.pref_report_an_issue), - summery = textResource(id = R.string.pref_report_an_issue_summery), - icon = Icons.Default.ErrorOutline, - modifier = Modifier - .background(AppTheme.colors.tileBackgroundColor, BottomTileShape) - ) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(CP.medium) + ) { + Button( + label = stringResource(R.string.rate_us), + icon = painter(Icons.Outlined.Star), + onClick = facade::launchAppStore, + colors = ButtonDefaults.buttonColors( + backgroundColor = AppTheme.colors.background(2.dp), + contentColor = AppTheme.colors.accent + ), + elevation = null, + shape = AppTheme.shapes.small + ) + + Button( + label = stringResource(R.string.share_app_label), + icon = painter(Icons.Outlined.Share), + onClick = { facade.launch(Settings.ShareAppIntent) }, + colors = ButtonDefaults.buttonColors( + backgroundColor = AppTheme.colors.background(2.dp), + contentColor = AppTheme.colors.accent + ), + elevation = null, + shape = AppTheme.shapes.small + ) + } } +/** + * Represents the Settings screen. + */ @Composable -fun Settings( - viewState: SettingsViewState -) { - val behavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() - val insets = WindowInsets.contentInsets - Scaffold( - topBar = { Toolbar(behavior = behavior) }, - contentWindowInsets = WindowInsets.None, - modifier = Modifier.nestedScroll(behavior.nestedScrollConnection), - content = { +fun Settings(viewState: SettingsViewState) { + // Retrieve the current window size + val (width, _) = LocalWindowSize.current + // Determine the two-pane strategy based on window width range + // when in mobile portrait; we don't show second pane; + val strategy = when { + // TODO -Replace with OnePane Strategy when updating TwoPane Layout. + width < Range.Medium -> VerticalTwoPaneStrategy(0.5f) // Use stacked layout with bias to centre for small screens + else -> HorizontalTwoPaneStrategy(0.5f) // Use horizontal layout with 50% split for large screens + } + // Layout Modes: + // When the width exceeds the "Compact" threshold, the layout is no longer immersive. + // This is because a navigation rail is likely displayed, requiring content to be + // indented rather than filling the entire screen width. + // + // The threshold helps to dynamically adjust the UI for different device form factors + // and orientations, ensuring appropriate use of space. In non-compact layouts, + // elements like the navigation rail or side panels prevent an immersive, full-width + // layout, making the design more suitable for larger screens. + val immersive = width < Range.Medium + // Define the scroll behavior for the top app bar + val topAppBarScrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() + // obtain the padding of BottomNavBar/NavRail + val navBarPadding = WindowInsets.contentInsets + // Place the content + // FIXME: Width < 650dp then screen is single pane what if navigationBars are at end. + TwoPane( + spacing = CP.normal, + strategy = strategy, + topBar = { + TopAppBar( + behaviour = topAppBarScrollBehavior, + insets = WindowInsets.statusBars, + shape = if (immersive) null else RoundedTopBarShape, + ) + }, + details = { + // this will not be called when in single pane mode + // this is just for decoration + if (strategy is VerticalTwoPaneStrategy) return@TwoPane Column( modifier = Modifier .verticalScroll(rememberScrollState()) - .padding(it) - .padding(WindowInsets.contentInsets) - .padding( - horizontal = AppTheme.padding.large, - vertical = AppTheme.padding.normal - ), + .padding(top = CP.medium) + .widthIn(max = sPaneMaxWidth) + .systemBarsPadding() + .padding(navBarPadding), content = { - GroupHeader(text = stringResource(id = R.string.general)) - General(viewState = viewState) - GroupHeader(text = stringResource(id = R.string.appearance)) - Appearance(viewState = viewState) - GroupHeader(text = stringResource(id = R.string.about_gallery)) - AboutUs(viewState = viewState) + Header( + stringResource(R.string.about_us), + color = AppTheme.colors.accent, + // drawDivider = true, + style = AppTheme.typography.titleSmall, + contentPadding = PaddingValues(vertical = CP.normal, horizontal = CP.medium) + ) + AboutUs() } ) + }, + content = { + val state = rememberLazyListState() + LazyColumn( + state = state, + // In immersive mode, add horizontal padding to prevent settings from touching the screen edges. + // Immersive layouts typically have a bottom app bar, so extra padding improves aesthetics. + // Non-immersive layouts only need vertical padding. + contentPadding = Padding(if (immersive) CP.large else CP.medium, vertical = CP.normal) + navBarPadding + WindowInsets.contentInsets, + modifier = Modifier + .nestedScroll(topAppBarScrollBehavior.nestedScrollConnection) + .fadeEdge(AppTheme.colors.background(2.dp), state, false) + .thenIf(immersive) { navigationBarsPadding() }, + ) { + // General + item(contentType = CONTENT_TYPE_HEADER) { + GroupHeader( + text = stringResource(id = R.string.general), + paddingValues = Padding(CP.normal, CP.small, CP.normal, CP.xLarge) + ) + } + General(viewState) + + // Appearence + item(CONTENT_TYPE_HEADER) { GroupHeader(text = stringResource(id = R.string.appearance)) } + Appearence(viewState = viewState) + + // AboutUs + // Load AboutUs here if this is mobile port + if (strategy !is VerticalTwoPaneStrategy) + return@LazyColumn + + item(contentType = CONTENT_TYPE_HEADER) { + Header( + stringResource(R.string.about_us), + color = AppTheme.colors.accent, + //drawDivider = true, + style = AppTheme.typography.titleSmall, + contentPadding = Padding(vertical = CP.normal, horizontal = CP.medium) + ) + } + + item(contentType = "about_us") { + Column { AboutUs() } + } + } } ) } - diff --git a/app/src/main/java/com/zs/gallery/settings/SettingsViewState.kt b/app/src/main/java/com/zs/gallery/settings/SettingsViewState.kt index 69e1102..d5aaa6c 100644 --- a/app/src/main/java/com/zs/gallery/settings/SettingsViewState.kt +++ b/app/src/main/java/com/zs/gallery/settings/SettingsViewState.kt @@ -87,37 +87,13 @@ private fun FontFamily(name: String): FontFamily { } /** - * Immutable data class representing a preference. - * - * @property value The value of the preference. - * @property title The title text of the preference. - * @property vector The optional vector image associated with the preference. - * @property summery The optional summary text of the preference. - * @param P The type of the preference value. + * Represents the state of the [Settings] screen. */ -@Stable -data class Preference( - val value: P, - @JvmField val title: CharSequence, - val vector: ImageVector? = null, - @JvmField val summery: CharSequence? = null, -) - interface SettingsViewState { - val nightMode: Preference - val trashCanEnabled: Preference - val gridItemSizeMultiplier: Preference - val liveGallery: Preference - val fontScale: Preference - - val isSystemBarsTransparent: Preference - val immersiveView: Preference - val secureMode: Preference - val applock: Preference - fun set(key: Key, value: O) } + /** * ## Settings * @@ -225,7 +201,7 @@ object Settings { putExtra(Intent.EXTRA_SUBJECT, "Feedback/Suggestion for Audiofy") } val PrivacyPolicyIntent = Intent(Intent.ACTION_VIEW).apply { - data = Uri.parse("https://docs.google.com/document/d/1AWStMw3oPY8H2dmdLgZu_kRFN-A8L6PDShVuY8BAhCw/edit?usp=sharing") + data = Uri.parse("https://docs.google.com/document/d/1D9wswWSrt65ol7h3HLKhk31OVTqDtN4uLJ73_Rk9hT8/edit?usp=sharing") } val GitHubIssuesPage = Intent(Intent.ACTION_VIEW).apply { data = Uri.parse("https://github.com/iZakirSheikh/Gallery/issues") @@ -241,7 +217,7 @@ object Settings { } val ShareAppIntent = Intent(Intent.ACTION_SEND).apply { type = "text/plain" - putExtra(Intent.EXTRA_TEXT, "Hey, check out this cool app: [app link here]") + putExtra(Intent.EXTRA_TEXT, "Hey, check out this cool app: [https://play.google.com/store/apps/details?id=com.googol.android.apps.photos&pcampaignid=web_share]") } } diff --git a/app/src/main/java/com/zs/gallery/viewer/Details.kt b/app/src/main/java/com/zs/gallery/viewer/Details.kt index 4837dac..fc83195 100644 --- a/app/src/main/java/com/zs/gallery/viewer/Details.kt +++ b/app/src/main/java/com/zs/gallery/viewer/Details.kt @@ -51,7 +51,7 @@ import com.zs.domain.store.isImage import com.zs.foundation.AppTheme import com.zs.foundation.ContentPadding import com.zs.foundation.Header -import com.zs.foundation.menu.MenuItem +import com.zs.foundation.menu.Action import com.zs.gallery.R import java.text.SimpleDateFormat import java.util.Date @@ -65,8 +65,8 @@ private val MediaFile.megapixels @Composable private inline fun MainMenu( - actions: List, - crossinline onAction: (action: MenuItem) -> Unit + actions: List, + crossinline onAction: (action: Action) -> Unit ) { val state = rememberScrollState() Row( @@ -121,8 +121,8 @@ private fun Detail( @Composable fun Details( value: MediaFile, - actions: List, - onAction: (action: MenuItem) -> Unit, + actions: List, + onAction: (action: Action) -> Unit, modifier: Modifier = Modifier, contentPadding: PaddingValues = PaddingValues(0.dp), shape: Shape = RoundedCornerShape(topStartPercent = 8, topEndPercent = 8) diff --git a/app/src/main/java/com/zs/gallery/viewer/Viewer.kt b/app/src/main/java/com/zs/gallery/viewer/Viewer.kt index 1c8bfb0..f790930 100644 --- a/app/src/main/java/com/zs/gallery/viewer/Viewer.kt +++ b/app/src/main/java/com/zs/gallery/viewer/Viewer.kt @@ -21,6 +21,7 @@ import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.pager.HorizontalPager @@ -42,13 +43,10 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.draw.drawWithCache -import androidx.compose.ui.draw.drawWithContent import androidx.compose.ui.draw.scale import androidx.compose.ui.geometry.Size import androidx.compose.ui.geometry.isUnspecified -import androidx.compose.ui.graphics.BlendMode import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter @@ -59,7 +57,6 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import coil3.compose.AsyncImagePainter import coil3.compose.rememberAsyncImagePainter -import coil3.request.CachePolicy import coil3.request.ImageRequest import com.primex.core.SignalWhite import com.primex.core.findActivity @@ -85,16 +82,15 @@ import com.zs.foundation.adaptive.margin import com.zs.foundation.adaptive.padding import com.zs.foundation.adaptive.shape import com.zs.foundation.menu.Menu -import com.zs.foundation.menu.MenuItem +import com.zs.foundation.menu.Action import com.zs.foundation.player.PlayerController import com.zs.foundation.player.rememberPlayerController import com.zs.foundation.sharedBounds -import com.zs.foundation.sharedElement import com.zs.foundation.thenIf -import com.zs.gallery.R import com.zs.gallery.common.LocalNavController import com.zs.gallery.common.LocalSystemFacade import com.zs.domain.coil.preferCachedThumbnail +import com.zs.foundation.menu.FloatingActionMenu import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import me.saket.telephoto.zoomable.DoubleClickToZoomListener @@ -153,29 +149,6 @@ private fun VideoIntent(uri: Uri) = addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) // Grant read permission } -@Composable -private fun FloatingActionMenu( - actions: List, - modifier: Modifier = Modifier, - onAction: (action: MenuItem) -> Unit -) { - Surface( - modifier = modifier.scale(0.85f), - color = AppTheme.colors.background(elevation = 2.dp), - contentColor = AppTheme.colors.onBackground, - shape = CircleShape, - border = BorderStroke(1.dp, AppTheme.colors.background(elevation = 4.dp)), - elevation = 12.dp, - content = { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.animateContentSize() - ) { - Menu(actions, onItemClicked = onAction, collapsed = 3) - } - }, - ) -} @Composable private fun TopAppBar( @@ -456,13 +429,17 @@ fun Viewer( enter = fadeIn(), exit = slideOutVertically() + fadeOut(), modifier = Modifier - .padding(bottom = AppTheme.padding.normal), + .padding(bottom = ContentPadding.medium) + .navigationBarsPadding(), // .renderInSharedTransitionScopeOverlay(navController.zIndexScr + 0.02f), content = { - FloatingActionMenu( - actions = viewState.actions, - onAction = { viewState.onAction(it, context.findActivity()) }, - ) + FloatingActionMenu { + Menu( + viewState.actions, + onItemClicked = { viewState.onAction(it, context.findActivity()) }, + collapsed = 3 + ) + } } ) }, diff --git a/app/src/main/java/com/zs/gallery/viewer/ViewerViewState.kt b/app/src/main/java/com/zs/gallery/viewer/ViewerViewState.kt index 6fde200..0daa6f0 100644 --- a/app/src/main/java/com/zs/gallery/viewer/ViewerViewState.kt +++ b/app/src/main/java/com/zs/gallery/viewer/ViewerViewState.kt @@ -22,7 +22,7 @@ import android.app.Activity import android.net.Uri import androidx.lifecycle.SavedStateHandle import com.zs.domain.store.MediaFile -import com.zs.foundation.menu.MenuItem +import com.zs.foundation.menu.Action import com.zs.gallery.common.NULL_STRING import com.zs.gallery.common.SafeArgs @@ -151,15 +151,15 @@ interface ViewerViewState { var showDetails: Boolean /** - * The list of actions supported by the currently displayed file, represented as [MenuItem] objects. + * The list of actions supported by the currently displayed file, represented as [Action] objects. */ - val actions: List + val actions: List /** * Callback function to be invoked when an action is selected. * - * @param item The [MenuItem] representing the selected action. + * @param item The [Action] representing the selected action. * @param activity The [Activity] context in which the action is invoked. */ - fun onAction(item: MenuItem, activity: Activity) + fun onAction(item: Action, activity: Activity) } \ No newline at end of file diff --git a/app/src/main/res/values-b+es+419/strings.xml b/app/src/main/res/values-b+es+419/strings.xml new file mode 100644 index 0000000..8ab1da7 --- /dev/null +++ b/app/src/main/res/values-b+es+419/strings.xml @@ -0,0 +1,196 @@ + + + + Galería + Permiso de Almacenamiento + Permitir + Galería requiere permiso para acceder al + almacenamiento de tu dispositivo para mostrar imágenes y vídeos. + \n\nEn Android 14 o superior: + + \n 1. Por favor seleccione Permitir todo en el cuadro de diálogo de permisos para que la aplicación funcione correctamente. + + + Fotos + Ajustes + Carpetas + Cargando + Ups está Vacío!! + + %1$d Archivos - %2$s + Tamaño + Nombre + Reciente + Oscuro + Claro + Sincronizar con el sistema + General + Apariencia + Comentarios + Sobre Galería + Algo salió mal. Ocurrió un error desconocido. + Informe + Cronología + 123%1$s + %1$d Seleccionados + Seleccionar Todo + Papelera + Papelera de Reciclaje + Favoritos + + %1$sx + Última modificación + Ups, algo salió mal al compartir los archivos. Por favor, inténtelo de nuevo. + Ups, algo salió mal al eliminar los archivos. Por favor, inténtelo de nuevo. + Ups, algo salió mal al eliminar los archivos. Inténtalo de nuevo. + Basura + Ups, algo salió mal al restaurar los archivos. Por favor, inténtelo de nuevo. + Compartir + Editar + Borrar + Información + Editar En + Usar como + Establecer como fondo de pantalla + Favorito + Quitar de Favoritos + Detalles + Título + Ruta + Metadatos + Borrar + \n¿Estás seguro de que deseas eliminarlo? No podrás recuperarlo + + + ¡Ups! Algo salió mal al buscar actualizaciones. + ¡Ya está todo listo! No hay actualizaciones disponibles. + ¡Actualización lista! La instalación puede continuar ahora. + Instalar + Verifica tu identidad + Autenticarse con biometría + Autenticación con huella dactilar o reconocimiento facial + Para continuar. Este paso es crucial para proteger sus datos. + + Habilite la autenticación biométrica en la configuración. + ¡Favoritos actualizados! + ¡Nueva aplicación de reproductor multimedia! + \n🎶 Echa un vistazo a nuestro nuevo reproductor multimedia! Disfruta de tus canciones favoritas + y vídeos sin esfuerzo. ¡Pruébalo! 🎥 + + + Conseguir + Descartar + ¡Autenticación Fallida! Inténtelo de nuevo o utilice un método alternativo. + Error de autenticación: %1$s + Restaurar + Papelera Vacía + %1$s Archivos + Ups! No se detectó ninguna aplicación de fondo de pantalla. Instale uno para continuar. + Establecer como + Hoy + Queda 1 día + %1$s Días restantes + Confirmar Datos Biométricos + Tema de la aplicación + + \n%1$s + \nPersonaliza la apariencia de la aplicación con las opciones de tema Noche, Día o Sistema. + + + Vista Inmersiva + + \nOculte la barra de estado y la barra de navegación para disfrutar de una experiencia de pantalla completa sin distracciones. + + + Papelera de Reciclaje(Android 11+) + + \nHabilite la función para mover archivos eliminados a la Papelera de reciclaje en lugar de eliminarlos de forma permanente. + + + Multiplicador del Tamaño de la Cuadrícula + + \nAjuste el espacio que ocupa cada elemento en el diseño de la cuadrícula. + + + Auto Colorear Beta + + \nGenera dinámicamente un tema basado en los colores de la ilustración del medio que se está reproduciendo actualmente. + + + Política de Privacidad + + \nHaga clic aquí para ver la política de privacidad. + + + Galería en Vivo + + \nHabilitar animaciones y reproducción automática de videos en la galería. + + Escala de fuente + + \nAjuste el tamaño de la fuente para facilitar la lectura del texto. Elija una escala que se adapte a sus + preferencias para mejor legibilidad. + + + Barras del sistema + + \nHabilite las barras de sistema transparentes para una experiencia de borde a borde \n(Android 6.0+) + + + Informar un problema + + \nPóngase en contacto con nuestro equipo de soporte para obtener ayuda o informar problemas. + + + + Oscuro + Claro + Sincronizar con el Sistema + + Versión + Versión: %1$s + Modo Seguro + + \nEl modo seguro oculta el contenido de la aplicación en la vista previa de la aplicación, lo que protege la privacidad del usuario cuando + la aplicación no está en uso activo. + + + Bloqueo de la Aplicación Beta + \n%1$s + + \nAsegura la Galería con autenticación biométrica. + + + Barra de Navegación Coloreada + + Utilice el color de acento en la barra de navegación. + + + Sobre nosotros + %1$.1fx + Sistema + Únete a la Beta + Actualiza la Galería + Calificanos + Comparte con Amigos + + Desactivado + Instantáneo + 1 Min + 30 Min + + diff --git a/app/src/main/res/values-es-rES/strings.xml b/app/src/main/res/values-es-rES/strings.xml index 5a94a1b..8ab1da7 100644 --- a/app/src/main/res/values-es-rES/strings.xml +++ b/app/src/main/res/values-es-rES/strings.xml @@ -40,27 +40,8 @@ Sincronizar con el sistema General Apariencia - Tema de la App - Personaliza la apariencia de la aplicación con las opciones de tema Noche, Día o Sistema. - Vista Inmersiva - Oculte la barra de estado y la barra de navegación para disfrutar de una experiencia de pantalla completa sin distracciones. - Bote de basura - Cuando está habilitado, los archivos eliminados se mueven a una carpeta de papelera en lugar de eliminarse permanentemente. - Tamaño de cuadrícula - Controle cuánto espacio ocupa cada elemento en el diseño de cuadrícula. - Galería en vivo - Habilite animaciones y reproducción automática de videos en la galería. - Escala de fuente - Ajusta el tamaño del texto en toda la aplicación. - Barras del Sistema - Controle la transparencia de las barras del sistema para personalizar la apariencia de la aplicación Comentarios Sobre Galería - Política de Privacidad - Haga clic aquí para ver la política de privacidad - Informar un problema - Póngase en contacto con nuestro equipo de soporte para obtener ayuda o informar problemas - %1$s\nHaga clic para buscar actualizaciones. Algo salió mal. Ocurrió un error desconocido. Informe Cronología @@ -70,10 +51,6 @@ Papelera Papelera de Reciclaje Favoritos - Modo Seguro - El modo seguro oculta el contenido de la aplicación en la vista previa de la aplicación, - protegiendo la privacidad del usuario cuando la aplicación no está en uso activo. - %1$sx Última modificación @@ -103,17 +80,12 @@ ¡Ya está todo listo! No hay actualizaciones disponibles. ¡Actualización lista! La instalación puede continuar ahora. Instalar - Versión de la aplicación Verifica tu identidad Autenticarse con biometría Autenticación con huella dactilar o reconocimiento facial Para continuar. Este paso es crucial para proteger sus datos. Habilite la autenticación biométrica en la configuración. - Bloqueo de aplicación Beta - Desbloqueo con biometría\nCuando esté habilitado, necesitarás - utilizar la huella digital, el rostro u otros identificadores únicos para abrir la Galería. - ¡Favoritos actualizados! ¡Nueva aplicación de reproductor multimedia! \n🎶 Echa un vistazo a nuestro nuevo reproductor multimedia! Disfruta de tus canciones favoritas @@ -121,8 +93,6 @@ Conseguir - Barra de Navegación de Color - Utilice el color de acento en la barra de navegación. Descartar ¡Autenticación Fallida! Inténtelo de nuevo o utilice un método alternativo. Error de autenticación: %1$s @@ -135,10 +105,92 @@ Queda 1 día %1$s Días restantes Confirmar Datos Biométricos - - Bloqueo de Aplicación Deshabilitado - Bloquear Inmediatamente - Bloquear después de 1 minuto - Bloquear después de 30 minutos + Tema de la aplicación + + \n%1$s + \nPersonaliza la apariencia de la aplicación con las opciones de tema Noche, Día o Sistema. + + + Vista Inmersiva + + \nOculte la barra de estado y la barra de navegación para disfrutar de una experiencia de pantalla completa sin distracciones. + + + Papelera de Reciclaje(Android 11+) + + \nHabilite la función para mover archivos eliminados a la Papelera de reciclaje en lugar de eliminarlos de forma permanente. + + + Multiplicador del Tamaño de la Cuadrícula + + \nAjuste el espacio que ocupa cada elemento en el diseño de la cuadrícula. + + + Auto Colorear Beta + + \nGenera dinámicamente un tema basado en los colores de la ilustración del medio que se está reproduciendo actualmente. + + + Política de Privacidad + + \nHaga clic aquí para ver la política de privacidad. + + + Galería en Vivo + + \nHabilitar animaciones y reproducción automática de videos en la galería. + + Escala de fuente + + \nAjuste el tamaño de la fuente para facilitar la lectura del texto. Elija una escala que se adapte a sus + preferencias para mejor legibilidad. + + + Barras del sistema + + \nHabilite las barras de sistema transparentes para una experiencia de borde a borde \n(Android 6.0+) + + + Informar un problema + + \nPóngase en contacto con nuestro equipo de soporte para obtener ayuda o informar problemas. + + + + Oscuro + Claro + Sincronizar con el Sistema + + Versión + Versión: %1$s + Modo Seguro + + \nEl modo seguro oculta el contenido de la aplicación en la vista previa de la aplicación, lo que protege la privacidad del usuario cuando + la aplicación no está en uso activo. + + + Bloqueo de la Aplicación Beta + \n%1$s + + \nAsegura la Galería con autenticación biométrica. + + + Barra de Navegación Coloreada + + Utilice el color de acento en la barra de navegación. + + + Sobre nosotros + %1$.1fx + Sistema + Únete a la Beta + Actualiza la Galería + Calificanos + Comparte con Amigos + + Desactivado + Instantáneo + 1 Min + 30 Min diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0fc8a42..96d23cc 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -15,7 +15,7 @@ 5. The snackbar message should have this structure: Title\n bold + experimental + message. The string resources should follow the above rules strictly --> - Gallery + Gallery Storage Permission Allow Gallery requires permission to access your @@ -40,27 +40,9 @@ Sync with System General Appearance - App Theme - Customize the app\'s look with Night, Day, or System theme options. - Immersive View - Hide the status bar and navigation bar for a distraction-free, full-screen experience. - Trash Can - When enabled, deleted files are moved to a Trash Can folder instead of being permanently deleted. - Grid size - Control how much space each item takes up in the grid layout. - Live gallery - Enable animations and video autoplay in the gallery. - Font scale - Adjust the size of text throughout the app. - System Bars - Control the transparency of the system bars to customize the app\'s appearance Feedback About Gallery - Privacy Policy - Click here to view the privacy policy - Report an issue - Get in touch with our support team for assistance or to report issues - %1$s\nClick to check for updates. + Something went wrong. An unknown error occurred. Report Timeline @@ -70,10 +52,7 @@ Bin Recycle Bin Favourites - Secure Mode - Secure Mode obscures app content in the app preview, - protecting user privacy when the app is not in active use. - + %1$sx Last modified @@ -104,17 +83,14 @@ You’re all set! No updates available. Update ready! Installation can proceed now. Install - App Version + Verify Your Identity Authenticate with Biometrics Authenticate with your fingerprint or facial recognition to continue. This step is crucial for safeguarding your data. Enable biometric authentication in settings. - App Lock Beta - Unlock with biometrics\nWhen enabled, you\'ll need - to use the fingerprint, face or other unique identifiers to open Gallery. - + Favorites updated! New Media Player App! \n🎶 Check out our new Media Player! Enjoy your favorite tunes @@ -122,8 +98,7 @@ Get - Color NavBar - Use the accent color in the navigation bar. + Dismiss Authentication Failed! Please try again or use an alternative method. Authentication Error: %1$s @@ -136,11 +111,105 @@ 1 day left %1$s days left Confirm Biometric - - App Lock Disabled - Lock Immediately - Lock After 1 Minute - Lock After 30 Minutes + + App Theme + + \n%1$s + \nCustomize the app\'s look with Night, Day, or System theme options. + + + + Immersive View + + \nHide the status bar and navigation bar for a distraction-free, full-screen experience. + + + + Recycle Bin (Android 11+) + + \nEnable to move deleted files to the Recycle Bin instead of permanently deleting them. + + + + Grid Size Multiplier + + \nAdjust the space each item takes up in the grid layout. + + + + Auto Colorize Beta + + \nDynamically generate a theme based on the colors of the currently playing media artwork. + + + + Privacy Policy + + \nClick here to view the privacy policy. + + + + Live gallery + + \nEnable animations and video autoplay in the gallery. + + Font Scale + + \nAdjust the font size to make text easier to read. Choose a scale that suits your + preference for better readability. + + + + System Bars + + \nEnable transparent system bars for an edge-to-edge experience \n(Android 6.0+) + + + + Report an issue + + \nGet in touch with our support team for assistance or to report issues + + + + + Dark + Light + Sync with System + Version + Version: %1$s + Secure Mode + + \nSecure Mode obscures app content in the app preview, protecting user privacy when the + app is not in active use. + + + + App Lock Beta + \n%1$s + + \nSecure Gallery with biometric authentication. + + + Color NavBar + + Use the accent color in the navigation bar. + + + About us + %1$.1fx + System + Join the Beta + Update Gallery + Rate Us + Share with Friends + + + Disabled + Instant + 1 Min + 30 Min + \ No newline at end of file diff --git a/crowdin.yml b/crowdin.yml index 8dce3d2..f5fab77 100644 --- a/crowdin.yml +++ b/crowdin.yml @@ -1,4 +1,5 @@ preserve_hierarchy: true +project_id: 718355 files: - source: "**/values/strings.xml" translation: "/values-%android_code%/%original_file_name%" #CORRECT diff --git a/domain/src/main/java/com/zs/domain/store/MediaProviderImpl.kt b/domain/src/main/java/com/zs/domain/store/MediaProviderImpl.kt index 273ae2c..5add12b 100644 --- a/domain/src/main/java/com/zs/domain/store/MediaProviderImpl.kt +++ b/domain/src/main/java/com/zs/domain/store/MediaProviderImpl.kt @@ -269,7 +269,7 @@ internal class MediaProviderImpl( } val id = c.getLong(0) val size = c.getInt(2) - val lastModified = c.getLong(3) + val lastModified = c.getLong(3) * 1000 val index = list.indexOfFirst { it.path == parent } if (index == -1) { list += Folder(id, parent, 1, size, lastModified) diff --git a/foundation/src/main/java/com/zs/foundation/Headers.kt b/foundation/src/main/java/com/zs/foundation/Headers.kt index b71ce32..8180d76 100644 --- a/foundation/src/main/java/com/zs/foundation/Headers.kt +++ b/foundation/src/main/java/com/zs/foundation/Headers.kt @@ -38,12 +38,14 @@ import com.primex.material2.Label import androidx.compose.foundation.layout.PaddingValues as Padding import com.zs.foundation.ContentPadding as CP + /** * Item header. * //TODO: Handle padding in parent composable. */ -private val HEADER_MARGIN = Padding(CP.medium, CP.large, CP.medium, CP.normal) +private val HEADER_MARGIN = Padding(0.dp, CP.normal, 0.dp, CP.medium) private val CHAR_HEADER_SHAPE = RoundedCornerShape(50, 25, 25, 25) +private val NORMAL_HEADER_SHAPE = RoundedCornerShape(16, 50, 16, 16) /** * Represents header for list/grid item groups. @@ -67,7 +69,7 @@ fun ListHeader( style = AppTheme.typography.headlineLarge, modifier = modifier .padding(HEADER_MARGIN) - .border(0.5.dp, Color.Gray.copy(0.12f), CHAR_HEADER_SHAPE) + .border(0.5.dp, AppTheme.colors.background(30.dp), CHAR_HEADER_SHAPE) .background(AppTheme.colors.background(1.dp), CHAR_HEADER_SHAPE) .padding(horizontal = CP.large, vertical = CP.medium), ) @@ -77,18 +79,18 @@ fun ListHeader( else -> Label( text = value, maxLines = 2, - fontWeight = FontWeight.Normal, - style = AppTheme.typography.titleSmall, + style = AppTheme.typography.titleMedium, modifier = modifier .padding(HEADER_MARGIN) .widthIn(max = 220.dp) - .border(0.5.dp, Color.Gray.copy(0.12f), CircleShape) + .border(0.5.dp, AppTheme.colors.background(30.dp), CircleShape) .background(AppTheme.colors.background(1.dp), CircleShape) .padding(horizontal = CP.normal, vertical = CP.small) ) } } + @NonRestartableComposable @Composable fun ListHeader( @@ -105,7 +107,7 @@ fun ListHeader( style = AppTheme.typography.headlineLarge, modifier = modifier .padding(HEADER_MARGIN) - .border(0.5.dp, AppTheme.colors.background(5.dp), CHAR_HEADER_SHAPE) + .border(0.5.dp, AppTheme.colors.background(30.dp), CHAR_HEADER_SHAPE) .background(AppTheme.colors.background(1.dp), CHAR_HEADER_SHAPE) .padding(horizontal = CP.large, vertical = CP.medium), ) @@ -115,12 +117,11 @@ fun ListHeader( else -> Label( text = value, maxLines = 2, - fontWeight = FontWeight.Normal, - style = AppTheme.typography.titleSmall, + style = AppTheme.typography.titleMedium, modifier = modifier .padding(HEADER_MARGIN) .widthIn(max = 220.dp) - .border(0.5.dp, AppTheme.colors.background(5.dp), CircleShape) + .border(0.5.dp, AppTheme.colors.background(30.dp), CircleShape) .background(AppTheme.colors.background(1.dp), CircleShape) .padding(horizontal = CP.normal, vertical = CP.medium) ) diff --git a/foundation/src/main/java/com/zs/foundation/menu/Action.kt b/foundation/src/main/java/com/zs/foundation/menu/Action.kt new file mode 100644 index 0000000..c8d8a0d --- /dev/null +++ b/foundation/src/main/java/com/zs/foundation/menu/Action.kt @@ -0,0 +1,71 @@ +package com.zs.foundation.menu + +import androidx.annotation.StringRes +import androidx.compose.ui.graphics.vector.ImageVector + +/** + * Represents a single item within a menu. The [label] represents the id of this item as well. + * + * @property label The text label displayed for the menu item. + * Can include a subtitle on a new line, up to two lines maximum. + * @property icon Optional icon associated with the menu item. + * @property enabled Indicates whether the menu item is currently enabled and interactive. + */ +interface Action { + @get:StringRes val label: Int + val id: String + val icon: ImageVector? + val enabled: Boolean + + /** + * Creates a copy of this [MenuItem] with the specified modifications. + * + * @param title The text label for the new menu item. + * @param icon The icon for the new menu item. + * @param enabled Whether the new menu item is enabled. + * @return A new [MenuItem] instance with the applied modifications. + */ + fun copy( + enabled: Boolean = this.enabled, + ): Action = ActionImpl(label, id, icon, enabled) + + companion object { + + /** + * @see MenuItem + */ + operator fun invoke( + @StringRes label: Int, + icon: ImageVector? = null, + enabled: Boolean = true, + id: String = label.toString() + ): Action = ActionImpl(label, id, icon, enabled) + } +} + +/** + * Default implementation of [MenuItem]. + */ +private class ActionImpl( + override val label: Int , + override val id: String, + override val icon: ImageVector?, + override val enabled: Boolean +) : Action { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as ActionImpl + + return id == other.id + } + + override fun hashCode(): Int { + return id.hashCode() + } + + override fun toString(): String { + return "Action(Label = $label, id = $id, icon = $icon, enabled = $enabled)" + } +} \ No newline at end of file diff --git a/foundation/src/main/java/com/zs/foundation/menu/Menu.kt b/foundation/src/main/java/com/zs/foundation/menu/Menu.kt index dd8055a..7064680 100644 --- a/foundation/src/main/java/com/zs/foundation/menu/Menu.kt +++ b/foundation/src/main/java/com/zs/foundation/menu/Menu.kt @@ -29,8 +29,8 @@ import com.primex.material2.menu.DropDownMenu2 */ @Composable inline fun RowScope.Menu( - items: List, - noinline onItemClicked: (item: MenuItem) -> Unit, + items: List, + noinline onItemClicked: (item: Action) -> Unit, moreIcon: ImageVector = Icons.Outlined.MoreVert, collapsed: Int = 4 ) { diff --git a/foundation/src/main/java/com/zs/foundation/menu/MenuItem.kt b/foundation/src/main/java/com/zs/foundation/menu/MenuItem.kt deleted file mode 100644 index d76732b..0000000 --- a/foundation/src/main/java/com/zs/foundation/menu/MenuItem.kt +++ /dev/null @@ -1,90 +0,0 @@ -package com.zs.foundation.menu - -import androidx.annotation.StringRes -import androidx.compose.ui.graphics.vector.ImageVector - -/** - * Represents a single item within a menu. - */ -interface MenuItem { - - /** - * Unique identifier for the menu item. - */ - val id: String - - /** - * The text label displayed for the menu item. - * Can include a subtitle on a new line, up to two lines maximum. - */ - @get:StringRes - val label: Int - - /** - * Optional icon associated with the menu item. - */ - val icon: ImageVector? - - /** - * Indicates whether the menu item is currently enabled and interactive. - */ - val enabled: Boolean - - /** - * Creates a copy of this [MenuItem] with the specified modifications. - * - * @param title The text label for the new menu item. - * @param icon The icon for the new menu item. - * @param enabled Whether the new menu item is enabled. - * @return A new [MenuItem] instance with the applied modifications. - */ - fun copy( - enabled: Boolean = this.enabled, - @StringRes label: Int = this.label, - icon: ImageVector? = this.icon, - ): MenuItem = MenuItemImpl(id, label, icon, enabled) - - - companion object { - - /** - * Creates a [MenuItem] instance. - * - * @param id Unique identifier for the menu item. - * @param label The text label displayed for the menu item. - * Can include a subtitle on a new line, up to two lines maximum. - * @param icon Optional icon associated with the menu item. - * @param enabled Indicates whether the menu item is currently enabled. - */ - operator fun invoke( - id: String, - @StringRes label: Int, - icon: ImageVector? = null, - enabled: Boolean = true - ): MenuItem = MenuItemImpl(id, label, icon, enabled) - } -} - - -/** - * Default implementation of [MenuItem]. - */ -private class MenuItemImpl( - override val id: String, - override val label: Int , - override val icon: ImageVector?, - override val enabled: Boolean -) : MenuItem { - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as MenuItemImpl - - return id == other.id - } - - override fun hashCode(): Int { - return id.hashCode() - } -} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 41ec3bd..6a84d6e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,9 +1,9 @@ [versions] -agp = "8.7.2" +agp = "8.7.3" kotlin = "2.1.0" compose = "1.8.0-alpha06" media3 = "1.5.0" -toolkit = "2.1.0" +toolkit = "2.2.3" material_icons = "1.7.5" coil = "3.0.4"