diff --git a/app/src/main/assets/osicons/alpinelinux.svg b/app/src/main/assets/osicons/alpinelinux.svg
new file mode 100644
index 0000000..c554fb8
--- /dev/null
+++ b/app/src/main/assets/osicons/alpinelinux.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/src/main/assets/osicons/artixlinux.svg b/app/src/main/assets/osicons/artixlinux.svg
new file mode 100644
index 0000000..4b653dd
--- /dev/null
+++ b/app/src/main/assets/osicons/artixlinux.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/src/main/assets/osicons/garudalinux.svg b/app/src/main/assets/osicons/garudalinux.svg
new file mode 100644
index 0000000..d0f7d8b
--- /dev/null
+++ b/app/src/main/assets/osicons/garudalinux.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/src/main/assets/osicons/kalilinux.svg b/app/src/main/assets/osicons/kalilinux.svg
new file mode 100644
index 0000000..f308f11
--- /dev/null
+++ b/app/src/main/assets/osicons/kalilinux.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/src/main/assets/osicons/linux.svg b/app/src/main/assets/osicons/linux.svg
new file mode 100644
index 0000000..381d3d8
--- /dev/null
+++ b/app/src/main/assets/osicons/linux.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/src/main/assets/osicons/mxlinux.svg b/app/src/main/assets/osicons/mxlinux.svg
new file mode 100644
index 0000000..7805a52
--- /dev/null
+++ b/app/src/main/assets/osicons/mxlinux.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/src/main/assets/osicons/nobaralinux.svg b/app/src/main/assets/osicons/nobaralinux.svg
new file mode 100644
index 0000000..6ce9e46
--- /dev/null
+++ b/app/src/main/assets/osicons/nobaralinux.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/src/main/assets/osicons/proxmox.svg b/app/src/main/assets/osicons/proxmox.svg
new file mode 100644
index 0000000..395ffb8
--- /dev/null
+++ b/app/src/main/assets/osicons/proxmox.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/src/main/assets/osicons/rockylinux.svg b/app/src/main/assets/osicons/rockylinux.svg
new file mode 100644
index 0000000..af412a5
--- /dev/null
+++ b/app/src/main/assets/osicons/rockylinux.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/src/main/assets/osicons/voidlinux.svg b/app/src/main/assets/osicons/voidlinux.svg
new file mode 100644
index 0000000..a664c64
--- /dev/null
+++ b/app/src/main/assets/osicons/voidlinux.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/src/main/java/sh/sar/isodroid/ui/components/FileBrowser.kt b/app/src/main/java/sh/sar/isodroid/ui/components/FileBrowser.kt
index 622e2ed..6da334e 100644
--- a/app/src/main/java/sh/sar/isodroid/ui/components/FileBrowser.kt
+++ b/app/src/main/java/sh/sar/isodroid/ui/components/FileBrowser.kt
@@ -1,8 +1,10 @@
package sh.sar.isodroid.ui.components
import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.background
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.PaddingValues
import androidx.compose.foundation.layout.Row
@@ -10,11 +12,13 @@ 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.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.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Album
import androidx.compose.material.icons.filled.Folder
@@ -27,12 +31,48 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
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.hapticfeedback.HapticFeedbackType
+import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
+import coil.compose.AsyncImage
+import coil.decode.SvgDecoder
+import coil.request.ImageRequest
import sh.sar.isodroid.data.IsoFile
+/**
+ * Finds a matching OS icon filename for a given file by dynamically checking available icons.
+ * Returns the icon filename (e.g., "archlinux.svg") if a match is found, null otherwise.
+ * When multiple matches exist (e.g., "nixos" and "linux"), prioritizes:
+ * 1. Longest match (most specific)
+ * 2. Earliest position in filename (if same length)
+ */
+private fun findOsIcon(context: android.content.Context, filename: String): String? {
+ return try {
+ // Dynamically load available icon files from assets
+ val availableIcons = context.assets.list("osicons")
+ ?.filter { it.endsWith(".svg", ignoreCase = true) }
+ ?.map { it.removeSuffix(".svg").lowercase() }
+ ?: emptyList()
+
+ val lowerFilename = filename.lowercase()
+
+ // Find ALL matching icons, then return the best match
+ availableIcons
+ .filter { lowerFilename.contains(it) }
+ .maxWithOrNull(compareBy(
+ { it.length }, // Prefer longer matches
+ { -lowerFilename.indexOf(it) } // Then prefer earlier position (negated for descending)
+ ))
+ ?.let { "$it.svg" }
+ } catch (e: Exception) {
+ null
+ }
+}
+
@Composable
fun FileBrowser(
files: List,
@@ -95,6 +135,7 @@ fun FileBrowser(
name = file.name,
size = file.formattedSize,
isIso = file.name.lowercase().endsWith(".iso"),
+ isImg = file.name.lowercase().endsWith(".img"),
onClick = { onFileClick(file) },
onLongClick = {
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
@@ -114,9 +155,14 @@ private fun FileItem(
size: String,
isDirectory: Boolean = false,
isIso: Boolean = true,
+ isImg: Boolean = false,
onClick: () -> Unit,
onLongClick: (() -> Unit)?
) {
+ val context = LocalContext.current
+ val osIcon = if (!isDirectory) findOsIcon(context, name) else null
+ val iconTint = MaterialTheme.colorScheme.primary
+
Card(
modifier = Modifier
.fillMaxWidth()
@@ -134,20 +180,59 @@ private fun FileItem(
.padding(12.dp),
verticalAlignment = Alignment.CenterVertically
) {
- Icon(
- imageVector = when {
- isDirectory -> Icons.Default.Folder
- isIso -> Icons.Default.Album
- else -> Icons.Default.InsertDriveFile
- },
- contentDescription = null,
- tint = when {
- isDirectory -> MaterialTheme.colorScheme.primary
- isIso -> MaterialTheme.colorScheme.primary
- else -> MaterialTheme.colorScheme.secondary
- },
+ // Icon with file type indicator
+ Box(
modifier = Modifier.size(40.dp)
- )
+ ) {
+ if (osIcon != null) {
+ // Show OS icon from SVG
+ AsyncImage(
+ model = ImageRequest.Builder(context)
+ .data("file:///android_asset/osicons/$osIcon")
+ .decoderFactory(SvgDecoder.Factory())
+ .build(),
+ contentDescription = name,
+ modifier = Modifier.fillMaxSize(),
+ colorFilter = ColorFilter.tint(iconTint)
+ )
+
+ // Small file type indicator in bottom-right corner
+ Icon(
+ imageVector = when {
+ isIso -> Icons.Default.Album
+ isImg -> Icons.Default.InsertDriveFile
+ else -> Icons.Default.InsertDriveFile
+ },
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.onPrimary,
+ modifier = Modifier
+ .size(16.dp)
+ .align(Alignment.BottomEnd)
+ .offset(x = 2.dp, y = 2.dp)
+ .clip(CircleShape)
+ .background(MaterialTheme.colorScheme.primary)
+ .padding(2.dp)
+ )
+ } else {
+ // Fallback to standard Material icons
+ Icon(
+ imageVector = when {
+ isDirectory -> Icons.Default.Folder
+ isIso -> Icons.Default.Album
+ isImg -> Icons.Default.InsertDriveFile
+ else -> Icons.Default.InsertDriveFile
+ },
+ contentDescription = null,
+ tint = when {
+ isDirectory -> MaterialTheme.colorScheme.primary
+ isIso -> MaterialTheme.colorScheme.primary
+ else -> MaterialTheme.colorScheme.secondary
+ },
+ modifier = Modifier.fillMaxSize()
+ )
+ }
+ }
+
Spacer(modifier = Modifier.width(12.dp))
Column(modifier = Modifier.weight(1f)) {
Text(