new feature: create custom images

This commit is contained in:
2026-03-10 04:21:44 +05:00
parent ffdb600c1c
commit 800f0fa15a
7 changed files with 562 additions and 5 deletions

View File

@@ -19,12 +19,16 @@ import kotlinx.coroutines.launch
import sh.sar.isodroid.data.IsoFile
import sh.sar.isodroid.data.MountOptions
import sh.sar.isodroid.data.MountStatus
import sh.sar.isodroid.isodrive.CreateImgEvent
import sh.sar.isodroid.isodrive.CreateImgEventBus
import sh.sar.isodroid.isodrive.IsoDriveManager
import sh.sar.isodroid.isodrive.MountEvent
import sh.sar.isodroid.isodrive.MountEventBus
import sh.sar.isodroid.isodrive.SupportStatus
import sh.sar.isodroid.notification.NotificationHelper
import sh.sar.isodroid.root.RootManager
import sh.sar.isodroid.ui.components.CreateImgOptions
import sh.sar.isodroid.ui.components.CreateImgProgress
import java.io.File
private val Application.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
@@ -39,6 +43,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
val uiState: StateFlow<MainUiState> = _uiState.asStateFlow()
private var navigationStack = mutableListOf<String>()
private var currentCreateImgFileName = ""
companion object {
private val KEY_ISO_DIRECTORY = stringPreferencesKey("iso_directory")
@@ -62,6 +67,58 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
}
}
}
// Observe create img events
viewModelScope.launch {
CreateImgEventBus.events.collect { event ->
when (event) {
is CreateImgEvent.Progress -> {
val progress = event.bytesWritten.toFloat() / event.totalBytes.toFloat()
_uiState.update {
it.copy(
createImgProgress = CreateImgProgress(
isCreating = true,
fileName = currentCreateImgFileName,
progress = progress,
bytesWritten = event.bytesWritten,
totalBytes = event.totalBytes
)
)
}
notificationHelper.showCreateProgressNotification(
currentCreateImgFileName,
(progress * 100).toInt(),
event.bytesWritten,
event.totalBytes
)
}
is CreateImgEvent.Complete -> {
_uiState.update {
it.copy(
createImgProgress = CreateImgProgress(),
successMessage = if (event.success) "Image created successfully" else "Failed to create image"
)
}
notificationHelper.showCreateCompleteNotification(
event.filePath?.substringAfterLast("/") ?: "image.img",
event.success
)
if (event.success) {
loadFiles()
}
}
is CreateImgEvent.Cancelled -> {
_uiState.update {
it.copy(
createImgProgress = CreateImgProgress(),
successMessage = "Image creation cancelled"
)
}
notificationHelper.hideProgressNotification()
}
}
}
}
}
fun initialize() {
@@ -351,6 +408,99 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
}
}
fun createImg(options: CreateImgOptions) {
viewModelScope.launch {
val fileName = "${options.fileName}.img"
currentCreateImgFileName = fileName
val filePath = "${_uiState.value.currentPath}/$fileName"
val totalBytes = options.totalBytes
// Check if file already exists
val checkResult = RootManager.executeCommand("test -f \"$filePath\" && echo exists")
if (checkResult.output.trim() == "exists") {
_uiState.update { it.copy(errorMessage = "File already exists: $fileName") }
return@launch
}
// Start creating
_uiState.update {
it.copy(
createImgProgress = CreateImgProgress(
isCreating = true,
fileName = fileName,
progress = 0f,
bytesWritten = 0,
totalBytes = totalBytes
)
)
}
CreateImgEventBus.resetCancel()
// Use dd with progress reporting
// We'll create in chunks and report progress
val blockSize = 1024 * 1024L // 1MB blocks
val totalBlocks = totalBytes / blockSize
var writtenBlocks = 0L
// Create file with dd in background, checking for cancellation
val result = kotlinx.coroutines.withContext(kotlinx.coroutines.Dispatchers.IO) {
try {
// First, create the file with truncate to reserve space indication
val createResult = RootManager.executeCommand(
"dd if=/dev/zero of=\"$filePath\" bs=1M count=0 seek=$totalBlocks 2>/dev/null"
)
if (!createResult.success) {
return@withContext false
}
// Now fill with zeros in chunks for progress reporting
while (writtenBlocks < totalBlocks) {
if (CreateImgEventBus.isCancelRequested()) {
// Clean up partial file
RootManager.executeCommand("rm -f \"$filePath\"")
return@withContext false
}
// Write a chunk (up to 64MB at a time for efficiency)
val chunksToWrite = minOf(64, totalBlocks - writtenBlocks)
val chunkResult = RootManager.executeCommand(
"dd if=/dev/zero of=\"$filePath\" bs=1M count=$chunksToWrite seek=$writtenBlocks conv=notrunc 2>/dev/null"
)
if (!chunkResult.success) {
RootManager.executeCommand("rm -f \"$filePath\"")
return@withContext false
}
writtenBlocks += chunksToWrite
val bytesWritten = writtenBlocks * blockSize
CreateImgEventBus.emitProgress(bytesWritten, totalBytes)
}
true
} catch (e: Exception) {
RootManager.executeCommand("rm -f \"$filePath\"")
false
}
}
if (CreateImgEventBus.isCancelRequested()) {
CreateImgEventBus.emitComplete(false, null)
} else {
CreateImgEventBus.emitComplete(result, if (result) filePath else null)
}
}
}
fun cancelCreateImg() {
viewModelScope.launch {
CreateImgEventBus.cancel()
}
}
fun clearError() {
_uiState.update { it.copy(errorMessage = null) }
}
@@ -370,5 +520,6 @@ data class MainUiState(
val currentPath: String = "",
val isoDirectory: String = "",
val errorMessage: String? = null,
val successMessage: String? = null
val successMessage: String? = null,
val createImgProgress: CreateImgProgress = CreateImgProgress()
)