Skip to content

Commit e068d03

Browse files
committed
[feature|fix] File picker supports displaying the current path and clicking on the path to jump quickly; fix incorrect serialization of file picker callback, i.e. using navigation stack to pass the parameter
1 parent 7037a15 commit e068d03

File tree

8 files changed

+117
-40
lines changed

8 files changed

+117
-40
lines changed

app/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ android {
2222
minSdk = 24
2323
targetSdk = 34
2424
versionCode = 18
25-
versionName = "1.1-beta54"
25+
versionName = "1.1-beta55"
2626

2727
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
2828

app/src/main/java/com/skyd/anivu/config/Const.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,4 +50,6 @@ object Const {
5050
.apply { if (!exists()) mkdirs() }
5151

5252
val PICTURES_DIR = appContext.getExternalFilesDir(Environment.DIRECTORY_PICTURES)!!
53+
54+
val INTERNAL_STORAGE = Environment.getExternalStorageDirectory().absolutePath
5355
}

app/src/main/java/com/skyd/anivu/ui/component/AnimatedPlaceholder.kt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import androidx.compose.foundation.layout.Box
44
import androidx.compose.foundation.layout.Column
55
import androidx.compose.foundation.layout.fillMaxSize
66
import androidx.compose.foundation.layout.padding
7+
import androidx.compose.foundation.rememberScrollState
8+
import androidx.compose.foundation.verticalScroll
79
import androidx.compose.material3.MaterialTheme
810
import androidx.compose.material3.Text
911
import androidx.compose.runtime.Composable
@@ -18,7 +20,11 @@ fun AnimatedPlaceholder(
1820
@androidx.annotation.RawRes resId: Int,
1921
tip: String,
2022
) {
21-
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
23+
Box(
24+
modifier = Modifier
25+
.fillMaxSize()
26+
.verticalScroll(rememberScrollState()), contentAlignment = Alignment.Center
27+
) {
2228
Column(
2329
modifier = modifier,
2430
horizontalAlignment = Alignment.CenterHorizontally

app/src/main/java/com/skyd/anivu/ui/fragment/filepicker/FilePickerFragment.kt

Lines changed: 81 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,44 @@
11
package com.skyd.anivu.ui.fragment.filepicker
22

33
import android.os.Bundle
4-
import android.os.Environment
54
import android.view.LayoutInflater
65
import android.view.View
76
import android.view.ViewGroup
87
import androidx.activity.compose.BackHandler
8+
import androidx.compose.animation.animateContentSize
99
import androidx.compose.foundation.clickable
10+
import androidx.compose.foundation.horizontalScroll
1011
import androidx.compose.foundation.layout.Column
1112
import androidx.compose.foundation.layout.PaddingValues
13+
import androidx.compose.foundation.layout.Row
1214
import androidx.compose.foundation.layout.calculateEndPadding
1315
import androidx.compose.foundation.layout.calculateStartPadding
1416
import androidx.compose.foundation.layout.fillMaxWidth
1517
import androidx.compose.foundation.layout.padding
1618
import androidx.compose.foundation.lazy.LazyColumn
19+
import androidx.compose.foundation.rememberScrollState
20+
import androidx.compose.foundation.shape.RoundedCornerShape
1721
import androidx.compose.material.icons.Icons
22+
import androidx.compose.material.icons.automirrored.outlined.NavigateNext
1823
import androidx.compose.material.icons.outlined.Close
1924
import androidx.compose.material.icons.outlined.PhoneAndroid
2025
import androidx.compose.material3.Button
2126
import androidx.compose.material3.Icon
2227
import androidx.compose.material3.ListItem
28+
import androidx.compose.material3.MaterialTheme
2329
import androidx.compose.material3.Scaffold
2430
import androidx.compose.material3.SnackbarHost
2531
import androidx.compose.material3.SnackbarHostState
2632
import androidx.compose.material3.Text
2733
import androidx.compose.material3.TopAppBarDefaults
2834
import androidx.compose.material3.rememberTopAppBarState
2935
import androidx.compose.runtime.Composable
36+
import androidx.compose.runtime.LaunchedEffect
3037
import androidx.compose.runtime.getValue
3138
import androidx.compose.runtime.remember
39+
import androidx.compose.ui.Alignment
3240
import androidx.compose.ui.Modifier
41+
import androidx.compose.ui.draw.clip
3342
import androidx.compose.ui.input.nestedscroll.nestedScroll
3443
import androidx.compose.ui.platform.LocalLayoutDirection
3544
import androidx.compose.ui.res.painterResource
@@ -40,6 +49,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
4049
import com.skyd.anivu.R
4150
import com.skyd.anivu.base.BaseComposeFragment
4251
import com.skyd.anivu.base.mvi.getDispatcher
52+
import com.skyd.anivu.config.Const
4353
import com.skyd.anivu.ext.findMainNavController
4454
import com.skyd.anivu.ext.getMimeType
4555
import com.skyd.anivu.ext.popBackStackWithLifecycle
@@ -51,7 +61,6 @@ import com.skyd.anivu.ui.local.LocalNavController
5161
import com.skyd.anivu.util.fileicon.getFileIcon
5262
import dagger.hilt.android.AndroidEntryPoint
5363
import java.io.File
54-
import java.io.Serializable
5564

5665

5766
@AndroidEntryPoint
@@ -61,39 +70,31 @@ class FilePickerFragment : BaseComposeFragment() {
6170
container: ViewGroup?,
6271
savedInstanceState: Bundle?
6372
): View = setContentBase {
64-
val callback = arguments?.getSerializable(CALLBACK_KEY) as? FilePickerCallback
6573
val path = arguments?.getString(PATH_KEY)
6674
val pickFolder = arguments?.getBoolean(PICK_FOLDER_KEY)
6775
val extensionName = arguments?.getString(EXTENSION_NAME_KEY)
68-
if (callback == null || path == null || pickFolder == null || extensionName == null) {
76+
if (path == null || pickFolder == null || extensionName == null) {
6977
findMainNavController().popBackStackWithLifecycle()
7078
} else {
7179
FilePickerScreen(
7280
path = path,
7381
pickFolder = pickFolder,
7482
extensionName = extensionName,
75-
onFilePicked = { callback.onFilePicked(it) }
7683
)
7784
}
7885
}
7986
}
8087

8188
const val PATH_KEY = "path"
82-
const val CALLBACK_KEY = "callback"
8389
const val PICK_FOLDER_KEY = "pickFolder"
8490
const val EXTENSION_NAME_KEY = "extensionName"
85-
86-
87-
fun interface FilePickerCallback : Serializable {
88-
fun onFilePicked(file: File)
89-
}
91+
const val FILE_PICKER_NEW_PATH_KEY = "newPath"
9092

9193
@Composable
9294
fun FilePickerScreen(
9395
path: String,
9496
pickFolder: Boolean = false,
9597
extensionName: String? = null,
96-
onFilePicked: (File) -> Unit,
9798
viewModel: FilePickerViewModel = hiltViewModel(),
9899
) {
99100
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
@@ -112,8 +113,8 @@ fun FilePickerScreen(
112113
val current = File(uiState.path)
113114
val parent = current.parent
114115
if (!parent.isNullOrBlank() &&
115-
uiState.path != Environment.getExternalStorageDirectory().absolutePath &&
116-
uiState.path.startsWith(Environment.getExternalStorageDirectory().absolutePath)
116+
uiState.path != Const.INTERNAL_STORAGE &&
117+
uiState.path.startsWith(Const.INTERNAL_STORAGE)
117118
) {
118119
dispatch(FilePickerIntent.NewLocation(parent))
119120
} else {
@@ -137,15 +138,19 @@ fun FilePickerScreen(
137138
},
138139
actions = {
139140
AniVuIconButton(
140-
onClick = { dispatch(FilePickerIntent.NewLocation(Environment.getExternalStorageDirectory().absolutePath)) },
141+
onClick = { dispatch(FilePickerIntent.NewLocation(Const.INTERNAL_STORAGE)) },
141142
imageVector = Icons.Outlined.PhoneAndroid,
142143
contentDescription = stringResource(id = R.string.file_picker_screen_internal_storage),
143144
)
144145
}
145146
)
146147
}
147148
) { paddingValues ->
148-
Column {
149+
Column(modifier = Modifier.padding(top = paddingValues.calculateTopPadding())) {
150+
PathLevelIndication(
151+
path = uiState.path,
152+
onRouteTo = { dispatch(FilePickerIntent.NewLocation(it)) },
153+
)
149154
LazyColumn(
150155
modifier = Modifier
151156
.fillMaxWidth()
@@ -154,7 +159,6 @@ fun FilePickerScreen(
154159
contentPadding = PaddingValues(
155160
start = paddingValues.calculateStartPadding(LocalLayoutDirection.current),
156161
end = paddingValues.calculateEndPadding(LocalLayoutDirection.current),
157-
top = paddingValues.calculateTopPadding(),
158162
),
159163
) {
160164
(uiState.fileListState as? FileListState.Success)?.list?.forEach { file ->
@@ -165,7 +169,9 @@ fun FilePickerScreen(
165169
dispatch(FilePickerIntent.NewLocation(file.path))
166170
} else {
167171
if (!pickFolder) {
168-
onFilePicked(file)
172+
navController.previousBackStackEntry
173+
?.savedStateHandle
174+
?.set(FILE_PICKER_NEW_PATH_KEY, file.absolutePath)
169175
}
170176
}
171177
},
@@ -195,7 +201,9 @@ fun FilePickerScreen(
195201
.padding(horizontal = 16.dp)
196202
.fillMaxWidth(),
197203
onClick = {
198-
onFilePicked(File(uiState.path))
204+
navController.previousBackStackEntry
205+
?.savedStateHandle
206+
?.set(FILE_PICKER_NEW_PATH_KEY, uiState.path)
199207
navController.popBackStackWithLifecycle()
200208
},
201209
) {
@@ -212,3 +220,57 @@ fun FilePickerScreen(
212220
}
213221
}
214222
}
223+
224+
@Composable
225+
private fun PathLevelIndication(path: String, onRouteTo: (String) -> Unit) {
226+
val scrollState = rememberScrollState()
227+
LaunchedEffect(scrollState.maxValue) {
228+
if (scrollState.canScrollForward) {
229+
scrollState.animateScrollTo(scrollState.maxValue)
230+
}
231+
}
232+
Row(
233+
modifier = Modifier
234+
.horizontalScroll(scrollState)
235+
.padding(horizontal = 12.dp)
236+
.animateContentSize(),
237+
verticalAlignment = Alignment.CenterVertically,
238+
) {
239+
val items = remember(path) {
240+
mutableListOf<String>().apply {
241+
var newPath = path
242+
if (newPath.startsWith(Const.INTERNAL_STORAGE)) {
243+
add(Const.INTERNAL_STORAGE)
244+
newPath = newPath.removePrefix(Const.INTERNAL_STORAGE)
245+
}
246+
newPath.removeSurrounding(File.separator)
247+
newPath.split(File.separator).forEach {
248+
if (it.isNotBlank()) add(it)
249+
}
250+
}
251+
}
252+
253+
items.forEachIndexed { index, item ->
254+
Text(
255+
modifier = Modifier
256+
.clip(RoundedCornerShape(3.dp))
257+
.clickable {
258+
onRouteTo(
259+
items
260+
.subList(0, index + 1)
261+
.joinToString(File.separator)
262+
)
263+
}
264+
.padding(horizontal = 6.dp, vertical = 8.dp),
265+
text = item,
266+
style = MaterialTheme.typography.labelLarge,
267+
)
268+
if (index != items.size - 1) {
269+
Icon(
270+
imageVector = Icons.AutoMirrored.Outlined.NavigateNext,
271+
contentDescription = null,
272+
)
273+
}
274+
}
275+
}
276+
}

app/src/main/java/com/skyd/anivu/ui/fragment/filepicker/FilePickerState.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.skyd.anivu.ui.fragment.filepicker
22

33
import com.skyd.anivu.base.mvi.MviViewState
4+
import com.skyd.anivu.config.Const
45
import java.io.File
56

67
data class FilePickerState(
@@ -10,7 +11,7 @@ data class FilePickerState(
1011
) : MviViewState {
1112
companion object {
1213
fun initial() = FilePickerState(
13-
path = "",
14+
path = Const.INTERNAL_STORAGE,
1415
fileListState = FileListState.Init(),
1516
loadingDialog = false,
1617
)

app/src/main/java/com/skyd/anivu/ui/fragment/license/LicenseFragment.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,5 +185,10 @@ private fun getLicenseList(): List<LicenseBean> {
185185
license = "MIT",
186186
link = "https://github.com/mpv-android/mpv-android",
187187
),
188+
LicenseBean(
189+
name = "Lottie",
190+
license = "MIT",
191+
link = "https://github.com/airbnb/lottie",
192+
),
188193
).sortedBy { it.name }
189194
}

app/src/main/java/com/skyd/anivu/ui/fragment/media/list/MediaList.kt

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@ import androidx.compose.foundation.layout.width
1111
import androidx.compose.foundation.lazy.grid.GridCells
1212
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
1313
import androidx.compose.foundation.lazy.grid.items
14-
import androidx.compose.foundation.rememberScrollState
15-
import androidx.compose.foundation.verticalScroll
1614
import androidx.compose.material.pullrefresh.PullRefreshIndicator
1715
import androidx.compose.material.pullrefresh.pullRefresh
1816
import androidx.compose.material.pullrefresh.rememberPullRefreshState
@@ -159,8 +157,7 @@ private fun EmptyPlaceholder() {
159157
AnimatedPlaceholder(
160158
modifier = Modifier
161159
.width(220.dp)
162-
.padding(vertical = 20.dp)
163-
.verticalScroll(rememberScrollState()),
160+
.padding(vertical = 20.dp),
164161
resId = R.raw.lottie_empty_1,
165162
tip = stringResource(id = R.string.empty_tip_1),
166163
)

app/src/main/java/com/skyd/anivu/ui/fragment/settings/data/DataFragment.kt

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import androidx.compose.material3.TextButton
2424
import androidx.compose.material3.TopAppBarDefaults
2525
import androidx.compose.material3.rememberDatePickerState
2626
import androidx.compose.runtime.Composable
27+
import androidx.compose.runtime.LaunchedEffect
2728
import androidx.compose.runtime.derivedStateOf
2829
import androidx.compose.runtime.getValue
2930
import androidx.compose.runtime.mutableStateOf
@@ -50,17 +51,16 @@ import com.skyd.anivu.ui.component.BaseSettingsItem
5051
import com.skyd.anivu.ui.component.CategorySettingsItem
5152
import com.skyd.anivu.ui.component.dialog.DeleteWarningDialog
5253
import com.skyd.anivu.ui.component.dialog.WaitingDialog
53-
import com.skyd.anivu.ui.fragment.filepicker.CALLBACK_KEY
5454
import com.skyd.anivu.ui.fragment.filepicker.EXTENSION_NAME_KEY
55-
import com.skyd.anivu.ui.fragment.filepicker.FilePickerCallback
55+
import com.skyd.anivu.ui.fragment.filepicker.FILE_PICKER_NEW_PATH_KEY
5656
import com.skyd.anivu.ui.fragment.filepicker.PATH_KEY
5757
import com.skyd.anivu.ui.fragment.filepicker.PICK_FOLDER_KEY
5858
import com.skyd.anivu.ui.local.LocalMediaLibLocation
5959
import com.skyd.anivu.ui.local.LocalNavController
6060
import dagger.hilt.android.AndroidEntryPoint
61-
import kotlinx.coroutines.CoroutineScope
62-
import kotlinx.coroutines.Dispatchers
63-
import java.io.File
61+
import kotlinx.coroutines.flow.collect
62+
import kotlinx.coroutines.flow.filterNotNull
63+
import kotlinx.coroutines.flow.onEach
6464

6565

6666
@AndroidEntryPoint
@@ -84,6 +84,19 @@ fun DataScreen(viewModel: DataViewModel = hiltViewModel()) {
8484
val uiEvent by viewModel.singleEvent.collectAsStateWithLifecycle(initialValue = null)
8585
val dispatch = viewModel.getDispatcher(startWith = DataIntent.Init)
8686

87+
LaunchedEffect(Unit) {
88+
navController.currentBackStackEntry?.savedStateHandle
89+
?.getStateFlow<String?>(FILE_PICKER_NEW_PATH_KEY, null)
90+
?.filterNotNull()
91+
?.onEach { newPath ->
92+
MediaLibLocationPreference.put(
93+
context,
94+
this,
95+
newPath,
96+
)
97+
}?.collect()
98+
}
99+
87100
Scaffold(
88101
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
89102
topBar = {
@@ -126,15 +139,6 @@ fun DataScreen(viewModel: DataViewModel = hiltViewModel()) {
126139
} else localMediaLibLocation
127140
)
128141
putString(EXTENSION_NAME_KEY, "")
129-
putSerializable(CALLBACK_KEY, object : FilePickerCallback {
130-
override fun onFilePicked(file: File) {
131-
MediaLibLocationPreference.put(
132-
context,
133-
CoroutineScope(Dispatchers.IO), // Do not use `val scope = rememberCoroutineScope()`
134-
file.absolutePath,
135-
)
136-
}
137-
})
138142
}
139143
)
140144
}

0 commit comments

Comments
 (0)