Skip to content

Add Compose Desktop GUI scaffold and README docs - #2193

Open
soloturn wants to merge 6 commits into
mainfrom
compose-gui-scaffold
Open

Add Compose Desktop GUI scaffold and README docs#2193
soloturn wants to merge 6 commits into
mainfrom
compose-gui-scaffold

Conversation

@soloturn

Copy link
Copy Markdown
Collaborator

Summary

  • Adds a Compose Multiplatform desktop GUI scaffold alongside the existing Swing MainWindow, with full feature parity (ripping, config, history, log, queue, system tray) — see Future/Roadmap: New GUI - compose-jb #2082.
  • Selectable at launch via -Dripme.gui=compose (Swing stays the default; -Dripme.gui=swing is equivalent to today's behavior).
  • Documents in the README how to launch the Compose GUI both from a downloaded release jar and from a local source build.

Test plan

  • ./gradlew compileKotlin compileJava — compiles clean (rebased onto latest main).
  • Manual: ./gradlew assemble && java -Dripme.gui=compose -jar build/libs/ripme-<version>.jar launches the Compose GUI; default launch (no property) still opens Swing.

🤖 Generated with Claude Code

soloturn and others added 4 commits August 15, 2026 13:53
Adds a Kotlin + Compose Multiplatform Desktop toolchain alongside the
existing Java/Swing app, with a single minimal screen (URL input,
Rip/Stop/Panic, status line, progress bar, scrolling log) wired to the
existing AbstractRipper/RipStatusHandler business logic rather than a
reimplementation.

- build.gradle.kts: Kotlin 2.4.10 + Compose 1.11.1 plugins/deps, google()
  repo, Kotlin targets JVM 21, shadowJar opts into zip64 (Compose's
  bundled natives push the fat jar past the 65535-entry zip limit).
- gradle.properties: relaxes JVM-target validation from error to the
  benign warning it's meant to be (Java release 25 vs Kotlin JVM 21).
- App.java: new -Dripme.gui=compose system property selects the Compose
  window; default (no property) remains Swing's MainWindow, unchanged.
- src/main/kotlin/.../ui/compose/{ComposeMain,RipController,MainScreen}.kt:
  the new screen. RipController implements RipStatusHandler and mirrors
  MainWindow's handleEvent/ripAlbum logic (URL normalize -> getRipper ->
  setup -> setObserver -> Thread(ripper).start(); stop/panic; status-color
  mapping; log capped at 2000 lines), marshaling ripper callbacks onto the
  EDT via SwingUtilities.invokeLater since Compose Desktop composes there.

Deliberately deferred (scaffold scope only): decompose navigation,
queue/history/config panels, tray icon, i18n, native installers/jpackage,
a --compose CLI flag, and Compose UI tests.

Verified: ./gradlew clean build green (full Java test suite passes),
shadowJar produces a launchable fat jar, both launch paths (default
Swing, -Dripme.gui=compose) smoke-tested from the shaded jar.
Brings the Compose Desktop GUI (scaffold from the previous commit) up to
feature parity with the Swing MainWindow, so it's a real alternative UI
rather than a single-screen demo. Launch/default behavior unchanged
(-Dripme.gui=compose, defaulting to swing).

Queue (ui/compose/queue/): QueueController owns RipController rather than
duplicating its start/stop/panic/update logic. Ports MainWindow's
RipButtonHandler/ripNextAlbum/canRip 1:1: multi-URL queueing,
{start-end} range expansion, duplicate rejection, stop/panic re-enqueue,
persistence to config, load-on-startup.

History (ui/compose/history/): HistoryStore is a thin observable bridge
over the existing Java History/HistoryEntry classes (shared file format
with the CLI, not forked) - load/save/remove/clear(confirm)/re-rip,
written on every RIP_COMPLETE via QueueController.

Configuration (ui/compose/config/): every MainWindow setting, read/written
through the same Utils.getConfig*/setConfig* statics - no parallel store.
Save-dir/URL-list file pickers and a cookie dialog (CookieDialog.kt).

Tray (ui/compose/tray/): Compose Desktop's Tray API - hide/show, About,
clipboard-autorip checkbox (shared state with the config panel), Exit.

Navigation: a plain enum Panel + NavController (mutableStateOf-based),
not a navigation library. Evaluated and rejected decompose/Navigation-3:
ripme's actual need is 5 mutually-exclusive panel states with no back
stack and no process-death restoration, desktop-only - that machinery
solves problems this app doesn't have. Confirmed decompose 3.5.0 does
resolve/compile/run cleanly against Compose Multiplatform 1.11.1 if ever
needed for real multiplatform navigation later.

i18n: Strings.kt's tr(key) + a manual localeVersion recomposition trigger,
since Utils.getLocalizedString reads a static, non-observable resource
bundle - the declarative equivalent of MainWindow's changeLocale().

Shared Java changes (both backward-compatible, Swing behavior unchanged):
- ClipboardUtils: MainWindow.ripAlbumStatic call replaced with a settable
  Consumer<String> dispatch handler so Compose's QueueController can
  receive clipboard-autorip'd URLs instead.
