Compare commits

..
7 Commits
Author SHA1 Message Date
OneWay 71ad849166 Remove the brightness dip in screen transition crossfades
- Fading the outgoing and incoming screens simultaneously makes content
  present on both (the camera FAB) dip to ~75% opacity mid-transition
- Fade only one layer: hold the outgoing destination opaque under the
  fading-in incoming one, and reveal the destination instantly under the
  fading-out one on back
2026-10-15 13:50:31 +08:00
OneWay 761db7a69d Keep the camera FAB stable across folder navigation
- Skip rendering the FAB until saved preferences load, so it is never
  drawn at the default corner and never jumps to its dragged position
- Share user preferences as one hot process-wide StateFlow instead of a
  cold disk read per screen, so a newly created gallery screen receives
  the FAB position synchronously in the ViewModel constructor
2026-10-15 13:50:31 +08:00
OneWay 428749d8a8 Show the gallery folder name from the first frame
- Carry the folder name through GalleryRoute into the ViewModel's
  SavedStateHandle, so the TopBar title is correct immediately on
  navigation, before any directory listing completes
- Seed a provisional name from the SAF URI segment in loadPhotos as a
  fallback for entry points without a name (after-camera navigation)
- Drop the 'Gallery' fallback title and the unused gallery_title string
2026-10-15 13:50:31 +08:00
OneWay 460c50bfd9 Fix photo viewer flicker when opening an image
- Keep the grid composed under the viewer instead of swapping it for a
  black box, removing the momentary black flash while the pager draws
  its first frame after tapping a photo
- Restore the pre-audit fade: thumbnail stays fully opaque under the
  fading-in full image instead of crossfading both over black
2026-10-15 13:50:31 +08:00
OneWay 5832e2db4a Replace launcher icon with adaptive-only vector glyph 2026-10-15 13:50:31 +08:00
OneWay 76ea8ba9a0 Make GalleryUiState derivations lazy and drop dead sorts
copy() ran on every state update (each drag-select step included) and eagerly
recomputed five derived properties: two unused O(N log N) sorts, an O(N)
selection filter, an O(N) selectable-URI set and its containsAll check.

- Remove sortedPhotos/sortedSubfolders: never read (the screen sorts with its
  own remember() calls)
- selectedPhotos/selectablePhotoUris/allPhotosSelected become by lazy:
  computed at most once per instance, only when actually read; body delegates
  stay out of generated copy()/equals(), @Immutable semantics hold
- Memoize selectablePhotoUris across drag-select steps in the ViewModel keyed
  by photos list identity, so a drag step no longer recomputes the O(N) set
2026-10-14 16:06:12 +08:00
OneWay c2ddfdf89d Apply Compose performance audit fixes
- FolderTreePickerSheet: add contentType to lazy folder items for slot reuse
- GalleryScreen: use plain remember instead of derivedStateOf over non-state
  inputs; defer photo fade alpha to Draw phase via graphicsLayer; react to
  zoom threshold with snapshotFlow instead of per-frame effect relaunch
- GalleryViewModel: drop @Immutable from MovePhotoSheetState so Strong
  Skipping compares the lazy feed by O(1) reference equality
- SystemCameraCapture: move temp-file disk I/O off the main thread
2026-10-14 16:06:12 +08:00
20 changed files with 130 additions and 115 deletions

No files matched your search

