Skip to content

Commit 484c196

Browse files
External storage support, auto-show changelog, ANR fix
- Auto-open update sheet during first launch after an update - Introduced last seen app version tracking in UserPreferencesRepository. - Enhanced FilesystemFolderPickerSheet to display device storage options. - Improved blur handling in various screens to enhance visual clarity. - Updated Gradle dependencies for better performance and compatibility. - Adding issue templates
1 parent b9eedca commit 484c196

19 files changed

Lines changed: 436 additions & 103 deletions

File tree

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
name: Bug Report
2+
description: File a report for any bug you find in the app.
3+
title: "[BUG] "
4+
labels: ["bug"]
5+
body:
6+
- type: textarea
7+
id: description
8+
attributes:
9+
label: "Describe the bug"
10+
description: "A clear and concise description of what the bug is."
11+
validations:
12+
required: true
13+
14+
- type: textarea
15+
id: steps
16+
attributes:
17+
label: "Steps to reproduce"
18+
description: "Steps to reproduce the behavior"
19+
placeholder: |
20+
1. Open the app
21+
2. Navigate to...
22+
3. Click on...
23+
4. See error
24+
validations:
25+
required: true
26+
27+
- type: textarea
28+
id: expected
29+
attributes:
30+
label: "Expected behavior"
31+
description: "A clear and concise description of what you expected to happen."
32+
placeholder: "What should have happened instead?"
33+
validations:
34+
required: true
35+
36+
- type: textarea
37+
id: device_info
38+
attributes:
39+
label: "Device information"
40+
description: |
41+
Please fill out the details below.
42+
*Tip: Check display/font size at Android Settings -> Display -> Display size & text.*
43+
value: |
44+
- Device:
45+
- OS:
46+
- Display size & font size:
47+
- FilePipe version:
48+
validations:
49+
required: true
50+
51+
- type: textarea
52+
id: logs
53+
attributes:
54+
label: "FilePipe logs"
55+
description: |
56+
Get the log file: Go to FilePipe settings, scroll to the bottom, tap the 🐞 bug icon at the top-right of the About section, and save that file.
57+
Then attach that log file here by dragging and dropping it into this text box.
58+
*Note: Bug reports without logs will be closed without further notice.*
59+
placeholder: "Drag & drop or paste your log file here"
60+
validations:
61+
required: true
62+
63+
- type: checkboxes
64+
id: checklist
65+
attributes:
66+
label: "Checklist"
67+
description: "Please confirm the following before submitting"
68+
options:
69+
- label: "I have searched for existing issues with this problem"
70+
required: true
71+
- label: "I am using the latest version of the app"
72+
required: true
73+
- label: "I have attached the FilePipe log file (retrieved from settings -> 🐞 bug icon)"
74+
required: true
75+
- label: "I have provided enough information to reproduce the issue"
76+
required: true

.github/ISSUE_TEMPLATE/config.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
blank_issues_enabled: false
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
name: "Feature request"
2+
description: "Suggest a new feature for the app."
3+
labels: ["enhancement", "to check"]
4+
body:
5+
- type: checkboxes
6+
id: prerequisites
7+
attributes:
8+
label: Prerequisites
9+
options:
10+
- label: I confirm this request is not part of an existing issue.
11+
required: true
12+
- type: textarea
13+
id: feature-description
14+
attributes:
15+
label: Describe the feature
16+
description: |
17+
A clear and concise description of what you want to happen.
18+
validations:
19+
required: true
20+
- type: textarea
21+
id: alternatives
22+
attributes:
23+
label: Describe alternatives you've considered (if applicable)
24+
description: |
25+
A clear and concise description of any alternative solutions or features you've considered.
26+
validations:
27+
required: true
28+
- type: textarea
29+
id: additional-context
30+
attributes:
31+
label: Additional context
32+
description: Add any other context or screenshots about the feature request here.
33+
validations:
34+
required: false

app/build.gradle.kts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,8 @@ extensions.configure<ApplicationExtension>("android") {
5656
applicationId = filePipeApplicationId
5757
minSdk = 31
5858
targetSdk = 37
59-
versionCode = 387
60-
versionName = "3.8.7"
59+
versionCode = 388
60+
versionName = "3.8.8"
6161

6262
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
6363

app/src/main/java/dev/bikram/filepipe/MainActivity.kt

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,15 @@ class MainActivity : ComponentActivity() {
5555
statusBarStyle = SystemBarStyle.auto(AndroidColor.TRANSPARENT, AndroidColor.TRANSPARENT),
5656
navigationBarStyle = SystemBarStyle.auto(AndroidColor.TRANSPARENT, AndroidColor.TRANSPARENT),
5757
)
58+
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
59+
window.isNavigationBarContrastEnforced = false
60+
window.isStatusBarContrastEnforced = false
61+
}
5862
handleShortcutIntent(intent)
5963
handleOpenHistoryIntent(intent)
6064
handleOpenHistoryDetailIntent(intent)
6165
handleOpenSettingsUpdatesIntent(intent)
66+
openSettingsUpdatesIfAppWasUpdated()
6267