- RipStatusComplete: added getCount() (was package-private, needed by
  HistoryStore in a different package).

Also:
- App.java: logs which GUI launched, and warns (rather than silently
  falling back) on an unrecognized -Dripme.gui value.
- build.gradle.kts: compose.runtime/foundation/material3/ui DSL aliases
  (deprecated by the Compose Gradle plugin) replaced with direct artifact
  coordinates; material3 pins independently at 1.9.0 (verified via
  `gradlew dependencies`, doesn't track the 1.11.1 Compose release train).
- ConfigScreen.kt: deprecated ClickableText replaced with Text +
  Modifier.clickable.
- TrayIntegration.kt: deprecated painterResource(String) replaced with
  ImageIO.read + BitmapPainter, matching how MainWindow.setupTrayIcon
  already loads the same icon.png off the classpath.

Known gaps vs strict MainWindow parity: no live "<host> album detected"
preview while typing, no history table column sorting, tray icon visual
rendering unconfirmed (no real display in the build/test environment).

Verified: ./gradlew build -x test green, shadowJar launches cleanly under
both -Dripme.gui=compose and the default Swing path. Per explicit
instruction, the live-network-dependent ripper unit test suite was not
run as part of this work.
…ource

The Compose Desktop GUI (com.rarchives.ripme.ui.compose) is opt-in via the
-Dripme.gui=compose system property; Swing remains the default. Add a README
section covering both invocation from a downloaded release jar and from a
locally built one.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018v7wmgDn3VKQRo8hbFCR6S
ConfigScreen laid out every setting as its own full-width row, making
the panel run roughly 2x taller than MainWindow's equivalent
GridBagLayout grid. Rebuilt it as a two-column grid (SettingsRow) that
mirrors MainWindow's addItemToConfigGridBagConstraints(...) calls
row-for-row - checkboxes pack two to a line, buttons get compact
content padding instead of Material3's default.

Also gives the Log/History/Queue/Configuration nav buttons the same
icons (comment.png/time.png/list.png/gear.png) MainWindow's JButtons
use, loaded off the classpath via the same ImageIO -> ComposeImageBitmap
path tray/TrayIntegration.kt already uses for the tray icon (new
ResourceIcons.kt).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@soloturn
soloturn force-pushed the compose-gui-scaffold branch from f1b679b to 7041122 Compare August 15, 2026 11:57
soloturn and others added 2 commits August 15, 2026 14:39
The two-column SettingsRow grid from the previous commit nested a
row's label and its OutlinedTextField together inside the left
column only, leaving the right column empty - squeezing both into
half the row's width instead of giving each its own column like
MainWindow's GridBagLayout does (col0 = label, col1 = field).
Shrinking field width made this worse, not better.

Split every field row into left = label, right = field, and swap
Material3's OutlinedTextField (~56.dp tall by default, floating-label
chrome) for a compact bordered BasicTextField sized like a Swing
JTextField.

Verified live: launched the built jar (-Dripme.gui=compose) and
visually confirmed the settings panel end to end.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Rip and Stop reuse icon.png/stop.png, the same assets MainWindow's
ripButton/stopButton use. MainWindow's panicButton has no icon of its
own; Panic does an immediate hard-abort (ripper.stop() + panic(), no
graceful finish-current-item like Stop) so it gets a warning-triangle
vector icon from material-icons-core (new dependency) rather than an
unrelated resource PNG - a first attempt reused the otherwise-unused
wrench.png, but a wrench reads as "settings/tool", not "abort".

Verified live against the built jar (-Dripme.gui=compose).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@soloturn
soloturn requested review from metaprime and a lite review from Copilot August 15, 2026 17:02

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Introduces an opt-in JetBrains Compose Multiplatform Desktop GUI scaffold that runs alongside the existing Swing MainWindow, selectable via -Dripme.gui=compose, and documents how to launch it from both release jars and local builds.

Changes:

  • Adds a new Compose Desktop UI implementation (main screen, navigation, log/history/queue/config panels, tray integration, AWT interop).
  • Adds controller/state bridges to reuse existing ripping, history, config, and clipboard autorip logic.
  • Updates build + docs to include Compose dependencies and describe launching the Compose GUI.

Reviewed changes

