Skip to content

Commit f2457c7

Browse files
Merge pull request #21 from TheByteArray/cursor/enhance-audio-and-video-conversion-options-66b7
Enhance audio and video conversion options
2 parents 2f52878 + a63ab4d commit f2457c7

File tree

9 files changed

+191
-21
lines changed

9 files changed

+191
-21
lines changed

app/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,4 +79,6 @@ dependencies {
7979
implementation(libs.androidx.ui.text.google.fonts)
8080
implementation(libs.androidx.navigation.compose)
8181
implementation(libs.androidx.material.icons.extended)
82+
implementation(libs.androidx.documentfile)
83+
8284
}

app/src/main/java/com/nasahacker/convertit/service/ConvertItService.kt

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,11 +60,9 @@ class ConvertItService : Service() {
6060
super.onDestroy()
6161
Log.i(TAG, "Service destroyed")
6262

63-
// Cancel ongoing conversion job
6463
conversionJob?.cancel()
6564
conversionJob = null
6665

67-
// Cancel all FFmpeg sessions
6866
FFmpegKit.cancel()
6967
Log.i(TAG, "Cancelled all FFmpeg sessions in onDestroy")
7068
}
@@ -87,11 +85,9 @@ class ConvertItService : Service() {
8785
if (intent?.action == ACTION_STOP_SERVICE) {
8886
Log.i(TAG, "Stopping service as per user request. startId: $startId")
8987

90-
// Cancel ongoing conversion job
9188
conversionJob?.cancel()
9289
conversionJob = null
9390

94-
// Cancel all FFmpeg sessions
9591
FFmpegKit.cancel()
9692
Log.i(TAG, "Cancelled all FFmpeg sessions")
9793

app/src/main/java/com/nasahacker/convertit/ui/component/ExpandableFab.kt

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import androidx.compose.material.icons.filled.Audiotrack
99
import androidx.compose.material.icons.filled.Close
1010
import androidx.compose.material.icons.filled.Edit
1111
import androidx.compose.material.icons.filled.Settings
12+
import androidx.compose.material.icons.filled.VideoLibrary
13+
import androidx.compose.material.icons.filled.Folder
1214
import androidx.compose.material3.*
1315
import androidx.compose.runtime.*
1416
import androidx.compose.ui.Alignment
@@ -35,6 +37,8 @@ import com.nasahacker.convertit.R
3537
fun ExpandableFab(
3638
onEditMetadataClick: () -> Unit,
3739
onConvertAudioClick: () -> Unit,
40+
onConvertVideoClick: () -> Unit,
41+
onCustomSaveLocationClick: () -> Unit,
3842
modifier: Modifier = Modifier
3943
) {
4044
var isExpanded by remember { mutableStateOf(false) }
@@ -73,6 +77,26 @@ fun ExpandableFab(
7377
scale = itemScale
7478
)
7579

80+
ExpandableFabItem(
81+
icon = Icons.Filled.Folder,
82+
label = stringResource(R.string.label_custom_save_location_action),
83+
onClick = {
84+
onCustomSaveLocationClick()
85+
isExpanded = false
86+
},
87+
scale = itemScale
88+
)
89+
90+
ExpandableFabItem(
91+
icon = Icons.Filled.VideoLibrary,
92+
label = stringResource(R.string.label_convert_video_action),
93+
onClick = {
94+
onConvertVideoClick()
95+
isExpanded = false
96+
},
97+
scale = itemScale
98+
)
99+
76100
ExpandableFabItem(
77101
icon = Icons.Filled.Audiotrack,
78102
label = stringResource(R.string.label_convert_audio_action),

app/src/main/java/com/nasahacker/convertit/ui/screen/HomeScreen.kt

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.nasahacker.convertit.ui.screen
22

33
import android.app.Activity
4+
import android.content.Intent
45
import android.widget.Toast
56
import androidx.activity.compose.rememberLauncherForActivityResult
67
import androidx.activity.result.contract.ActivityResultContracts
@@ -68,6 +69,32 @@ fun HomeScreen(
6869
}
6970
}
7071

72+
val videoPickFileLauncher =
73+
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
74+
if (result.resultCode == Activity.RESULT_OK) {
75+
viewModel.updateUriList(result.data)
76+
showDialog = true
77+
}
78+
}
79+
80+
val folderPickLauncher =
81+
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
82+
if (result.resultCode == Activity.RESULT_OK) {
83+
result.data?.data?.let { uri ->
84+
try {
85+
context.contentResolver.takePersistableUriPermission(
86+
uri,
87+
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
88+
)
89+
AppUtil.saveCustomSaveLocation(context, uri)
90+
val newLocation = AppUtil.getCurrentSaveLocationPath(context)
91+
Toast.makeText(context, "Save location: $newLocation", Toast.LENGTH_LONG).show()
92+
} catch (e: Exception) {
93+
Toast.makeText(context, "Failed to set custom location", Toast.LENGTH_SHORT).show()
94+
}
95+
}
96+
}
97+
}
7198

7299
val metadataPickFileLauncher =
73100
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
@@ -123,6 +150,20 @@ fun HomeScreen(
123150
AppUtil.openFilePicker(context, pickFileLauncher)
124151
}
125152
},
153+
onConvertVideoClick = {
154+
if (ConvertItService.isForegroundServiceStarted) {
155+
Toast.makeText(
156+
context, context.getString(R.string.label_warning), Toast.LENGTH_SHORT
157+
).show()
158+
} else {
159+
AppUtil.openVideoFilePicker(context, videoPickFileLauncher)
160+
}
161+
},
162+
onCustomSaveLocationClick = {
163+
val currentLocation = AppUtil.getCurrentSaveLocationPath(context)
164+
Toast.makeText(context, "Current: $currentLocation", Toast.LENGTH_LONG).show()
165+
AppUtil.openFolderPicker(context, folderPickLauncher)
166+
},
126167
modifier = Modifier
127168
.align(Alignment.BottomEnd)
128169
.padding(26.dp)
@@ -146,10 +187,9 @@ fun HomeScreen(
146187
audioUri = metadataUri,
147188
onDismissRequest = {
148189
showMetadataDialog = false
149-
viewModel.setMetadataUri(null) // Clear the URI
190+
viewModel.setMetadataUri(null)
150191
},
151192
onMetadataSaved = {
152-
// Optionally refresh or update UI after metadata is saved
153193
}
154194
)
155195
}