@@ -9,8 +9,13 @@ import androidx.datastore.preferences.core.floatPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import javax.inject.Inject
import javax.inject.Singleton
@@ -41,17 +46,26 @@ class UserPreferencesRepository @Inject constructor(
val SCREEN_TRANSITIONS_ENABLED = booleanPreferencesKey("screen_transitions_enabled")
}
val userPreferencesFlow: Flow<UserPreferences> = context.dataStore.data.map { prefs ->
UserPreferences(
rootFolderUri = prefs[Keys.ROOT_FOLDER_URI],
cameraFabXFraction = prefs[Keys.CAMERA_FAB_X_FRACTION]?.coerceIn(0f, 1f) ?: 1f,
cameraFabYFraction = prefs[Keys.CAMERA_FAB_Y_FRACTION]?.coerceIn(0f, 1f) ?: 1f,
gallerySortAscending = prefs[Keys.GALLERY_SORT_ASCENDING] ?: true,
galleryGridMode = prefs[Keys.GALLERY_GRID_MODE] ?: true,
zipJpegCompression = prefs[Keys.ZIP_JPEG_COMPRESSION] ?: true,
screenTransitionsEnabled = prefs[Keys.SCREEN_TRANSITIONS_ENABLED] ?: true,
// Hot, process-wide cache: the DataStore file is read once, and every screen
// created later receives the current value synchronously on subscription,
// instead of re-reading disk per screen and flashing default positions.
val userPreferencesFlow: StateFlow<UserPreferences> = context.dataStore.data
.map { prefs ->
UserPreferences(
rootFolderUri = prefs[Keys.ROOT_FOLDER_URI],
cameraFabXFraction = prefs[Keys.CAMERA_FAB_X_FRACTION]?.coerceIn(0f, 1f) ?: 1f,
cameraFabYFraction = prefs[Keys.CAMERA_FAB_Y_FRACTION]?.coerceIn(0f, 1f) ?: 1f,
gallerySortAscending = prefs[Keys.GALLERY_SORT_ASCENDING] ?: true,
galleryGridMode = prefs[Keys.GALLERY_GRID_MODE] ?: true,
zipJpegCompression = prefs[Keys.ZIP_JPEG_COMPRESSION] ?: true,
screenTransitionsEnabled = prefs[Keys.SCREEN_TRANSITIONS_ENABLED] ?: true,
)
}
.stateIn(
scope = CoroutineScope(SupervisorJob() + Dispatchers.IO),
started = SharingStarted.Eagerly,
initialValue = UserPreferences(),
)
}
suspend fun setRootFolderUri(uri: String) {
context.dataStore.edit { prefs ->
@@ -32,7 +32,10 @@ import kotlinx.serialization.Serializable
object FoldersRoute
@Serializable
data class GalleryRoute(val folderUri: String)
data class GalleryRoute(
val folderUri: String,
val folderName: String = "",
)
@Serializable
data class CameraRoute(
@@ -64,14 +67,20 @@ fun OneCameraNavGraph(navController: NavHostController) {
navController = navController,
startDestination = FoldersRoute,
enterTransition = { if (transitionsEnabled) fadeIn(tween(250)) else EnterTransition.None },
exitTransition = { if (transitionsEnabled) fadeOut(tween(250)) else ExitTransition.None },
popEnterTransition = { if (transitionsEnabled) fadeIn(tween(250)) else EnterTransition.None },
exitTransition = {
if (transitionsEnabled) {
ExitTransition.KeepUntilTransitionsFinished
} else {
ExitTransition.None
}
},
popEnterTransition = { EnterTransition.None },
popExitTransition = { if (transitionsEnabled) fadeOut(tween(250)) else ExitTransition.None },
) {
composable<FoldersRoute> {
FoldersScreen(
onNavigateToGallery = { folderUri ->
navController.navigate(GalleryRoute(folderUri))
onNavigateToGallery = { folderUri, folderName ->
navController.navigate(GalleryRoute(folderUri, folderName))
},
onNavigateToCamera = { folderUri ->
navController.navigate(
@@ -93,8 +102,8 @@ fun OneCameraNavGraph(navController: NavHostController) {
onNavigateToCamera = { folderUri ->
navController.navigate(CameraRoute(folderUri))
},
onNavigateToSubfolder = { folderUri ->
navController.navigate(GalleryRoute(folderUri))
onNavigateToSubfolder = { folderUri, folderName ->
navController.navigate(GalleryRoute(folderUri, folderName))
},
onNavigateBack = { navController.popBackStack() },
)
@@ -37,7 +37,9 @@ import androidx.lifecycle.compose.LifecycleResumeEffect
import com.example.onecamera.R
import com.example.onecamera.ui.components.OneCameraEmptyState
import com.example.onecamera.ui.components.OneCameraScreenPadding
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext
import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.launch
import java.io.File
@@ -111,23 +113,26 @@ fun SystemCameraCapture(
LaunchedEffect(hasLaunchedCamera) {
if (!hasLaunchedCamera) {
viewModel.clearLastSaveResult()
val tempDir = File(context.cacheDir, "camera_captures").apply { mkdirs() }
val (preparedPath, preparedUri) = withContext(Dispatchers.IO) {
val tempDir = File(context.cacheDir, "camera_captures").apply { mkdirs() }
tempDir.listFiles()?.forEach { runCatching { it.delete() } }
val tempFile = File(tempDir, "capture_${System.currentTimeMillis()}.jpg").apply {
createNewFile()
tempDir.listFiles()?.forEach { runCatching { it.delete() } }
val tempFile = File(tempDir, "capture_${System.currentTimeMillis()}.jpg").apply {
createNewFile()
}
val uri = FileProvider.getUriForFile(
context,
"${context.packageName}.fileprovider",
tempFile,
)
tempFile.absolutePath to uri.toString()
}
val uri = FileProvider.getUriForFile(
context,
"${context.packageName}.fileprovider",
tempFile,
)
tempPhotoUriString = uri.toString()
tempPhotoPath = tempFile.absolutePath
tempPhotoUriString = preparedUri
tempPhotoPath = preparedPath
hasLaunchedCamera = true
hasReceivedCameraResult = false
launchUptimeMillis = SystemClock.elapsedRealtime()
takePictureLauncher.launch(uri)
takePictureLauncher.launch(Uri.parse(preparedUri))
}
}
@@ -143,7 +148,7 @@ fun SystemCameraCapture(
tempPhotoUriString != null
) {
val tempLength = tempPhotoPath?.let { path ->
runCatching { File(path).length() }.getOrNull()
runCatching { withContext(Dispatchers.IO) { File(path).length() } }.getOrNull()
} ?: 0L
val tempPhotoUri = tempPhotoUriString?.let(Uri::parse)
if (tempLength > 0L && tempPhotoUri != null) {
@@ -105,7 +105,11 @@ fun FolderTreePickerSheet(
.fillMaxWidth()
.weight(1f, fill = false),
) {
items(state.folders, key = { it.uri }) { folder ->
items(
items = state.folders,
key = { it.uri },
contentType = { "folder" },
) { folder ->
val isCurrent = currentFolderUri == folder.uri.toString()
val isSelected = selectedFolderUri == folder.uri.toString()
ListItem(
@@ -57,7 +57,7 @@ import com.example.onecamera.ui.components.oneCameraTopAppBarColors
@OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class)
@Composable
fun FoldersScreen(
onNavigateToGallery: (String) -> Unit,
onNavigateToGallery: (folderUri: String, folderName: String) -> Unit,
onNavigateToCamera: (String) -> Unit,
onNavigateToSettings: () -> Unit,
viewModel: FoldersViewModel = hiltViewModel(),
@@ -230,7 +230,7 @@ fun FoldersScreen(
onClick = {
viewModel.prefetchFolderForNavigation(folder.uri.toString())
onNavigateToGallery(folder.uri.toString())
onNavigateToGallery(folder.uri.toString(), folder.name)
},
onCompress = { viewModel.zipFolderAndShare(folder) },
onCompressJpeg85 = {
@@ -41,11 +41,11 @@ import androidx.compose.material3.TopAppBar
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
@@ -86,13 +86,14 @@ import com.example.onecamera.ui.folders.RenameFolderDialog
import com.github.panpf.zoomimage.CoilZoomAsyncImage
import com.github.panpf.zoomimage.rememberCoilZoomState
import com.github.panpf.zoomimage.zoom.GestureType
import kotlinx.coroutines.flow.distinctUntilChanged
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class, ExperimentalComposeUiApi::class, ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun GalleryScreen(
folderUri: String,
onNavigateToCamera: (String) -> Unit,
onNavigateToSubfolder: (String) -> Unit,
onNavigateToSubfolder: (folderUri: String, folderName: String) -> Unit,
onNavigateBack: () -> Unit,
viewModel: GalleryViewModel = hiltViewModel(),
) {
@@ -198,10 +199,8 @@ fun GalleryScreen(
uiState.subfolders.sortedByDescending { it.name.lowercase() }
}
}
val sortedGalleryPhotos by remember(sortedPhotos) {
derivedStateOf {
sortedPhotos.filter { it.kind == GalleryFileKind.Photo }
}
val sortedGalleryPhotos = remember(sortedPhotos) {
sortedPhotos.filter { it.kind == GalleryFileKind.Photo }
}
when (val dialog = uiState.dialog) {
@@ -282,9 +281,7 @@ fun GalleryScreen(
title = {
Column {
Text(
uiState.folderName.ifEmpty {
stringResource(R.string.gallery_title)
},
uiState.folderName,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
@@ -409,14 +406,6 @@ fun GalleryScreen(
Box(modifier = Modifier.fillMaxSize()) {
when {
uiState.selectedPhoto != null -> {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Black),
)
}
!uiState.viewPreferencesLoaded -> {
Box(modifier = Modifier.fillMaxSize())
}
@@ -443,7 +432,7 @@ fun GalleryScreen(
onFolderClick = {
viewModel.prefetchFolderForNavigation(it.uri.toString())
onNavigateToSubfolder(it.uri.toString())
onNavigateToSubfolder(it.uri.toString(), it.name)
},
onFolderCompress = { viewModel.zipFolderAndShare(it) },
onFolderCompressJpeg85 = { viewModel.zipFolderCompressedJpegsAndShare(it) },
@@ -485,12 +474,14 @@ fun GalleryScreen(
enter = fadeIn(tween(250)),
exit = fadeOut(tween(150)),
) {
DraggableCameraFab(
xFraction = uiState.cameraFabXFraction,
yFraction = uiState.cameraFabYFraction,
onPositionChange = viewModel::saveCameraFabPosition,
onClick = { onNavigateToCamera(uiState.currentFolderUri ?: folderUri) },
)
if (uiState.viewPreferencesLoaded) {
DraggableCameraFab(
xFraction = uiState.cameraFabXFraction,
yFraction = uiState.cameraFabYFraction,
onPositionChange = viewModel::saveCameraFabPosition,
onClick = { onNavigateToCamera(uiState.currentFolderUri ?: folderUri) },
)
}
}
SelectionActionsBar(
@@ -537,10 +528,8 @@ internal fun FullscreenPhotoPagerContent(
onDismiss: () -> Unit,
onDelete: (Photo) -> Unit,
) {
val initialPage by remember(photos, initialPhoto.uri) {
derivedStateOf {
photos.indexOfFirst { it.uri == initialPhoto.uri }.coerceAtLeast(0)
}
val initialPage = remember(photos, initialPhoto.uri) {
photos.indexOfFirst { it.uri == initialPhoto.uri }.coerceAtLeast(0)
}
val pagerState = rememberPagerState(initialPage = initialPage) { photos.size.coerceAtLeast(1) }
@@ -646,7 +635,6 @@ private fun ZoomablePhoto(
animationSpec = tween(durationMillis = fadeMs),
label = "fullAlpha",
)
val scale by remember { derivedStateOf { zoomState.zoomable.transform.scaleX } }
LaunchedEffect(photo.uri) {
@@ -663,11 +651,13 @@ private fun ZoomablePhoto(
}
}
LaunchedEffect(scale) {
val zoomed = scale > 1.01f
onZoomChange(zoomed)
zoomState.subsampling.setDisabled(!zoomed)
LaunchedEffect(zoomState) {
snapshotFlow { zoomState.zoomable.transform.scaleX > 1.01f }
.distinctUntilChanged()
.collect { zoomed ->
onZoomChange(zoomed)
zoomState.subsampling.setDisabled(!zoomed)
}
}
Box(modifier = modifier.fillMaxSize()) {
@@ -4,6 +4,7 @@ import android.content.Context
import android.net.Uri
import androidx.compose.runtime.Immutable
import androidx.core.net.toUri
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import coil3.SingletonImageLoader
@@ -45,7 +46,6 @@ sealed interface GalleryDialog {
data class DeleteSubfolder(val folder: FolderNode) : GalleryDialog
}
@Immutable
data class MovePhotoSheetState(
val file: Photo? = null,
val files: List<Photo> = emptyList(),
@@ -96,26 +96,25 @@ data class GalleryUiState(
val error: String? = null,
) {
val isSelectionMode: Boolean = selectedPhotoUris.isNotEmpty()
val selectedPhotos: List<Photo> = photos.filter { it.uri.toString() in selectedPhotoUris }
val sortedSubfolders: List<FolderNode> = if (sortAscending) {
subfolders.sortedBy { it.name.lowercase() }
} else {
subfolders.sortedByDescending { it.name.lowercase() }
val selectedPhotos: List<Photo> by lazy {
photos.filter { it.uri.toString() in selectedPhotoUris }
}
val sortedPhotos: List<Photo> = if (sortAscending) {
photos.sortedBy { it.name.lowercase() }
} else {
photos.sortedByDescending { it.name.lowercase() }
val selectablePhotoUris: Set<String> by lazy {
photos.asSequence()
.filter { it.kind == GalleryFileKind.Photo }
.mapTo(HashSet()) { it.uri.toString() }
}
val allPhotosSelected: Boolean by lazy {
selectablePhotoUris.isNotEmpty() && selectedPhotoUris.containsAll(selectablePhotoUris)
}
val selectablePhotoUris: Set<String> = photos
.filter { it.kind == GalleryFileKind.Photo }
.map { it.uri.toString() }
.toSet()
val allPhotosSelected: Boolean = selectablePhotoUris.isNotEmpty() && selectedPhotoUris.containsAll(selectablePhotoUris)
}
@HiltViewModel
class GalleryViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
private val preferencesRepository: UserPreferencesRepository,
private val listGalleryUseCase: ListGalleryUseCase,
private val listFolderTreeUseCase: ListFolderTreeUseCase,
@@ -131,7 +130,11 @@ class GalleryViewModel @Inject constructor(
) : ViewModel() {
val uiState: StateFlow<GalleryUiState>
field: MutableStateFlow<GalleryUiState> = MutableStateFlow(GalleryUiState())
field: MutableStateFlow<GalleryUiState> = MutableStateFlow(
// Navigation puts type-safe route args into the SavedStateHandle, so the
// title is correct from the very first frame, before any listing completes.
GalleryUiState(folderName = savedStateHandle.get<String>("folderName").orEmpty()),
)
private var forceReload = false
@@ -202,6 +205,9 @@ class GalleryViewModel @Inject constructor(
loadJob = viewModelScope.launch {
uiState.value = uiState.value.copy(
currentFolderUri = folderUriString,
folderName = state.folderName.ifEmpty {
folderUriString.substringAfterLast(':').substringAfterLast('/')
},
isLoading = true,
error = null,
)
@@ -343,18 +349,31 @@ class GalleryViewModel @Inject constructor(
fun clearSelection() {
dragSelectionBaseline = emptySet()
dragSelectableCache = null
uiState.value = uiState.value.copy(selectedPhotoUris = emptySet())
}
private var dragSelectionBaseline: Set<String> = emptySet()
private var dragSelectableCache: Pair<List<Photo>, Set<String>>? = null
private fun currentSelectablePhotoUris(): Set<String> {
val photos = uiState.value.photos
dragSelectableCache?.let { (cachedPhotos, uris) ->
if (cachedPhotos === photos) return uris
}
val uris = uiState.value.selectablePhotoUris
dragSelectableCache = photos to uris
return uris
}
fun beginDragSelection(anchorUri: String) {
dragSelectionBaseline = uiState.value.selectedPhotoUris + anchorUri
uiState.value = uiState.value.copy(selectedPhotoUris = dragSelectionBaseline)
}
fun updateDragSelection(rangeUris: Set<String>) {
val selectable = uiState.value.selectablePhotoUris
val selectable = currentSelectablePhotoUris()
uiState.value = uiState.value.copy(
selectedPhotoUris = dragSelectionBaseline + rangeUris.intersect(selectable),
)
@@ -362,6 +381,7 @@ class GalleryViewModel @Inject constructor(
fun endDragSelection() {
dragSelectionBaseline = emptySet()
dragSelectableCache = null
}
fun zipSelectedPhotosAndShare() {
@@ -4,20 +4,7 @@
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:strokeColor="#000000"
android:strokeWidth="3.375"
android:strokeLineJoin="round"
android:pathData="M 43.453125 32.90625 L 64.546875 32.90625 a 10.546875 10.546875 0 0 1 10.546875 10.546875 L 75.09375 64.546875 a 10.546875 10.546875 0 0 1 -10.546875 10.546875 L 43.453125 75.09375 a 10.546875 10.546875 0 0 1 -10.546875 -10.546875 L 32.90625 43.453125 a 10.546875 10.546875 0 0 1 10.546875 -10.546875 Z" />
<path
android:strokeColor="#000000"
android:strokeWidth="3.375"
android:pathData="M 41.34375 54 a 12.65625 12.65625 0 1 0 25.3125 0 a 12.65625 12.65625 0 1 0 -25.3125 0 Z" />
<path
android:fillColor="#d94e47"
android:pathData="M 68.71875 38.390625 a 2.53125 2.53125 0 1 0 5.0625 0 a 2.53125 2.53125 0 1 0 -5.0625 0 Z" />
android:fillColor="#C9584D"
android:pathData="M66.90,53.85C66.90,54.93 66.82,56.08 66.67,57.08C66.53,58.07 66.30,58.95 66.00,59.80C65.71,60.66 65.34,61.46 64.91,62.20C64.48,62.94 63.97,63.63 63.41,64.25C62.86,64.87 62.23,65.42 61.56,65.90C60.89,66.38 60.16,66.79 59.39,67.11C58.61,67.44 57.82,67.69 56.92,67.85C56.02,68.02 54.97,68.10 54.00,68.10C53.03,68.10 51.98,68.02 51.08,67.85C50.18,67.69 49.39,67.44 48.61,67.11C47.84,66.79 47.11,66.38 46.44,65.90C45.77,65.42 45.14,64.87 44.59,64.25C44.03,63.63 43.52,62.94 43.09,62.20C42.66,61.46 42.29,60.66 42.00,59.80C41.70,58.95 41.47,58.07 41.33,57.08C41.18,56.08 41.10,54.93 41.10,53.85C41.10,52.77 41.18,51.62 41.33,50.62C41.47,49.63 41.70,48.75 42.00,47.90C42.29,47.04 42.66,46.24 43.09,45.50C43.52,44.76 44.03,44.07 44.59,43.45C45.14,42.83 45.77,42.28 46.44,41.80C47.11,41.32 47.84,40.91 48.61,40.59C49.39,40.26 50.18,40.01 51.08,39.85C51.98,39.68 53.03,39.60 54.00,39.60C54.97,39.60 56.02,39.68 56.92,39.85C57.82,40.01 58.61,40.26 59.39,40.59C60.16,40.91 60.89,41.32 61.56,41.80C62.23,42.28 62.86,42.83 63.41,43.45C63.97,44.07 64.48,44.76 64.91,45.50C65.34,46.24 65.71,47.04 66.00,47.90C66.30,48.75 66.53,49.63 66.67,50.62C66.82,51.62 66.90,52.77 66.90,53.85ZM61.50,54.00C61.50,53.30 61.46,52.54 61.37,51.90C61.29,51.25 61.16,50.67 60.99,50.12C60.82,49.56 60.61,49.03 60.36,48.55C60.12,48.07 59.83,47.62 59.51,47.21C59.20,46.81 58.84,46.45 58.46,46.14C58.08,45.83 57.66,45.56 57.22,45.35C56.78,45.13 56.33,44.97 55.81,44.86C55.30,44.75 54.70,44.70 54.15,44.70C53.60,44.70 53.00,44.75 52.49,44.86C51.97,44.97 51.52,45.13 51.08,45.35C50.64,45.56 50.22,45.83 49.84,46.14C49.46,46.45 49.10,46.81 48.79,47.21C48.47,47.62 48.18,48.07 47.94,48.55C47.69,49.03 47.48,49.56 47.31,50.12C47.14,50.67 47.01,51.25 46.93,51.90C46.84,52.54 46.80,53.30 46.80,54.00C46.80,54.70 46.84,55.46 46.93,56.10C47.01,56.75 47.14,57.33 47.31,57.88C47.48,58.44 47.69,58.97 47.94,59.45C48.18,59.93 48.47,60.38 48.79,60.79C49.10,61.19 49.46,61.55 49.84,61.86C50.22,62.17 50.64,62.44 51.08,62.65C51.52,62.87 51.97,63.03 52.49,63.14C53.00,63.25 53.60,63.30 54.15,63.30C54.70,63.30 55.30,63.25 55.81,63.14C56.33,63.03 56.78,62.87 57.22,62.65C57.66,62.44 58.08,62.17 58.46,61.86C58.84,61.55 59.20,61.19 59.51,60.79C59.83,60.38 60.12,59.93 60.36,59.45C60.61,58.97 60.82,58.44 60.99,57.88C61.16,57.33 61.29,56.75 61.37,56.10C61.46,55.46 61.50,54.70 61.50,54.00Z" />
</vector>
@@ -4,20 +4,7 @@
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:strokeColor="#FFFFFF"
android:strokeWidth="3.375"
android:strokeLineJoin="round"
android:pathData="M 43.453125 32.90625 L 64.546875 32.90625 a 10.546875 10.546875 0 0 1 10.546875 10.546875 L 75.09375 64.546875 a 10.546875 10.546875 0 0 1 -10.546875 10.546875 L 43.453125 75.09375 a 10.546875 10.546875 0 0 1 -10.546875 -10.546875 L 32.90625 43.453125 a 10.546875 10.546875 0 0 1 10.546875 -10.546875 Z" />
<path
android:strokeColor="#FFFFFF"
android:strokeWidth="3.375"
android:pathData="M 41.34375 54 a 12.65625 12.65625 0 1 0 25.3125 0 a 12.65625 12.65625 0 1 0 -25.3125 0 Z" />
<path
android:fillColor="#FFFFFF"
android:pathData="M 67.5 38.390625 a 2.109375 2.109375 0 1 0 4.21875 0 a 2.109375 2.109375 0 1 0 -4.21875 0 Z" />
android:pathData="M66.90,53.85C66.90,54.93 66.82,56.08 66.67,57.08C66.53,58.07 66.30,58.95 66.00,59.80C65.71,60.66 65.34,61.46 64.91,62.20C64.48,62.94 63.97,63.63 63.41,64.25C62.86,64.87 62.23,65.42 61.56,65.90C60.89,66.38 60.16,66.79 59.39,67.11C58.61,67.44 57.82,67.69 56.92,67.85C56.02,68.02 54.97,68.10 54.00,68.10C53.03,68.10 51.98,68.02 51.08,67.85C50.18,67.69 49.39,67.44 48.61,67.11C47.84,66.79 47.11,66.38 46.44,65.90C45.77,65.42 45.14,64.87 44.59,64.25C44.03,63.63 43.52,62.94 43.09,62.20C42.66,61.46 42.29,60.66 42.00,59.80C41.70,58.95 41.47,58.07 41.33,57.08C41.18,56.08 41.10,54.93 41.10,53.85C41.10,52.77 41.18,51.62 41.33,50.62C41.47,49.63 41.70,48.75 42.00,47.90C42.29,47.04 42.66,46.24 43.09,45.50C43.52,44.76 44.03,44.07 44.59,43.45C45.14,42.83 45.77,42.28 46.44,41.80C47.11,41.32 47.84,40.91 48.61,40.59C49.39,40.26 50.18,40.01 51.08,39.85C51.98,39.68 53.03,39.60 54.00,39.60C54.97,39.60 56.02,39.68 56.92,39.85C57.82,40.01 58.61,40.26 59.39,40.59C60.16,40.91 60.89,41.32 61.56,41.80C62.23,42.28 62.86,42.83 63.41,43.45C63.97,44.07 64.48,44.76 64.91,45.50C65.34,46.24 65.71,47.04 66.00,47.90C66.30,48.75 66.53,49.63 66.67,50.62C66.82,51.62 66.90,52.77 66.90,53.85ZM61.50,54.00C61.50,53.30 61.46,52.54 61.37,51.90C61.29,51.25 61.16,50.67 60.99,50.12C60.82,49.56 60.61,49.03 60.36,48.55C60.12,48.07 59.83,47.62 59.51,47.21C59.20,46.81 58.84,46.45 58.46,46.14C58.08,45.83 57.66,45.56 57.22,45.35C56.78,45.13 56.33,44.97 55.81,44.86C55.30,44.75 54.70,44.70 54.15,44.70C53.60,44.70 53.00,44.75 52.49,44.86C51.97,44.97 51.52,45.13 51.08,45.35C50.64,45.56 50.22,45.83 49.84,46.14C49.46,46.45 49.10,46.81 48.79,47.21C48.47,47.62 48.18,48.07 47.94,48.55C47.69,49.03 47.48,49.56 47.31,50.12C47.14,50.67 47.01,51.25 46.93,51.90C46.84,52.54 46.80,53.30 46.80,54.00C46.80,54.70 46.84,55.46 46.93,56.10C47.01,56.75 47.14,57.33 47.31,57.88C47.48,58.44 47.69,58.97 47.94,59.45C48.18,59.93 48.47,60.38 48.79,60.79C49.10,61.19 49.46,61.55 49.84,61.86C50.22,62.17 50.64,62.44 51.08,62.65C51.52,62.87 51.97,63.03 52.49,63.14C53.00,63.25 53.60,63.30 54.15,63.30C54.70,63.30 55.30,63.25 55.81,63.14C56.33,63.03 56.78,62.87 57.22,62.65C57.66,62.44 58.08,62.17 58.46,61.86C58.84,61.55 59.20,61.19 59.51,60.79C59.83,60.38 60.12,59.93 60.36,59.45C60.61,58.97 60.82,58.44 60.99,57.88C61.16,57.33 61.29,56.75 61.37,56.10C61.46,55.46 61.50,54.70 61.50,54.00Z" />
</vector>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

-1
View File
@@ -46,7 +46,6 @@
<item quantity="other">%d photos</item>
</plurals>
<string name="gallery_title">Gallery</string>
<string name="gallery_create_subfolder">Create subfolder</string>
<string name="gallery_sort_ascending">Sort ascending</string>
<string name="gallery_sort_descending">Sort descending</string>