6368
setContent {
6469
val preferencesState by userPreferencesRepository.preferencesFlow
@@ -165,4 +170,17 @@ class MainActivity : ComponentActivity() {
165170
sourceIntent.removeExtra(PendingShortcutRepository.EXTRA_OPEN_SETTINGS_UPDATES)
166171
}
167172
}
173+
174+
/** First launch after an update (fresh installs are not announced): auto-opens the update sheet with the changelog. */
175+
private fun openSettingsUpdatesIfAppWasUpdated() {
176+
lifecycleScope.launch {
177+
val lastSeenVersion = userPreferencesRepository.getLastSeenAppVersion()
178+
val currentVersion = BuildConfig.VERSION_NAME
179+
val wasUpdated = !lastSeenVersion.isNullOrBlank() && lastSeenVersion != currentVersion
180+
if (wasUpdated && BuildConfig.SHOW_UPDATES) {
181+
pendingShortcutRepository.requestOpenSettingsForUpdates()
182+
}
183+
userPreferencesRepository.setLastSeenAppVersion(currentVersion)
184+
}
185+
}
168186
}

app/src/main/java/dev/bikram/filepipe/data/preferences/UserPreferencesRepository.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ private object PrefKeys {
7777
val UPDATE_LAST_NOTIFIED_DEDUPE_KEY = stringPreferencesKey("update_last_notified_dedupe_key")
7878
val GITHUB_ACK_FINGERPRINT = stringPreferencesKey("github_last_acknowledged_release_fingerprint")
7979
val GITHUB_ACK_INSTALLED_VERSION = stringPreferencesKey("github_acknowledged_for_installed_version")
80+
val LAST_SEEN_APP_VERSION = stringPreferencesKey("last_seen_app_version")
8081
val SAVE_UPDATE_APK_TO_DOWNLOADS = booleanPreferencesKey("save_update_apk_to_downloads")
8182
val UPDATE_APK_DOWNLOADS_COPY_SUCCEEDED = booleanPreferencesKey("update_apk_downloads_copy_succeeded")
8283
val USE_GRADIENT_BACKGROUND = booleanPreferencesKey("use_gradient_background")
@@ -419,6 +420,13 @@ class UserPreferencesRepository
419420
}
420421
}
421422

423+
/** Null means this install has never recorded a version, i.e. it's a fresh install. */
424+
suspend fun getLastSeenAppVersion(): String? = dataStore.data.first()[PrefKeys.LAST_SEEN_APP_VERSION]
425+
426+
suspend fun setLastSeenAppVersion(version: String) {
427+
dataStore.edit { it[PrefKeys.LAST_SEEN_APP_VERSION] = version }
428+
}
429+
422430
/**
423431
* One-time: `auto_check_for_updates` boolean -> [PrefKeys.UPDATE_CHECK_SCHEDULE]; removes legacy key.
424432
*/

app/src/main/java/dev/bikram/filepipe/ui/components/FilesystemFolderPickerSheet.kt

Lines changed: 94 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import androidx.compose.runtime.setValue
3939
import androidx.compose.ui.Alignment
4040
import androidx.compose.ui.Modifier
4141
import androidx.compose.ui.graphics.Color
42+
import androidx.compose.ui.platform.LocalContext
4243
import androidx.compose.ui.platform.LocalDensity
4344
import androidx.compose.ui.res.stringResource
4445
import androidx.compose.ui.text.style.TextOverflow
@@ -76,6 +77,12 @@ private data class FolderPickerDirectoryEntry(
7677
val listKey: String,
7778
)
7879

80+
private data class StorageVolumeRoot(
81+
val label: String,
82+
val path: String,
83+
val isPrimary: Boolean,
84+
)
85+
7986
private fun folderPickerDirectoryEntry(file: File): FolderPickerDirectoryEntry? {
8087
if (!file.isDirectory || !file.canRead() || file.name.startsWith(".")) return null
8188
val resolvedPath =
@@ -95,14 +102,19 @@ private fun buildBreadcrumbSegments(
95102
primaryRoot: String,
96103
internalStorageLabel: String,
97104
sdCardLabel: String,
105+
devicesLabel: String,
98106
): List<BreadcrumbSegment> {
99107
val normalized = normalizeFilesystemFolderPath(canonicalPath) ?: return emptyList()
108+
if (normalized == "/storage") {
109+
return listOf(BreadcrumbSegment(devicesLabel, "/storage"))
110+
}
100111
val primary = primaryRoot.trimEnd('/')
101112

102113
if (normalized == primary || normalized.startsWith(primary + File.separator)) {
103114
val tail = if (normalized == primary) "" else normalized.removePrefix(primary + File.separator)
104115
val parts = if (tail.isEmpty()) emptyList() else tail.split('/').filter { it.isNotEmpty() }
105116
val out = ArrayList<BreadcrumbSegment>()
117+
out.add(BreadcrumbSegment(devicesLabel, "/storage"))
106118
out.add(BreadcrumbSegment(internalStorageLabel, primary))
107119
var accumulated = primary
108120
for (part in parts) {
@@ -119,6 +131,7 @@ private fun buildBreadcrumbSegments(
119131
val rest = sdMatch.groupValues[2].orEmpty().trim('/')
120132
val parts = if (rest.isEmpty()) emptyList() else rest.split('/').filter { it.isNotEmpty() }
121133
val out = ArrayList<BreadcrumbSegment>()
134+
out.add(BreadcrumbSegment(devicesLabel, "/storage"))
122135
out.add(BreadcrumbSegment(sdCardLabel, sdRoot))
123136
var accumulated = sdRoot
124137
for (part in parts) {
@@ -129,7 +142,10 @@ private fun buildBreadcrumbSegments(
129142
}
130143

131144
val fallbackLabel = normalized.substringAfterLast(File.separator).ifEmpty { normalized }
132-
return listOf(BreadcrumbSegment(fallbackLabel, normalized))
145+
return listOf(
146+
BreadcrumbSegment(devicesLabel, "/storage"),
147+
BreadcrumbSegment(fallbackLabel, normalized),
148+
)
133149
}
134150

135151
/**
@@ -143,14 +159,58 @@ fun FilesystemFolderPickerSheetContent(
143159
onDismiss: () -> Unit,
144160
onFolderChosen: (normalizedAbsolutePath: String) -> Unit,
145161
) {
162+
// Keep these literals so that the font subset harvester detects them:
163+
@Suppress("UNUSED_EXPRESSION")
164+
if (false) {
165+
FilePipeMaterialRoundedSymbol(name = "folder")
166+
FilePipeMaterialRoundedSymbol(name = "mobile")
167+
FilePipeMaterialRoundedSymbol(name = "sd_card")
168+
}
169+
146170
val internalStorageLabel = stringResource(R.string.filesystem_folder_picker_internal_storage)
147171
val sdCardLabel = stringResource(R.string.filesystem_folder_picker_sd_card)
172+
val devicesLabel = stringResource(R.string.filesystem_folder_picker_devices)
148173
val primaryRoot =
149174
remember {
150175
runCatching { Environment.getExternalStorageDirectory().canonicalPath }.getOrNull()
151176
?: "/storage/emulated/0"
152177
}
153178

179+
val context = LocalContext.current
180+
val storageVolumes =
181+
remember(context, internalStorageLabel, sdCardLabel) {
182+
val list = mutableListOf<StorageVolumeRoot>()
183+
val primaryPath =
184+
runCatching { Environment.getExternalStorageDirectory().canonicalPath }.getOrNull()
185+
?: "/storage/emulated/0"
186+
list.add(StorageVolumeRoot(internalStorageLabel, primaryPath, isPrimary = true))
187+
188+
val externalDirs = context.getExternalFilesDirs(null)
189+
for (dir in externalDirs) {
190+
if (dir == null) continue
191+
val path = dir.absolutePath
192+
if (path.contains("/Android/data/")) {
193+
val rootPath = path.substringBefore("/Android/data/")
194+
if (rootPath != primaryPath && File(rootPath).exists()) {
195+
val sdName = rootPath.substringAfterLast('/')
196+
list.add(
197+
StorageVolumeRoot(
198+
label =
199+
if (sdName.matches(Regex("^[0-9A-F]{4}-[0-9A-F]{4}$", RegexOption.IGNORE_CASE))) {
200+
"$sdCardLabel ($sdName)"
201+
} else {
202+
sdCardLabel
203+
},
204+
path = rootPath,
205+
isPrimary = false,
206+
),
207+
)
208+
}
209+
}
210+
}
211+
list.distinctBy { it.path }
212+
}
213+
154214
val startPath =
155215
remember(initialDirectory) {
156216
normalizeFilesystemFolderPath(initialDirectory)
@@ -168,8 +228,8 @@ fun FilesystemFolderPickerSheetContent(
168228
var newFolderDialogErrorResId by remember { mutableStateOf<Int?>(null) }
169229

170230
val breadcrumbSegments =
171-
remember(currentPath, primaryRoot, internalStorageLabel, sdCardLabel) {
172-
buildBreadcrumbSegments(currentPath, primaryRoot, internalStorageLabel, sdCardLabel)
231+
remember(currentPath, primaryRoot, internalStorageLabel, sdCardLabel, devicesLabel) {
232+
buildBreadcrumbSegments(currentPath, primaryRoot, internalStorageLabel, sdCardLabel, devicesLabel)
173233
}
174234

175235
var childDirectories by remember(currentPath, childListRefreshKey) {
@@ -178,12 +238,22 @@ fun FilesystemFolderPickerSheetContent(
178238
LaunchedEffect(currentPath, childListRefreshKey) {
179239
childDirectories =
180240
withContext(Dispatchers.IO) {
181-
File(currentPath)
182-
.listFiles()
183-
?.mapNotNull(::folderPickerDirectoryEntry)
184-
?.distinctBy { entry -> entry.listKey }
185-
?.sortedBy { entry -> entry.name.lowercase() }
186-
?: emptyList()
241+
if (currentPath == "/storage") {
242+
storageVolumes.map { volume ->
243+
FolderPickerDirectoryEntry(
244+
name = volume.label,
245+
path = volume.path,
246+
listKey = volume.path,
247+
)
248+
}
249+
} else {
250+
File(currentPath)
251+
.listFiles()
252+
?.mapNotNull(::folderPickerDirectoryEntry)
253+
?.distinctBy { entry -> entry.listKey }
254+
?.sortedBy { entry -> entry.name.lowercase() }
255+
?: emptyList()
256+
}
187257
}
188258
}
189259

@@ -332,9 +402,11 @@ fun FilesystemFolderPickerSheetContent(
332402
modifier =
333403
Modifier.tapSoundClickable {
334404
val target = normalizeFilesystemFolderPath(segment.path) ?: return@tapSoundClickable
335-
if (File(target).isDirectory &&
336-
File(target).canRead() &&
337-
isFilesystemFolderPathAllowedForRules(target)
405+
if (target == "/storage" || (
406+
File(target).isDirectory &&
407+
File(target).canRead() &&
408+
isFilesystemFolderPathAllowedForRules(target)
409+
)
338410
) {
339411
currentPath = target
340412
}
@@ -366,8 +438,17 @@ fun FilesystemFolderPickerSheetContent(
366438
ListItem(
367439
colors = ListItemDefaults.colors(containerColor = Color.Transparent),
368440
leadingContent = {
441+
val isDrive =
442+
entry.path == "/storage/emulated/0" ||
443+
Regex("^/storage/[0-9A-F]{4}-[0-9A-F]{4}$", RegexOption.IGNORE_CASE).matches(entry.path)
444+
val iconName =
445+
if (isDrive) {
446+
if (entry.path == "/storage/emulated/0") "mobile" else "sd_card"
447+
} else {
448+
"folder"
449+
}
369450
FilePipeMaterialRoundedSymbol(
370-
name = "folder",
451+
name = iconName,
371452
contentDescription = null,
372453
size = 24.dp,
373454
modifier = Modifier.size(24.dp),

app/src/main/java/dev/bikram/filepipe/ui/navigation/AdaptiveRoutes.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,7 @@ fun SettingsTwoPaneRoute(
322322
onNavigateBack = { showSettingsSection(SettingsSectionKey.About) },
323323
updateVm = updateVm,
324324
showNavigateBack = showDetailNavigateBack,
325+
suppressBlur = true,
325326
)
326327
} else {
327328
SettingsScreen(
@@ -338,6 +339,7 @@ fun SettingsTwoPaneRoute(
338339
selectedSectionKey = selectedSectionKey,
339340
showTopBar = false,
340341
showSectionHeaders = false,
342+
suppressBlur = true,
341343
)
342344
}
343345
}

0 commit comments

Comments
 (0)