Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
860ff7c479
|
|||
|
fdc63b49ed
|
|||
|
30ab181d75
|
|||
|
1d8d6d0904
|
|||
|
a40986f9b8
|
|||
|
f44703a9fa
|
|||
|
65a1d79e32
|
|||
|
0c0476b413
|
|||
|
71534ac79f
|
|||
|
e7aadd5998
|
|||
|
5c9b398cb1
|
|||
|
cd807ff304
|
|||
|
0d5a73251c
|
|||
|
9f09ac1ad0
|
|||
|
6cdd812144
|
|||
|
cbee1786ca
|
|||
|
5ea665c02f
|
|||
|
7c269d5e59
|
|||
|
fbda9fedfb
|
|||
|
79ccd6753e
|
|||
|
3decb7307e
|
|||
|
d297ab1059
|
|||
|
371e38dc2f
|
|||
|
1e850a2d1a
|
|||
|
4cd9202609
|
|||
|
6becf2907e
|
38
CHANGELOG.md
@@ -5,6 +5,44 @@ 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.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
|
||||
- Directory browser for changing ISO directory in settings
|
||||
- Create new directories from the directory browser
|
||||
- Delete directories created by the app (long press)
|
||||
- Shows ISO/IMG files with OS icons in directory browser
|
||||
|
||||
### Changed
|
||||
- Empty state on home screen now shows current path and helpful hints
|
||||
- Version number is now read dynamically from app config
|
||||
- Renamed "folder" to "directory" throughout the UI
|
||||
|
||||
## [1.3] - 2025-03-11
|
||||
|
||||
### Changed
|
||||
- Fix default image dir to be /sdcard/isodroid instead of /sdcard/isodrive
|
||||
|
||||
## [1.2] - 2025-03-10
|
||||
|
||||
### Changed
|
||||
- Fix app name displaying as "ISO Drive" instead of "ISO Droid" in some places
|
||||
|
||||
## [1.1] - 2025-03-10
|
||||
|
||||
### Changed
|
||||
|
||||
@@ -35,7 +35,7 @@ Android app for mounting ISO/IMG files as USB mass storage or CD-ROM devices on
|
||||
1. Download the APK from the links above
|
||||
2. Install the APK on your rooted Android device
|
||||
3. Grant root access when prompted
|
||||
4. Place your ISO/IMG files in `/sdcard/isodrive/` (or configure a different directory in settings)
|
||||
4. Place your ISO/IMG files in `/sdcard/isodroid/` (or configure a different directory in settings)
|
||||
|
||||
> **Note**: The app includes a bundled `isodrive` binary. No additional setup required!
|
||||
|
||||
|
||||
@@ -12,8 +12,8 @@ android {
|
||||
applicationId = "sh.sar.isodroid"
|
||||
minSdk = 26
|
||||
targetSdk = 36
|
||||
versionCode = 1
|
||||
versionName = "1.1"
|
||||
versionCode = 5
|
||||
versionName = "1.5"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
@@ -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 |
@@ -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
|
||||
|
||||
@@ -55,7 +55,7 @@ import sh.sar.isodroid.data.IsoFile
|
||||
* 1. Longest match (most specific)
|
||||
* 2. Earliest position in filename (if same length)
|
||||
*/
|
||||
private fun findOsIcon(context: android.content.Context, filename: String): String? {
|
||||
fun findOsIcon(context: android.content.Context, filename: String): String? {
|
||||
return try {
|
||||
// Dynamically load available icon files from assets
|
||||
val availableIcons = context.assets.list("osicons")
|
||||
@@ -111,10 +111,19 @@ fun FileBrowser(
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = "Place ISO or IMG files in this directory",
|
||||
text = "Place ISO or IMG files in:\n$currentPath",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f)
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f),
|
||||
textAlign = androidx.compose.ui.text.style.TextAlign.Center
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(
|
||||
text = "Tap + to create an empty IMG file\nChange directory in Settings",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f),
|
||||
textAlign = androidx.compose.ui.text.style.TextAlign.Center
|
||||
)
|
||||
}
|
||||
} else {
|
||||
@@ -125,7 +134,7 @@ fun FileBrowser(
|
||||
// Parent directory item
|
||||
if (canNavigateUp) {
|
||||
item {
|
||||
FileItem(
|
||||
FileItemCard(
|
||||
name = "..",
|
||||
size = "",
|
||||
isDirectory = true,
|
||||
@@ -136,7 +145,7 @@ fun FileBrowser(
|
||||
}
|
||||
|
||||
items(files) { file ->
|
||||
FileItem(
|
||||
FileItemCard(
|
||||
name = file.name,
|
||||
size = file.formattedSize,
|
||||
isIso = file.name.lowercase().endsWith(".iso"),
|
||||
@@ -155,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,41 +43,16 @@ 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
|
||||
|
||||
/**
|
||||
* Finds a matching OS icon filename for a given file by dynamically checking available icons.
|
||||
*/
|
||||
private fun findOsIcon(context: android.content.Context, filename: String): String? {
|
||||
return try {
|
||||
val availableIcons = context.assets.list("osicons")
|
||||
?.filter { it.endsWith(".svg", ignoreCase = true) }
|
||||
?.map { it.removeSuffix(".svg").lowercase() }
|
||||
?: emptyList()
|
||||
|
||||
val lowerFilename = filename.lowercase()
|
||||
|
||||
availableIcons
|
||||
.filter { lowerFilename.contains(it) }
|
||||
.maxWithOrNull(compareBy(
|
||||
{ it.length },
|
||||
{ -lowerFilename.indexOf(it) }
|
||||
))
|
||||
?.let { "$it.svg" }
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun StatusCard(
|
||||
mountStatus: MountStatus,
|
||||
rootAvailable: Boolean?,
|
||||
deviceSupported: Boolean?,
|
||||
modifier: Modifier = Modifier
|
||||
rootDenied: Boolean = false,
|
||||
modifier: Modifier = Modifier,
|
||||
onRequestRoot: (() -> Unit)? = null
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
@@ -97,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
|
||||
@@ -125,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)
|
||||
)
|
||||
@@ -145,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
|
||||
)
|
||||
@@ -214,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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,6 +50,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,6 +68,7 @@ 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", null),
|
||||
description = obj.optString("description", ""),
|
||||
icon = if (obj.isNull("icon")) null else obj.optString("icon", null),
|
||||
url = obj.getString("url")
|
||||
@@ -88,11 +90,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) {
|
||||
@@ -114,7 +114,8 @@ fun DownloadsScreen(
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.primaryContainer,
|
||||
titleContentColor = MaterialTheme.colorScheme.onPrimaryContainer
|
||||
titleContentColor = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||
navigationIconContentColor = MaterialTheme.colorScheme.onPrimaryContainer
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -166,22 +167,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,
|
||||
|
||||
@@ -5,18 +5,28 @@
|
||||
|
||||
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.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
|
||||
@@ -42,10 +52,9 @@ import androidx.compose.ui.Modifier
|
||||
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.viewmodel.MainViewModel
|
||||
@@ -66,6 +75,22 @@ fun MainScreen(
|
||||
var showCreateImgDialog by remember { mutableStateOf(false) }
|
||||
var contextMenuFile by remember { mutableStateOf<IsoFile?>(null) }
|
||||
|
||||
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 ->
|
||||
@@ -89,10 +114,11 @@ fun MainScreen(
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("ISO Drive") },
|
||||
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 +130,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,
|
||||
@@ -147,43 +167,118 @@ fun MainScreen(
|
||||
}
|
||||
}
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
val hapticFeedback = LocalHapticFeedback.current
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.padding(16.dp)
|
||||
.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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,23 +10,38 @@ import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.provider.Settings
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.automirrored.filled.InsertDriveFile
|
||||
import androidx.compose.material.icons.filled.Album
|
||||
import androidx.compose.material.icons.filled.CheckCircle
|
||||
import androidx.compose.material.icons.filled.Code
|
||||
import androidx.compose.material.icons.filled.CreateNewFolder
|
||||
import androidx.compose.material.icons.filled.Error
|
||||
import androidx.compose.material.icons.filled.Folder
|
||||
import androidx.compose.material.icons.filled.Info
|
||||
@@ -35,6 +50,7 @@ import androidx.compose.material.icons.filled.Security
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
@@ -47,20 +63,33 @@ import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
import coil.compose.AsyncImage
|
||||
import coil.decode.SvgDecoder
|
||||
import coil.request.ImageRequest
|
||||
import kotlinx.coroutines.launch
|
||||
import sh.sar.isodroid.root.RootManager
|
||||
import sh.sar.isodroid.ui.components.findOsIcon
|
||||
import sh.sar.isodroid.viewmodel.MainViewModel
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@@ -73,7 +102,6 @@ fun SettingsScreen(
|
||||
val context = LocalContext.current
|
||||
val activity = context as? androidx.activity.ComponentActivity
|
||||
var showPathDialog by remember { mutableStateOf(false) }
|
||||
var tempPath by remember(uiState.currentPath) { mutableStateOf(uiState.isoDirectory) }
|
||||
|
||||
// Track notification permission with lifecycle-aware refresh
|
||||
var hasNotificationPermission by remember {
|
||||
@@ -130,7 +158,8 @@ fun SettingsScreen(
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.primaryContainer,
|
||||
titleContentColor = MaterialTheme.colorScheme.onPrimaryContainer
|
||||
titleContentColor = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||
navigationIconContentColor = MaterialTheme.colorScheme.onPrimaryContainer
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -144,11 +173,20 @@ fun SettingsScreen(
|
||||
// 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))
|
||||
@@ -231,11 +269,18 @@ fun SettingsScreen(
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
Column {
|
||||
Text(
|
||||
text = "ISO Drive",
|
||||
text = "ISO Droid",
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
val versionName = remember {
|
||||
try {
|
||||
context.packageManager.getPackageInfo(context.packageName, 0).versionName
|
||||
} catch (e: Exception) {
|
||||
"Unknown"
|
||||
}
|
||||
}
|
||||
Text(
|
||||
text = "Version 1.0",
|
||||
text = "Version $versionName",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
@@ -259,13 +304,13 @@ fun SettingsScreen(
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Text(
|
||||
text = "git.shihaam.dev/shihaam/ISODrive",
|
||||
text = "git.shihaam.dev/shihaam/ISODroid",
|
||||
style = MaterialTheme.typography.bodySmall.copy(
|
||||
textDecoration = TextDecoration.Underline
|
||||
),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.clickable {
|
||||
openUrl("https://git.shihaam.dev/shihaam/ISODrive")
|
||||
openUrl("https://git.shihaam.dev/shihaam/ISODroid")
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -306,7 +351,7 @@ fun SettingsScreen(
|
||||
}
|
||||
}
|
||||
Text(
|
||||
text = "The CLI tool that powers ISO Drive. Mounts ISO/IMG files as bootable USB devices using configfs.",
|
||||
text = "The CLI tool that powers ISO Droid. Mounts ISO/IMG files as bootable USB devices using configfs.",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(top = 8.dp)
|
||||
@@ -369,40 +414,394 @@ fun SettingsScreen(
|
||||
}
|
||||
}
|
||||
|
||||
// Path edit dialog
|
||||
// Directory browser dialog
|
||||
if (showPathDialog) {
|
||||
DirectoryBrowserDialog(
|
||||
initialPath = uiState.isoDirectory,
|
||||
onDismiss = { showPathDialog = false },
|
||||
onSelect = { selectedPath ->
|
||||
viewModel.setIsoDirectory(selectedPath)
|
||||
showPathDialog = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private data class BrowserItem(
|
||||
val name: String,
|
||||
val isDirectory: Boolean,
|
||||
val fullPath: String,
|
||||
val isDeletable: Boolean = false
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
private fun DirectoryBrowserDialog(
|
||||
initialPath: String,
|
||||
onDismiss: () -> Unit,
|
||||
onSelect: (String) -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
val hapticFeedback = LocalHapticFeedback.current
|
||||
var currentPath by remember { mutableStateOf(initialPath) }
|
||||
var items by remember { mutableStateOf<List<BrowserItem>>(emptyList()) }
|
||||
var isLoading by remember { mutableStateOf(true) }
|
||||
var showCreateFolderDialog by remember { mutableStateOf(false) }
|
||||
var showDeleteDialog by remember { mutableStateOf<String?>(null) }
|
||||
var newFolderName by remember { mutableStateOf("") }
|
||||
|
||||
val storageRoot = Environment.getExternalStorageDirectory().absolutePath
|
||||
|
||||
fun loadContents(path: String) {
|
||||
scope.launch {
|
||||
isLoading = true
|
||||
// Load directories
|
||||
val dirResult = RootManager.executeCommand(
|
||||
"find \"$path\" -maxdepth 1 -mindepth 1 -type d 2>/dev/null"
|
||||
)
|
||||
val directories = if (dirResult.success && dirResult.output.isNotBlank()) {
|
||||
dirResult.output.lines()
|
||||
.filter { it.isNotBlank() }
|
||||
.map { it.trim() }
|
||||
.filter { !it.substringAfterLast("/").startsWith(".") }
|
||||
.map { dirPath ->
|
||||
// Check if this directory was created by the app (has .isodroiddir marker)
|
||||
val markerCheck = RootManager.executeCommand(
|
||||
"test -f \"$dirPath/.isodroiddir\" && echo 'yes' || echo 'no'"
|
||||
)
|
||||
val isDeletable = markerCheck.output.trim() == "yes"
|
||||
BrowserItem(dirPath.substringAfterLast("/"), true, dirPath, isDeletable)
|
||||
}
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
|
||||
// Load ISO/IMG files
|
||||
val fileResult = RootManager.executeCommand(
|
||||
"find \"$path\" -maxdepth 1 -type f \\( -iname '*.iso' -o -iname '*.img' \\) 2>/dev/null"
|
||||
)
|
||||
val files = if (fileResult.success && fileResult.output.isNotBlank()) {
|
||||
fileResult.output.lines()
|
||||
.filter { it.isNotBlank() }
|
||||
.map { it.trim() }
|
||||
.map { BrowserItem(it.substringAfterLast("/"), false, it) }
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
|
||||
items = (directories.sortedBy { it.name.lowercase() } + files.sortedBy { it.name.lowercase() })
|
||||
isLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
fun createFolder(name: String) {
|
||||
scope.launch {
|
||||
val trimmedName = name.trim()
|
||||
if (trimmedName.isEmpty()) return@launch
|
||||
|
||||
val newPath = "$currentPath/$trimmedName"
|
||||
RootManager.executeCommand("mkdir -p \"$newPath\"")
|
||||
// Create marker file to indicate this folder was created by the app
|
||||
RootManager.executeCommand("touch \"$newPath/.isodroiddir\"")
|
||||
// Auto-navigate into the new folder
|
||||
currentPath = newPath
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteFolder(path: String) {
|
||||
scope.launch {
|
||||
RootManager.executeCommand("rm -rf \"$path\"")
|
||||
loadContents(currentPath)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(currentPath) {
|
||||
loadContents(currentPath)
|
||||
}
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text("Select Directory")
|
||||
IconButton(onClick = { showCreateFolderDialog = true }) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.CreateNewFolder,
|
||||
contentDescription = "Create directory",
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
text = {
|
||||
Column {
|
||||
// Current path display
|
||||
Text(
|
||||
text = currentPath,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
HorizontalDivider()
|
||||
|
||||
// Content list
|
||||
if (isLoading) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = 200.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
CircularProgressIndicator(modifier = Modifier.size(32.dp))
|
||||
}
|
||||
} else {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = 200.dp, max = 300.dp)
|
||||
) {
|
||||
// Parent directory (..)
|
||||
if (currentPath != "/" && currentPath != storageRoot) {
|
||||
item {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
val parent = currentPath.substringBeforeLast("/")
|
||||
currentPath = parent.ifEmpty { "/" }
|
||||
}
|
||||
.padding(vertical = 12.dp, horizontal = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Folder,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
Text(
|
||||
text = "..",
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Items (directories and files)
|
||||
items(items) { item ->
|
||||
val isIso = item.name.lowercase().endsWith(".iso")
|
||||
val isImg = item.name.lowercase().endsWith(".img")
|
||||
val osIcon = if (!item.isDirectory) findOsIcon(context, item.name) else null
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.combinedClickable(
|
||||
onClick = {
|
||||
if (item.isDirectory) {
|
||||
currentPath = item.fullPath
|
||||
}
|
||||
},
|
||||
onLongClick = {
|
||||
if (item.isDirectory) {
|
||||
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
when {
|
||||
item.fullPath == initialPath -> {
|
||||
Toast.makeText(
|
||||
context,
|
||||
"Can't delete your current ISO directory. Change it first, then try again.",
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
item.isDeletable -> {
|
||||
showDeleteDialog = item.fullPath
|
||||
}
|
||||
else -> {
|
||||
Toast.makeText(
|
||||
context,
|
||||
"Can't delete this directory — it wasn't created by ISO Droid",
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
.padding(vertical = 8.dp, horizontal = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// Icon with badge
|
||||
Box(modifier = Modifier.size(32.dp)) {
|
||||
if (item.isDirectory) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Folder,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
} else if (osIcon != null) {
|
||||
// OS icon with file type badge
|
||||
AsyncImage(
|
||||
model = ImageRequest.Builder(context)
|
||||
.data("file:///android_asset/osicons/$osIcon")
|
||||
.decoderFactory(SvgDecoder.Factory())
|
||||
.build(),
|
||||
contentDescription = item.name,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.primary)
|
||||
)
|
||||
// File type badge
|
||||
Icon(
|
||||
imageVector = if (isIso) Icons.Default.Album else Icons.AutoMirrored.Filled.InsertDriveFile,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onPrimary,
|
||||
modifier = Modifier
|
||||
.size(14.dp)
|
||||
.align(Alignment.BottomEnd)
|
||||
.offset(x = 2.dp, y = 2.dp)
|
||||
.clip(CircleShape)
|
||||
.background(MaterialTheme.colorScheme.primary)
|
||||
.padding(2.dp)
|
||||
)
|
||||
} else {
|
||||
// Fallback icon
|
||||
Icon(
|
||||
imageVector = if (isIso) Icons.Default.Album else Icons.AutoMirrored.Filled.InsertDriveFile,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
Text(
|
||||
text = item.name,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
maxLines = 3,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.weight(1f),
|
||||
color = if (item.isDirectory)
|
||||
MaterialTheme.colorScheme.onSurface
|
||||
else
|
||||
MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Empty state
|
||||
if (items.isEmpty()) {
|
||||
item {
|
||||
Text(
|
||||
text = "Empty directory",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(vertical = 16.dp, horizontal = 4.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = { onSelect(currentPath) }) {
|
||||
Text("Select")
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text("Cancel")
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// Create folder dialog
|
||||
if (showCreateFolderDialog) {
|
||||
val hasInvalidChars = newFolderName.any { !it.isLetterOrDigit() && it !in "-_. ()[]+," }
|
||||
val isValidInput = newFolderName.isNotBlank() && !hasInvalidChars
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = { showPathDialog = false },
|
||||
title = { Text("ISO Directory") },
|
||||
onDismissRequest = {
|
||||
showCreateFolderDialog = false
|
||||
newFolderName = ""
|
||||
},
|
||||
title = { Text("Create Directory") },
|
||||
text = {
|
||||
Column {
|
||||
Text(
|
||||
text = "Enter the path to the directory containing your ISO/IMG files.",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
OutlinedTextField(
|
||||
value = tempPath,
|
||||
onValueChange = { tempPath = it },
|
||||
label = { Text("Path") },
|
||||
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 = {
|
||||
viewModel.setIsoDirectory(tempPath)
|
||||
showPathDialog = false
|
||||
}
|
||||
if (isValidInput) {
|
||||
createFolder(newFolderName)
|
||||
showCreateFolderDialog = false
|
||||
newFolderName = ""
|
||||
}
|
||||
},
|
||||
enabled = isValidInput
|
||||
) {
|
||||
Text("Save")
|
||||
Text("Create")
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { showPathDialog = false }) {
|
||||
TextButton(onClick = {
|
||||
showCreateFolderDialog = false
|
||||
newFolderName = ""
|
||||
}) {
|
||||
Text("Cancel")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Delete confirmation dialog
|
||||
showDeleteDialog?.let { pathToDelete ->
|
||||
AlertDialog(
|
||||
onDismissRequest = { showDeleteDialog = null },
|
||||
title = { Text("Delete Directory") },
|
||||
text = {
|
||||
Text("Are you sure you want to delete \"${pathToDelete.substringAfterLast("/")}\" and all its contents?")
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
deleteFolder(pathToDelete)
|
||||
showDeleteDialog = null
|
||||
}
|
||||
) {
|
||||
Text("Delete", color = MaterialTheme.colorScheme.error)
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { showDeleteDialog = null }) {
|
||||
Text("Cancel")
|
||||
}
|
||||
}
|
||||
@@ -425,8 +824,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()
|
||||
@@ -437,19 +840,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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ package sh.sar.isodroid.ui.screens
|
||||
import android.Manifest
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
@@ -97,7 +98,7 @@ private fun WelcomeStep(
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
Text(
|
||||
text = "Welcome to ISO Drive",
|
||||
text = "Welcome to ISO Droid",
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
@@ -152,7 +153,7 @@ private fun RootAccessStep(
|
||||
title = "Root Access",
|
||||
description = when (hasRoot) {
|
||||
false -> "Root access was denied. The app requires root to mount ISO files. You can try again or skip and grant access later from your root manager."
|
||||
else -> "ISO Drive needs superuser (root) access to mount ISO files as USB devices. This is required because mounting USB gadgets is a system-level operation."
|
||||
else -> "ISO Droid needs superuser (root) access to mount ISO files as USB devices. This is required because mounting USB gadgets is a system-level operation."
|
||||
},
|
||||
granted = hasRoot
|
||||
)
|
||||
@@ -258,7 +259,7 @@ private fun NotificationStep(
|
||||
description = when {
|
||||
permanentlyDenied -> "Notification permission was denied. You can enable it later in system settings if you change your mind."
|
||||
wasDenied -> "Notification permission was denied. You can try again or continue without notifications."
|
||||
else -> "ISO Drive shows a notification when an ISO is mounted, with a quick unmount button. This helps you keep track of the mount status."
|
||||
else -> "ISO Droid shows a notification when an ISO is mounted, with a quick unmount button. This helps you keep track of the mount status."
|
||||
},
|
||||
granted = if (wasDenied) false else if (hasPermission) true else null
|
||||
)
|
||||
@@ -323,7 +324,7 @@ private fun CompleteStep(
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Text(
|
||||
text = "ISO Drive is ready to use. Place your ISO or IMG files in the isodrive folder and start mounting.",
|
||||
text = "ISO Droid is ready to use. Place your ISO or IMG files in the isodroid directory and start mounting.",
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
textAlign = TextAlign.Center,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
@@ -332,7 +333,7 @@ private fun CompleteStep(
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Text(
|
||||
text = "Default directory: /sdcard/isodrive/",
|
||||
text = "Default directory: ${Environment.getExternalStorageDirectory().absolutePath}/isodroid/",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
textAlign = TextAlign.Center,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f)
|
||||
@@ -344,7 +345,7 @@ private fun CompleteStep(
|
||||
onClick = onFinish,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text("Start Using ISO Drive")
|
||||
Text("Start Using ISO Droid")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||
|
||||
companion object {
|
||||
private val KEY_ISO_DIRECTORY = stringPreferencesKey("iso_directory")
|
||||
private val DEFAULT_ISO_DIRECTORY = "${Environment.getExternalStorageDirectory().absolutePath}/isodrive"
|
||||
private val DEFAULT_ISO_DIRECTORY = "${Environment.getExternalStorageDirectory().absolutePath}/isodroid"
|
||||
}
|
||||
|
||||
private var initialized = false
|
||||
@@ -206,6 +206,8 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||
// Create directory if it doesn't exist
|
||||
if (!directory.exists()) {
|
||||
RootManager.executeCommand("mkdir -p \"$currentPath\"")
|
||||
// Create marker file to indicate this folder was created by the app
|
||||
RootManager.executeCommand("touch \"$currentPath/.isodroiddir\"")
|
||||
}
|
||||
|
||||
// Try multiple methods to list files
|
||||
|
||||
@@ -229,13 +229,13 @@ fun toCommandArgs(): List<String> {
|
||||
**Example commands:**
|
||||
```bash
|
||||
# Mount as read-only mass storage
|
||||
isodrive "/sdcard/isodrive/ubuntu.iso" -configfs
|
||||
isodrive "/sdcard/isodroid/ubuntu.iso" -configfs
|
||||
|
||||
# Mount as writable drive
|
||||
isodrive "/sdcard/isodrive/drive.img" -rw -configfs
|
||||
isodrive "/sdcard/isodroid/drive.img" -rw -configfs
|
||||
|
||||
# Mount as CD-ROM
|
||||
isodrive "/sdcard/isodrive/windows.iso" -cdrom -configfs
|
||||
isodrive "/sdcard/isodroid/windows.iso" -cdrom -configfs
|
||||
```
|
||||
|
||||
## Event System
|
||||
@@ -338,7 +338,7 @@ private val KEY_ISO_DIRECTORY = stringPreferencesKey("iso_directory")
|
||||
```
|
||||
|
||||
Stores:
|
||||
- Custom ISO directory path (default: `/sdcard/isodrive/`)
|
||||
- Custom ISO directory path (default: `/sdcard/isodroid/`)
|
||||
|
||||
### SharedPreferences
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 117 KiB After Width: | Height: | Size: 124 KiB |
|
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 162 KiB After Width: | Height: | Size: 165 KiB |
|
Before Width: | Height: | Size: 94 KiB After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 109 KiB |
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
|
||||
@@ -16,7 +16,7 @@ Requirements:
|
||||
* Android 8.0+ (API 26)
|
||||
|
||||
Usage:
|
||||
1. Place your ISO/IMG files in /sdcard/isodrive/
|
||||
1. Place your ISO/IMG files in /sdcard/isodroid/
|
||||
2. Select an ISO/IMG file from the list
|
||||
3. Choose mount options (Mass Storage or CD-ROM)
|
||||
4. Tap Mount
|
||||
|
||||
|
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 94 KiB After Width: | Height: | Size: 109 KiB |
|
Before Width: | Height: | Size: 117 KiB After Width: | Height: | Size: 165 KiB |
|
Before Width: | Height: | Size: 162 KiB After Width: | Height: | Size: 124 KiB |