Copilot reviewed 25 out of 26 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
src/main/kotlin/com/rarchives/ripme/ui/compose/tray/TrayIntegration.kt Compose Desktop tray menu + About dialog integration.
src/main/kotlin/com/rarchives/ripme/ui/compose/Strings.kt Compose-localized string helper with manual recomposition trigger.
src/main/kotlin/com/rarchives/ripme/ui/compose/RipController.kt Compose-facing rip lifecycle/state adapter over existing ripper logic.
src/main/kotlin/com/rarchives/ripme/ui/compose/ResourceIcons.kt Loads classpath PNG icons for Compose UI.
src/main/kotlin/com/rarchives/ripme/ui/compose/queue/QueueScreen.kt Compose queue panel UI (list + remove actions).
src/main/kotlin/com/rarchives/ripme/ui/compose/queue/QueueController.kt Queue draining/persistence + clipboard handler wiring.
src/main/kotlin/com/rarchives/ripme/ui/compose/queue/QueueComponent.kt Minimal component wrapper for queue screen.
src/main/kotlin/com/rarchives/ripme/ui/compose/nav/Panel.kt Panel enum + simple nav controller for panel toggling.
src/main/kotlin/com/rarchives/ripme/ui/compose/MainScreen.kt Compose main screen scaffold (URL entry, actions, panels).
src/main/kotlin/com/rarchives/ripme/ui/compose/log/LogScreen.kt Compose log panel UI.
src/main/kotlin/com/rarchives/ripme/ui/compose/log/LogComponent.kt Minimal component wrapper for log screen.
src/main/kotlin/com/rarchives/ripme/ui/compose/history/HistoryStore.kt Compose-observable wrapper over shared Java History persistence.
src/main/kotlin/com/rarchives/ripme/ui/compose/history/HistoryScreen.kt Compose history UI (table-like list + actions).
src/main/kotlin/com/rarchives/ripme/ui/compose/history/HistoryComponent.kt Minimal component wrapper for history screen.
src/main/kotlin/com/rarchives/ripme/ui/compose/config/CookieDialog.kt Compose cookie settings dialog (SiteCookieStorage-backed).
src/main/kotlin/com/rarchives/ripme/ui/compose/config/ConfigScreen.kt Compose configuration panel UI.
src/main/kotlin/com/rarchives/ripme/ui/compose/config/ConfigController.kt Compose config state + persistence and update-check wiring.
src/main/kotlin/com/rarchives/ripme/ui/compose/config/ConfigComponent.kt Minimal component wrapper for config screen.
src/main/kotlin/com/rarchives/ripme/ui/compose/ComposeMain.kt Compose GUI entry point + lifecycle wiring + window persistence.
src/main/kotlin/com/rarchives/ripme/ui/compose/AwtInterop.kt Swing/AWT interop helpers for file pickers and Desktop actions.
src/main/java/com/rarchives/ripme/ui/RipStatusComplete.java Exposes count via getter for non-Swing consumers.
src/main/java/com/rarchives/ripme/ui/ClipboardUtils.java Makes ClipboardUtils public + adds configurable autorip dispatch hook.
src/main/java/com/rarchives/ripme/App.java Adds ripme.gui property switch to launch Swing vs Compose GUI.
README.md Documents how to launch the new Compose GUI (release jar + source).
gradle.properties Adjusts Kotlin JVM target validation mode for mixed targets.
build.gradle.kts Adds Kotlin + Compose plugins/dependencies and enables Zip64 for shadow jar.
Suppressed comments (1)

src/main/kotlin/com/rarchives/ripme/ui/compose/AwtInterop.kt:42

  • JFileChooser is Swing UI and should be created/shown on the EDT. Showing it from a background thread is not Swing-thread-safe; marshal the dialog display via SwingUtilities.invokeAndWait and then invoke the callback on the EDT.
    fun chooseFile(startDir: String, onChosen: (File) -> Unit) {
        thread(name = "AwtInterop-chooseFile") {
            UIManager.put("FileChooser.useSystemExtensionHiding", false)
            val jfc = JFileChooser(startDir)
            jfc.fileSelectionMode = JFileChooser.FILES_ONLY

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +22 to +35
fun chooseDirectory(startDir: String, onChosen: (File) -> Unit) {
thread(name = "AwtInterop-chooseDirectory") {
UIManager.put("FileChooser.useSystemExtensionHiding", false)
val jfc = JFileChooser(startDir)
jfc.fileSelectionMode = JFileChooser.DIRECTORIES_ONLY
val result = jfc.showDialog(null, "select directory")
if (result == JFileChooser.APPROVE_OPTION) {
val chosen = jfc.selectedFile
if (chosen != null) {
onChosen(chosen)
}
}
}
}
Comment on lines +55 to +58
val painter = remember {
val stream = Thread.currentThread().contextClassLoader.getResourceAsStream("icon.png")
BitmapPainter(ImageIO.read(stream).toComposeImageBitmap())
}
}
Text("Do you want to visit the project homepage on GitHub?")
Row(horizontalArrangement = Arrangement.End, modifier = Modifier.fillMaxWidth()) {
Button(onClick = { AwtInterop.browse("http://github.com/ripmeapp/ripme") }) { Text("Yes") }
Comment on lines +28 to +32
static Consumer<String> ripHandler = MainWindow::ripAlbumStatic;

public static void setRipHandler(Consumer<String> handler) {
ripHandler = handler;
}
rippedURLs.add(url);
// TODO Queue rip instead of just starting it
MainWindow.ripAlbumStatic(url);
ClipboardUtils.ripHandler.accept(url);
progress = 0f
setStatus("Starting rip...", Color.Black)

Thread(newRipper).start()
Comment thread README.md
Comment on lines +41 to +42
./gradlew assemble
java -Dripme.gui=compose -jar build/libs/ripme-<version>.jar
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants