Skip to content
Draft
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,20 @@ android {
}
}

flavorDimensions += "distribution"
productFlavors {
create("full") {
dimension = "distribution"
isDefault = true
}
create("foss") {
dimension = "distribution"
// Steam integration relies on the JavaSteam library, which is only available as a
// prebuilt binary (not on Maven Central). The FOSS flavor -- used for the F-Droid
// build -- ships without it; see app/src/foss for no-op Steam stubs.
}
}

compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
Expand Down Expand Up @@ -245,8 +259,9 @@ dependencies {
implementation(libs.xz)
implementation(libs.zstd.jni) { artifact { type = "aar" } }

// Steam (JavaSteam)
implementation(libs.bundles.steam)
// Steam (JavaSteam) -- prebuilt, non-Maven-Central; limited to the "full" flavor.
// The "foss" flavor builds without it (app/src/foss stubs) for F-Droid.
"fullImplementation"(libs.bundles.steam)

// argosy-sigil — title id / serial extraction (replaces in-tree :libchdr +
// Iso9660Utils + AesXts + ZArchiveReader + GameCubeHeaderParser.parseRomHeader)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.nendo.argosy.data.steam

import com.nendo.argosy.data.local.entity.SteamAccountEntity
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import javax.inject.Inject
import javax.inject.Singleton

/**
* FOSS-flavor no-op stub. The `full` flavor implements Steam QR authentication via the
* JavaSteam library; the FOSS build ships without Steam, so this exposes the same public
* surface consumed by shared code while doing nothing.
*/
@Singleton
class SteamAuthManager @Inject constructor() {
val qrAuthState: StateFlow<QrAuthState> = MutableStateFlow(QrAuthState.Idle)
val authEvents: SharedFlow<SteamAuthEvent> = MutableSharedFlow()
val isLoggedIn: StateFlow<Boolean> = MutableStateFlow(false)

@Volatile
var sessionDead: Boolean = false

var connectingForAuth: Boolean = false

suspend fun getActiveAccount(): SteamAccountEntity? = null

suspend fun deleteAccount(accountId: Long) {}

fun startQrAuth() {}

fun cancelQrAuth() {}

fun logout() {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.nendo.argosy.data.steam

import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import javax.inject.Inject
import javax.inject.Singleton

/**
* FOSS-flavor no-op stub. The `full` flavor implements Steam library discovery and depot
* downloads via the JavaSteam library. The FOSS build ships without Steam, so downloads
* never start and queues stay empty; this exposes the same public surface consumed by
* shared code (download/queue state flows and control methods) as no-ops.
*/
@Singleton
class SteamContentManager @Inject constructor() {
val downloadState: StateFlow<SteamDownloadState> = MutableStateFlow(SteamDownloadState.Idle)
val activeDownload: StateFlow<SteamDownloadProgress?> = MutableStateFlow(null)
val downloadQueue: StateFlow<List<QueuedSteamDownload>> = MutableStateFlow(emptyList())
val completedDownloads: StateFlow<List<SteamDownloadProgress>> = MutableStateFlow(emptyList())

suspend fun hasPendingDownloads(): Boolean = false

suspend fun discoverLocalSteamGames(): Int = 0

fun queueDownloadOptimistic(appId: Long, gameName: String, coverPath: String?) {}

fun pauseDownload() {}

fun cancelDownload() {}

fun cancelQueuedDownload(appId: Long) {}

fun clearCompletedDownloads() {}

fun hasActiveSteamDownload(): Boolean = false

fun onDownloadSlotFreed() {}

fun cleanup() {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.nendo.argosy.data.steam

import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import javax.inject.Inject
import javax.inject.Singleton

/**
* FOSS-flavor no-op stub. The `full` flavor syncs the user's Steam library via the JavaSteam
* library. The FOSS build ships without Steam, so sync is inert; this exposes the same public
* surface consumed by shared code (sync state flow and control methods) as no-ops.
*/
@Singleton
class SteamLibraryManager @Inject constructor() {
val syncState: StateFlow<LibrarySyncState> = MutableStateFlow(LibrarySyncState.Idle)

fun forceSync() {}

fun forceSyncWithOverwrite() {}

suspend fun resetLibrary(): Int = 0

fun requestLibrarySync() {}
Comment thread
gantoine marked this conversation as resolved.
Outdated

fun cleanup() {}
}
94 changes: 94 additions & 0 deletions app/src/foss/kotlin/com/nendo/argosy/data/steam/SteamModels.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package com.nendo.argosy.data.steam

/**
* FOSS-flavor shared Steam value types.
*
* In the `full` flavor these types are declared inside the JavaSteam-backed manager
* implementations (see app/src/full/.../data/steam/). The `foss` flavor ships no Steam
* integration, but shared code in src/main still references and pattern-matches on these
* types, so identical definitions are provided here. Keep them in sync with the `full`
* declarations. `KeyValue` (a JavaSteam type) is replaced with `Any?` since no shared code
* reads that field.
*/

enum class SteamConnectionState {
DISCONNECTED,
CONNECTING,
CONNECTED,
LOGGING_IN,
LOGGED_IN,
LOGGED_OUT
}

data class SteamServiceState(
val connectionState: SteamConnectionState = SteamConnectionState.DISCONNECTED,
val username: String? = null,
val steamId: Long? = null,
val error: String? = null
)

sealed class QrAuthState {
data object Idle : QrAuthState()
data object Starting : QrAuthState()
data class WaitingForScan(val challengeUrl: String) : QrAuthState()
data object Polling : QrAuthState()
data class Success(val username: String, val steamId: Long) : QrAuthState()
data class Error(val message: String) : QrAuthState()
}

sealed class SteamAuthEvent {
data class LoggedIn(val steamId: Long, val username: String) : SteamAuthEvent()
data object LoggedOut : SteamAuthEvent()
data class LoginFailed(val reason: String) : SteamAuthEvent()
}

sealed class SteamDownloadState {
data object Idle : SteamDownloadState()
data class Preparing(val appId: Long, val gameName: String) : SteamDownloadState()
data class Connecting(val appId: Long, val gameName: String) : SteamDownloadState()
data class FetchingManifest(val appId: Long, val gameName: String, val depotId: Int) : SteamDownloadState()
data class Validating(val appId: Long, val gameName: String, val statusDetail: String = "") : SteamDownloadState()
data class Downloading(
val appId: Long,
val gameName: String,
val progress: Float,
val currentDepot: Int,
val totalDepots: Int
) : SteamDownloadState()
data class Moving(val appId: Long, val gameName: String) : SteamDownloadState()
data class Completed(val appId: Long, val gameName: String, val installPath: String) : SteamDownloadState()
data class Failed(val appId: Long, val gameName: String, val error: String) : SteamDownloadState()
data class Paused(val appId: Long, val gameName: String, val progress: Float, val needsVerification: Boolean = false) : SteamDownloadState()
data class Cleaning(val appId: Long, val gameName: String) : SteamDownloadState()
}

data class SteamDownloadProgress(
val appId: Long,
val gameName: String,
val coverPath: String?,
val progress: Float,
val totalBytes: Long,
val bytesDownloaded: Long,
val state: SteamDownloadState,
val bytesPerSecond: Long = 0L
) {
val progressPercent: Int get() = (progress * 100).toInt()
}

data class QueuedSteamDownload(
val appId: Long,
val gameName: String,
val coverPath: String?,
val appInfo: Any? = null,
val targetInstallPath: String? = null
)

sealed class LibrarySyncState {
data object Idle : LibrarySyncState()
data object SyncingLicenses : LibrarySyncState()
data class FetchingPackages(val current: Int, val total: Int) : LibrarySyncState()
data class FetchingApps(val current: Int, val total: Int) : LibrarySyncState()
data class FetchingProtonDbRatings(val current: Int, val total: Int) : LibrarySyncState()
data class Complete(val gamesAdded: Int, val gamesUpdated: Int) : LibrarySyncState()
data class Error(val message: String) : LibrarySyncState()
}
40 changes: 40 additions & 0 deletions app/src/foss/kotlin/com/nendo/argosy/data/steam/SteamService.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.nendo.argosy.data.steam

import android.app.Service
import android.content.Intent
import android.os.Binder
import android.os.IBinder
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow

/**
* FOSS-flavor no-op stub. The `full` flavor runs a foreground Service that maintains the
* Steam client connection via the JavaSteam library. The FOSS build ships without Steam, so
* this Service does nothing: it never connects and stops itself immediately if started. It
* keeps the same public surface (state flow, disconnect, LocalBinder, extras) so shared code
* and the manifest entry continue to compile and bind.
*/
class SteamService : Service() {
val state: StateFlow<SteamServiceState> = MutableStateFlow(SteamServiceState())

private val binder = LocalBinder()

inner class LocalBinder : Binder() {
fun getService(): SteamService = this@SteamService
}

override fun onBind(intent: Intent?): IBinder = binder

override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
stopSelf()
return START_NOT_STICKY
}

fun disconnect() {}

companion object {
const val EXTRA_AUTO_CONNECT = "auto_connect"
const val EXTRA_FORCE_CONNECT = "force_connect"
const val EXTRA_CONNECT_FOR_AUTH = "connect_for_auth"
}
}
52 changes: 52 additions & 0 deletions fastlane/metadata/android/en-US/full_description.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
Argosy Launcher is a gamepad-first Android home screen for retro gaming handhelds. Sync your entire game library from your self-hosted RomM server, download games and BIOS files on demand, track your achievements, and play across devices with automatic save sync — all from a controller-native interface designed for Anbernic, Retroid Pocket, Odin, AYN, and similar devices.

<b>Native RomM client</b>
First-class integration with RomM. Sync your whole library with rich IGDB metadata: cover art, descriptions, genres, franchises, player counts, and more. Your collection, your server, your handheld.

<b>Automatic downloads</b>
Download ROMs and BIOS files directly from your RomM server. Games are sorted by platform automatically. Queue multiple downloads and let Argosy handle the rest.

<b>Save sync</b>
Continue playing across devices. Bidirectional save sync keeps your progress up to date, with conflict detection so you never lose data.

<b>RetroAchievements</b>
View earned achievements and track your progress, synced from your RomM server.

<b>Collections</b>
Organize your library your way with custom collections or smart ones like Top Unplayed, Recently Added, and Most Played. Pin favorites to the home screen for quick access.

<b>Gamepad-first design</b>
Built for controllers from the ground up. Navigate the whole library, manage downloads, and launch games without touching the screen. Full D-pad, analog stick, and button support.

<b>Smart emulator management</b>
<ul>
<li>Auto-detects installed emulators and assigns them per platform</li>
<li>Per-platform RetroArch core selection</li>
<li>Automatic multi-disc handling with a disc picker</li>
<li>Works with RetroArch, PPSSPP, DuckStation, AetherSX2, Dolphin, DraStic, melonDS, Mupen64Plus FZ, Pizza Boy, Lime3DS, Azahar, Flycast, Redream, and more</li>
</ul>

<b>Make it yours</b>
<ul>
<li>Video wallpaper: YouTube previews as dynamic backgrounds</li>
<li>Ambient audio: background music while you browse</li>
<li>Themes: light, dark, or system-matched</li>
<li>Background blur, saturation, and opacity controls</li>
<li>Box art styles, grid density, and navigation sound effects</li>
</ul>

<b>Quick Menu (L3)</b> — press the left stick anywhere to search, pick a random game, or jump to Most Played, Top Unplayed, Recent, and Favorites.

<b>Quick Settings (R3)</b> — press the right stick anywhere to switch theme, toggle haptics, UI sounds, and background music. On supported Odin, AYN, and Retroid devices it also exposes Performance Mode and Fan Control.

<b>More</b>
<ul>
<li>Offline mode with full access to downloaded games and cached metadata</li>
<li>Local image caching for fast, offline browsing</li>
<li>Library filtering by genre, player count, franchise, or region</li>
<li>User ratings synced back to RomM</li>
<li>Index and launch Steam games installed via GameHub or GameNative</li>
Comment thread
gantoine marked this conversation as resolved.
Outdated
<li>App launcher, in-app updates, first-run setup wizard, favorites, and hide games</li>
</ul>

Requires Android 8.0 (Oreo) or higher. A RomM server is optional and only needed for sync features. Argosy runs on any Android device, but the gamepad-first interface is optimized for handheld gaming and Android TV.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added fastlane/metadata/android/en-US/images/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions fastlane/metadata/android/en-US/short_description.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Gamepad-first retro-gaming launcher with native RomM library sync
1 change: 1 addition & 0 deletions fastlane/metadata/android/en-US/title.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Argosy Launcher
56 changes: 56 additions & 0 deletions fdroid/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# F-Droid submission — handoff notes

This directory holds the **F-Droid build recipe** for Argosy Launcher and the notes
needed to submit it. Nothing here ships in the app; it is packaging material.

- `com.nendo.argosy.yml` — the build-metadata recipe. It belongs in the
[`fdroiddata`](https://gitlab.com/fdroid/fdroiddata) repo at
`metadata/com.nendo.argosy.yml`, **not** in this app repo. It lives here only as a
reviewed, ready-to-copy artifact.

The media metadata (descriptions, icon, feature graphic, screenshots) already lives in
this repo under `fastlane/metadata/android/en-US/` and F-Droid pulls it automatically.

## App facts the recipe encodes

| Field | Value | Source |
|-------|-------|--------|
| Application ID | `com.nendo.argosy` | `app/build.gradle.kts` |
| versionName / base versionCode | `1.18.0` / `298` | `app/build.gradle.kts` |
| License | `GPL-3.0-only` (confirmed) | `LICENSE` (verbatim GPLv3, no "or later") |
| minSdk / targetSdk | 26 / 35 | `app/build.gradle.kts` |
| Release tag | `v1.18.0` | `git tag` |
| Submodules | `sigil`, `libretrodroid/.../rcheevos` | `.gitmodules` |

## Submission steps (fdroiddata lives on GitLab: gitlab.com/fdroid/fdroiddata)

Per fdroiddata's `CONTRIBUTING.md`:

1. Register on GitLab, **fork** `gitlab.com/fdroid/fdroiddata`, clone your fork:
```shell
git clone https://gitlab.com/YOUR_USERNAME/fdroiddata.git
cd fdroiddata
```
2. Create a branch **named after the app id** — do NOT work on `master` (it is protected
and you cannot open an MR from it):
```shell
git checkout -b com.nendo.argosy
```
3. Add the recipe as `metadata/com.nendo.argosy.yml` (copy this repo's
`fdroid/com.nendo.argosy.yml`). Optionally scaffold instead with
`fdroid import --url https://github.com/rommapp/argosy-launcher --subdir app`.
4. With `fdroidserver` installed (`pip install git+https://gitlab.com/fdroid/fdroiddata.git`),
run the local checks:
```shell
fdroid readmeta # syntax
fdroid rewritemeta com.nendo.argosy # canonical formatting
fdroid checkupdates com.nendo.argosy # fills Auto Name / Current Version
fdroid lint com.nendo.argosy # fix all warnings
fdroid build -v -l com.nendo.argosy # must produce at least one working build
```
5. Commit with a clear message (the "New App: com.nendo.argosy" convention is common),
push the branch to your fork, and check **CI/CD → Pipelines** passes; fix metadata until
it does.
6. Open a **merge request** against fdroiddata from your `com.nendo.argosy` branch and
**fill in the MR template**. Squash is on by default. Reply promptly to packager
questions.
Loading
Loading