Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
6cdd812144
|
|||
|
cbee1786ca
|
|||
|
5ea665c02f
|
|||
|
7c269d5e59
|
|||
|
fbda9fedfb
|
|||
|
79ccd6753e
|
|||
|
3decb7307e
|
|||
|
d297ab1059
|
|||
|
371e38dc2f
|
|||
|
1e850a2d1a
|
|||
|
4cd9202609
|
|||
|
6becf2907e
|
|||
|
c34900a2b1
|
|||
|
f654ba52a1
|
|||
|
e7735a0446
|
|||
|
1e0d5dd640
|
|||
|
2672f8dfa5
|
|||
|
157f5103f1
|
48
CHANGELOG.md
Normal file
@@ -0,0 +1,48 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [1.4] - 2026-03-11
|
||||
|
||||
### Added
|
||||
- Directory browser for changing ISO directory in settings
|
||||
- Create new directories from the directory browser
|
||||
- Delete directories created by the app (long press)
|
||||
- Shows ISO/IMG files with OS icons in directory browser
|
||||
|
||||
### Changed
|
||||
- Empty state on home screen now shows current path and helpful hints
|
||||
- Version number is now read dynamically from app config
|
||||
- Renamed "folder" to "directory" throughout the UI
|
||||
|
||||
## [1.3] - 2025-03-11
|
||||
|
||||
### Changed
|
||||
- Fix default image dir to be /sdcard/isodroid instead of /sdcard/isodrive
|
||||
|
||||
## [1.2] - 2025-03-10
|
||||
|
||||
### Changed
|
||||
- Fix app name displaying as "ISO Drive" instead of "ISO Droid" in some places
|
||||
|
||||
## [1.1] - 2025-03-10
|
||||
|
||||
### Changed
|
||||
- New app icon
|
||||
|
||||
## [1.0] - 2025-03-10
|
||||
|
||||
### Added
|
||||
- Mount ISO/IMG files as USB mass storage or CD-ROM
|
||||
- Create blank IMG files for writable USB drives
|
||||
- Read-only or read-write mount options
|
||||
- Persistent notification with quick unmount button
|
||||
- OS-specific icons for popular distributions (Linux, BSD, etc.)
|
||||
- Download links to popular operating systems (Linux, BSD, Windows, Recovery tools)
|
||||
- Rename and delete files
|
||||
- Material 3 dynamic theming with dark mode support
|
||||
- Support for Magisk, KernelSU, and APatch root solutions
|
||||
- Bundled isodrive binary for all architectures (arm64-v8a, armeabi-v7a, x86_64, x86)
|
||||
@@ -35,7 +35,7 @@ Android app for mounting ISO/IMG files as USB mass storage or CD-ROM devices on
|
||||
1. Download the APK from the links above
|
||||
2. Install the APK on your rooted Android device
|
||||
3. Grant root access when prompted
|
||||
4. Place your ISO/IMG files in `/sdcard/isodrive/` (or configure a different directory in settings)
|
||||
4. Place your ISO/IMG files in `/sdcard/isodroid/` (or configure a different directory in settings)
|
||||
|
||||
> **Note**: The app includes a bundled `isodrive` binary. No additional setup required!
|
||||
|
||||
|
||||
@@ -12,8 +12,8 @@ android {
|
||||
applicationId = "sh.sar.isodroid"
|
||||
minSdk = 26
|
||||
targetSdk = 36
|
||||
versionCode = 1
|
||||
versionName = "1.1"
|
||||
versionCode = 4
|
||||
versionName = "1.4"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Shiham Abdul Rahman <shihaam@shihaam.dev>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package sh.sar.isodroid
|
||||
|
||||
import android.app.Application
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Shiham Abdul Rahman <shihaam@shihaam.dev>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package sh.sar.isodroid
|
||||
|
||||
import android.content.Context
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Shiham Abdul Rahman <shihaam@shihaam.dev>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package sh.sar.isodroid.data
|
||||
|
||||
import java.io.File
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Shiham Abdul Rahman <shihaam@shihaam.dev>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package sh.sar.isodroid.data
|
||||
|
||||
data class MountOptions(
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Shiham Abdul Rahman <shihaam@shihaam.dev>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package sh.sar.isodroid.data
|
||||
|
||||
enum class MountType {
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Shiham Abdul Rahman <shihaam@shihaam.dev>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package sh.sar.isodroid.isodrive
|
||||
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Shiham Abdul Rahman <shihaam@shihaam.dev>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package sh.sar.isodroid.isodrive
|
||||
|
||||
import android.content.Context
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Shiham Abdul Rahman <shihaam@shihaam.dev>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package sh.sar.isodroid.isodrive
|
||||
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Shiham Abdul Rahman <shihaam@shihaam.dev>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package sh.sar.isodroid.notification
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Shiham Abdul Rahman <shihaam@shihaam.dev>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package sh.sar.isodroid.notification
|
||||
|
||||
import android.app.NotificationChannel
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Shiham Abdul Rahman <shihaam@shihaam.dev>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package sh.sar.isodroid.notification
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Shiham Abdul Rahman <shihaam@shihaam.dev>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package sh.sar.isodroid.root
|
||||
|
||||
import com.topjohnwu.superuser.Shell
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Shiham Abdul Rahman <shihaam@shihaam.dev>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package sh.sar.isodroid.ui.components
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Shiham Abdul Rahman <shihaam@shihaam.dev>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package sh.sar.isodroid.ui.components
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
@@ -50,7 +55,7 @@ import sh.sar.isodroid.data.IsoFile
|
||||
* 1. Longest match (most specific)
|
||||
* 2. Earliest position in filename (if same length)
|
||||
*/
|
||||
private fun findOsIcon(context: android.content.Context, filename: String): String? {
|
||||
fun findOsIcon(context: android.content.Context, filename: String): String? {
|
||||
return try {
|
||||
// Dynamically load available icon files from assets
|
||||
val availableIcons = context.assets.list("osicons")
|
||||
@@ -106,10 +111,19 @@ fun FileBrowser(
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = "Place ISO or IMG files in this directory",
|
||||
text = "Place ISO or IMG files in:\n$currentPath",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f)
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f),
|
||||
textAlign = androidx.compose.ui.text.style.TextAlign.Center
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(
|
||||
text = "Tap + to create an empty IMG file\nChange directory in Settings",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f),
|
||||
textAlign = androidx.compose.ui.text.style.TextAlign.Center
|
||||
)
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Shiham Abdul Rahman <shihaam@shihaam.dev>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package sh.sar.isodroid.ui.components
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Shiham Abdul Rahman <shihaam@shihaam.dev>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package sh.sar.isodroid.ui.components
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Shiham Abdul Rahman <shihaam@shihaam.dev>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package sh.sar.isodroid.ui.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
@@ -42,30 +47,6 @@ import sh.sar.isodroid.ui.theme.MountedGreen
|
||||
import sh.sar.isodroid.ui.theme.UnmountedGray
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* Finds a matching OS icon filename for a given file by dynamically checking available icons.
|
||||
*/
|
||||
private fun findOsIcon(context: android.content.Context, filename: String): String? {
|
||||
return try {
|
||||
val availableIcons = context.assets.list("osicons")
|
||||
?.filter { it.endsWith(".svg", ignoreCase = true) }
|
||||
?.map { it.removeSuffix(".svg").lowercase() }
|
||||
?: emptyList()
|
||||
|
||||
val lowerFilename = filename.lowercase()
|
||||
|
||||
availableIcons
|
||||
.filter { lowerFilename.contains(it) }
|
||||
.maxWithOrNull(compareBy(
|
||||
{ it.length },
|
||||
{ -lowerFilename.indexOf(it) }
|
||||
))
|
||||
?.let { "$it.svg" }
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun StatusCard(
|
||||
mountStatus: MountStatus,
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Shiham Abdul Rahman <shihaam@shihaam.dev>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package sh.sar.isodroid.ui.screens
|
||||
|
||||
import android.content.Context
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Shiham Abdul Rahman <shihaam@shihaam.dev>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package sh.sar.isodroid.ui.screens
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
@@ -84,7 +89,7 @@ fun MainScreen(
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("ISO Drive") },
|
||||
title = { Text("ISO Droid") },
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.primaryContainer,
|
||||
titleContentColor = MaterialTheme.colorScheme.onPrimaryContainer
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Shiham Abdul Rahman <shihaam@shihaam.dev>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package sh.sar.isodroid.ui.screens
|
||||
|
||||
import android.Manifest
|
||||
@@ -5,23 +10,38 @@ import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.provider.Settings
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.automirrored.filled.InsertDriveFile
|
||||
import androidx.compose.material.icons.filled.Album
|
||||
import androidx.compose.material.icons.filled.CheckCircle
|
||||
import androidx.compose.material.icons.filled.Code
|
||||
import androidx.compose.material.icons.filled.CreateNewFolder
|
||||
import androidx.compose.material.icons.filled.Error
|
||||
import androidx.compose.material.icons.filled.Folder
|
||||
import androidx.compose.material.icons.filled.Info
|
||||
@@ -30,6 +50,7 @@ import androidx.compose.material.icons.filled.Security
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
@@ -42,20 +63,33 @@ import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
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.clip
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
import coil.compose.AsyncImage
|
||||
import coil.decode.SvgDecoder
|
||||
import coil.request.ImageRequest
|
||||
import kotlinx.coroutines.launch
|
||||
import sh.sar.isodroid.root.RootManager
|
||||
import sh.sar.isodroid.ui.components.findOsIcon
|
||||
import sh.sar.isodroid.viewmodel.MainViewModel
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@@ -68,7 +102,6 @@ fun SettingsScreen(
|
||||
val context = LocalContext.current
|
||||
val activity = context as? androidx.activity.ComponentActivity
|
||||
var showPathDialog by remember { mutableStateOf(false) }
|
||||
var tempPath by remember(uiState.currentPath) { mutableStateOf(uiState.isoDirectory) }
|
||||
|
||||
// Track notification permission with lifecycle-aware refresh
|
||||
var hasNotificationPermission by remember {
|
||||
@@ -226,11 +259,18 @@ fun SettingsScreen(
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
Column {
|
||||
Text(
|
||||
text = "ISO Drive",
|
||||
text = "ISO Droid",
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
val versionName = remember {
|
||||
try {
|
||||
context.packageManager.getPackageInfo(context.packageName, 0).versionName
|
||||
} catch (e: Exception) {
|
||||
"Unknown"
|
||||
}
|
||||
}
|
||||
Text(
|
||||
text = "Version 1.0",
|
||||
text = "Version $versionName",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
@@ -254,13 +294,13 @@ fun SettingsScreen(
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Text(
|
||||
text = "git.shihaam.dev/shihaam/ISODrive",
|
||||
text = "git.shihaam.dev/shihaam/ISODroid",
|
||||
style = MaterialTheme.typography.bodySmall.copy(
|
||||
textDecoration = TextDecoration.Underline
|
||||
),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.clickable {
|
||||
openUrl("https://git.shihaam.dev/shihaam/ISODrive")
|
||||
openUrl("https://git.shihaam.dev/shihaam/ISODroid")
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -301,7 +341,7 @@ fun SettingsScreen(
|
||||
}
|
||||
}
|
||||
Text(
|
||||
text = "The CLI tool that powers ISO Drive. Mounts ISO/IMG files as bootable USB devices using configfs.",
|
||||
text = "The CLI tool that powers ISO Droid. Mounts ISO/IMG files as bootable USB devices using configfs.",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(top = 8.dp)
|
||||
@@ -364,40 +404,378 @@ fun SettingsScreen(
|
||||
}
|
||||
}
|
||||
|
||||
// Path edit dialog
|
||||
// Directory browser dialog
|
||||
if (showPathDialog) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showPathDialog = false },
|
||||
title = { Text("ISO Directory") },
|
||||
text = {
|
||||
Column {
|
||||
Text(
|
||||
text = "Enter the path to the directory containing your ISO/IMG files.",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
OutlinedTextField(
|
||||
value = tempPath,
|
||||
onValueChange = { tempPath = it },
|
||||
label = { Text("Path") },
|
||||
singleLine = true,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
DirectoryBrowserDialog(
|
||||
initialPath = uiState.isoDirectory,
|
||||
onDismiss = { showPathDialog = false },
|
||||
onSelect = { selectedPath ->
|
||||
viewModel.setIsoDirectory(selectedPath)
|
||||
showPathDialog = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private data class BrowserItem(
|
||||
val name: String,
|
||||
val isDirectory: Boolean,
|
||||
val fullPath: String,
|
||||
val isDeletable: Boolean = false
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
private fun DirectoryBrowserDialog(
|
||||
initialPath: String,
|
||||
onDismiss: () -> Unit,
|
||||
onSelect: (String) -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
val hapticFeedback = LocalHapticFeedback.current
|
||||
var currentPath by remember { mutableStateOf(initialPath) }
|
||||
var items by remember { mutableStateOf<List<BrowserItem>>(emptyList()) }
|
||||
var isLoading by remember { mutableStateOf(true) }
|
||||
var showCreateFolderDialog by remember { mutableStateOf(false) }
|
||||
var showDeleteDialog by remember { mutableStateOf<String?>(null) }
|
||||
var newFolderName by remember { mutableStateOf("") }
|
||||
|
||||
val storageRoot = Environment.getExternalStorageDirectory().absolutePath
|
||||
|
||||
fun loadContents(path: String) {
|
||||
scope.launch {
|
||||
isLoading = true
|
||||
// Load directories
|
||||
val dirResult = RootManager.executeCommand(
|
||||
"find \"$path\" -maxdepth 1 -mindepth 1 -type d 2>/dev/null"
|
||||
)
|
||||
val directories = if (dirResult.success && dirResult.output.isNotBlank()) {
|
||||
dirResult.output.lines()
|
||||
.filter { it.isNotBlank() }
|
||||
.map { it.trim() }
|
||||
.filter { !it.substringAfterLast("/").startsWith(".") }
|
||||
.map { dirPath ->
|
||||
// Check if this directory was created by the app (has .isodroiddir marker)
|
||||
val markerCheck = RootManager.executeCommand(
|
||||
"test -f \"$dirPath/.isodroiddir\" && echo 'yes' || echo 'no'"
|
||||
)
|
||||
val isDeletable = markerCheck.output.trim() == "yes"
|
||||
BrowserItem(dirPath.substringAfterLast("/"), true, dirPath, isDeletable)
|
||||
}
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
|
||||
// Load ISO/IMG files
|
||||
val fileResult = RootManager.executeCommand(
|
||||
"find \"$path\" -maxdepth 1 -type f \\( -iname '*.iso' -o -iname '*.img' \\) 2>/dev/null"
|
||||
)
|
||||
val files = if (fileResult.success && fileResult.output.isNotBlank()) {
|
||||
fileResult.output.lines()
|
||||
.filter { it.isNotBlank() }
|
||||
.map { it.trim() }
|
||||
.map { BrowserItem(it.substringAfterLast("/"), false, it) }
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
|
||||
items = (directories.sortedBy { it.name.lowercase() } + files.sortedBy { it.name.lowercase() })
|
||||
isLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
fun createFolder(name: String) {
|
||||
scope.launch {
|
||||
val trimmedName = name.trim()
|
||||
if (trimmedName.isEmpty()) return@launch
|
||||
val newPath = "$currentPath/$trimmedName"
|
||||
RootManager.executeCommand("mkdir -p \"$newPath\"")
|
||||
// Create marker file to indicate this folder was created by the app
|
||||
RootManager.executeCommand("touch \"$newPath/.isodroiddir\"")
|
||||
// Auto-navigate into the new folder
|
||||
currentPath = newPath
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteFolder(path: String) {
|
||||
scope.launch {
|
||||
RootManager.executeCommand("rm -rf \"$path\"")
|
||||
loadContents(currentPath)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(currentPath) {
|
||||
loadContents(currentPath)
|
||||
}
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text("Select Directory")
|
||||
IconButton(onClick = { showCreateFolderDialog = true }) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.CreateNewFolder,
|
||||
contentDescription = "Create directory",
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
text = {
|
||||
Column {
|
||||
// Current path display
|
||||
Text(
|
||||
text = currentPath,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
HorizontalDivider()
|
||||
|
||||
// Content list
|
||||
if (isLoading) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = 200.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
CircularProgressIndicator(modifier = Modifier.size(32.dp))
|
||||
}
|
||||
} else {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = 200.dp, max = 300.dp)
|
||||
) {
|
||||
// Parent directory (..)
|
||||
if (currentPath != "/" && currentPath != storageRoot) {
|
||||
item {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
val parent = currentPath.substringBeforeLast("/")
|
||||
currentPath = parent.ifEmpty { "/" }
|
||||
}
|
||||
.padding(vertical = 12.dp, horizontal = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Folder,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
Text(
|
||||
text = "..",
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Items (directories and files)
|
||||
items(items) { item ->
|
||||
val isIso = item.name.lowercase().endsWith(".iso")
|
||||
val isImg = item.name.lowercase().endsWith(".img")
|
||||
val osIcon = if (!item.isDirectory) findOsIcon(context, item.name) else null
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.combinedClickable(
|
||||
onClick = {
|
||||
if (item.isDirectory) {
|
||||
currentPath = item.fullPath
|
||||
}
|
||||
},
|
||||
onLongClick = {
|
||||
if (item.isDirectory) {
|
||||
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
when {
|
||||
item.fullPath == initialPath -> {
|
||||
Toast.makeText(
|
||||
context,
|
||||
"Can't delete your current ISO directory. Change it first, then try again.",
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
item.isDeletable -> {
|
||||
showDeleteDialog = item.fullPath
|
||||
}
|
||||
else -> {
|
||||
Toast.makeText(
|
||||
context,
|
||||
"Can't delete this directory — it wasn't created by ISO Droid",
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
.padding(vertical = 8.dp, horizontal = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// Icon with badge
|
||||
Box(modifier = Modifier.size(32.dp)) {
|
||||
if (item.isDirectory) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Folder,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
} else if (osIcon != null) {
|
||||
// OS icon with file type badge
|
||||
AsyncImage(
|
||||
model = ImageRequest.Builder(context)
|
||||
.data("file:///android_asset/osicons/$osIcon")
|
||||
.decoderFactory(SvgDecoder.Factory())
|
||||
.build(),
|
||||
contentDescription = item.name,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.primary)
|
||||
)
|
||||
// File type badge
|
||||
Icon(
|
||||
imageVector = if (isIso) Icons.Default.Album else Icons.AutoMirrored.Filled.InsertDriveFile,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onPrimary,
|
||||
modifier = Modifier
|
||||
.size(14.dp)
|
||||
.align(Alignment.BottomEnd)
|
||||
.offset(x = 2.dp, y = 2.dp)
|
||||
.clip(CircleShape)
|
||||
.background(MaterialTheme.colorScheme.primary)
|
||||
.padding(2.dp)
|
||||
)
|
||||
} else {
|
||||
// Fallback icon
|
||||
Icon(
|
||||
imageVector = if (isIso) Icons.Default.Album else Icons.AutoMirrored.Filled.InsertDriveFile,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
Text(
|
||||
text = item.name,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
maxLines = 3,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.weight(1f),
|
||||
color = if (item.isDirectory)
|
||||
MaterialTheme.colorScheme.onSurface
|
||||
else
|
||||
MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Empty state
|
||||
if (items.isEmpty()) {
|
||||
item {
|
||||
Text(
|
||||
text = "Empty directory",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(vertical = 16.dp, horizontal = 4.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = { onSelect(currentPath) }) {
|
||||
Text("Select")
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text("Cancel")
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// Create folder dialog
|
||||
if (showCreateFolderDialog) {
|
||||
AlertDialog(
|
||||
onDismissRequest = {
|
||||
showCreateFolderDialog = false
|
||||
newFolderName = ""
|
||||
},
|
||||
title = { Text("Create Directory") },
|
||||
text = {
|
||||
OutlinedTextField(
|
||||
value = newFolderName,
|
||||
onValueChange = { newFolderName = it },
|
||||
label = { Text("Directory name") },
|
||||
singleLine = true,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
viewModel.setIsoDirectory(tempPath)
|
||||
showPathDialog = false
|
||||
}
|
||||
if (newFolderName.isNotBlank()) {
|
||||
createFolder(newFolderName)
|
||||
showCreateFolderDialog = false
|
||||
newFolderName = ""
|
||||
}
|
||||
},
|
||||
enabled = newFolderName.isNotBlank()
|
||||
) {
|
||||
Text("Save")
|
||||
Text("Create")
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { showPathDialog = false }) {
|
||||
TextButton(onClick = {
|
||||
showCreateFolderDialog = false
|
||||
newFolderName = ""
|
||||
}) {
|
||||
Text("Cancel")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Delete confirmation dialog
|
||||
showDeleteDialog?.let { pathToDelete ->
|
||||
AlertDialog(
|
||||
onDismissRequest = { showDeleteDialog = null },
|
||||
title = { Text("Delete Directory") },
|
||||
text = {
|
||||
Text("Are you sure you want to delete \"${pathToDelete.substringAfterLast("/")}\" and all its contents?")
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
deleteFolder(pathToDelete)
|
||||
showDeleteDialog = null
|
||||
}
|
||||
) {
|
||||
Text("Delete", color = MaterialTheme.colorScheme.error)
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { showDeleteDialog = null }) {
|
||||
Text("Cancel")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Shiham Abdul Rahman <shihaam@shihaam.dev>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package sh.sar.isodroid.ui.screens
|
||||
|
||||
import android.Manifest
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
@@ -92,7 +98,7 @@ private fun WelcomeStep(
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
Text(
|
||||
text = "Welcome to ISO Drive",
|
||||
text = "Welcome to ISO Droid",
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
@@ -147,7 +153,7 @@ private fun RootAccessStep(
|
||||
title = "Root Access",
|
||||
description = when (hasRoot) {
|
||||
false -> "Root access was denied. The app requires root to mount ISO files. You can try again or skip and grant access later from your root manager."
|
||||
else -> "ISO Drive needs superuser (root) access to mount ISO files as USB devices. This is required because mounting USB gadgets is a system-level operation."
|
||||
else -> "ISO Droid needs superuser (root) access to mount ISO files as USB devices. This is required because mounting USB gadgets is a system-level operation."
|
||||
},
|
||||
granted = hasRoot
|
||||
)
|
||||
@@ -253,7 +259,7 @@ private fun NotificationStep(
|
||||
description = when {
|
||||
permanentlyDenied -> "Notification permission was denied. You can enable it later in system settings if you change your mind."
|
||||
wasDenied -> "Notification permission was denied. You can try again or continue without notifications."
|
||||
else -> "ISO Drive shows a notification when an ISO is mounted, with a quick unmount button. This helps you keep track of the mount status."
|
||||
else -> "ISO Droid shows a notification when an ISO is mounted, with a quick unmount button. This helps you keep track of the mount status."
|
||||
},
|
||||
granted = if (wasDenied) false else if (hasPermission) true else null
|
||||
)
|
||||
@@ -318,7 +324,7 @@ private fun CompleteStep(
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Text(
|
||||
text = "ISO Drive is ready to use. Place your ISO or IMG files in the isodrive folder and start mounting.",
|
||||
text = "ISO Droid is ready to use. Place your ISO or IMG files in the isodroid directory and start mounting.",
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
textAlign = TextAlign.Center,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
@@ -327,7 +333,7 @@ private fun CompleteStep(
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Text(
|
||||
text = "Default directory: /sdcard/isodrive/",
|
||||
text = "Default directory: ${Environment.getExternalStorageDirectory().absolutePath}/isodroid/",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
textAlign = TextAlign.Center,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f)
|
||||
@@ -339,7 +345,7 @@ private fun CompleteStep(
|
||||
onClick = onFinish,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text("Start Using ISO Drive")
|
||||
Text("Start Using ISO Droid")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Shiham Abdul Rahman <shihaam@shihaam.dev>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package sh.sar.isodroid.ui.theme
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Shiham Abdul Rahman <shihaam@shihaam.dev>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package sh.sar.isodroid.ui.theme
|
||||
|
||||
import android.app.Activity
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Shiham Abdul Rahman <shihaam@shihaam.dev>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package sh.sar.isodroid.ui.theme
|
||||
|
||||
import androidx.compose.material3.Typography
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Shiham Abdul Rahman <shihaam@shihaam.dev>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package sh.sar.isodroid.viewmodel
|
||||
|
||||
import android.app.Application
|
||||
@@ -47,7 +52,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||
|
||||
companion object {
|
||||
private val KEY_ISO_DIRECTORY = stringPreferencesKey("iso_directory")
|
||||
private val DEFAULT_ISO_DIRECTORY = "${Environment.getExternalStorageDirectory().absolutePath}/isodrive"
|
||||
private val DEFAULT_ISO_DIRECTORY = "${Environment.getExternalStorageDirectory().absolutePath}/isodroid"
|
||||
}
|
||||
|
||||
private var initialized = false
|
||||
@@ -201,6 +206,8 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||
// Create directory if it doesn't exist
|
||||
if (!directory.exists()) {
|
||||
RootManager.executeCommand("mkdir -p \"$currentPath\"")
|
||||
// Create marker file to indicate this folder was created by the app
|
||||
RootManager.executeCommand("touch \"$currentPath/.isodroiddir\"")
|
||||
}
|
||||
|
||||
// Try multiple methods to list files
|
||||
|
||||
77
build_isodrive.sh
Executable file
@@ -0,0 +1,77 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Build isodrive from source for all Android architectures
|
||||
# Requires: Android NDK (or runs via nix-shell on NixOS)
|
||||
|
||||
SCRIPT_DIR=$(dirname "$(realpath "$0")")
|
||||
ISODRIVE_DIR="/tmp/isodrive"
|
||||
OUTPUT_DIR="$SCRIPT_DIR/app/src/main/assets/bin"
|
||||
|
||||
# Clone isodrive source
|
||||
if [[ -d "$ISODRIVE_DIR" ]]; then
|
||||
echo "Updating isodrive source..."
|
||||
git -C "$ISODRIVE_DIR" pull
|
||||
else
|
||||
echo "Cloning isodrive..."
|
||||
git clone --depth 1 https://github.com/nitanmarcel/isodrive "$ISODRIVE_DIR"
|
||||
fi
|
||||
|
||||
SRCS="$ISODRIVE_DIR/src/util.cpp $ISODRIVE_DIR/src/configfsisomanager.cpp $ISODRIVE_DIR/src/androidusbisomanager.cpp $ISODRIVE_DIR/src/main.cpp"
|
||||
CFLAGS="-I$ISODRIVE_DIR/src/include -static-libstdc++ -Os -s"
|
||||
|
||||
build_all() {
|
||||
local NDK="$1"
|
||||
local TOOLCHAIN="$NDK/toolchains/llvm/prebuilt/linux-x86_64/bin"
|
||||
|
||||
echo "Building arm64-v8a..."
|
||||
"$TOOLCHAIN/aarch64-linux-android26-clang++" $CFLAGS $SRCS -o "$OUTPUT_DIR/arm64-v8a/isodrive"
|
||||
|
||||
echo "Building armeabi-v7a..."
|
||||
"$TOOLCHAIN/armv7a-linux-androideabi26-clang++" $CFLAGS $SRCS -o "$OUTPUT_DIR/armeabi-v7a/isodrive"
|
||||
|
||||
echo "Building x86_64..."
|
||||
"$TOOLCHAIN/x86_64-linux-android26-clang++" $CFLAGS $SRCS -o "$OUTPUT_DIR/x86_64/isodrive"
|
||||
|
||||
echo "Building x86..."
|
||||
"$TOOLCHAIN/i686-linux-android26-clang++" $CFLAGS $SRCS -o "$OUTPUT_DIR/x86/isodrive"
|
||||
|
||||
echo "Done! Built isodrive for all architectures."
|
||||
ls -la "$OUTPUT_DIR"/*/isodrive
|
||||
}
|
||||
|
||||
# On NixOS, prefer nix-shell (local Android SDK has /bin/bash issues)
|
||||
if command -v nix-shell &>/dev/null; then
|
||||
echo "Using nix-shell to get Android NDK..."
|
||||
export SRCS CFLAGS OUTPUT_DIR
|
||||
NIXPKGS_ALLOW_UNFREE=1 nix-shell -p androidenv.androidPkgs.ndk-bundle --run '
|
||||
SDK_ROOT=$(find /nix/store -maxdepth 1 -name "*android-sdk-ndk*" -type d 2>/dev/null | head -1)
|
||||
NDK=$(ls -d "$SDK_ROOT/libexec/android-sdk/ndk/"* | head -1)
|
||||
TOOLCHAIN="$NDK/toolchains/llvm/prebuilt/linux-x86_64/bin"
|
||||
|
||||
echo "Using NDK: $NDK"
|
||||
|
||||
echo "Building arm64-v8a..."
|
||||
"$TOOLCHAIN/aarch64-linux-android26-clang++" $CFLAGS $SRCS -o "$OUTPUT_DIR/arm64-v8a/isodrive"
|
||||
|
||||
echo "Building armeabi-v7a..."
|
||||
"$TOOLCHAIN/armv7a-linux-androideabi26-clang++" $CFLAGS $SRCS -o "$OUTPUT_DIR/armeabi-v7a/isodrive"
|
||||
|
||||
echo "Building x86_64..."
|
||||
"$TOOLCHAIN/x86_64-linux-android26-clang++" $CFLAGS $SRCS -o "$OUTPUT_DIR/x86_64/isodrive"
|
||||
|
||||
echo "Building x86..."
|
||||
"$TOOLCHAIN/i686-linux-android26-clang++" $CFLAGS $SRCS -o "$OUTPUT_DIR/x86/isodrive"
|
||||
|
||||
echo "Done!"
|
||||
ls -la "$OUTPUT_DIR"/*/isodrive
|
||||
'
|
||||
elif [[ -n "${ANDROID_NDK_HOME:-}" ]]; then
|
||||
build_all "$ANDROID_NDK_HOME"
|
||||
elif [[ -n "${ANDROID_NDK:-}" ]]; then
|
||||
build_all "$ANDROID_NDK"
|
||||
else
|
||||
echo "Error: Android NDK not found."
|
||||
echo "Set ANDROID_NDK_HOME or ANDROID_NDK, or install nix-shell."
|
||||
exit 1
|
||||
fi
|
||||
@@ -229,13 +229,13 @@ fun toCommandArgs(): List<String> {
|
||||
**Example commands:**
|
||||
```bash
|
||||
# Mount as read-only mass storage
|
||||
isodrive "/sdcard/isodrive/ubuntu.iso" -configfs
|
||||
isodrive "/sdcard/isodroid/ubuntu.iso" -configfs
|
||||
|
||||
# Mount as writable drive
|
||||
isodrive "/sdcard/isodrive/drive.img" -rw -configfs
|
||||
isodrive "/sdcard/isodroid/drive.img" -rw -configfs
|
||||
|
||||
# Mount as CD-ROM
|
||||
isodrive "/sdcard/isodrive/windows.iso" -cdrom -configfs
|
||||
isodrive "/sdcard/isodroid/windows.iso" -cdrom -configfs
|
||||
```
|
||||
|
||||
## Event System
|
||||
@@ -338,7 +338,7 @@ private val KEY_ISO_DIRECTORY = stringPreferencesKey("iso_directory")
|
||||
```
|
||||
|
||||
Stores:
|
||||
- Custom ISO directory path (default: `/sdcard/isodrive/`)
|
||||
- Custom ISO directory path (default: `/sdcard/isodroid/`)
|
||||
|
||||
### SharedPreferences
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 117 KiB After Width: | Height: | Size: 124 KiB |
|
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 162 KiB After Width: | Height: | Size: 165 KiB |
|
Before Width: | Height: | Size: 94 KiB After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 109 KiB |
9
fastlane/metadata/android/en-US/changelogs/1.txt
Normal file
@@ -0,0 +1,9 @@
|
||||
* New app icon
|
||||
* Mount ISO/IMG files as USB mass storage or CD-ROM
|
||||
* Create blank IMG files for writable USB drives
|
||||
* Read-only or read-write mount options
|
||||
* Persistent notification with quick unmount button
|
||||
* OS-specific icons for popular distributions
|
||||
* Download links to popular operating systems
|
||||
* Rename and delete files
|
||||
* Material 3 dynamic theming with dark mode support
|
||||
24
fastlane/metadata/android/en-US/full_description.txt
Normal file
@@ -0,0 +1,24 @@
|
||||
ISO Droid lets you mount ISO and IMG files as USB mass storage or CD-ROM devices on rooted Android devices. Connect your phone to any PC and it will appear as a bootable USB drive or CD-ROM.
|
||||
|
||||
Features:
|
||||
* Mount ISO/IMG files as USB mass storage or CD-ROM
|
||||
* Create blank IMG files for writable USB drives
|
||||
* Read-only or read-write mount options
|
||||
* Persistent notification with quick unmount button
|
||||
* OS-specific icons for popular distributions (Linux, BSD, etc.)
|
||||
* Download links to popular operating systems
|
||||
* Rename and delete files
|
||||
* Material 3 dynamic theming with dark mode support
|
||||
|
||||
Requirements:
|
||||
* Rooted Android device (Magisk, KernelSU, APatch, etc.)
|
||||
* USB gadget support (configfs or sysfs)
|
||||
* Android 8.0+ (API 26)
|
||||
|
||||
Usage:
|
||||
1. Place your ISO/IMG files in /sdcard/isodroid/
|
||||
2. Select an ISO/IMG file from the list
|
||||
3. Choose mount options (Mass Storage or CD-ROM)
|
||||
4. Tap Mount
|
||||
5. Connect your phone to a PC via USB
|
||||
6. Unmount via the app or notification when done
|
||||
BIN
fastlane/metadata/android/en-US/images/icon.png
Normal file
|
After Width: | Height: | Size: 50 KiB |
BIN
fastlane/metadata/android/en-US/images/phoneScreenshots/1.jpg
Normal file
|
After Width: | Height: | Size: 86 KiB |
BIN
fastlane/metadata/android/en-US/images/phoneScreenshots/2.jpg
Normal file
|
After Width: | Height: | Size: 100 KiB |
BIN
fastlane/metadata/android/en-US/images/phoneScreenshots/3.jpg
Normal file
|
After Width: | Height: | Size: 109 KiB |
BIN
fastlane/metadata/android/en-US/images/phoneScreenshots/4.jpg
Normal file
|
After Width: | Height: | Size: 165 KiB |
BIN
fastlane/metadata/android/en-US/images/phoneScreenshots/5.jpg
Normal file
|
After Width: | Height: | Size: 124 KiB |
1
fastlane/metadata/android/en-US/short_description.txt
Normal file
@@ -0,0 +1 @@
|
||||
Mount ISO/IMG files as USB drives on rooted Android
|
||||
1
fastlane/metadata/android/en-US/title.txt
Normal file
@@ -0,0 +1 @@
|
||||
ISO Droid
|
||||
18
get_isodrive.sh
Normal file
@@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Get the directory where this script lives
|
||||
SCRIPT_DIR=$(dirname "$(realpath "$0")")
|
||||
|
||||
ISODRIVE_VERSION=$(curl -sI https://github.com/nitanmarcel/isodrive-magisk/releases/latest | grep -i ^location | grep -oP 'v\K[\d.]+')
|
||||
curl -sL https://github.com/nitanmarcel/isodrive-magisk/releases/download/v$ISODRIVE_VERSION/isodrive-magisk-v$ISODRIVE_VERSION.zip -o /tmp/isodrive-magisk.zip
|
||||
|
||||
unzip -q /tmp/isodrive-magisk.zip -d /tmp/isodrive-magisk
|
||||
|
||||
# Move the isodrive binary for each architecture
|
||||
mv /tmp/isodrive-magisk/libs/arm64-v8a/isodrive $SCRIPT_DIR/app/src/main/assets/bin/arm64-v8a/
|
||||
mv /tmp/isodrive-magisk/libs/armeabi-v7a/isodrive $SCRIPT_DIR/app/src/main/assets/bin/armeabi-v7a/
|
||||
mv /tmp/isodrive-magisk/libs/x86/isodrive $SCRIPT_DIR/app/src/main/assets/bin/x86/
|
||||
mv /tmp/isodrive-magisk/libs/x86_64/isodrive $SCRIPT_DIR/app/src/main/assets/bin/x86_64/
|
||||
|
||||
# Clean up temp files
|
||||
rm -rf /tmp/isodrive-magisk /tmp/isodrive-magisk.zip
|
||||