app/src/main/java/com/nasahacker/convertit/ui/viewmodel/AppViewModel.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@ class AppViewModel : ViewModel() {
4545
val isSuccess = intent?.getBooleanExtra(IS_SUCCESS, false) == true
4646
viewModelScope.launch {
4747
_conversionStatus.value = isSuccess
48-
// Clear URI list regardless of success or failure
4948
clearUriList()
5049
}
5150
}

app/src/main/java/com/nasahacker/convertit/util/AppConfig.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,4 +66,5 @@ object AppConfig {
6666
const val TELEGRAM_CHANNEL = "https://t.me/thebytearray"
6767
const val APP_PREF = "app_prefs"
6868
const val PREF_DONT_SHOW_AGAIN = "pref_dont_show_again"
69+
const val PREF_CUSTOM_SAVE_LOCATION = "pref_custom_save_location"
6970
}

app/src/main/java/com/nasahacker/convertit/util/AppUtil.kt

Lines changed: 118 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -32,18 +32,22 @@ import com.nasahacker.convertit.util.AppConfig.FOLDER_DIR
3232
import com.nasahacker.convertit.util.AppConfig.FORMAT_ARRAY
3333
import com.nasahacker.convertit.util.AppConfig.STORAGE_PERMISSION_CODE
3434
import com.nasahacker.convertit.util.AppConfig.URI_LIST
35+
import com.nasahacker.convertit.util.AppConfig.APP_PREF
36+
import com.nasahacker.convertit.util.AppConfig.PREF_CUSTOM_SAVE_LOCATION
3537
import java.io.File
3638
import java.io.FileOutputStream
3739
import kotlin.math.log10
3840
import kotlin.math.pow
3941
import androidx.core.net.toUri
42+
import androidx.documentfile.provider.DocumentFile
4043
import android.graphics.Bitmap
4144
import com.kyant.taglib.Picture
4245
import com.kyant.taglib.TagLib
4346
import com.nasahacker.convertit.dto.Metadata
4447
import kotlinx.coroutines.Dispatchers
4548
import kotlinx.coroutines.withContext
4649
import java.io.ByteArrayOutputStream
50+
import androidx.core.content.edit
4751

