Compare commits
53 Commits
v1.4
...
659c79e5dd
| Author | SHA1 | Date | |
|---|---|---|---|
|
659c79e5dd
|
|||
|
0512662466
|
|||
|
842bb0553e
|
|||
|
96e5cc1213
|
|||
|
621618a3da
|
|||
|
b6b05a71de
|
|||
|
72645c054a
|
|||
|
676df8e8b3
|
|||
|
b15c7c8213
|
|||
|
7c2f06bceb
|
|||
|
4a0817b222
|
|||
|
2b34d3f2ac
|
|||
|
085552b759
|
|||
|
4379656f16
|
|||
|
4787302f25
|
|||
|
0ec26cac29
|
|||
|
e9dfbce049
|
|||
|
ccb2af558b
|
|||
|
d0817240ec
|
|||
|
b1abee3579
|
|||
|
148b494926
|
|||
|
cdba2582fa
|
|||
|
3068939d93
|
|||
|
dc6f72bcc5
|
|||
|
804e4c3ae3
|
|||
|
b9a95bd12d
|
|||
|
32406e335a
|
|||
|
235053eba6
|
|||
|
8e3b29b5df
|
|||
|
22d729ce53
|
|||
|
06b67d64c7
|
|||
|
f15882aea2
|
|||
|
f3dc0b65d6
|
|||
|
4b22871ab4
|
|||
|
d15b56c9c0
|
|||
|
88ed16f89e
|
|||
|
71842b01d4
|
|||
|
fb74e3663d
|
|||
|
f031a96193
|
|||
|
860ff7c479
|
|||
|
fdc63b49ed
|
|||
|
30ab181d75
|
|||
|
1d8d6d0904
|
|||
|
a40986f9b8
|
|||
|
f44703a9fa
|
|||
|
65a1d79e32
|
|||
|
0c0476b413
|
|||
|
71534ac79f
|
|||
|
e7aadd5998
|
|||
|
5c9b398cb1
|
|||
|
cd807ff304
|
|||
|
0d5a73251c
|
|||
|
9f09ac1ad0
|
3
.build/release/.env.example
Normal file
@@ -0,0 +1,3 @@
|
||||
KEYSTORE_PASSWORD=your_keystore_password_here
|
||||
KEY_ALIAS=your_key_alias_here
|
||||
KEY_PASSWORD=your_key_password_here
|
||||
2
.build/release/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
.env
|
||||
release/
|
||||
11
.build/release/compose.yml
Normal file
@@ -0,0 +1,11 @@
|
||||
services:
|
||||
release:
|
||||
# image: git.shihaam.dev/dockerfiles/android-builder
|
||||
image: git.shihaam.dev/dockerfiles/runners/gradle
|
||||
hostname: isodroid
|
||||
network_mode: host
|
||||
env_file: .env
|
||||
volumes:
|
||||
- ./release:/release
|
||||
- ../../:/source
|
||||
- ./cache:/root/.gradle
|
||||
40
.build/release/create-release.sh
Normal file
@@ -0,0 +1,40 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
TAG="$1"
|
||||
TITLE="$2"
|
||||
NOTES_FILE="$3"
|
||||
ASSET_PATH="$4"
|
||||
|
||||
API_URL="${GITEA_SERVER_URL}/api/v1/repos/${GITEA_REPOSITORY}/releases"
|
||||
|
||||
# Create release
|
||||
RELEASE_RESPONSE=$(curl -s -X POST "$API_URL" \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{
|
||||
\"tag_name\": \"${TAG}\",
|
||||
\"name\": \"${TITLE}\",
|
||||
\"body\": $(jq -Rs . < "$NOTES_FILE")
|
||||
}")
|
||||
|
||||
RELEASE_ID=$(echo "$RELEASE_RESPONSE" | jq -r '.id')
|
||||
|
||||
if [ "$RELEASE_ID" = "null" ] || [ -z "$RELEASE_ID" ]; then
|
||||
echo "Failed to create release:"
|
||||
echo "$RELEASE_RESPONSE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Created release with ID: $RELEASE_ID"
|
||||
|
||||
# Upload asset
|
||||
ASSET_NAME=$(basename "$ASSET_PATH")
|
||||
UPLOAD_URL="${API_URL}/${RELEASE_ID}/assets?name=${ASSET_NAME}"
|
||||
|
||||
curl -s -X POST "$UPLOAD_URL" \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
-H "Content-Type: application/octet-stream" \
|
||||
--data-binary "@${ASSET_PATH}"
|
||||
|
||||
echo "Uploaded asset: $ASSET_NAME"
|
||||
17
.build/release/extract-changelog.sh
Normal file
@@ -0,0 +1,17 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
VERSION="${1#v}"
|
||||
VERSION_CODE=$(grep 'versionCode = ' app/build.gradle.kts | sed 's/.*versionCode = \([0-9]*\).*/\1/')
|
||||
FASTLANE_DIR="fastlane/metadata/android/en-US/changelogs"
|
||||
|
||||
mkdir -p "$FASTLANE_DIR"
|
||||
|
||||
awk -v ver="$VERSION" '
|
||||
BEGIN { found=0 }
|
||||
/^## \[/ {
|
||||
if (found) exit
|
||||
if ($0 ~ "\\[" ver "\\]") { found=1; next }
|
||||
}
|
||||
found { print }
|
||||
' CHANGELOG.md | tee release_notes.md > "$FASTLANE_DIR/${VERSION_CODE}.txt"
|
||||
30
.gitea/workflows/auto-tag.yml
Normal file
@@ -0,0 +1,30 @@
|
||||
name: Auto Tag on Version Change
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
check-version:
|
||||
runs-on: docker-compose
|
||||
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.PAT_GITEA }}
|
||||
|
||||
- name: Create tag if version changed
|
||||
run: |
|
||||
VERSION=$(grep 'versionName = ' app/build.gradle.kts | sed 's/.*versionName = "\(.*\)".*/\1/')
|
||||
echo "Current version: $VERSION"
|
||||
|
||||
if git tag -l | grep -q "^v${VERSION}$"; then
|
||||
echo "Tag v${VERSION} already exists, skipping"
|
||||
else
|
||||
git tag "v${VERSION}"
|
||||
git push origin "v${VERSION}"
|
||||
echo "Created and pushed tag v${VERSION}"
|
||||
fi
|
||||
38
.gitea/workflows/build-apk.yml
Normal file
@@ -0,0 +1,38 @@
|
||||
name: Build and Release APK
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: docker-compose
|
||||
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup keystore and environment
|
||||
run: |
|
||||
echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 -d > app/key.jks
|
||||
echo "${{ secrets.DOTENV_BASE64 }}" | base64 -d > .build/release/.env
|
||||
|
||||
- name: Build APK
|
||||
working-directory: .build/release
|
||||
run: docker compose run --rm release
|
||||
|
||||
- name: Extract changelog
|
||||
run: bash .build/release/extract-changelog.sh ${{ gitea.ref_name }}
|
||||
|
||||
- name: Create Gitea Release
|
||||
env:
|
||||
GITEA_SERVER_URL: ${{ gitea.server_url }}
|
||||
GITEA_REPOSITORY: ${{ gitea.repository }}
|
||||
GITEA_TOKEN: ${{ secrets.PAT_GITEA }}
|
||||
run: |
|
||||
bash .build/release/create-release.sh \
|
||||
"${{ gitea.ref_name }}" \
|
||||
"ISODroid ${{ gitea.ref_name }}" \
|
||||
"release_notes.md" \
|
||||
".build/release/release/ISODroid-${{ gitea.ref_name }}.apk"
|
||||
2
.gitignore
vendored
@@ -16,3 +16,5 @@ local.properties
|
||||
|
||||
app/release/
|
||||
app/debug/
|
||||
app/key.jks
|
||||
.build/release/release/*.apk
|
||||
|
||||
3
.gitmodules
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
[submodule "isodrive"]
|
||||
path = isodrive
|
||||
url = https://git.shihaam.dev/shihaam/isodrive
|
||||
1
.idea/vcs.xml
generated
@@ -2,5 +2,6 @@
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
<mapping directory="$PROJECT_DIR$/isodrive" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
32
CHANGELOG.md
@@ -5,6 +5,38 @@ 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.7] - 2026-03-13
|
||||
|
||||
### Changed
|
||||
- Always use app-bundled isodrive binary instead of system binary
|
||||
- Prep for edge-to-edge compatibility
|
||||
|
||||
## [1.6] - 2026-03-13
|
||||
|
||||
### Added
|
||||
- Disclaimer screen in welcome wizard about user responsibility
|
||||
- USB services restart warning dialog before mount/unmount operations
|
||||
- Toggle in Settings to enable/disable USB restart warning
|
||||
|
||||
### Fixed
|
||||
- Prevent shell escape exploits in shell commands
|
||||
- Disable logcat on release builds
|
||||
|
||||
## [1.5] - 2026-03-12
|
||||
|
||||
### Added
|
||||
- More OS download links
|
||||
- Sub-categories for OS downloads (Desktop, Server, etc.)
|
||||
|
||||
### Changed
|
||||
- Replaced refresh button with pull-to-refresh gesture
|
||||
- Disable ISO directory change when root access is not available
|
||||
- Status card now shows "Tap to request root" and updates when granted
|
||||
- Improved monet/dynamic theme support for icons and text colors
|
||||
|
||||
### Fixed
|
||||
- Sanitize user input for folder creation, IMG file creation, and file renaming to prevent shell escape vulnerabilities
|
||||
|
||||
## [1.4] - 2026-03-11
|
||||
|
||||
### Added
|
||||
|
||||
42
README.md
@@ -1,12 +1,26 @@
|
||||
# ISO Droid
|
||||
|
||||
[](https://sladge.net)
|
||||
[](LICENSE)
|
||||

|
||||

|
||||

|
||||
[](docs/DISCLAIMER.md)
|
||||

|
||||
[](https://git.shihaam.dev/shihaam/ISODroid/releases/latest)
|
||||
|
||||
> **Note:** This app requires root access and was developed with AI assistance (Claude).
|
||||
> I use it on my own devices, but as with any root tool, understand what you're running and keep backups.
|
||||
>
|
||||
> See [full disclaimer](docs/DISCLAIMER.md) | [Report issues](https://git.sargit.com/sargit/ISODroid/issues)
|
||||
|
||||
Android app for mounting ISO/IMG files as USB mass storage or CD-ROM devices on rooted Android devices.
|
||||
|
||||
## Screenshots
|
||||
|
||||
| OS images listing | Mounted Status | Mount Options Dialog | Create IMG Dialog | Download OS ISOs |
|
||||
| OS images listing | Mount Options Dialog | Mounted Status | Create IMG Dialog | Download OS ISOs |
|
||||
|:--:|:--:|:--:|:--:|:--:|
|
||||
|  |  |  |  |  |
|
||||
|  |  |  |  |  |
|
||||
|
||||
## Features
|
||||
|
||||
@@ -27,8 +41,9 @@ Android app for mounting ISO/IMG files as USB mass storage or CD-ROM devices on
|
||||
|
||||
## Download
|
||||
|
||||
- **F-Droid**: *Coming soon*
|
||||
- **Gitea Releases**: [Download latest APK](https://git.sargit.com/sargit/ISODroid/releases)
|
||||
| F-Droid | Gitea Releases |
|
||||
|:--:|:--:|
|
||||
| <!-- [](https://f-droid.org/en/packages/sh.sar.isodroid) --> [Coming Soon](https://gitlab.com/fdroid/fdroiddata/-/merge_requests/34539) | [Download latest APK](https://git.shihaam.dev/shihaam/ISODroid/releases/latest) |
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -37,8 +52,6 @@ Android app for mounting ISO/IMG files as USB mass storage or CD-ROM devices on
|
||||
3. Grant root access when prompted
|
||||
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!
|
||||
|
||||
## Usage
|
||||
|
||||
1. Open ISO Droid
|
||||
@@ -59,11 +72,24 @@ Android app for mounting ISO/IMG files as USB mass storage or CD-ROM devices on
|
||||
|
||||
- **[Building & Technical Details](docs/BUILDING.md)** - How to build from source, architecture overview, and technical documentation
|
||||
|
||||
## Credits
|
||||
## Credits & Inspiration
|
||||
|
||||
Around 2014/2015 I rooted my first phone and went down the rabbit hole of what you could do with a rooted Android.
|
||||
That's when I found [DriveDroid](https://softwarebakery.com/projects/drivedroid) ([archive](https://web.archive.org/web/20160901000000*/https://softwarebakery.com/projects/drivedroid)) by Software Bakery. Teenage me thought it was the coolest thing ever.
|
||||
|
||||
Fast forward a 10 years: DriveDroid is no longer maintained, 404 on Play Store, and even if you find an APK ([Kali NetHunter App Store](https://store.nethunter.com/packages/com.softwarebakery.drivedroid/) keeps one) it does not work on modern Android.
|
||||
|
||||
As someone who gives tech support often to relatives and friends, I've wanted something like it for years. Having Linux to chroot or HBCD for Windows password reset at anytime without having to carry around USB drives is very useful for me.
|
||||
Then I found [isodrive](https://github.com/nitanmarcel/isodrive) by nitanmarcel ([mirror](https://git.shihaam.dev/shihaam/isodrive)). I tried the Magisk module and Termux, it worked. So here we are.
|
||||
|
||||
ISO Droid is basically me chasing that teenage nostalgia and trying to make something simple that just works.
|
||||
The name comes from **iso**drive + Drive**Droid** = **ISO Droid**.
|
||||
|
||||
Special thanks to:
|
||||
- [isodrive](https://github.com/nitanmarcel/isodrive) by nitanmarcel - The CLI tool that powers ISO Droid
|
||||
- [ISODriveUT](https://github.com/fredldotme/ISODriveUT) by fredldotme - Original inspiration for isodrive
|
||||
- OS icons from [Simple Icons](https://simpleicons.org/) and [SVG Repo](https://www.svgrepo.com/)
|
||||
- [DriveDroid](https://softwarebakery.com/projects/drivedroid) by Software Bakery - where it all started for me
|
||||
- [Simple Icons](https://simpleicons.org/) and [SVG Repo](https://www.svgrepo.com/) for OS icons
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -1,9 +1,127 @@
|
||||
import javax.inject.Inject
|
||||
import org.gradle.process.ExecOperations
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.android.application)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
alias(libs.plugins.kotlin.compose)
|
||||
}
|
||||
|
||||
// Build isodrive native binary from submodule (only if binaries are missing)
|
||||
abstract class BuildIsodriveTask @Inject constructor(
|
||||
private val execOps: ExecOperations
|
||||
) : DefaultTask() {
|
||||
|
||||
@get:InputDirectory
|
||||
abstract val isodriveDir: DirectoryProperty
|
||||
|
||||
@get:OutputDirectory
|
||||
abstract val outputDir: DirectoryProperty
|
||||
|
||||
@TaskAction
|
||||
fun build() {
|
||||
val isodrivePath = isodriveDir.get().asFile
|
||||
val outputPath = outputDir.get().asFile
|
||||
|
||||
if (!isodrivePath.resolve("src").exists()) {
|
||||
throw GradleException("isodrive submodule not initialized. Run: git submodule update --init")
|
||||
}
|
||||
|
||||
val sourceFiles = listOf(
|
||||
"src/util.cpp",
|
||||
"src/configfsisomanager.cpp",
|
||||
"src/androidusbisomanager.cpp",
|
||||
"src/main.cpp"
|
||||
)
|
||||
val srcs = sourceFiles.joinToString(" ") { "$isodrivePath/$it" }
|
||||
val cflags = "-I$isodrivePath/src/include -static-libstdc++ -Os -s"
|
||||
|
||||
val compilers = mapOf(
|
||||
"arm64-v8a" to "aarch64-linux-android26-clang++",
|
||||
"armeabi-v7a" to "armv7a-linux-androideabi26-clang++",
|
||||
"x86_64" to "x86_64-linux-android26-clang++",
|
||||
"x86" to "i686-linux-android26-clang++"
|
||||
)
|
||||
|
||||
val useNixShell = File("/etc/NIXOS").exists() ||
|
||||
Runtime.getRuntime().exec(arrayOf("which", "nix-shell")).waitFor() == 0
|
||||
|
||||
if (useNixShell) {
|
||||
println("NixOS detected, using nix-shell for NDK...")
|
||||
|
||||
val buildScript = compilers.entries.joinToString("\n") { (arch, compiler) ->
|
||||
val archDir = outputPath.resolve(arch)
|
||||
val output = archDir.resolve("isodrive")
|
||||
if (output.exists()) {
|
||||
"echo 'isodrive for $arch already exists, skipping'"
|
||||
} else {
|
||||
"""
|
||||
mkdir -p "$archDir"
|
||||
echo "Building isodrive for $arch..."
|
||||
"${'$'}TOOLCHAIN/$compiler" $cflags $srcs -o "$output"
|
||||
""".trimIndent()
|
||||
}
|
||||
}
|
||||
|
||||
execOps.exec {
|
||||
environment("NIXPKGS_ALLOW_UNFREE", "1")
|
||||
commandLine("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"
|
||||
$buildScript
|
||||
echo "Done building isodrive"
|
||||
""".trimIndent())
|
||||
}
|
||||
} else {
|
||||
val ndkDir = listOfNotNull(
|
||||
System.getenv("ANDROID_NDK_HOME"),
|
||||
System.getenv("ANDROID_NDK")
|
||||
).firstOrNull { File(it).exists() }
|
||||
?: throw GradleException("Android NDK not found. Set ANDROID_NDK_HOME or install NDK via SDK Manager.")
|
||||
|
||||
val toolchain = "$ndkDir/toolchains/llvm/prebuilt/linux-x86_64/bin"
|
||||
if (!File(toolchain).exists()) {
|
||||
throw GradleException("NDK toolchain not found at: $toolchain")
|
||||
}
|
||||
|
||||
compilers.forEach { (arch, compiler) ->
|
||||
val archDir = outputPath.resolve(arch)
|
||||
val output = archDir.resolve("isodrive")
|
||||
|
||||
if (output.exists()) {
|
||||
println("isodrive for $arch already exists, skipping")
|
||||
return@forEach
|
||||
}
|
||||
|
||||
archDir.mkdirs()
|
||||
println("Building isodrive for $arch...")
|
||||
execOps.exec {
|
||||
commandLine("sh", "-c", "$toolchain/$compiler $cflags $srcs -o $output")
|
||||
}
|
||||
}
|
||||
println("Done building isodrive")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val buildIsodrive by tasks.registering(BuildIsodriveTask::class) {
|
||||
group = "native"
|
||||
description = "Build isodrive binary for all Android architectures"
|
||||
|
||||
isodriveDir.set(rootProject.file("isodrive"))
|
||||
outputDir.set(file("src/main/assets/bin"))
|
||||
|
||||
val architectures = listOf("arm64-v8a", "armeabi-v7a", "x86_64", "x86")
|
||||
val outDir = file("src/main/assets/bin")
|
||||
onlyIf { !architectures.all { arch -> outDir.resolve("$arch/isodrive").exists() } }
|
||||
}
|
||||
|
||||
tasks.matching { it.name.startsWith("merge") && it.name.endsWith("Assets") }.configureEach {
|
||||
dependsOn(buildIsodrive)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "sh.sar.isodroid"
|
||||
compileSdk = 36
|
||||
@@ -12,14 +130,24 @@ android {
|
||||
applicationId = "sh.sar.isodroid"
|
||||
minSdk = 26
|
||||
targetSdk = 36
|
||||
versionCode = 4
|
||||
versionName = "1.4"
|
||||
versionCode = 7
|
||||
versionName = "1.7"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
create("release") {
|
||||
storeFile = file("key.jks")
|
||||
storePassword = System.getenv("KEYSTORE_PASSWORD")
|
||||
keyAlias = System.getenv("KEY_ALIAS")
|
||||
keyPassword = System.getenv("KEY_PASSWORD")
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
signingConfig = signingConfigs.getByName("release")
|
||||
isMinifyEnabled = false
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
|
||||
@@ -2,13 +2,23 @@
|
||||
{
|
||||
"name": "Arch Linux",
|
||||
"category": "Linux",
|
||||
"subcategory": "Enthusiast",
|
||||
"description": "A simple, lightweight distribution",
|
||||
"icon": "archlinux.svg",
|
||||
"url": "https://archlinux.org/download/"
|
||||
},
|
||||
{
|
||||
"name": "Gentoo Linux",
|
||||
"category": "Linux",
|
||||
"subcategory": "Enthusiast",
|
||||
"description": "A highly flexible, source-based Linux distribution.",
|
||||
"icon": "gentoo.svg",
|
||||
"url": "https://www.gentoo.org/downloads/"
|
||||
},
|
||||
{
|
||||
"name": "Debian",
|
||||
"category": "Linux",
|
||||
"subcategory": "Server",
|
||||
"description": "Debian is a complete Free Operating System!",
|
||||
"icon": "debian.svg",
|
||||
"url": "https://www.debian.org/download"
|
||||
@@ -16,6 +26,7 @@
|
||||
{
|
||||
"name": "Fedora",
|
||||
"category": "Linux",
|
||||
"subcategory": "Desktop",
|
||||
"description": "The Fedora Project is a community of people working together to build a free and open source software platform and to collaborate on and share user-focused solutions built on that platform.",
|
||||
"icon": "fedora.svg",
|
||||
"url": "https://www.fedoraproject.org/"
|
||||
@@ -23,6 +34,7 @@
|
||||
{
|
||||
"name": "Linux Mint",
|
||||
"category": "Linux",
|
||||
"subcategory": "Desktop",
|
||||
"description": "Linux Mint is an operating system for desktop and laptop computers. It is designed to work 'out of the box' and comes fully equipped with the apps most people need.",
|
||||
"icon": "linuxmint.svg",
|
||||
"url": "https://linuxmint.com/download.php"
|
||||
@@ -30,6 +42,7 @@
|
||||
{
|
||||
"name": "NixOS",
|
||||
"category": "Linux",
|
||||
"subcategory": "Enthusiast",
|
||||
"description": "Declarative builds and deployments.",
|
||||
"icon": "nixos.svg",
|
||||
"url": "https://nixos.org/download/#nixos-iso"
|
||||
@@ -37,6 +50,7 @@
|
||||
{
|
||||
"name": "Pop!_OS",
|
||||
"category": "Linux",
|
||||
"subcategory": "Desktop",
|
||||
"description": "Unleash your potential on a Linux operating system made to be productive and personal.",
|
||||
"icon": "popos.svg",
|
||||
"url": "https://system76.com/pop/download/"
|
||||
@@ -44,6 +58,7 @@
|
||||
{
|
||||
"name": "Tails",
|
||||
"category": "Linux",
|
||||
"subcategory": "Security",
|
||||
"description": "Tails is a portable operating system that protects against surveillance and censorship.",
|
||||
"icon": "tails.svg",
|
||||
"url": "https://tails.net/install/download/index.en.html"
|
||||
@@ -51,36 +66,281 @@
|
||||
{
|
||||
"name": "Ubuntu",
|
||||
"category": "Linux",
|
||||
"description": "",
|
||||
"subcategory": "Desktop",
|
||||
"description": "Ubuntu Desktop delivers new tools and enhancements for developers, creators, gamers, and administrators.",
|
||||
"icon": "ubuntu.svg",
|
||||
"url": "https://ubuntu.com/download"
|
||||
},
|
||||
{
|
||||
"name": "openSUSE",
|
||||
"category": "Linux",
|
||||
"subcategory": "Desktop",
|
||||
"description": "openSUSE makes open source Linux operating systems for desktops, servers and containers.",
|
||||
"icon": "opensuse.svg",
|
||||
"url": "https://get.opensuse.org/"
|
||||
},
|
||||
{
|
||||
"name": "Rocky Linux",
|
||||
"category": "Linux",
|
||||
"subcategory": "Server",
|
||||
"description": "Rocky Linux is an open-source enterprise operating system designed to be 100% bug-for-bug compatible with Red Hat Enterprise Linux®.",
|
||||
"icon": "rockylinux.svg",
|
||||
"url": "https://rockylinux.org/download"
|
||||
},
|
||||
{
|
||||
"name": "AlmaLinux",
|
||||
"category": "Linux",
|
||||
"subcategory": "Server",
|
||||
"description": "An Open Source, community owned and governed, forever-free enterprise Linux distribution, focused on long-term stability, providing a robust production-grade platform. AlmaLinux OS is binary compatible with RHEL®.",
|
||||
"icon": "almalinux.svg",
|
||||
"url": "https://almalinux.org/get-almalinux/"
|
||||
},
|
||||
{
|
||||
"name": "Ubuntu Server",
|
||||
"category": "Linux",
|
||||
"subcategory": "Server",
|
||||
"description": "Whether you want to configure a simple file server or build a fifty thousand-node cloud, you can rely on Ubuntu Server and its five years of free updates.",
|
||||
"icon": "ubuntu.svg",
|
||||
"url": "https://ubuntu.com/download/server"
|
||||
},
|
||||
{
|
||||
"name": "Alpine Linux",
|
||||
"category": "Linux",
|
||||
"subcategory": "Server",
|
||||
"description": "Alpine Linux is a security-oriented, lightweight Linux distribution based on musl libc and busybox.",
|
||||
"icon": "alpinelinux.svg",
|
||||
"url": "https://alpinelinux.org/downloads/"
|
||||
},
|
||||
{
|
||||
"name": "FreeBSD",
|
||||
"category": "BSD",
|
||||
"subcategory": null,
|
||||
"description": "FreeBSD is an operating system for a variety of platforms which focuses on features, speed, and stability.",
|
||||
"icon": "freebsd.svg",
|
||||
"url": "https://www.freebsd.org/where/"
|
||||
},
|
||||
{
|
||||
"name": "OpenBSD",
|
||||
"category": "BSD",
|
||||
"subcategory": null,
|
||||
"description": "A free, multi-platform 4.4BSD-based UNIX-like operating system emphasizing security and correctness.",
|
||||
"icon": "openbsd.svg",
|
||||
"url": "https://www.openbsd.org/faq/faq4.html#Download"
|
||||
},
|
||||
{
|
||||
"name": "GhostBSD",
|
||||
"category": "BSD",
|
||||
"subcategory": "Desktop",
|
||||
"description": "A simple, elegant desktop BSD Operating System",
|
||||
"icon": null,
|
||||
"url": "https://ghostbsd.org/download"
|
||||
},
|
||||
{
|
||||
"name": "TrueNAS",
|
||||
"category": "BSD",
|
||||
"subcategory": "Networking",
|
||||
"description": "The world's most popular software-defined storage operating system.",
|
||||
"icon": "truenas.svg",
|
||||
"url": "https://www.truenas.com/download-truenas-core/"
|
||||
},
|
||||
{
|
||||
"name": "OPNsense",
|
||||
"category": "BSD",
|
||||
"subcategory": "Networking",
|
||||
"description": "OPNsense® is an open source, feature rich firewall and routing platform, offering cutting-edge network protection.",
|
||||
"icon": "opnsense.svg",
|
||||
"url": "https://opnsense.org/download/"
|
||||
},
|
||||
{
|
||||
"name": "pfSense",
|
||||
"category": "BSD",
|
||||
"subcategory": "Networking",
|
||||
"description": "Secure networks start here.™",
|
||||
"icon": "pfsense.svg",
|
||||
"url": "https://www.pfsense.org/download/"
|
||||
},
|
||||
{
|
||||
"name": "Windows 11",
|
||||
"category": "Windows",
|
||||
"subcategory": null,
|
||||
"description": "",
|
||||
"icon": "windows11.svg",
|
||||
"url": "https://www.microsoft.com/en-us/software-download/windows11"
|
||||
},
|
||||
{
|
||||
"name": "Windows 10",
|
||||
"category": "Windows",
|
||||
"subcategory": null,
|
||||
"description": "",
|
||||
"icon": "windows10.svg",
|
||||
"url": "https://www.microsoft.com/en-us/software-download/windows10ISO"
|
||||
},
|
||||
{
|
||||
"name": "Hiren's BootCD PE",
|
||||
"category": "Recovery",
|
||||
"subcategory": "Toolkit",
|
||||
"description": "Hiren's BootCD PE (Preinstallation Environment) is a restored edition of Hiren's BootCD based on Windows PE",
|
||||
"icon": null,
|
||||
"url": "https://www.hirensbootcd.org/download/"
|
||||
},
|
||||
{
|
||||
"name": "Clonezilla",
|
||||
"category": "Recovery",
|
||||
"subcategory": "Disk",
|
||||
"description": "Clonezilla is a partition and disk imaging/cloning program.",
|
||||
"icon": null,
|
||||
"url": "https://clonezilla.org/downloads.php"
|
||||
},
|
||||
{
|
||||
"name": "SystemRescue",
|
||||
"category": "Recovery",
|
||||
"subcategory": "Toolkit",
|
||||
"description": "A Linux system rescue toolkit for administering or repairing your system and data.",
|
||||
"icon": null,
|
||||
"url": "https://www.system-rescue.org/Download/"
|
||||
},
|
||||
{
|
||||
"name": "GParted Live",
|
||||
"category": "Recovery",
|
||||
"subcategory": "Disk",
|
||||
"description": "A small bootable GNU/Linux distribution for x86 based computers with the GParted partition editor.",
|
||||
"icon": null,
|
||||
"url": "https://gparted.org/download.php"
|
||||
},
|
||||
{
|
||||
"name": "Rescuezilla",
|
||||
"category": "Recovery",
|
||||
"subcategory": "Disk",
|
||||
"description": "The Swiss Army Knife of system recovery. Easy-to-use disk imaging and cloning with a friendly GUI.",
|
||||
"icon": null,
|
||||
"url": "https://rescuezilla.com/download"
|
||||
},
|
||||
{
|
||||
"name": "Memtest86+",
|
||||
"category": "Recovery",
|
||||
"subcategory": "Hardware",
|
||||
"description": "Free, open-source memory testing software for x86 and x86-64 computers.",
|
||||
"icon": null,
|
||||
"url": "https://memtest.org/"
|
||||
},
|
||||
{
|
||||
"name": "ShredOS",
|
||||
"category": "Recovery",
|
||||
"subcategory": "Disk",
|
||||
"description": "A USB bootable small linux distro for securely erasing your disks using nwipe.",
|
||||
"icon": null,
|
||||
"url": "https://github.com/PartialVolume/shredos.x86_64/releases"
|
||||
},
|
||||
{
|
||||
"name": "Super Grub2",
|
||||
"category": "Recovery",
|
||||
"subcategory": "Boot",
|
||||
"description": "Get back to your GNU/Linux & Windows computers !",
|
||||
"icon": null,
|
||||
"url": "https://www.supergrubdisk.org/category/download/"
|
||||
},
|
||||
{
|
||||
"name": "Bazzite",
|
||||
"category": "Linux",
|
||||
"subcategory": "Desktop",
|
||||
"description": "Bazzite makes gaming and everyday use smoother and simpler across desktop PCs, handhelds, tablets, and home theater PCs.",
|
||||
"icon": null,
|
||||
"url": "https://bazzite.gg/#image-picker"
|
||||
},
|
||||
{
|
||||
"name": "Manjaro",
|
||||
"category": "Linux",
|
||||
"subcategory": "Desktop",
|
||||
"description": "Taking the raw power and flexibility of Arch Linux and making it more accessible for a greater audience.",
|
||||
"icon": "manjaro.svg",
|
||||
"url": "https://manjaro.org/products"
|
||||
},
|
||||
{
|
||||
"name": "Kali Linux",
|
||||
"category": "Linux",
|
||||
"subcategory": "Security",
|
||||
"description": "The most advanced penetration testing platform ever made.",
|
||||
"icon": "kalilinux.svg",
|
||||
"url": "https://www.kali.org/get-kali/#kali-installer-images"
|
||||
},
|
||||
{
|
||||
"name": "ParrotOS",
|
||||
"category": "Linux",
|
||||
"subcategory": "Security",
|
||||
"description": "The ultimate framework for your Cyber Security operations",
|
||||
"icon": "parrotsecurity.svg",
|
||||
"url": "https://www.parrotsec.org/download/"
|
||||
},
|
||||
{
|
||||
"name": "ProxmoxVE",
|
||||
"category": "Linux",
|
||||
"subcategory": "Server",
|
||||
"description": "Proxmox Virtual Environment is a complete open-source platform for enterprise virtualization.",
|
||||
"icon": "proxmox.svg",
|
||||
"url": "https://www.proxmox.com/en/downloads/proxmox-virtual-environment/iso"
|
||||
},
|
||||
{
|
||||
"name": "Android-x86",
|
||||
"category": "Android",
|
||||
"subcategory": null,
|
||||
"description": "This is a project to port Android Open Source Project to x86 platform",
|
||||
"icon": "android.svg",
|
||||
"url": "https://www.android-x86.org/download"
|
||||
},
|
||||
{
|
||||
"name": "RemixOS",
|
||||
"category": "Android",
|
||||
"subcategory": null,
|
||||
"description": "RemixOS brings Android to laptops and PCs with a powerful desktop-like experience.",
|
||||
"icon": null,
|
||||
"url": "https://www.fosshub.com/Remix-OS.html"
|
||||
},
|
||||
{
|
||||
"name": "TempleOS",
|
||||
"category": "Other",
|
||||
"subcategory": null,
|
||||
"description": "A biblical-themed lightweight operating system designed to be the Third Temple.",
|
||||
"icon": null,
|
||||
"url": "https://templeos.org/Downloads/"
|
||||
},
|
||||
{
|
||||
"name": "Haiku",
|
||||
"category": "Other",
|
||||
"subcategory": null,
|
||||
"description": "An open-source operating system inspired by BeOS, targeting personal computing.",
|
||||
"icon": null,
|
||||
"url": "https://www.haiku-os.org/get-haiku/"
|
||||
},
|
||||
{
|
||||
"name": "ReactOS",
|
||||
"category": "Other",
|
||||
"subcategory": null,
|
||||
"description": "A free, open-source operating system aiming for Windows compatibility.",
|
||||
"icon": "reactos.svg",
|
||||
"url": "https://reactos.org/download/"
|
||||
},
|
||||
{
|
||||
"name": "ChromeOS Flex",
|
||||
"category": "ChromeOS",
|
||||
"subcategory": null,
|
||||
"description": "Google's cloud-first operating system for PCs and Macs.",
|
||||
"icon": "chromeos.svg",
|
||||
"url": "https://chromeenterprise.google/os/chromeosflex/"
|
||||
},
|
||||
{
|
||||
"name": "FydeOS",
|
||||
"category": "ChromeOS",
|
||||
"subcategory": null,
|
||||
"description": "A ChromiumOS-based operating system with Android app support.",
|
||||
"icon": null,
|
||||
"url": "https://fydeos.io/download"
|
||||
},
|
||||
{
|
||||
"name": "Brunch",
|
||||
"category": "ChromeOS",
|
||||
"subcategory": null,
|
||||
"description": "A framework to boot ChromeOS on generic x86 hardware.",
|
||||
"icon": null,
|
||||
"url": "https://github.com/sebanc/brunch/releases"
|
||||
}
|
||||
]
|
||||
|
||||
1
app/src/main/assets/osicons/almalinux.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>AlmaLinux</title><path d="M23.994 15.133c.079 1.061-.668 1.927-1.69 2.005a1.8 1.8 0 0 1-1.928-1.651c-.078-1.062.63-1.849 1.691-1.967 1.023-.078 1.849.59 1.927 1.613zm-12.623 4.955c-.944 0-1.73.786-1.73 1.809 0 1.14.747 1.848 1.887 1.848.904-.04 1.691-.865 1.691-1.809 0-.983-.904-1.848-1.848-1.848zm1.061-9.675c-.039-.865-.078-1.73.08-2.556.156-.944.314-1.887.904-2.674.707-.983 1.809-.944 2.399.118.314.511.432 1.062.471 1.652 0 .354.158.432.472.393.944-.157 1.888-.157 2.792.197.118.039.236.118.394 0 .314-.276.393-1.652.196-2.006-.354-.63-.904-.55-1.455-.55-.629.039-1.18-.158-1.612-.67-.393-.471-.511-1.06-.59-1.65-.04-.276-.079-.512-.315-.709-.55-.55-1.809-.432-2.477.118-2.556 2.045-2.989 5.467-1.534 8.18.04.118.118.236.275.157zm7.984 3.658c.354-.511.865-.747 1.415-.983a.973.973 0 0 0 .59-.472c.354-.669-.078-1.81-.747-2.36-2.595-2.006-5.938-1.612-8.18.433-.118.078-.157.196-.078.314.786-.236 1.612-.472 2.477-.51.905-.08 1.848-.158 2.753.235 1.14.472 1.337 1.534.472 2.36-.393.393-.905.668-1.455.825-.315.08-.354.236-.236.551.354.865.59 1.77.472 2.753-.04.157-.079.275.078.393.354.236 1.691 0 1.967-.275.511-.472.314-1.023.196-1.534-.157-.63-.078-1.219.276-1.73zm-7.197-2.045c-.118-.079-.197-.118-.315 0 .472.708.905 1.455 1.259 2.241.314.866.668 1.73.55 2.714-.118 1.18-1.1 1.69-2.123 1.101-.511-.275-.905-.669-1.22-1.14-.196-.276-.393-.276-.629-.08-.747.63-1.533 1.102-2.516 1.26-.158 0-.315 0-.394.157-.118.393.472 1.612.826 1.809.59.354 1.062 0 1.534-.276.55-.314 1.101-.432 1.73-.236.59.197.983.63 1.337 1.102.158.196.315.353.63.432.747.197 1.77-.59 2.084-1.376 1.18-3.028-.157-6.135-2.753-7.708zm-2.556 2.438c.472-.669.826-1.416.983-2.202-.157-.04-.197.04-.315.078-.904.944-1.848 1.849-3.067 2.478-.472.236-.983.433-1.534.433-.865 0-1.376-.551-1.298-1.416a2.92 2.92 0 0 1 .787-1.849c.236-.275.236-.432-.04-.668-.786-.55-1.494-1.22-1.848-2.124-.078-.275-.275-.275-.51-.157a4.293 4.293 0 0 0-.434.236c-1.022.63-1.14 1.416-.275 2.28.63.63.944 1.338.708 2.203-.118.433-.354.747-.63 1.101a.95.95 0 0 0-.235.787c.079.747.826 1.494 1.73 1.573 2.517.236 4.562-.63 5.978-2.753zm-4.68-5.152c1.376 1.18 3.067 1.455 4.837 1.377.157 0 .315 0 .354-.118.04-.197-.157-.197-.275-.236-.826-.354-1.691-.63-2.438-1.14S6.848 8.25 6.534 7.266c-.236-.747.078-1.415.825-1.651.669-.236 1.337-.236 1.967 0 .393.157.55.078.629-.354.118-.747.354-1.455.826-2.085.55-.786.55-.865-.354-1.376-.04 0-.04-.04-.079-.04-.865-.471-1.534-.196-1.848.709-.472 1.376-1.377 1.887-2.832 1.612-.196-.04-.393-.079-.472-.079-.747.118-1.18.55-1.297 1.14-.158 1.81.786 3.107 2.084 4.17zm-2.32 3.658c-.079-.944-1.023-1.652-2.045-1.534-.905.079-1.691 1.022-1.613 1.966.08.983 1.023 1.77 1.967 1.652 1.14-.079 1.73-1.18 1.69-2.084zm15.18-8.298c.943-.079 1.73-.983 1.651-1.927-.078-.983-1.022-1.77-2.005-1.691-1.023.079-1.73.983-1.652 1.966s.983 1.73 2.006 1.652zm-12.27-.826c1.062-.157 1.77-1.023 1.652-2.045C8.107.897 7.163.149 6.18.267c-1.062.118-1.691.944-1.573 2.085.118.865 1.061 1.612 1.966 1.494z"/></svg>
|
||||
|
After Width: | Height: | Size: 3.0 KiB |
1
app/src/main/assets/osicons/android.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Android</title><path d="M18.4395 5.5586c-.675 1.1664-1.352 2.3318-2.0274 3.498-.0366-.0155-.0742-.0286-.1113-.043-1.8249-.6957-3.484-.8-4.42-.787-1.8551.0185-3.3544.4643-4.2597.8203-.084-.1494-1.7526-3.021-2.0215-3.4864a1.1451 1.1451 0 0 0-.1406-.1914c-.3312-.364-.9054-.4859-1.379-.203-.475.282-.7136.9361-.3886 1.5019 1.9466 3.3696-.0966-.2158 1.9473 3.3593.0172.031-.4946.2642-1.3926 1.0177C2.8987 12.176.452 14.772 0 18.9902h24c-.119-1.1108-.3686-2.099-.7461-3.0683-.7438-1.9118-1.8435-3.2928-2.7402-4.1836a12.1048 12.1048 0 0 0-2.1309-1.6875c.6594-1.122 1.312-2.2559 1.9649-3.3848.2077-.3615.1886-.7956-.0079-1.1191a1.1001 1.1001 0 0 0-.8515-.5332c-.5225-.0536-.9392.3128-1.0488.5449zm-.0391 8.461c.3944.5926.324 1.3306-.1563 1.6503-.4799.3197-1.188.0985-1.582-.4941-.3944-.5927-.324-1.3307.1563-1.6504.4727-.315 1.1812-.1086 1.582.4941zM7.207 13.5273c.4803.3197.5506 1.0577.1563 1.6504-.394.5926-1.1038.8138-1.584.4941-.48-.3197-.5503-1.0577-.1563-1.6504.4008-.6021 1.1087-.8106 1.584-.4941z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
1
app/src/main/assets/osicons/asahilinux.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Asahi Linux</title><path d="m13.835 0-1.72 1.323v.97h2.178zm-1.95.057L9.81 1.095l2.076 4.153zm.23 3.768V6.22l-1.057-2.113L6.43 5.678 12 8.009l5.57-2.331zM6.21 5.835.533 15.957 11.885 24V8.21L6.222 5.84Zm11.58 0-.012.004-5.6 2.345 7.512 10.449 3.777-2.675zm-3.955 7.926v5.422l1.952-2.711zm2.864 3.981-4.411 6.135 5.846-4.14z"/></svg>
|
||||
|
After Width: | Height: | Size: 410 B |
1
app/src/main/assets/osicons/chromeos.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Google Chrome</title><path d="M12 0C8.21 0 4.831 1.757 2.632 4.501l3.953 6.848A5.454 5.454 0 0 1 12 6.545h10.691A12 12 0 0 0 12 0zM1.931 5.47A11.943 11.943 0 0 0 0 12c0 6.012 4.42 10.991 10.189 11.864l3.953-6.847a5.45 5.45 0 0 1-6.865-2.29zm13.342 2.166a5.446 5.446 0 0 1 1.45 7.09l.002.001h-.002l-5.344 9.257c.206.01.413.016.621.016 6.627 0 12-5.373 12-12 0-1.54-.29-3.011-.818-4.364zM12 16.364a4.364 4.364 0 1 1 0-8.728 4.364 4.364 0 0 1 0 8.728Z"/></svg>
|
||||
|
After Width: | Height: | Size: 535 B |
1
app/src/main/assets/osicons/elementary.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>elementary</title><path d="M12 0a12 12 0 1 0 0 24 12 12 0 0 0 0-24zm0 1a11 11 0 0 1 10.59 8.01 19.09 19.09 0 0 1-4.66 6.08c-.94.81-1.96 1.53-3.08 2.04-1.13.5-2.37.8-3.6.72a6.23 6.23 0 0 1-2.66-.76 20.02 20.02 0 0 0 5.68-4.58 9.97 9.97 0 0 0 2.31-4.17c.18-.79.2-1.6.04-2.4a4.42 4.42 0 0 0-1.08-2.11 4.33 4.33 0 0 0-2-1.19 5.25 5.25 0 0 0-2.33-.08A7.8 7.8 0 0 0 7.2 4.85a9.77 9.77 0 0 0-2.94 7.49 7.88 7.88 0 0 0 1.95 4.59 18 18 0 0 1-3.56.85A11 11 0 0 1 12 1zm.07 2.22c.77 0 1.55.24 2.17.7.55.42.97 1.02 1.2 1.68.23.65.3 1.37.21 2.06a7.85 7.85 0 0 1-1.7 3.76 16.22 16.22 0 0 1-6.37 4.96c-.48-.42-.9-.92-1.2-1.48a6.61 6.61 0 0 1-.75-3.87c.12-1.32.58-2.6 1.2-3.79a7.92 7.92 0 0 1 3.02-3.42c.68-.37 1.45-.6 2.22-.6zm10.83 7.3A11 11 0 0 1 3.52 19a19.8 19.8 0 0 0 3.63-1.2c.51.4 1.08.71 1.67.94a8 8 0 0 0 5.44-.04 13.3 13.3 0 0 0 4.64-2.95 20 20 0 0 0 4-5.22z"/></svg>
|
||||
|
After Width: | Height: | Size: 940 B |
1
app/src/main/assets/osicons/freenas.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>FreeNAS</title><path d="M19.598 2.707h.143c.06 0 .101.01.124.032a.107.107 0 0 1 .034.083c0 .045-.017.077-.051.097a.307.307 0 0 1-.153.029h-.098v-.241zm0 .391h.09l.214.337h.203l-.242-.356v-.008a.29.29 0 0 0 .161-.081.237.237 0 0 0 .059-.168.254.254 0 0 0-.03-.125.241.241 0 0 0-.08-.082.349.349 0 0 0-.114-.045.645.645 0 0 0-.133-.013h-.303v.879h.175v-.338m-.485-.368a.628.628 0 0 1 .348-.367.688.688 0 0 1 .277-.053.64.64 0 0 1 .625.42.735.735 0 0 1 .049.27.74.74 0 0 1-.049.271.642.642 0 0 1-.348.366.675.675 0 0 1-.277.054.646.646 0 0 1-.624-.421.712.712 0 0 1-.049-.27c0-.095.015-.185.048-.27zm.014.884a.835.835 0 0 0 .273.179.884.884 0 0 0 .338.064c.12 0 .233-.021.339-.064A.83.83 0 0 0 20.598 3a.852.852 0 0 0-.249-.613.815.815 0 0 0-.272-.179c-.105-.043-.218-.064-.339-.064s-.233.021-.338.064a.82.82 0 0 0-.454.45.838.838 0 0 0-.067.342c0 .125.021.239.067.343a.796.796 0 0 0 .181.271m-1.864 12.361a3.268 3.268 0 0 1-.931 1.215 3.203 3.203 0 0 1-2.008.695 3.199 3.199 0 0 1-2.423-1.085 1.989 1.989 0 0 1-.439-.855 2.223 2.223 0 0 1-.06-.519c.002-.854.428-1.71.845-2.362.21-.326.418-.602.575-.794l.208.254.036.046a7.499 7.499 0 0 0 1.126 1.083c.766.597 1.85 1.197 3.126 1.229.012 0 .023.003.035.004a.172.172 0 0 1 .064 0 .16.16 0 0 1 .126.189c-.061.33-.158.628-.28.9zm6.719-7.025a5.339 5.339 0 0 1-.821.905c-.752.664-1.936 1.343-3.649 1.435l-.505.926a.173.173 0 0 1-.299.008l-.581-.954c-.275.051-.984.168-1.808.168-1.376-.03-1.807-.241-2.263-.532l1.538-2.072-3.297-.764 4.136-.795c1.208-2.437 1.583-4.521 1.675-5.157-4.638.514-8.102 1.666-10.329 2.632l-.179.079-.034.014-.249-.241a9.292 9.292 0 0 0-1.459-.985 9.404 9.404 0 0 0-4.516-1.175 8.05 8.05 0 0 0-.894.043c-.491.031-.253.153-.194.203.225.184.544.573.753 1.112.211.541.354 1.27.354 2.254 0 .275-.012.579-.036.896-.195.362-.376.741-.539 1.132C.311 9.227 0 10.479 0 11.767c0 2.291.9 4.378 2.181 6.074l.173.262-1.355 2.7a.063.063 0 0 0 .021.08c.011.007.023.01.035.01a.067.067 0 0 0 .047-.02l2.117-1.863.248.24a10.1 10.1 0 0 0 6.812 2.63c4.516 0 8.342-2.953 9.652-7.032l.288-.124a6.314 6.314 0 0 0 1.132-.555c.684-.424 1.502-1.107 2.045-2.141.362-.687.604-1.534.604-2.576 0-.163-.006-.33-.018-.502"/></svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
1
app/src/main/assets/osicons/gentoo.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Gentoo</title><path d="M9.94 0a7.31 7.31 0 00-1.26.116c-4.344.795-7.4 4.555-7.661 7.031-.126 1.215.53 2.125.89 2.526.977 1.085 2.924 1.914 4.175 2.601-1.81 1.543-2.64 2.296-3.457 3.154C1.403 16.712.543 18.125.54 19.138c0 .325-.053 1.365.371 2.187.16.309.613 1.338 1.98 2.109.874.494 2.119.675 3.337.501 3.772-.538 8.823-3.737 12.427-6.716 2.297-1.9 3.977-3.739 4.462-4.644.39-.731.434-2.043.207-2.866-.645-2.337-5.887-7.125-10.172-9.051A7.824 7.824 0 009.94 0zm-.008.068a7.4 7.4 0 013.344.755c3.46 1.7 9.308 6.482 9.739 8.886.534 2.972-9.931 11.017-16.297 12.272-2.47.485-4.576.618-5.537-1.99-.832-2.262.783-3.916 3.16-6.09a92.546 92.546 0 012.96-2.576c.065-.069-5.706-2.059-5.89-4.343C1.221 4.634 4.938.3 9.697.076c.08-.004.157-.007.235-.008zm-.112.52a5.647 5.647 0 00-.506.032c-2.337.245-2.785.547-4.903 2.149-.71.537-2.016 1.844-2.35 3.393-.128.59.024 1.1.448 1.458 1.36 1.144 3.639 2.072 5.509 2.97.547.263.185.74-.698 1.505-2.227 1.928-5.24 4.276-5.45 6.066-.099.842.19 1.988 1.213 2.574 1.195.685 3.676.238 5.333-.379 2.422-.902 5.602-2.892 8.127-4.848 2.625-2.034 5.067-4.617 5.188-5.038.148-.517.133-.996-.154-1.546-.448-.862-1.049-1.503-1.694-2.22-1.732-1.825-3.563-3.43-5.754-4.658C12.694 1.242 11.417.564 9.82.588zm1.075 3.623c.546 0 1.176.173 1.853.5 1.688.817 3.422 2.961-.015 4.195-.935.336-3.9-.824-3.81-2.407.09-1.57.854-2.289 1.972-2.288zm.285 1.367c-.317-.002-.575.079-.694.263-.557.861-.303 1.472.212 1.862.192-.457 2.156.043 2.148.472a.32.32 0 00.055-.032c1.704-1.282-.472-2.557-1.72-2.565z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
1
app/src/main/assets/osicons/manjaro.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Manjaro</title><path d="M2.182 0A2.177 2.177 0 0 0 0 2.182v19.636C0 23.027.973 24 2.182 24h4.363V6.545h8.728V0Zm15.273 0v24h4.363A2.177 2.177 0 0 0 24 21.818V2.182A2.177 2.177 0 0 0 21.818 0ZM8.727 8.727V24h6.546V8.727Z"/></svg>
|
||||
|
After Width: | Height: | Size: 306 B |
1
app/src/main/assets/osicons/netbsd.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>NetBSD</title><path d="M22.686 10.681c0-.181.064-.336.193-.464a.634.634 0 0 1 .464-.193c.182 0 .336.065.465.193a.633.633 0 0 1 .192.464.635.635 0 0 1-.192.465.632.632 0 0 1-.465.193.634.634 0 0 1-.464-.193.634.634 0 0 1-.193-.465zm1.206 0a.53.53 0 0 0-.16-.388.53.53 0 0 0-.389-.16.53.53 0 0 0-.388.16.528.528 0 0 0-.161.388.53.53 0 0 0 .16.389.53.53 0 0 0 .39.161.529.529 0 0 0 .388-.161.53.53 0 0 0 .16-.389zm-.344.396-.207-.349h-.117v.349h-.114v-.808h.207c.194 0 .292.074.292.223 0 .104-.053.177-.157.22l.221.365zm-.324-.71v.27l.076.001c.075 0 .126-.01.151-.028.026-.02.04-.056.04-.11 0-.09-.059-.134-.175-.134h-.092m-3.892 3.28c0 .403.014.667.146.82.132.147.344.213.607.213 1.266 0 1.698-1.127 1.698-2.122 0-1.318-.695-2.1-2.02-2.1-.197 0-.336.036-.38.095-.044.058-.051.197-.051.424v2.67zm-1.046-2.319c0-.695-.015-.834-.352-.87l-.139-.015c-.073-.037-.073-.25.015-.257a30.521 30.521 0 0 1 1.96-.065c.6 0 1.2.059 1.706.241.958.344 1.485 1.208 1.485 2.122 0 .981-.468 1.771-1.31 2.188-.497.25-1.097.344-1.85.344-.345 0-.71-.044-.974-.044-.351 0-.724.008-1.141.022-.059-.044-.059-.22 0-.256l.226-.036c.33-.059.374-.11.374-.783v-2.59m-2.405 3.76c-.673 0-1.09-.19-1.244-.277-.139-.161-.234-.688-.234-1.186.051-.095.22-.102.278-.022.146.476.636 1.149 1.258 1.149.542 0 .79-.373.79-.74 0-.592-.555-.943-.994-1.163-.527-.263-1.098-.702-1.105-1.427 0-.827.636-1.398 1.697-1.398.242 0 .542.03.834.118.095.029.161.043.25.058.057.161.13.556.13 1.047-.036.087-.219.095-.285.022-.124-.374-.439-.908-.965-.908-.483 0-.747.315-.747.68 0 .337.3.645.666.835l.483.256c.454.242 1.032.666 1.032 1.471 0 .9-.74 1.486-1.844 1.486m-4.2-1.354c0 .57.072.93.643.93.542 0 .827-.418.827-1.01 0-.637-.366-1.084-1.068-1.084-.403 0-.403.007-.403.3v.864zm0-1.69c0 .19.007.204.387.204.63 0 .863-.402.863-.841 0-.637-.395-.952-.9-.952-.343 0-.35.06-.35.381zm-1.01-.71c0-.74-.015-.82-.322-.857l-.198-.03c-.066-.036-.08-.255.03-.263.555-.036 1.09-.065 1.821-.065.703 0 1.171.08 1.493.27.314.19.505.498.505.93 0 .615-.52.856-.747.915-.073.014-.146.044-.146.08 0 .022.037.044.103.059.578.124 1.068.504 1.075 1.214.007.673-.395 1.069-.856 1.23-.461.16-1.01.183-1.456.183-.263 0-.541-.03-.754-.03-.358 0-.717.008-1.134.022-.058-.044-.058-.234 0-.256l.213-.044c.329-.065.373-.117.373-.775v-2.584M9.038 12.44c-.095 0-.102.007-.102.168v1.097c0 .41 0 .864.512.864.102 0 .22-.051.307-.11.073.022.117.103.102.19-.204.22-.6.425-1.053.425-.607 0-.82-.351-.82-.834v-1.632c0-.154-.007-.168-.139-.168H7.62c-.08-.03-.103-.176-.044-.227.226-.08.431-.213.607-.33.132-.095.315-.248.541-.57.051-.03.183-.022.22.036v.549c0 .139.007.146.139.146h.651c.037.03.059.074.059.14 0 .08-.022.211-.095.256h-.66m-2.627.475c.103 0 .22-.015.3-.066.037-.022.051-.095.051-.168 0-.241-.139-.402-.388-.402-.307 0-.57.292-.57.526 0 .103.102.11.336.11zm-.483.322c-.168 0-.183.015-.183.132 0 .549.351 1.083 1.032 1.083.205 0 .483-.044.68-.38.08-.015.19.043.19.168-.3.622-.84.834-1.28.834-.988 0-1.522-.695-1.522-1.493 0-.922.666-1.64 1.58-1.64.762 0 1.171.491 1.171 1.055 0 .139-.036.241-.263.241H5.927m-1.255.49c0 .476 0 .937.014 1.179-.05.087-.256.168-.431.168-.008 0-.25-.373-.593-.798l-1.69-2.093c-.417-.527-.666-.826-.798-.936-.036.073-.036.197-.036.468v1.42c0 .593.029 1.141.11 1.339.065.154.234.198.424.234l.205.03c.058.058.044.212 0 .255a26.585 26.585 0 0 0-.98-.022c-.271 0-.542.008-.82.022-.044-.043-.059-.197 0-.256l.124-.022c.198-.044.337-.087.403-.241.073-.198.102-.746.102-1.34v-1.858c0-.402 0-.52-.051-.622-.051-.124-.161-.198-.417-.249l-.205-.029c-.051-.059-.044-.234.03-.256.343.014.709.022 1.009.022.249 0 .46-.008.615-.022.073.33.548.885 1.207 1.668l.614.725c.33.38.535.636.703.805.03-.073.03-.198.03-.33v-1.01c0-.592-.03-1.141-.11-1.339-.067-.153-.227-.197-.425-.234l-.198-.029c-.058-.059-.043-.212 0-.256.396.014.68.022.98.022.272 0 .535-.008.82-.022.044.044.059.197 0 .256l-.131.022c-.19.044-.33.088-.396.242-.08.197-.11.746-.11 1.339v1.749M21.537 3.59c-2.848-1.367-5.425-.715-8.306.148-2.902.868-5.482 1.337-8.381.154l.79 1.41.87 1.557.79 1.41c2.309.652 4.22-.194 6.271-1.22 2.463-1.23 4.688-2.337 7.502-1.696-2.378-1.19-4.534-.895-7.02-.22 2.434-1.24 4.726-2.204 7.484-1.543M13.16 20.478l-2.272-4.385H9.91l2.283 4.826s.23.455.724.203c.493-.25.245-.644.245-.644M4.634 4.025s-.068-.159-.26-.053c-.16.089-.077.253-.077.253l3.004 6.351h.728L4.634 4.025"/></svg>
|
||||
|
After Width: | Height: | Size: 4.3 KiB |
1
app/src/main/assets/osicons/openbsd.svg
Normal file
|
After Width: | Height: | Size: 16 KiB |
1
app/src/main/assets/osicons/opensuse.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>openSUSE</title><path d="M10.724 0a12 12 0 0 0-9.448 4.623c1.464.391 2.5.727 2.81.832.005-.19.037-1.893.037-1.893s.004-.04.025-.06c.026-.026.065-.018.065-.018.385.056 8.602 1.274 12.066 3.292.427.25.638.517.902.786.958.99 2.223 5.108 2.359 5.957.005.033-.036.07-.054.083a5.177 5.177 0 0 1-.313.228c-.82.55-2.708 1.872-5.13 1.656-2.176-.193-5.018-1.44-8.445-3.699.336.79.668 1.58 1 2.371.497.258 5.287 2.7 7.651 2.651 1.904-.04 3.941-.968 4.756-1.458 0 0 .179-.108.257-.048.085.066.061.167.041.27-.05.234-.164.66-.242.863l-.065.165c-.093.25-.183.482-.356.625-.48.436-1.246.784-2.446 1.305-1.855.812-4.865 1.328-7.66 1.31-1.001-.022-1.968-.133-2.817-.232-1.743-.197-3.161-.357-4.026.269A12 12 0 0 0 10.724 24a12 12 0 0 0 12-12 12 12 0 0 0-12-12zM13.4 6.963a3.503 3.503 0 0 0-2.521.942 3.498 3.498 0 0 0-1.114 2.449 3.528 3.528 0 0 0 3.39 3.64 3.48 3.48 0 0 0 2.524-.946 3.504 3.504 0 0 0 1.114-2.446 3.527 3.527 0 0 0-3.393-3.64zm-.03 1.035a2.458 2.458 0 0 1 2.368 2.539 2.43 2.43 0 0 1-.774 1.706 2.456 2.456 0 0 1-1.762.659 2.461 2.461 0 0 1-2.364-2.542c.02-.655.3-1.26.777-1.707a2.419 2.419 0 0 1 1.756-.655zm.402 1.23c-.602 0-1.087.325-1.087.727 0 .4.485.725 1.087.725.6 0 1.088-.326 1.088-.725 0-.402-.487-.726-1.088-.726Z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
1
app/src/main/assets/osicons/parrot-security.svg
Symbolic link
@@ -0,0 +1 @@
|
||||
./parrotsecurity.svg
|
||||
1
app/src/main/assets/osicons/parrotos.svg
Symbolic link
@@ -0,0 +1 @@
|
||||
./parrotsecurity.svg
|
||||
1
app/src/main/assets/osicons/parrotsecurity.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Parrot Security</title><path d="M12 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0Zm6.267 2.784L13.03 5.54l8.05-.179-8.05 3.333-2.154 2.688 5.007 9.038-1.536-1.605 1.645 3.456-4.937-5.527-6.268-6.28L2.77 12.11l.7-3.442 4.018-.261.823-4.06Z"/></svg>
|
||||
|
After Width: | Height: | Size: 351 B |
1
app/src/main/assets/osicons/pfsense.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>pfSense</title><path d="M2.013 0C.898 0 0 .929 0 2.044v17.775L3.252 8.27h3.282L6.1 9.785h.063c.186-.217.433-.403.742-.62.31-.216.62-.402.96-.588.342-.186.713-.31 1.116-.433.402-.124.805-.155 1.208-.155.867 0 1.579.154 2.198.433.62.279 1.084.712 1.455 1.239.31.464.5 1.019.593 1.669.006.06.027.135.027.189.062.712-.031 1.518-.28 2.385a8.571 8.571 0 0 1-1.02 2.322 9.885 9.885 0 0 1-1.58 1.95 8.125 8.125 0 0 1-2.044 1.364 5.536 5.536 0 0 1-2.354.495 5.655 5.655 0 0 1-1.982-.34c-.588-.217-.99-.62-1.238-1.177h-.062L2.353 24h19.603A2.042 2.042 0 0 0 24 21.956V4.706c-.093-.03-.186-.06-.248-.092a2.771 2.771 0 0 0-.557-.062c-.557 0-1.022.124-1.394.372-.34.248-.65.743-.867 1.518l-.526 1.826h2.013l.495 1.58-1.3 1.27h-2.014l-2.446 8.67h-3.53l2.446-8.67h-1.455l.805-2.85h1.425l.557-2.044c.185-.619.403-1.238.681-1.795a4.996 4.996 0 0 1 1.053-1.487c.433-.434.99-.775 1.641-1.022.65-.248 1.487-.372 2.447-.372.248 0 .464 0 .712.031A2.082 2.082 0 0 0 21.988 0zm6.565 11.118c-.898 0-1.672.278-2.323.805-.65.526-1.083 1.239-1.331 2.106-.248.867-.217 1.579.155 2.105.31.557.929.805 1.858.805.898 0 1.672-.278 2.322-.805.65-.526 1.115-1.238 1.363-2.105.247-.867.185-1.58-.155-2.106-.34-.527-.991-.805-1.89-.805Z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
1
app/src/main/assets/osicons/proxmox-ve.svg
Symbolic link
@@ -0,0 +1 @@
|
||||
proxmox.svg
|
||||
1
app/src/main/assets/osicons/reactos.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>ReactOS</title><path d="M20.21 24c-1.148-.007-2.477-.334-3.89-.915-2.823-1.163-6.04-3.372-9.036-6.367C4.289 13.723 2.075 10.505.913 7.68-.25 4.857-.383 2.36.988.989 2.358-.38 4.855-.248 7.679.915c.306.125.617.265.932.415-.331.113-.658.24-.974.383l-.141-.058C4.832.558 2.698.519 1.607 1.609.517 2.7.557 4.83 1.653 7.494c1.097 2.663 3.235 5.793 6.147 8.704 2.91 2.911 6.044 5.05 8.708 6.147 2.664 1.097 4.79 1.136 5.88.045 1.091-1.09 1.056-3.22-.041-5.884-.108-.263-.23-.531-.358-.803.134-.317.25-.642.354-.973.282.54.53 1.07.744 1.589 1.163 2.823 1.292 5.32-.079 6.691-.685.685-1.651.997-2.799.99zM3.79 24c-1.148.008-2.117-.305-2.802-.99-1.37-1.37-1.238-3.868-.075-6.691.235-.572.517-1.16.836-1.76.098.333.212.66.34.978a17.67 17.67 0 00-.436.969C.556 19.169.521 21.3 1.611 22.39c1.091 1.091 3.221 1.051 5.885-.045.922-.38 3.021-1.69 4.026-2.308.216.162.433.32.649.474-1.157.733-3.415 2.13-4.492 2.574-1.412.581-2.74.907-3.888.915zm9.753-4.458c-.214-.14-.429-.282-.645-.433a34.547 34.547 0 003.302-2.911c2.912-2.911 5.05-6.04 6.147-8.704 1.097-2.664 1.132-4.794.042-5.885-1.091-1.09-3.217-1.055-5.88.042l-.072.029a10.726 10.726 0 00-.99-.379c.295-.14.587-.272.874-.39 2.824-1.163 5.321-1.292 6.691.078s1.238 3.864.075 6.688c-1.162 2.823-3.376 6.046-6.37 9.04a35.747 35.747 0 01-3.174 2.825zm1.95 1.156c-.325-.17-1.798-1.073-2.135-1.273 1.002-.806 2.423-1.97 3.396-2.944 1.718-1.718 3.981-4.787 5.162-6.555-.008.111-.093 2.49-.105 2.6a9.802 9.802 0 01-6.318 8.172zm-6.928-.034c-3.407-1.308-6.043-4.71-6.287-8.198-.01-.151-.06-.399-.054-.984.007-.602.056-1.423.159-1.283 1.036 1.42 3.976 5.455 5.352 6.83.973.973 1.927 1.624 2.929 2.43a112.45 112.45 0 01-2.1 1.205zm3.43-2.208a33.27 33.27 0 01-3.443-3.01c-2.54-2.54-4.462-5.254-5.568-7.582 1.45-3.597 4.973-6.138 9.087-6.138 4.051 0 7.53 2.465 9.02 5.976-1.093 2.363-3.045 5.145-5.643 7.743a33.161 33.161 0 01-3.452 3.011z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
1
app/src/main/assets/osicons/slackware.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Slackware</title><path d="M12.924 1.228c-.584-.01-1.251 0-1.485.027-2.46.282-4.138 1.3-4.753 2.891-.218.552-.274 1.002-.243 1.772.048 1.21.419 2.004 1.262 2.742 1.225 1.06 2.98 1.508 5.998 1.508 2.737 0 3.71.413 3.916 1.675.313 1.867-1.57 3.07-4.414 2.827-1.878-.16-3.496-.912-4.223-1.967a7.772 7.772 0 01-.355-.62c-.382-.76-.64-.978-1.176-.978-.43.005-.732.165-.918.494l-.133.24v4.03l.137.296c.165.344.4.546.744.63.35.09.794-.036 1.42-.402l.5-.29.826.185c1.82.403 2.75.523 4.065.523 1.103.005 1.548-.046 2.455-.285 1.124-.297 1.974-.785 2.717-1.57.8-.844 1.15-1.853 1.097-3.147-.069-1.628-.695-2.698-2-3.414-.96-.525-2.292-.79-4.377-.88-2.042-.086-2.794-.155-3.515-.32-.51-.12-.785-.25-1.076-.515-.653-.589-.59-1.755.136-2.482.642-.637 1.511-.928 2.774-.928 1.432.005 2.393.27 3.412.955.185.127.721.62 1.193 1.092.886.902 1.135 1.082 1.506 1.082.244 0 .59-.163.732-.344.26-.329.303-.63.303-2.2 0-1.66-.043-1.91-.377-2.282-.387-.425-.848-.42-1.75.031l-.59.297-.63-.17c-1.496-.392-2.038-.477-3.178-.504zM0 13.775v9h24v-1H1v-8z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
1
app/src/main/assets/osicons/solus.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Solus</title><path d="M7.453 0c-.18.587-.369 1.167-.565 1.75A11.638 11.638 0 0 0 0 12.364a11.638 11.638 0 0 0 .516 3.403l-.339.598L0 16.73l.279.143a3.448 3.448 0 0 0 .741.222A11.638 11.638 0 0 0 2 18.868c4.034.343 8.55.512 12.446-.056 3.192-.463 5.94-1.423 7.735-3.117.252-.233.474-.474.674-.722.019-.038.037-.053.06-.076.011 0 .026-.037.038-.052.015 0 .03-.038.041-.057.008 0 .015-.038.023-.038.33-.444.587-.892.801-1.31l.181-.365-.365-.365a5.936 5.936 0 0 0-.361-.35A11.638 11.638 0 0 0 11.635.722a11.638 11.638 0 0 0-3.211.463C7.96.508 7.596.041 7.453 0zm.365 1.637C9.06 3.82 10.13 5.06 11.454 7.457c.132 1.524.67 9.45.727 10.181-.392-.037-2.485-.24-5.104-.515-1.43-.147-2.899-.316-4.092-.49l-1.9-.447c2.149-3.787 5.551-9.727 6.737-14.548zm4.543 6.18s4.991 3.927 7.092 8.73c-2.56 1.26-4.916 1.098-6.361 1.09 1.023-2.634 1.023-6.21-.73-9.82zm3.456 2.184a45.14 45.14 0 0 1 2.91.907c1.768.629 3.417 1.49 4.365 2.364a6.956 6.956 0 0 1-2.91 2.91c.151-1.495-.39-2.933-1.456-4.002-.787-.787-1.822-1.453-2.91-2.183zm6.707 6.478c-2.352 1.667-5.126 2.68-7.965 3.112a41.026 41.026 0 0 1-3.715.34h-.323a53.48 53.48 0 0 1-3.727 0 85.763 85.763 0 0 1-4.178-.23h-.003c2.555 3.255 6.993 4.893 11.092 4.102a11.367 11.367 0 0 0 4.498-1.852 11.638 11.638 0 0 0 .007 0c.312-.214.614-.444.903-.685a11.638 11.638 0 0 0 .038-.037 11.555 11.555 0 0 0 3.376-4.762zM2.511 19.584a11.638 11.638 0 0 0 .023.038c-.008 0-.015-.038-.023-.038z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
1
app/src/main/assets/osicons/truenas.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>TrueNAS</title><path d="M24 10.049v5.114l-10.949 6.324v-5.114L24 10.049zm-24 0v5.114l10.956 6.324v-5.114L0 10.049zm12.004-.605l-4.433 2.559 4.433 2.559 4.429-2.559-4.429-2.559zm10.952-1.207l-9.905-5.723v5.118l5.473 3.164 4.432-2.559zm-12-.605V2.513L1.044 8.236l4.432 2.555 5.48-3.159z"/></svg>
|
||||
|
After Width: | Height: | Size: 371 B |
1
app/src/main/assets/osicons/zorin.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Zorin</title><path d="M4 18.944L5.995 22.4h12.01L20 18.944H4zM24 12l-2.013 3.488H9.216l12.771-6.976L24 12zM0 12l2.013-3.488h12.771L2.013 15.488 0 12zm4-6.944L5.995 1.6h12.01L20 5.056H4z"/></svg>
|
||||
|
After Width: | Height: | Size: 272 B |
@@ -25,7 +25,7 @@ class ISODroidApp : Application() {
|
||||
companion object {
|
||||
init {
|
||||
// Set settings before the main shell can be created
|
||||
Shell.enableVerboseLogging = true
|
||||
Shell.enableVerboseLogging = BuildConfig.DEBUG
|
||||
Shell.setDefaultBuilder(
|
||||
Shell.Builder.create()
|
||||
.setFlags(Shell.FLAG_MOUNT_MASTER)
|
||||
|
||||
@@ -58,42 +58,14 @@ class IsoDriveManager(private val context: Context) {
|
||||
binaryPath = targetFile.absolutePath
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
// Binary not bundled yet, will use system isodrive if available
|
||||
checkSystemBinary()
|
||||
// Binary extraction failed
|
||||
false
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun checkSystemBinary(): Boolean = withContext(Dispatchers.IO) {
|
||||
// Check common locations first (more reliable than 'which')
|
||||
val commonPaths = listOf(
|
||||
"/data/adb/modules/isodrive-magisk/system/bin/isodrive",
|
||||
"/data/adb/modules/isodrive/system/bin/isodrive",
|
||||
"/system/bin/isodrive",
|
||||
"/system/xbin/isodrive",
|
||||
"/data/local/tmp/isodrive",
|
||||
"/vendor/bin/isodrive"
|
||||
)
|
||||
for (path in commonPaths) {
|
||||
val checkResult = RootManager.executeCommand("test -f $path && echo exists")
|
||||
if (checkResult.success && checkResult.output.contains("exists")) {
|
||||
binaryPath = path
|
||||
return@withContext true
|
||||
}
|
||||
}
|
||||
|
||||
// Try 'which' as fallback
|
||||
val result = RootManager.executeCommand("which isodrive 2>/dev/null")
|
||||
if (result.success && result.output.isNotBlank()) {
|
||||
binaryPath = result.output.trim()
|
||||
return@withContext true
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
suspend fun isSupported(): SupportStatus = withContext(Dispatchers.IO) {
|
||||
// First check if binary is available
|
||||
if (binaryPath == null && !extractBinary()) {
|
||||
@@ -150,8 +122,9 @@ class IsoDriveManager(private val context: Context) {
|
||||
)
|
||||
}
|
||||
|
||||
// Validate file exists
|
||||
val fileCheck = RootManager.executeCommand("test -f \"$isoPath\" && echo exists")
|
||||
// Validate file exists using shell-safe escaping
|
||||
val safePath = RootManager.shellEscape(isoPath)
|
||||
val fileCheck = RootManager.executeCommand("test -f $safePath && echo exists")
|
||||
if (!fileCheck.success || !fileCheck.output.contains("exists")) {
|
||||
return@withContext MountResult(
|
||||
success = false,
|
||||
@@ -167,9 +140,9 @@ class IsoDriveManager(private val context: Context) {
|
||||
)
|
||||
}
|
||||
|
||||
// Build command
|
||||
// Build command with safe path escaping
|
||||
val args = options.toCommandArgs().joinToString(" ")
|
||||
val command = "$binaryPath \"$isoPath\" $args"
|
||||
val command = "$binaryPath $safePath $args"
|
||||
|
||||
val result = RootManager.executeCommand(command)
|
||||
|
||||
|
||||
@@ -6,16 +6,29 @@
|
||||
package sh.sar.isodroid.root
|
||||
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import sh.sar.isodroid.BuildConfig
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
object RootManager {
|
||||
|
||||
/**
|
||||
* Escapes a string for safe use in shell commands.
|
||||
* Uses single quotes and escapes any single quotes within the string.
|
||||
* This prevents command injection via $(), ``, ;, &&, ||, etc.
|
||||
*/
|
||||
fun shellEscape(s: String): String {
|
||||
// Single quotes prevent all shell interpretation except for single quotes themselves
|
||||
// To include a single quote, we end the single-quoted string, add an escaped single quote, and start a new single-quoted string
|
||||
// Example: "test'file" becomes 'test'\''file'
|
||||
return "'" + s.replace("'", "'\\''") + "'"
|
||||
}
|
||||
|
||||
init {
|
||||
Shell.enableVerboseLogging = true
|
||||
Shell.enableVerboseLogging = BuildConfig.DEBUG
|
||||
Shell.setDefaultBuilder(
|
||||
Shell.Builder.create()
|
||||
.setFlags(Shell.FLAG_MOUNT_MASTER or Shell.FLAG_REDIRECT_STDERR)
|
||||
.setFlags(Shell.FLAG_MOUNT_MASTER)
|
||||
.setTimeout(10)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -76,9 +76,10 @@ fun CreateImgDialog(
|
||||
|
||||
val fullFileName = if (fileName.isNotBlank()) "$fileName.img" else ""
|
||||
val fileExists = fullFileName.isNotEmpty() && existingFiles.contains(fullFileName)
|
||||
val hasInvalidChars = fileName.any { !it.isLetterOrDigit() && it !in "-_. ()[]+," }
|
||||
|
||||
val isValidInput = fileName.isNotBlank() &&
|
||||
!fileName.contains("/") &&
|
||||
!hasInvalidChars &&
|
||||
sizeValue.toLongOrNull()?.let { it > 0 } == true &&
|
||||
!fileExists
|
||||
|
||||
@@ -135,11 +136,11 @@ fun CreateImgDialog(
|
||||
// File name input
|
||||
OutlinedTextField(
|
||||
value = fileName,
|
||||
onValueChange = { fileName = it.replace("/", "") },
|
||||
onValueChange = { fileName = it },
|
||||
label = { Text("File Name") },
|
||||
suffix = { Text(".img") },
|
||||
singleLine = true,
|
||||
isError = fileExists,
|
||||
isError = fileExists || hasInvalidChars,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
@@ -153,6 +154,16 @@ fun CreateImgDialog(
|
||||
)
|
||||
}
|
||||
|
||||
// Show error if invalid characters
|
||||
if (hasInvalidChars) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = "Invalid file name",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.error
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
// Size input
|
||||
|
||||
@@ -134,7 +134,7 @@ fun FileBrowser(
|
||||
// Parent directory item
|
||||
if (canNavigateUp) {
|
||||
item {
|
||||
FileItem(
|
||||
FileItemCard(
|
||||
name = "..",
|
||||
size = "",
|
||||
isDirectory = true,
|
||||
@@ -145,7 +145,7 @@ fun FileBrowser(
|
||||
}
|
||||
|
||||
items(files) { file ->
|
||||
FileItem(
|
||||
FileItemCard(
|
||||
name = file.name,
|
||||
size = file.formattedSize,
|
||||
isIso = file.name.lowercase().endsWith(".iso"),
|
||||
@@ -164,7 +164,7 @@ fun FileBrowser(
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
private fun FileItem(
|
||||
fun FileItemCard(
|
||||
name: String,
|
||||
size: String,
|
||||
isDirectory: Boolean = false,
|
||||
|
||||
@@ -170,7 +170,8 @@ private fun RenameDialog(
|
||||
val nameWithoutExtension = currentName.removeSuffix(extension)
|
||||
|
||||
var newName by remember { mutableStateOf(nameWithoutExtension) }
|
||||
val isValid = newName.isNotBlank() && !newName.contains("/")
|
||||
val hasInvalidChars = newName.any { !it.isLetterOrDigit() && it !in "-_. ()[]+," }
|
||||
val isValid = newName.isNotBlank() && !hasInvalidChars
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
@@ -179,12 +180,22 @@ private fun RenameDialog(
|
||||
Column {
|
||||
OutlinedTextField(
|
||||
value = newName,
|
||||
onValueChange = { newName = it.replace("/", "") },
|
||||
onValueChange = { newName = it },
|
||||
label = { Text("File Name") },
|
||||
suffix = { Text(extension) },
|
||||
singleLine = true,
|
||||
isError = hasInvalidChars,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
if (hasInvalidChars) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = "Invalid file name",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.error
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
package sh.sar.isodroid.ui.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -42,9 +43,6 @@ import coil.decode.SvgDecoder
|
||||
import coil.request.ImageRequest
|
||||
import sh.sar.isodroid.data.MountStatus
|
||||
import sh.sar.isodroid.data.MountType
|
||||
import sh.sar.isodroid.ui.theme.ErrorRed
|
||||
import sh.sar.isodroid.ui.theme.MountedGreen
|
||||
import sh.sar.isodroid.ui.theme.UnmountedGray
|
||||
import java.io.File
|
||||
|
||||
@Composable
|
||||
@@ -52,7 +50,9 @@ fun StatusCard(
|
||||
mountStatus: MountStatus,
|
||||
rootAvailable: Boolean?,
|
||||
deviceSupported: Boolean?,
|
||||
modifier: Modifier = Modifier
|
||||
rootDenied: Boolean = false,
|
||||
modifier: Modifier = Modifier,
|
||||
onRequestRoot: (() -> Unit)? = null
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
@@ -73,7 +73,15 @@ fun StatusCard(
|
||||
val isImg = fileName?.lowercase()?.endsWith(".img") == true
|
||||
|
||||
Card(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.then(
|
||||
if (showRootError && onRequestRoot != null) {
|
||||
Modifier.clickable(onClick = onRequestRoot)
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = when {
|
||||
hasError -> MaterialTheme.colorScheme.errorContainer
|
||||
@@ -101,9 +109,9 @@ fun StatusCard(
|
||||
},
|
||||
contentDescription = null,
|
||||
tint = when {
|
||||
hasError -> ErrorRed
|
||||
mountStatus.mounted -> MountedGreen
|
||||
else -> UnmountedGray
|
||||
hasError -> MaterialTheme.colorScheme.onErrorContainer
|
||||
mountStatus.mounted -> MaterialTheme.colorScheme.onPrimaryContainer
|
||||
else -> MaterialTheme.colorScheme.onSurfaceVariant
|
||||
},
|
||||
modifier = Modifier.size(32.dp)
|
||||
)
|
||||
@@ -121,7 +129,7 @@ fun StatusCard(
|
||||
)
|
||||
if (showRootError) {
|
||||
Text(
|
||||
text = "Grant root access to use ISODroid",
|
||||
text = if (rootDenied) "Grant root access to use ISO Droid" else "Tap to grant root access",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
@@ -190,20 +198,20 @@ fun StatusCard(
|
||||
text = fileName,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Row {
|
||||
Text(
|
||||
text = if (mountStatus.type == MountType.CDROM) "CD-ROM" else "Mass Storage",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.8f)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
text = if (mountStatus.readOnly) "Read-Only" else "Read-Write",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.secondary
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.8f)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* 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
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun UsbWarningDialog(
|
||||
isUnmount: Boolean,
|
||||
onDismiss: () -> Unit,
|
||||
onConfirm: (dontShowAgain: Boolean) -> Unit
|
||||
) {
|
||||
var dontShowAgain by remember { mutableStateOf(false) }
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = {
|
||||
Text(
|
||||
text = "USB Services Will Restart",
|
||||
style = MaterialTheme.typography.headlineSmall
|
||||
)
|
||||
},
|
||||
text = {
|
||||
Column {
|
||||
Text(
|
||||
text = if (isUnmount) {
|
||||
"Unmounting will restart USB services on your device, including:"
|
||||
} else {
|
||||
"Mounting will restart USB services on your device, including:"
|
||||
},
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
Text(
|
||||
text = "• MTP file transfer\n• USB ADB\n• USB tethering\n• Other USB functions",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
Text(
|
||||
text = "Active file transfers may be interrupted and could result in data corruption. Make sure no transfers are in progress.",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.error
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Checkbox(
|
||||
checked = dontShowAgain,
|
||||
onCheckedChange = { dontShowAgain = it }
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
text = "Don't show this again",
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = { onConfirm(dontShowAgain) }
|
||||
) {
|
||||
Text(if (isUnmount) "Unmount" else "Mount")
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text("Cancel")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -12,6 +12,8 @@ import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
@@ -22,8 +24,8 @@ import androidx.compose.foundation.rememberScrollState
|
||||
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.OpenInNew
|
||||
import androidx.compose.material.icons.filled.Download
|
||||
import androidx.compose.material.icons.filled.OpenInNew
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
@@ -50,6 +52,7 @@ import org.json.JSONArray
|
||||
data class OsDownload(
|
||||
val name: String,
|
||||
val category: String,
|
||||
val subcategory: String?,
|
||||
val description: String,
|
||||
val icon: String?,
|
||||
val url: String
|
||||
@@ -67,8 +70,9 @@ private fun loadOsDownloads(context: Context): List<OsDownload> {
|
||||
OsDownload(
|
||||
name = obj.getString("name"),
|
||||
category = obj.getString("category"),
|
||||
subcategory = if (obj.isNull("subcategory")) null else obj.optString("subcategory"),
|
||||
description = obj.optString("description", ""),
|
||||
icon = if (obj.isNull("icon")) null else obj.optString("icon", null),
|
||||
icon = if (obj.isNull("icon")) null else obj.optString("icon"),
|
||||
url = obj.getString("url")
|
||||
)
|
||||
)
|
||||
@@ -88,11 +92,9 @@ fun DownloadsScreen(
|
||||
val context = LocalContext.current
|
||||
val downloads = remember { loadOsDownloads(context) }
|
||||
|
||||
// Group by category and maintain order: Linux, BSD, Windows, Recovery
|
||||
val categoryOrder = listOf("Linux", "BSD", "Windows", "Recovery")
|
||||
// Group by category alphabetically
|
||||
val groupedDownloads = remember(downloads) {
|
||||
downloads.groupBy { it.category }
|
||||
.toSortedMap(compareBy { categoryOrder.indexOf(it).takeIf { i -> i >= 0 } ?: Int.MAX_VALUE })
|
||||
downloads.groupBy { it.category }.toSortedMap()
|
||||
}
|
||||
|
||||
fun openUrl(url: String) {
|
||||
@@ -101,6 +103,7 @@ fun DownloadsScreen(
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
contentWindowInsets = WindowInsets(0),
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("Download ISOs") },
|
||||
@@ -114,7 +117,8 @@ fun DownloadsScreen(
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.primaryContainer,
|
||||
titleContentColor = MaterialTheme.colorScheme.onPrimaryContainer
|
||||
titleContentColor = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||
navigationIconContentColor = MaterialTheme.colorScheme.onPrimaryContainer
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -123,6 +127,7 @@ fun DownloadsScreen(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.navigationBarsPadding()
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
groupedDownloads.forEach { (category, osList) ->
|
||||
@@ -166,22 +171,71 @@ private fun DownloadCategory(
|
||||
)
|
||||
) {
|
||||
Column {
|
||||
downloads.forEachIndexed { index, os ->
|
||||
DownloadItem(
|
||||
os = os,
|
||||
onClick = { onItemClick(os) }
|
||||
)
|
||||
if (index < downloads.lastIndex) {
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f)
|
||||
// Check if any items have subcategories
|
||||
val hasSubcategories = downloads.any { it.subcategory != null }
|
||||
|
||||
if (hasSubcategories) {
|
||||
// Group by subcategory alphabetically
|
||||
val groupedBySubcategory = downloads
|
||||
.groupBy { it.subcategory ?: "" }
|
||||
.toSortedMap()
|
||||
|
||||
var isFirstGroup = true
|
||||
groupedBySubcategory.forEach { (subcategory, osList) ->
|
||||
if (subcategory.isNotEmpty()) {
|
||||
// Subcategory header
|
||||
if (!isFirstGroup) {
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f)
|
||||
)
|
||||
}
|
||||
SubcategoryHeader(subcategory)
|
||||
}
|
||||
isFirstGroup = false
|
||||
|
||||
osList.sortedBy { it.name.lowercase() }.forEachIndexed { index, os ->
|
||||
DownloadItem(
|
||||
os = os,
|
||||
onClick = { onItemClick(os) }
|
||||
)
|
||||
if (index < osList.lastIndex) {
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.15f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// No subcategories, show flat list
|
||||
downloads.forEachIndexed { index, os ->
|
||||
DownloadItem(
|
||||
os = os,
|
||||
onClick = { onItemClick(os) }
|
||||
)
|
||||
if (index < downloads.lastIndex) {
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SubcategoryHeader(title: String) {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DownloadItem(
|
||||
os: OsDownload,
|
||||
@@ -230,7 +284,7 @@ private fun DownloadItem(
|
||||
}
|
||||
}
|
||||
Icon(
|
||||
imageVector = Icons.Default.OpenInNew,
|
||||
imageVector = Icons.AutoMirrored.Filled.OpenInNew,
|
||||
contentDescription = "Open in browser",
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.size(20.dp)
|
||||
|
||||
@@ -5,18 +5,30 @@
|
||||
|
||||
package sh.sar.isodroid.ui.screens
|
||||
|
||||
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.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material3.pulltorefresh.PullToRefreshContainer
|
||||
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Album
|
||||
import androidx.compose.material.icons.filled.Download
|
||||
import androidx.compose.material.icons.filled.Eject
|
||||
import androidx.compose.material.icons.filled.Refresh
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExtendedFloatingActionButton
|
||||
@@ -39,15 +51,17 @@ import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.coroutines.launch
|
||||
import sh.sar.isodroid.data.IsoFile
|
||||
import sh.sar.isodroid.data.MountOptions
|
||||
import sh.sar.isodroid.ui.components.CreateImgDialog
|
||||
import sh.sar.isodroid.ui.components.FileContextMenu
|
||||
import sh.sar.isodroid.ui.components.FileBrowser
|
||||
import sh.sar.isodroid.ui.components.FileItemCard
|
||||
import sh.sar.isodroid.ui.components.MountDialog
|
||||
import sh.sar.isodroid.ui.components.StatusCard
|
||||
import sh.sar.isodroid.ui.components.UsbWarningDialog
|
||||
import sh.sar.isodroid.viewmodel.MainViewModel
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@@ -60,12 +74,54 @@ fun MainScreen(
|
||||
val uiState by viewModel.uiState.collectAsState()
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
val scope = rememberCoroutineScope()
|
||||
val context = LocalContext.current
|
||||
|
||||
var selectedFile by remember { mutableStateOf<IsoFile?>(null) }
|
||||
var showMountDialog by remember { mutableStateOf(false) }
|
||||
var showCreateImgDialog by remember { mutableStateOf(false) }
|
||||
var contextMenuFile by remember { mutableStateOf<IsoFile?>(null) }
|
||||
|
||||
// USB warning dialog state
|
||||
var showUsbWarning by remember { mutableStateOf(false) }
|
||||
var pendingMountPath by remember { mutableStateOf<String?>(null) }
|
||||
var pendingMountOptions by remember { mutableStateOf<MountOptions?>(null) }
|
||||
var isUnmountWarning by remember { mutableStateOf(false) }
|
||||
|
||||
val prefs = remember { context.getSharedPreferences("iso_drive_prefs", android.content.Context.MODE_PRIVATE) }
|
||||
val skipUsbWarning = remember { mutableStateOf(prefs.getBoolean("skip_usb_warning", false)) }
|
||||
|
||||
fun showUsbWarningOrProceed(
|
||||
isUnmount: Boolean,
|
||||
mountPath: String? = null,
|
||||
mountOptions: MountOptions? = null,
|
||||
onProceed: () -> Unit
|
||||
) {
|
||||
if (skipUsbWarning.value) {
|
||||
onProceed()
|
||||
} else {
|
||||
isUnmountWarning = isUnmount
|
||||
pendingMountPath = mountPath
|
||||
pendingMountOptions = mountOptions
|
||||
showUsbWarning = true
|
||||
}
|
||||
}
|
||||
|
||||
val pullToRefreshState = rememberPullToRefreshState()
|
||||
|
||||
// Handle pull-to-refresh
|
||||
if (pullToRefreshState.isRefreshing) {
|
||||
LaunchedEffect(true) {
|
||||
viewModel.refresh()
|
||||
}
|
||||
}
|
||||
|
||||
// Stop refreshing when loading completes
|
||||
LaunchedEffect(uiState.isLoading) {
|
||||
if (!uiState.isLoading) {
|
||||
pullToRefreshState.endRefresh()
|
||||
}
|
||||
}
|
||||
|
||||
// Show error messages
|
||||
LaunchedEffect(uiState.errorMessage) {
|
||||
uiState.errorMessage?.let { message ->
|
||||
@@ -87,12 +143,14 @@ fun MainScreen(
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
contentWindowInsets = WindowInsets(0),
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("ISO Droid") },
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.primaryContainer,
|
||||
titleContentColor = MaterialTheme.colorScheme.onPrimaryContainer
|
||||
titleContentColor = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||
actionIconContentColor = MaterialTheme.colorScheme.onPrimaryContainer
|
||||
),
|
||||
actions = {
|
||||
IconButton(
|
||||
@@ -104,12 +162,6 @@ fun MainScreen(
|
||||
contentDescription = "Create IMG"
|
||||
)
|
||||
}
|
||||
IconButton(onClick = { viewModel.refresh() }) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Refresh,
|
||||
contentDescription = "Refresh"
|
||||
)
|
||||
}
|
||||
IconButton(onClick = onNavigateToDownloads) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Download,
|
||||
@@ -130,8 +182,10 @@ fun MainScreen(
|
||||
if (uiState.mountStatus.mounted) {
|
||||
ExtendedFloatingActionButton(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
viewModel.unmount()
|
||||
showUsbWarningOrProceed(isUnmount = true) {
|
||||
scope.launch {
|
||||
viewModel.unmount()
|
||||
}
|
||||
}
|
||||
},
|
||||
icon = {
|
||||
@@ -147,43 +201,119 @@ fun MainScreen(
|
||||
}
|
||||
}
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
val hapticFeedback = LocalHapticFeedback.current
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.padding(16.dp)
|
||||
.navigationBarsPadding()
|
||||
.nestedScroll(pullToRefreshState.nestedScrollConnection)
|
||||
) {
|
||||
StatusCard(
|
||||
mountStatus = uiState.mountStatus,
|
||||
rootAvailable = uiState.hasRoot,
|
||||
deviceSupported = uiState.isSupported
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
if (uiState.isLoading) {
|
||||
if (uiState.isLoading && !pullToRefreshState.isRefreshing) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
} else if (uiState.hasRoot == true && uiState.isSupported == true) {
|
||||
FileBrowser(
|
||||
files = uiState.isoFiles,
|
||||
currentPath = uiState.currentPath,
|
||||
onFileClick = { file ->
|
||||
selectedFile = file
|
||||
showMountDialog = true
|
||||
},
|
||||
onFileLongClick = { file ->
|
||||
contextMenuFile = file
|
||||
},
|
||||
onNavigateUp = { viewModel.navigateUp() },
|
||||
canNavigateUp = viewModel.canNavigateUp(),
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
} else {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = PaddingValues(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
// Status card
|
||||
item {
|
||||
StatusCard(
|
||||
mountStatus = uiState.mountStatus,
|
||||
rootAvailable = uiState.hasRoot,
|
||||
deviceSupported = uiState.isSupported,
|
||||
rootDenied = uiState.rootDenied,
|
||||
onRequestRoot = { viewModel.requestRootAccess() }
|
||||
)
|
||||
}
|
||||
|
||||
// File browser content
|
||||
if (uiState.hasRoot == true && uiState.isSupported == true) {
|
||||
if (uiState.isoFiles.isEmpty()) {
|
||||
// Empty state
|
||||
item {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillParentMaxSize()
|
||||
.padding(32.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Album,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(64.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(
|
||||
text = "No ISO/IMG files found",
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = "Place ISO or IMG files in:\n${uiState.currentPath}",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
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 {
|
||||
// Parent directory navigation
|
||||
if (viewModel.canNavigateUp()) {
|
||||
item {
|
||||
FileItemCard(
|
||||
name = "..",
|
||||
size = "",
|
||||
isDirectory = true,
|
||||
onClick = { viewModel.navigateUp() },
|
||||
onLongClick = null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// File list
|
||||
items(uiState.isoFiles) { file ->
|
||||
FileItemCard(
|
||||
name = file.name,
|
||||
size = file.formattedSize,
|
||||
isIso = file.name.lowercase().endsWith(".iso"),
|
||||
isImg = file.name.lowercase().endsWith(".img"),
|
||||
onClick = {
|
||||
selectedFile = file
|
||||
showMountDialog = true
|
||||
},
|
||||
onLongClick = {
|
||||
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
contextMenuFile = file
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PullToRefreshContainer(
|
||||
state = pullToRefreshState,
|
||||
modifier = Modifier.align(Alignment.TopCenter)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,8 +330,14 @@ fun MainScreen(
|
||||
val filePath = file.path // Capture path before clearing state
|
||||
showMountDialog = false
|
||||
selectedFile = null
|
||||
scope.launch {
|
||||
viewModel.mount(filePath, options)
|
||||
showUsbWarningOrProceed(
|
||||
isUnmount = false,
|
||||
mountPath = filePath,
|
||||
mountOptions = options
|
||||
) {
|
||||
scope.launch {
|
||||
viewModel.mount(filePath, options)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -242,4 +378,39 @@ fun MainScreen(
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// USB warning dialog
|
||||
if (showUsbWarning) {
|
||||
UsbWarningDialog(
|
||||
isUnmount = isUnmountWarning,
|
||||
onDismiss = {
|
||||
showUsbWarning = false
|
||||
pendingMountPath = null
|
||||
pendingMountOptions = null
|
||||
},
|
||||
onConfirm = { dontShowAgain ->
|
||||
if (dontShowAgain) {
|
||||
prefs.edit().putBoolean("skip_usb_warning", true).apply()
|
||||
skipUsbWarning.value = true
|
||||
}
|
||||
|
||||
// Capture values before clearing state
|
||||
val isUnmount = isUnmountWarning
|
||||
val path = pendingMountPath
|
||||
val options = pendingMountOptions
|
||||
|
||||
showUsbWarning = false
|
||||
pendingMountPath = null
|
||||
pendingMountOptions = null
|
||||
|
||||
scope.launch {
|
||||
if (isUnmount) {
|
||||
viewModel.unmount()
|
||||
} else if (path != null && options != null) {
|
||||
viewModel.mount(path, options)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,8 @@ 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.WindowInsets
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
@@ -47,6 +49,7 @@ import androidx.compose.material.icons.filled.Folder
|
||||
import androidx.compose.material.icons.filled.Info
|
||||
import androidx.compose.material.icons.filled.Notifications
|
||||
import androidx.compose.material.icons.filled.Security
|
||||
import androidx.compose.material.icons.filled.Warning
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
@@ -58,6 +61,7 @@ import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBar
|
||||
@@ -115,6 +119,10 @@ fun SettingsScreen(
|
||||
)
|
||||
}
|
||||
|
||||
// USB warning preference
|
||||
val prefs = remember { context.getSharedPreferences("iso_drive_prefs", android.content.Context.MODE_PRIVATE) }
|
||||
var showUsbWarning by remember { mutableStateOf(!prefs.getBoolean("skip_usb_warning", false)) }
|
||||
|
||||
// Re-check permissions when returning to the app
|
||||
if (activity != null) {
|
||||
androidx.compose.runtime.DisposableEffect(activity) {
|
||||
@@ -145,6 +153,7 @@ fun SettingsScreen(
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
contentWindowInsets = WindowInsets(0),
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("Settings") },
|
||||
@@ -158,7 +167,8 @@ fun SettingsScreen(
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.primaryContainer,
|
||||
titleContentColor = MaterialTheme.colorScheme.onPrimaryContainer
|
||||
titleContentColor = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||
navigationIconContentColor = MaterialTheme.colorScheme.onPrimaryContainer
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -167,16 +177,26 @@ fun SettingsScreen(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.navigationBarsPadding()
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
// Storage section
|
||||
SectionHeader(title = "Storage")
|
||||
|
||||
val hasRootForStorage = uiState.hasRoot ?: false
|
||||
SettingsItem(
|
||||
icon = Icons.Default.Folder,
|
||||
title = "ISO Directory",
|
||||
subtitle = uiState.isoDirectory,
|
||||
onClick = { showPathDialog = true }
|
||||
enabled = hasRootForStorage,
|
||||
disabledHint = if (!uiState.rootDenied) "Tap to grant root access" else null,
|
||||
onClick = {
|
||||
if (hasRootForStorage) {
|
||||
showPathDialog = true
|
||||
} else {
|
||||
viewModel.requestRootAccess()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
HorizontalDivider(modifier = Modifier.padding(horizontal = 16.dp))
|
||||
@@ -236,6 +256,61 @@ fun SettingsScreen(
|
||||
|
||||
HorizontalDivider(modifier = Modifier.padding(horizontal = 16.dp))
|
||||
|
||||
// Warnings section
|
||||
SectionHeader(title = "Warnings")
|
||||
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Warning,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
Column {
|
||||
Text(
|
||||
text = "USB restart warning",
|
||||
style = MaterialTheme.typography.titleSmall
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = "Show warning before mount/unmount about USB service interruption",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
Switch(
|
||||
checked = showUsbWarning,
|
||||
onCheckedChange = { enabled ->
|
||||
showUsbWarning = enabled
|
||||
prefs.edit().putBoolean("skip_usb_warning", !enabled).apply()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalDivider(modifier = Modifier.padding(horizontal = 16.dp))
|
||||
|
||||
// About section
|
||||
SectionHeader(title = "About")
|
||||
|
||||
@@ -446,9 +521,10 @@ private fun DirectoryBrowserDialog(
|
||||
fun loadContents(path: String) {
|
||||
scope.launch {
|
||||
isLoading = true
|
||||
val safePath = RootManager.shellEscape(path)
|
||||
// Load directories
|
||||
val dirResult = RootManager.executeCommand(
|
||||
"find \"$path\" -maxdepth 1 -mindepth 1 -type d 2>/dev/null"
|
||||
"find $safePath -maxdepth 1 -mindepth 1 -type d 2>/dev/null"
|
||||
)
|
||||
val directories = if (dirResult.success && dirResult.output.isNotBlank()) {
|
||||
dirResult.output.lines()
|
||||
@@ -457,8 +533,9 @@ private fun DirectoryBrowserDialog(
|
||||
.filter { !it.substringAfterLast("/").startsWith(".") }
|
||||
.map { dirPath ->
|
||||
// Check if this directory was created by the app (has .isodroiddir marker)
|
||||
val safeDirPath = RootManager.shellEscape(dirPath)
|
||||
val markerCheck = RootManager.executeCommand(
|
||||
"test -f \"$dirPath/.isodroiddir\" && echo 'yes' || echo 'no'"
|
||||
"test -f $safeDirPath/.isodroiddir && echo 'yes' || echo 'no'"
|
||||
)
|
||||
val isDeletable = markerCheck.output.trim() == "yes"
|
||||
BrowserItem(dirPath.substringAfterLast("/"), true, dirPath, isDeletable)
|
||||
@@ -469,7 +546,7 @@ private fun DirectoryBrowserDialog(
|
||||
|
||||
// Load ISO/IMG files
|
||||
val fileResult = RootManager.executeCommand(
|
||||
"find \"$path\" -maxdepth 1 -type f \\( -iname '*.iso' -o -iname '*.img' \\) 2>/dev/null"
|
||||
"find $safePath -maxdepth 1 -type f \\( -iname '*.iso' -o -iname '*.img' \\) 2>/dev/null"
|
||||
)
|
||||
val files = if (fileResult.success && fileResult.output.isNotBlank()) {
|
||||
fileResult.output.lines()
|
||||
@@ -489,10 +566,12 @@ private fun DirectoryBrowserDialog(
|
||||
scope.launch {
|
||||
val trimmedName = name.trim()
|
||||
if (trimmedName.isEmpty()) return@launch
|
||||
|
||||
val newPath = "$currentPath/$trimmedName"
|
||||
RootManager.executeCommand("mkdir -p \"$newPath\"")
|
||||
val safeNewPath = RootManager.shellEscape(newPath)
|
||||
RootManager.executeCommand("mkdir -p $safeNewPath")
|
||||
// Create marker file to indicate this folder was created by the app
|
||||
RootManager.executeCommand("touch \"$newPath/.isodroiddir\"")
|
||||
RootManager.executeCommand("touch $safeNewPath/.isodroiddir")
|
||||
// Auto-navigate into the new folder
|
||||
currentPath = newPath
|
||||
}
|
||||
@@ -500,7 +579,8 @@ private fun DirectoryBrowserDialog(
|
||||
|
||||
fun deleteFolder(path: String) {
|
||||
scope.launch {
|
||||
RootManager.executeCommand("rm -rf \"$path\"")
|
||||
val safePath = RootManager.shellEscape(path)
|
||||
RootManager.executeCommand("rm -rf $safePath")
|
||||
loadContents(currentPath)
|
||||
}
|
||||
}
|
||||
@@ -716,6 +796,9 @@ private fun DirectoryBrowserDialog(
|
||||
|
||||
// Create folder dialog
|
||||
if (showCreateFolderDialog) {
|
||||
val hasInvalidChars = newFolderName.any { !it.isLetterOrDigit() && it !in "-_. ()[]+," }
|
||||
val isValidInput = newFolderName.isNotBlank() && !hasInvalidChars
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = {
|
||||
showCreateFolderDialog = false
|
||||
@@ -723,24 +806,36 @@ private fun DirectoryBrowserDialog(
|
||||
},
|
||||
title = { Text("Create Directory") },
|
||||
text = {
|
||||
OutlinedTextField(
|
||||
value = newFolderName,
|
||||
onValueChange = { newFolderName = it },
|
||||
label = { Text("Directory name") },
|
||||
singleLine = true,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Column {
|
||||
OutlinedTextField(
|
||||
value = newFolderName,
|
||||
onValueChange = { newFolderName = it },
|
||||
label = { Text("Directory name") },
|
||||
singleLine = true,
|
||||
isError = hasInvalidChars,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
if (hasInvalidChars) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = "Invalid directory name",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.error
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
if (newFolderName.isNotBlank()) {
|
||||
if (isValidInput) {
|
||||
createFolder(newFolderName)
|
||||
showCreateFolderDialog = false
|
||||
newFolderName = ""
|
||||
}
|
||||
},
|
||||
enabled = newFolderName.isNotBlank()
|
||||
enabled = isValidInput
|
||||
) {
|
||||
Text("Create")
|
||||
}
|
||||
@@ -798,8 +893,12 @@ private fun SettingsItem(
|
||||
icon: ImageVector,
|
||||
title: String,
|
||||
subtitle: String,
|
||||
enabled: Boolean = true,
|
||||
disabledHint: String? = null,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
val contentAlpha = if (enabled) 1f else 0.38f
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -810,19 +909,28 @@ private fun SettingsItem(
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = contentAlpha)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = contentAlpha)
|
||||
)
|
||||
Text(
|
||||
text = subtitle,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = contentAlpha)
|
||||
)
|
||||
if (!enabled && disabledHint != null) {
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = disabledHint,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.error.copy(alpha = 0.7f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.Notifications
|
||||
import androidx.compose.material.icons.filled.Security
|
||||
import androidx.compose.material.icons.filled.Warning
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
@@ -69,15 +70,18 @@ fun SetupWizardScreen(
|
||||
0 -> WelcomeStep(
|
||||
onNext = { currentStep = 1 }
|
||||
)
|
||||
1 -> RootAccessStep(
|
||||
onNext = { currentStep = 2 },
|
||||
onSkip = { currentStep = 2 }
|
||||
1 -> DisclaimerStep(
|
||||
onNext = { currentStep = 2 }
|
||||
)
|
||||
2 -> NotificationStep(
|
||||
2 -> RootAccessStep(
|
||||
onNext = { currentStep = 3 },
|
||||
onSkip = { currentStep = 3 }
|
||||
)
|
||||
3 -> CompleteStep(
|
||||
3 -> NotificationStep(
|
||||
onNext = { currentStep = 4 },
|
||||
onSkip = { currentStep = 4 }
|
||||
)
|
||||
4 -> CompleteStep(
|
||||
onFinish = onSetupComplete
|
||||
)
|
||||
}
|
||||
@@ -131,6 +135,53 @@ private fun WelcomeStep(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DisclaimerStep(
|
||||
onNext: () -> Unit
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Warning,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(64.dp),
|
||||
tint = MaterialTheme.colorScheme.tertiary
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
Text(
|
||||
text = "Before You Continue",
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Text(
|
||||
text = "This app requires root access and performs system-level operations. While reasonable precautions have been taken, you are responsible for understanding what you're doing.",
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
textAlign = TextAlign.Center,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
Text(
|
||||
text = "Keep backups of important data. This software is provided \"as is\" without warranty.",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
textAlign = TextAlign.Center,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(48.dp))
|
||||
|
||||
Button(
|
||||
onClick = onNext,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text("Continue")
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RootAccessStep(
|
||||
onNext: () -> Unit,
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
|
||||
package sh.sar.isodroid.ui.theme
|
||||
|
||||
import android.app.Activity
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
@@ -14,11 +13,7 @@ import androidx.compose.material3.dynamicDarkColorScheme
|
||||
import androidx.compose.material3.dynamicLightColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.core.view.WindowCompat
|
||||
|
||||
private val DarkColorScheme = darkColorScheme(
|
||||
primary = Purple80,
|
||||
@@ -47,15 +42,6 @@ fun ISODroidTheme(
|
||||
else -> LightColorScheme
|
||||
}
|
||||
|
||||
val view = LocalView.current
|
||||
if (!view.isInEditMode) {
|
||||
SideEffect {
|
||||
val window = (view.context as Activity).window
|
||||
window.statusBarColor = android.graphics.Color.TRANSPARENT
|
||||
WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = !darkTheme
|
||||
}
|
||||
}
|
||||
|
||||
MaterialTheme(
|
||||
colorScheme = colorScheme,
|
||||
typography = Typography,
|
||||
|
||||
@@ -205,9 +205,10 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||
|
||||
// Create directory if it doesn't exist
|
||||
if (!directory.exists()) {
|
||||
RootManager.executeCommand("mkdir -p \"$currentPath\"")
|
||||
val safePath = RootManager.shellEscape(currentPath)
|
||||
RootManager.executeCommand("mkdir -p $safePath")
|
||||
// Create marker file to indicate this folder was created by the app
|
||||
RootManager.executeCommand("touch \"$currentPath/.isodroiddir\"")
|
||||
RootManager.executeCommand("touch $safePath/.isodroiddir")
|
||||
}
|
||||
|
||||
// Try multiple methods to list files
|
||||
@@ -220,8 +221,9 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||
|
||||
private suspend fun loadFilesViaFind(currentPath: String): List<IsoFile>? {
|
||||
// Use find command - more reliable for getting full paths
|
||||
val safePath = RootManager.shellEscape(currentPath)
|
||||
val result = RootManager.executeCommand(
|
||||
"find \"$currentPath\" -maxdepth 1 -type f \\( -iname '*.iso' -o -iname '*.img' \\) 2>/dev/null"
|
||||
"find $safePath -maxdepth 1 -type f \\( -iname '*.iso' -o -iname '*.img' \\) 2>/dev/null"
|
||||
)
|
||||
|
||||
if (!result.success || result.output.isBlank()) return null
|
||||
@@ -231,8 +233,9 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||
.mapNotNull { filePath ->
|
||||
val file = File(filePath.trim())
|
||||
val name = file.name
|
||||
// Get file size via stat
|
||||
val sizeResult = RootManager.executeCommand("stat -c %s \"$filePath\" 2>/dev/null")
|
||||
// Get file size via stat with safe escaping
|
||||
val safeFilePath = RootManager.shellEscape(filePath.trim())
|
||||
val sizeResult = RootManager.executeCommand("stat -c %s $safeFilePath 2>/dev/null")
|
||||
val size = sizeResult.output.trim().toLongOrNull() ?: 0L
|
||||
IsoFile(
|
||||
path = filePath.trim(),
|
||||
@@ -246,8 +249,9 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||
|
||||
private suspend fun loadFilesViaLs(currentPath: String): List<IsoFile>? {
|
||||
// Simple ls command - just get filenames
|
||||
val safePath = RootManager.shellEscape(currentPath)
|
||||
val result = RootManager.executeCommand(
|
||||
"ls \"$currentPath\" 2>/dev/null"
|
||||
"ls $safePath 2>/dev/null"
|
||||
)
|
||||
|
||||
if (!result.success || result.output.isBlank()) return null
|
||||
@@ -259,8 +263,9 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||
}
|
||||
.mapNotNull { name ->
|
||||
val filePath = "$currentPath/$name"
|
||||
// Get file size via stat
|
||||
val sizeResult = RootManager.executeCommand("stat -c %s \"$filePath\" 2>/dev/null")
|
||||
// Get file size via stat with safe escaping
|
||||
val safeFilePath = RootManager.shellEscape(filePath)
|
||||
val sizeResult = RootManager.executeCommand("stat -c %s $safeFilePath 2>/dev/null")
|
||||
val size = sizeResult.output.trim().toLongOrNull() ?: 0L
|
||||
IsoFile(
|
||||
path = filePath,
|
||||
@@ -444,13 +449,14 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||
val blockSize = 1024 * 1024L // 1MB blocks
|
||||
val totalBlocks = totalBytes / blockSize
|
||||
var writtenBlocks = 0L
|
||||
val safeFilePath = RootManager.shellEscape(filePath)
|
||||
|
||||
// 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"
|
||||
"dd if=/dev/zero of=$safeFilePath bs=1M count=0 seek=$totalBlocks 2>/dev/null"
|
||||
)
|
||||
|
||||
if (!createResult.success) {
|
||||
@@ -461,18 +467,18 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||
while (writtenBlocks < totalBlocks) {
|
||||
if (CreateImgEventBus.isCancelRequested()) {
|
||||
// Clean up partial file
|
||||
RootManager.executeCommand("rm -f \"$filePath\"")
|
||||
RootManager.executeCommand("rm -f $safeFilePath")
|
||||
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"
|
||||
"dd if=/dev/zero of=$safeFilePath bs=1M count=$chunksToWrite seek=$writtenBlocks conv=notrunc 2>/dev/null"
|
||||
)
|
||||
|
||||
if (!chunkResult.success) {
|
||||
RootManager.executeCommand("rm -f \"$filePath\"")
|
||||
RootManager.executeCommand("rm -f $safeFilePath")
|
||||
return@withContext false
|
||||
}
|
||||
|
||||
@@ -484,7 +490,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
RootManager.executeCommand("rm -f \"$filePath\"")
|
||||
RootManager.executeCommand("rm -f $safeFilePath")
|
||||
false
|
||||
}
|
||||
}
|
||||
@@ -508,14 +514,18 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||
val oldPath = file.path
|
||||
val newPath = "${oldPath.substringBeforeLast("/")}/$newName"
|
||||
|
||||
// Use shell-safe escaping to prevent command injection
|
||||
val safeOldPath = RootManager.shellEscape(oldPath)
|
||||
val safeNewPath = RootManager.shellEscape(newPath)
|
||||
|
||||
// Check if new file already exists
|
||||
val checkResult = RootManager.executeCommand("test -f \"$newPath\" && echo exists")
|
||||
val checkResult = RootManager.executeCommand("test -f $safeNewPath && echo exists")
|
||||
if (checkResult.output.trim() == "exists") {
|
||||
_uiState.update { it.copy(errorMessage = "File already exists: $newName") }
|
||||
return@launch
|
||||
}
|
||||
|
||||
val result = RootManager.executeCommand("mv \"$oldPath\" \"$newPath\"")
|
||||
val result = RootManager.executeCommand("mv $safeOldPath $safeNewPath")
|
||||
if (result.success) {
|
||||
_uiState.update { it.copy(successMessage = "Renamed to $newName") }
|
||||
loadFiles()
|
||||
@@ -527,7 +537,8 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||
|
||||
fun deleteFile(file: IsoFile) {
|
||||
viewModelScope.launch {
|
||||
val result = RootManager.executeCommand("rm -f \"${file.path}\"")
|
||||
val safePath = RootManager.shellEscape(file.path)
|
||||
val result = RootManager.executeCommand("rm -f $safePath")
|
||||
if (result.success) {
|
||||
_uiState.update { it.copy(successMessage = "Deleted ${file.name}") }
|
||||
loadFiles()
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
#!/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
|
||||
186
docs/BUILDING.md
@@ -1,53 +1,50 @@
|
||||
# Building ISO Droid
|
||||
|
||||
This document explains how to build ISO Droid from source and contribute to the project.
|
||||
This document explains how to build ISO Droid from source.
|
||||
|
||||
## Requirements
|
||||
|
||||
- **Android Studio** (or command-line Gradle)
|
||||
- **Android SDK** (API 26+)
|
||||
- **Android NDK** (for compiling isodrive binary)
|
||||
- **Git** (with submodule support)
|
||||
|
||||
## Building from Source
|
||||
|
||||
### Prerequisites
|
||||
### 1. Clone with Submodules
|
||||
|
||||
**1. isodrive Binary (Required)**
|
||||
|
||||
ISO Droid requires the `isodrive` binary to function. This binary is the actual tool that communicates with the Linux kernel's USB gadget subsystem to mount ISO/IMG files.
|
||||
|
||||
- **Source**: The binary is compiled from [nitanmarcel/isodrive](https://github.com/nitanmarcel/isodrive)
|
||||
- **Why required**: Android apps cannot directly access USB gadget interfaces - they need a native binary with root privileges
|
||||
- **Location**: Place the binary at `app/src/main/assets/bin/isodrive`
|
||||
- **Supported architectures**: ARM64 (arm64-v8a), ARM32 (armeabi-v7a), x86, x86_64
|
||||
|
||||
**How to obtain the binary:**
|
||||
|
||||
1. Download the latest release from [isodrive-magisk releases](https://github.com/nitanmarcel/isodrive-magisk/releases/latest)
|
||||
|
||||
2. Extract the `isodrive-magisk-v<version>.zip` file
|
||||
|
||||
3. The binaries for each architecture are in the `libs/` directory:
|
||||
```
|
||||
libs/
|
||||
├── arm64-v8a/
|
||||
│ └── isodrive
|
||||
├── armeabi-v7a/
|
||||
│ └── isodrive
|
||||
├── x86/
|
||||
│ └── isodrive
|
||||
└── x86_64/
|
||||
└── isodrive
|
||||
```
|
||||
|
||||
4. Copy the binary for your target architecture to `app/src/main/assets/bin/isodrive`
|
||||
- For most modern devices: use `arm64-v8a/isodrive`
|
||||
- The app can bundle multiple architectures if needed
|
||||
|
||||
### Build Steps
|
||||
ISO Droid uses [isodrive](https://git.shihaam.dev/shihaam/isodrive) as a git submodule. Clone with submodules:
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone https://github.com/sargit/ISODroid.git
|
||||
git clone --recurse-submodules https://git.shihaam.dev/shihaam/ISODroid.git
|
||||
cd ISODroid
|
||||
```
|
||||
|
||||
# Ensure isodrive binary is present
|
||||
ls -la app/src/main/assets/bin/isodrive
|
||||
Or if already cloned, initialize submodules:
|
||||
|
||||
```bash
|
||||
git submodule update --init
|
||||
```
|
||||
|
||||
### 2. Install Android NDK
|
||||
|
||||
The isodrive binary is compiled from source during the build. This requires the Android NDK.
|
||||
|
||||
**Android Studio:**
|
||||
1. Go to **Tools → SDK Manager**
|
||||
2. Click **SDK Tools** tab
|
||||
3. Check **NDK (Side by side)**
|
||||
4. Click **Apply**
|
||||
|
||||
**NixOS:**
|
||||
The build automatically uses `nix-shell` to get the NDK if available.
|
||||
|
||||
**Manual:**
|
||||
Set `ANDROID_NDK_HOME` environment variable to your NDK path.
|
||||
|
||||
### 3. Build
|
||||
|
||||
```bash
|
||||
# Build debug APK
|
||||
./gradlew assembleDebug
|
||||
|
||||
@@ -55,105 +52,38 @@ ls -la app/src/main/assets/bin/isodrive
|
||||
./gradlew assembleRelease
|
||||
```
|
||||
|
||||
The compiled APK will be in:
|
||||
- Debug: `app/debug/app-debug.apk`
|
||||
- Release: `app/release/app-release.apk`
|
||||
The Gradle build will automatically:
|
||||
- Compile isodrive for all architectures (arm64-v8a, armeabi-v7a, x86_64, x86)
|
||||
- Bundle the binaries into the APK assets
|
||||
- Skip compilation if binaries already exist
|
||||
|
||||
## Contributing
|
||||
**Output locations:**
|
||||
- Debug: `app/build/outputs/apk/debug/app-debug.apk`
|
||||
- Release: `app/build/outputs/apk/release/app-release.apk`
|
||||
|
||||
### Adding OS Icons
|
||||
### Build from Android Studio
|
||||
|
||||
OS icons are displayed in the file browser when ISO/IMG filenames match the icon name.
|
||||
1. Open the project in Android Studio
|
||||
2. Ensure NDK is installed (see above)
|
||||
3. Click **Build → Build Bundle(s) / APK(s) → Build APK(s)**
|
||||
|
||||
**Steps:**
|
||||
The isodrive binary will be compiled automatically before the APK is built.
|
||||
|
||||
1. Add your SVG file to `app/src/main/assets/osicons/`
|
||||
- Filename should be lowercase (e.g., `ubuntu.svg`, `archlinux.svg`)
|
||||
- SVG should be simple and recognizable
|
||||
- Recommended: Get icons from [Simple Icons](https://simpleicons.org/)
|
||||
### Manual isodrive Build (Optional)
|
||||
|
||||
2. The app automatically detects and matches icons:
|
||||
- Filename `ubuntu-22.04-desktop-amd64.iso` → matches `ubuntu.svg`
|
||||
- Filename `archlinux-2024.12.01-x86_64.iso` → matches `archlinux.svg`
|
||||
- Matching is case-insensitive and searches for icon name within filename
|
||||
To manually trigger isodrive compilation:
|
||||
|
||||
3. **Symlinks for filename variations:**
|
||||
|
||||
Sometimes ISO files use shortened or alternate names. Create symlinks to match these variations:
|
||||
```bash
|
||||
cd app/src/main/assets/osicons/
|
||||
|
||||
# Linux Mint ISOs often named "mint-*.iso"
|
||||
ln -s linuxmint.svg mint.svg
|
||||
|
||||
# Windows ISOs might be "Win11.iso" or "Win.iso"
|
||||
ln -s windows.svg win.svg
|
||||
ln -s windows.svg win11.svg
|
||||
|
||||
# FreeBSD might be shortened
|
||||
ln -s freebsd.svg bsd.svg
|
||||
```
|
||||
|
||||
This ensures `mint-21.3-cinnamon.iso` matches even though the icon is `linuxmint.svg`
|
||||
|
||||
4. Submit a pull request with your new icon(s) and any necessary symlinks
|
||||
|
||||
### Adding OS Download Links
|
||||
|
||||
The Downloads screen shows curated links to operating system ISOs.
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. Edit `app/src/main/assets/os.json`
|
||||
|
||||
2. Add a new entry with all required fields:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Ubuntu Desktop",
|
||||
"category": "Linux",
|
||||
"description": "Popular Linux distribution with GNOME desktop",
|
||||
"icon": "ubuntu.svg",
|
||||
"url": "https://ubuntu.com/download/desktop"
|
||||
}
|
||||
```bash
|
||||
./gradlew buildIsodrive
|
||||
```
|
||||
|
||||
**Field descriptions:**
|
||||
To force rebuild, delete existing binaries first:
|
||||
|
||||
- `name` (required): Display name for the OS
|
||||
- `category` (required): One of: `Linux`, `BSD`, `Windows`, or `Recovery`
|
||||
- `description` (required): Brief description (one sentence)
|
||||
- `icon` (required): Filename of SVG in `osicons/` directory, or `null` if no icon
|
||||
- `url` (required): Direct link to download page or ISO file
|
||||
|
||||
**Example with no icon:**
|
||||
```json
|
||||
{
|
||||
"name": "Custom Linux",
|
||||
"category": "Linux",
|
||||
"description": "A custom Linux distribution",
|
||||
"icon": null,
|
||||
"url": "https://example.com/download"
|
||||
}
|
||||
```bash
|
||||
rm -rf app/src/main/assets/bin/*/isodrive
|
||||
./gradlew buildIsodrive
|
||||
```
|
||||
|
||||
3. Test your changes:
|
||||
- Build the app
|
||||
- Navigate to Downloads screen
|
||||
- Verify your entry appears in the correct category
|
||||
- Verify the icon displays (if provided)
|
||||
- Verify the link opens correctly
|
||||
## F-Droid Build
|
||||
|
||||
4. Submit a pull request with:
|
||||
- Updated `os.json`
|
||||
- New SVG icon in `osicons/` (if applicable)
|
||||
- Brief description of what you added
|
||||
|
||||
## Testing
|
||||
|
||||
Before submitting a PR, test on a real rooted Android device:
|
||||
|
||||
- [ ] App builds successfully
|
||||
- [ ] New OS icon displays correctly
|
||||
- [ ] New download link opens properly
|
||||
- [ ] No crashes or errors in logcat
|
||||
F-Droid uses `srclibs` instead of git submodules. The F-Droid metadata pre-builds isodrive before running Gradle, so the Gradle task skips compilation (binaries already exist).
|
||||
|
||||
99
docs/CONTRIBUTING.md
Normal file
@@ -0,0 +1,99 @@
|
||||
# Contributing to ISO Droid
|
||||
|
||||
This document explains how to contribute OS icons and download links to ISO Droid.
|
||||
|
||||
For building the app, see [BUILDING.md](BUILDING.md).
|
||||
|
||||
## Adding OS Icons
|
||||
|
||||
OS icons are displayed in the file browser when ISO/IMG filenames match the icon name.
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. Add your SVG file to `app/src/main/assets/osicons/`
|
||||
- Filename should be lowercase (e.g., `ubuntu.svg`, `archlinux.svg`)
|
||||
- SVG should be simple and recognizable
|
||||
- Recommended: Get icons from [Simple Icons](https://simpleicons.org/)
|
||||
|
||||
2. The app automatically detects and matches icons:
|
||||
- Filename `ubuntu-22.04-desktop-amd64.iso` → matches `ubuntu.svg`
|
||||
- Filename `archlinux-2024.12.01-x86_64.iso` → matches `archlinux.svg`
|
||||
- Matching is case-insensitive and searches for icon name within filename
|
||||
|
||||
3. **Symlinks for filename variations:**
|
||||
|
||||
Sometimes ISO files use shortened or alternate names. Create symlinks to match these variations:
|
||||
```bash
|
||||
cd app/src/main/assets/osicons/
|
||||
|
||||
# Linux Mint ISOs often named "mint-*.iso"
|
||||
ln -s linuxmint.svg mint.svg
|
||||
|
||||
# Windows ISOs might be "Win11.iso" or "Win.iso"
|
||||
ln -s windows.svg win.svg
|
||||
ln -s windows.svg win11.svg
|
||||
```
|
||||
|
||||
This ensures `mint-21.3-cinnamon.iso` matches even though the icon is `linuxmint.svg`
|
||||
|
||||
4. Submit a pull request with your new icon(s) and any necessary symlinks
|
||||
|
||||
## Adding OS Download Links
|
||||
|
||||
The Downloads screen shows curated links to operating system ISOs.
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. Edit `app/src/main/assets/os.json`
|
||||
|
||||
2. Add a new entry with all required fields:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Ubuntu Desktop",
|
||||
"category": "Linux",
|
||||
"description": "Popular Linux distribution with GNOME desktop",
|
||||
"icon": "ubuntu.svg",
|
||||
"url": "https://ubuntu.com/download/desktop"
|
||||
}
|
||||
```
|
||||
|
||||
**Field descriptions:**
|
||||
|
||||
- `name` (required): Display name for the OS
|
||||
- `category` (required): One of: `Linux`, `BSD`, `Windows`, or `Recovery`
|
||||
- `description` (required): Brief description (one sentence)
|
||||
- `icon` (required): Filename of SVG in `osicons/` directory, or `null` if no icon
|
||||
- `url` (required): Direct link to download page or ISO file
|
||||
|
||||
**Example with no icon:**
|
||||
```json
|
||||
{
|
||||
"name": "Custom Linux",
|
||||
"category": "Linux",
|
||||
"description": "A custom Linux distribution",
|
||||
"icon": null,
|
||||
"url": "https://example.com/download"
|
||||
}
|
||||
```
|
||||
|
||||
3. Test your changes:
|
||||
- Build the app (see [BUILDING.md](BUILDING.md))
|
||||
- Navigate to Downloads screen
|
||||
- Verify your entry appears in the correct category
|
||||
- Verify the icon displays (if provided)
|
||||
- Verify the link opens correctly
|
||||
|
||||
4. Submit a pull request with:
|
||||
- Updated `os.json`
|
||||
- New SVG icon in `osicons/` (if applicable)
|
||||
- Brief description of what you added
|
||||
|
||||
## Testing
|
||||
|
||||
Before submitting a PR, test on a real rooted Android device:
|
||||
|
||||
- [ ] App builds successfully
|
||||
- [ ] New OS icon displays correctly
|
||||
- [ ] New download link opens properly
|
||||
- [ ] No crashes or errors in logcat
|
||||
38
docs/DISCLAIMER.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# Disclaimer
|
||||
|
||||
## About This Project
|
||||
|
||||
ISO Droid was developed with AI assistance (Claude by Anthropic). This is mentioned for transparency, regardless of who or what wrote it, it can contain bugs.
|
||||
|
||||
Reasonable precautions have been taken to prevent unintended behavior.
|
||||
|
||||
## Root Access
|
||||
|
||||
This app requires root access to function. Root access gives applications elevated privileges on your device, which means:
|
||||
|
||||
- Operations can affect system-level functionality
|
||||
- Mistakes can potentially cause issues that require recovery
|
||||
- You should understand what an operation does before executing it
|
||||
|
||||
## Recommendations
|
||||
|
||||
- **Keep backups** of important data (good practice regardless)
|
||||
- **Understand the operations** before running them
|
||||
- **Test on non-critical setups first** if you're unsure
|
||||
- **Report bugs** if you encounter them
|
||||
|
||||
## My Usage
|
||||
|
||||
I (the developer) use this app on my own devices regularly. It works for my use cases, but your device, kernel, and setup may differ, and there may be edge cases I haven't encountered.
|
||||
|
||||
## No Warranty
|
||||
|
||||
This software is provided "as is" without warranty of any kind. See the [LICENSE](../LICENSE) file for full details (GPL-3.0).
|
||||
|
||||
## Bug Reports & Contributions
|
||||
|
||||
Found a bug? Have a suggestion? Please open an issue:
|
||||
|
||||
- **Issues**: [git.sargit.com/sargit/ISODroid/issues](https://git.sargit.com/sargit/ISODroid/issues)
|
||||
|
||||
Contributions are welcome. If you fix something or improve the app, consider submitting a pull request.
|
||||
|
Before Width: | Height: | Size: 124 KiB After Width: | Height: | Size: 123 KiB |
|
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 165 KiB After Width: | Height: | Size: 145 KiB |
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 109 KiB After Width: | Height: | Size: 108 KiB |
1
fastlane/metadata/android/en-US/changelogs/2.txt
Normal file
@@ -0,0 +1 @@
|
||||
* Fix app name displaying as "ISO Drive" instead of "ISO Droid"
|
||||
1
fastlane/metadata/android/en-US/changelogs/3.txt
Normal file
@@ -0,0 +1 @@
|
||||
* Fix default ISO directory path to /sdcard/isodroid
|
||||
6
fastlane/metadata/android/en-US/changelogs/4.txt
Normal file
@@ -0,0 +1,6 @@
|
||||
* 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
|
||||
* Empty state on home screen now shows current path and helpful hints
|
||||
* Version number now read dynamically from app config
|
||||
7
fastlane/metadata/android/en-US/changelogs/5.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
* More OS download links
|
||||
* Sub-categories for OS downloads (Desktop, Server, etc.)
|
||||
* Replaced refresh button with pull-to-refresh gesture
|
||||
* Disable ISO directory change when root access is not available
|
||||
* Status card now shows "Tap to request root" and updates when granted
|
||||
* Improved monet/dynamic theme support for icons and text colors
|
||||
* Fixed: Sanitize user input for folder creation, IMG file creation, and file renaming
|
||||
5
fastlane/metadata/android/en-US/changelogs/6.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
• Added disclaimer screen in welcome wizard about user responsibility
|
||||
• Added USB services restart warning before mount/unmount (with "Don't show again" option)
|
||||
• Added toggle in Settings to enable/disable USB restart warning
|
||||
• Fixed shell escape exploits in shell commands
|
||||
• Disabled logcat on release builds
|
||||
2
fastlane/metadata/android/en-US/changelogs/7.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
- Always use app-bundled isodrive binary instead of system binary
|
||||
- Prep for edge-to-edge compatibility
|
||||
|
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 109 KiB After Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 165 KiB After Width: | Height: | Size: 123 KiB |
|
Before Width: | Height: | Size: 124 KiB After Width: | Height: | Size: 145 KiB |
@@ -1,18 +0,0 @@
|
||||
#!/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
|
||||