4852
/**
4953
* @author Tamim Hossain
@@ -64,8 +68,25 @@ object AppUtil {
6468
if (isStoragePermissionGranted(context)) {
6569
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
6670
addCategory(Intent.CATEGORY_OPENABLE)
67-
type = "audio/*, video/* ,*/*"
68-
putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("audio/*", "video/*", "*/*"))
71+
type = "audio/*"
72+
putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("audio/*"))
73+
putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
74+
}
75+
pickFileLauncher.launch(intent)
76+
} else {
77+
requestStoragePermissions(context)
78+
}
79+
}
80+
81+
fun openVideoFilePicker(
82+
context: Context,
83+
pickFileLauncher: ActivityResultLauncher<Intent>,
84+
) {
85+
if (isStoragePermissionGranted(context)) {
86+
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
87+
addCategory(Intent.CATEGORY_OPENABLE)
88+
type = "video/*"
89+
putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("video/*"))
6990
putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
7091
}
7192
pickFileLauncher.launch(intent)
@@ -74,6 +95,18 @@ object AppUtil {
7495
}
7596
}
7697

98+
fun openFolderPicker(
99+
context: Context,
100+
pickFolderLauncher: ActivityResultLauncher<Intent>,
101+
) {
102+
if (isStoragePermissionGranted(context)) {
103+
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
104+
pickFolderLauncher.launch(intent)
105+
} else {
106+
requestStoragePermissions(context)
107+
}
108+
}
109+
77110

78111
fun openMetadataEditorFilePicker(
79112
context: Context,
@@ -227,10 +260,7 @@ object AppUtil {
227260
}
228261

229262
fun getAudioFilesFromConvertedFolder(context: Context): List<AudioFile> {
230-
val convertedDir = File(
231-
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC),
232-
FOLDER_DIR,
233-
)
263+
val convertedDir = getOutputDirectory(context)
234264

235265
return convertedDir.takeIf { it.exists() && it.isDirectory }?.listFiles()?.filter { file ->
236266
FORMAT_ARRAY.any { file.extension.equals(it.trimStart('.'), ignoreCase = true) }
@@ -294,6 +324,87 @@ object AppUtil {
294324
Toast.makeText(context, resultMessage, Toast.LENGTH_SHORT).show()
295325
}
296326

327+
fun saveCustomSaveLocation(context: Context, uri: Uri) {
328+
val sharedPrefs = context.getSharedPreferences(APP_PREF, Context.MODE_PRIVATE)
329+
sharedPrefs.edit { putString(PREF_CUSTOM_SAVE_LOCATION, uri.toString()) }
330+
}
331+
332+
fun getCustomSaveLocation(context: Context): Uri? {
333+
val sharedPrefs = context.getSharedPreferences(APP_PREF, Context.MODE_PRIVATE)
334+
val uriString = sharedPrefs.getString(PREF_CUSTOM_SAVE_LOCATION, null)
335+
return uriString?.toUri()
336+
}
337+
338+
fun clearCustomSaveLocation(context: Context) {
339+
val sharedPrefs = context.getSharedPreferences(APP_PREF, Context.MODE_PRIVATE)
340+
sharedPrefs.edit { remove(PREF_CUSTOM_SAVE_LOCATION) }
341+
}
342+
343+
fun getCurrentSaveLocationPath(context: Context): String {
344+
val customSaveUri = getCustomSaveLocation(context)
345+
return if (customSaveUri != null) {
346+
try {
347+
val customPath = customSaveUri.path
348+
if (customPath != null && customPath.contains("/tree/primary:")) {
349+
customPath.replace("/tree/primary:", "/storage/emulated/0/")
350+
} else {
351+
"Music/ConvertIt (default)"
352+
}
353+
} catch (e: Exception) {
354+
"Music/ConvertIt (default)"
355+
}
356+
} else {
357+
"Music/ConvertIt (default)"
358+
}
359+
}
360+
361+
fun getOutputDirectory(context: Context): File {
362+
val customSaveUri = getCustomSaveLocation(context)
363+
return if (customSaveUri != null) {
364+
try {
365+
val customPath = customSaveUri.path
366+
if (customPath != null && customPath.contains("/tree/primary:")) {
367+
val actualPath = customPath.replace("/tree/primary:", "/storage/emulated/0/")
368+
val customDir = File(actualPath)
369+
if (customDir.exists() || customDir.mkdirs()) {
370+
if (customDir.canWrite()) {
371+
customDir
372+
} else {
373+
getDefaultOutputDirectory()
374+
}
375+
} else {
376+
getDefaultOutputDirectory()
377+
}
378+
} else if (customPath != null && customPath.contains("/tree/")) {
379+
val actualPath = customPath.replace("/tree/", "/storage/").replace(":", "/")
380+
val customDir = File(actualPath)
381+
if ((customDir.exists() || customDir.mkdirs()) && customDir.canWrite()) {
382+
customDir
383+
} else {
384+
getDefaultOutputDirectory()
385+
}
386+
} else {
387+
getDefaultOutputDirectory()
388+
}
389+
} catch (e: Exception) {
390+
getDefaultOutputDirectory()
391+
}
392+
} else {
393+
getDefaultOutputDirectory()
394+
}
395+
}
396+
397+
private fun getDefaultOutputDirectory(): File {
398+
return File(
399+
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC),
400+
FOLDER_DIR,
401+
).apply {
402+
setReadable(true)
403+
setWritable(true)
404+
mkdirs()
405+
}
406+
}
407+
297408
fun convertAudio(
298409
context: Context,
299410
playbackSpeed: String = "1.0",
@@ -304,14 +415,7 @@ object AppUtil {
304415
onFailure: (String) -> Unit,
305416
onProgress: (Int) -> Unit,
306417
) {
307-
val musicDir = File(
308-
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC),
309-
FOLDER_DIR,
310-
).apply {
311-
setReadable(true)
312-
setWritable(true)
313-
mkdirs()
314-
}
418+
val musicDir = getOutputDirectory(context)
315419

316420
val outputPaths = mutableListOf<String>()
317421
val totalFiles = uris.size

app/src/main/res/values/strings.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,8 @@
129129
<!-- Expandable FAB Strings -->
130130
<string name="label_edit_metadata_action">Edit Metadata</string>
131131
<string name="label_convert_audio_action">Convert Audio</string>
132+
<string name="label_convert_video_action">Convert Video</string>
133+
<string name="label_custom_save_location_action">Save Location</string>
132134
<string name="label_close">Close</string>
133135
<string name="label_actions">Actions</string>
134136

gradle/libs.versions.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
[versions]
22
agp = "8.10.1"
3+
documentfile = "1.1.0"
34
kotlin = "2.1.20"
45
coreKtx = "1.16.0"
56
junit = "4.13.2"
@@ -14,6 +15,7 @@ uiTextGoogleFonts = "1.8.2"
1415
runtimeLivedata = "1.8.2"
1516
[libraries]
1617
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
18+
androidx-documentfile = { module = "androidx.documentfile:documentfile", version.ref = "documentfile" }
1719
androidx-material-icons-extended = { module = "androidx.compose.material:material-icons-extended", version.ref = "materialIconsExtended" }
1820
androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationCompose" }
1921
androidx-ui-text-google-fonts = { module = "androidx.compose.ui:ui-text-google-fonts", version.ref = "uiTextGoogleFonts" }

0 commit comments

Comments
 (0)