Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
9d514cf
design(workshop): draft native browser UX mockup
PyXiion Oct 5, 2026
e0b073d
design(workshop): compare discovery navigation for mods and collections
PyXiion Oct 5, 2026
1214d05
feat(workshop-backend): add catalog API on TypeScript Worker
PyXiion Oct 5, 2026
4af5e52
fix: mod list hang when toggling thousands of mods
PyXiion Oct 5, 2026
f77b031
refactor: drop unused highlightedUuids list, null-safe highlight binding
PyXiion Oct 5, 2026
24bdb24
fix: sidebar filter lagged one change behind after toggling mods
PyXiion Oct 5, 2026
98195fd
fix: toasts rendered with a transparent background
PyXiion Oct 5, 2026
37a01f2
feat(workshop): add catalog client and WorkshopCatalog plugin
PyXiion Oct 5, 2026
28304d6
feat(workshop): add native Workshop browser UI and wiring
PyXiion Oct 5, 2026
cb065be
fix(workshop): show download progress bar only while downloading
PyXiion Oct 5, 2026
820ae41
style(workshop): match app tab, badge and button styling
PyXiion Oct 5, 2026
9f7a099
feat(workshop-backend): retry stalled Steam calls, log failures, add …
PyXiion Oct 5, 2026
703acac
feat(workshop): show collection thumbnails and compact busy indicator
PyXiion Oct 5, 2026
3d9b847
feat(workshop): redesign item detail page, drop game version label
PyXiion Oct 5, 2026
d972397
fix(workshop): hide catalog filters on Discover
PyXiion Oct 5, 2026
fffd2e3
feat(workshop-backend): local full-text search index fed by passive s…
PyXiion Oct 5, 2026
a9afbac
feat(workshop): queue downloads in background, serve cached pages fir…
PyXiion Oct 5, 2026
c2f2a22
merge: workshop catalog backend
PyXiion Oct 5, 2026
9a40c87
feat(workshop-backend): answer browse reads from stored data and refr…
PyXiion Oct 5, 2026
cb78b47
feat(workshop-backend): keep query pages in D1 and serve browsing fro…
PyXiion Oct 5, 2026
4339299
feat(workshop): persist catalog responses on disk and filter Installe…
PyXiion Oct 5, 2026
f3e7c90
refactor(workshop): split the view into per-tab loaders, InstalledLis…
PyXiion Oct 5, 2026
d31123e
perf(workshop): request Steam images at display size to cut decoded m…
PyXiion Oct 5, 2026
b1c18cf
refactor(workshop): centralize image sizing and fix hidden preview lo…
PyXiion Oct 5, 2026
86ea456
fix(workshop): sort detail versions and restore collection collages
PyXiion Oct 5, 2026
03cd43f
fix(workshop): disconnect view events before Qt destruction
PyXiion Oct 5, 2026
99d7ed4
feat(workshop): polish UX, metadata presentation, and build matrix va…
PyXiion Oct 5, 2026
cefbdad
feat(workshop): activate downloaded collections and colour-code buttons
PyXiion Oct 5, 2026
9a15ca9
fix(packaging): drop eager Steam WebView import from plugin package
PyXiion Oct 5, 2026
233197c
perf(workshop): virtualise the listing grid and free hidden grids
PyXiion Oct 6, 2026
9d52d5b
perf(workshop): cache the Installed list and render descriptions only…
PyXiion Oct 6, 2026
f5c67bc
ci: build a NoWorkshop variant without the downloader alongside Steam…
PyXiion Oct 6, 2026
2ebab06
Merge branch 'main' into design/native-workshop-browser
PyXiion Oct 6, 2026
8c35b52
fix(ui): enforce Basic QQuickStyle across platforms and filter style …
PyXiion Oct 6, 2026
3b2d7b9
fix(tests): wait for organizer rule window activation
PyXiion Oct 6, 2026
9a85a16
perf(workshop): lazily create card control tooltips
PyXiion Oct 6, 2026
22662f9
test(ui): increase async UI pump timeout to 10s for loaded CI runners
PyXiion Oct 6, 2026
0fc709f
fix(workshop): evict unavailable Steam catalog records
PyXiion Oct 6, 2026
19feade
fix(workshop): refresh installed sets and deactivate duplicate copies
PyXiion Oct 6, 2026
f611cfa
fix(workshop): isolate cancellation and handle unavailable details
PyXiion Oct 6, 2026
6898734
merge: main into design/native-workshop-browser
PyXiion Oct 6, 2026
55827cd
fix(build): preserve explicitly synced variant environments
PyXiion Oct 6, 2026
df0d572
fix(workshop): verify omitted index entries without starvation; keep …
PyXiion Oct 6, 2026
5ecec60
feat(ui): pxmodrim:// routes and detail history for Workshop back nav…
PyXiion Oct 6, 2026
0ddb75a
fix(ui): awaitable route dispatch; register ui.navigation layer
PyXiion Oct 6, 2026
5e9ca4d
feat(workshop): compact cards, collection banners and installed rows
PyXiion Oct 6, 2026
b7892ce
fix(workshop): bound index isolation, keep history on failed back, us…
PyXiion Oct 6, 2026
b44e973
feat(workshop): installed tiles with leading active checkbox
PyXiion Oct 6, 2026
21de37f
test(workshop): click the installed active checkbox end to end
PyXiion Oct 6, 2026
ae468f4
feat(workshop): denormalised collection download size from the catalo…
PyXiion Oct 6, 2026
c4d341e
feat(workshop): collection summaries with common versions, 64-bit siz…
PyXiion Oct 6, 2026
3e31679
feat(workshop): distinguish no common version from unstated; log cata…
PyXiion Oct 6, 2026
0015c4e
fix(workshop): collections without a summary report null size and no …
PyXiion Oct 6, 2026
7ee2a6b
fix(workshop): migration 0006 cleans up objects of the earlier collec…
PyXiion Oct 6, 2026
8d40bd1
refactor(ui): route every view switch and settings through modrim:// …
PyXiion Oct 6, 2026
e50399f
fix(ui): schedule settings from its route as a tracked task
PyXiion Oct 6, 2026
a0e50d6
refactor(workshop): tab, item and settings clicks navigate through mo…
PyXiion Oct 6, 2026
1588cb7
feat(ui): log modrim:// navigation and unhandled routes
PyXiion Oct 6, 2026
97dc12c
fix(workshop): refresh unchanged pages in place and accept bare view …
PyXiion Oct 6, 2026
06a1b9b
refactor(plugins): plugin-owned TOML settings and settings sections; …
PyXiion Oct 6, 2026
7fc3c5a
refactor(plugins): store plugin settings as JSON
PyXiion Oct 6, 2026
54d8584
fix(ui): plugin settings as tabs, Wayland popup parents, clean Settin…
PyXiion Oct 6, 2026
2a3e910
fix(workshop): collection download buttons show preparing/queued/down…
PyXiion Oct 6, 2026
976010d
feat(workshop): collection and dependency members use the Installed m…
PyXiion Oct 6, 2026
4a828f2
style(settings): drop card box around setting groups
PyXiion Oct 6, 2026
e025412
feat(workshop): favourite collections with a Favourites tab
PyXiion Oct 6, 2026
1b7e010
fix(settings): unload QML after the click handler that closes the dia…
PyXiion Oct 6, 2026
a2902e4
fix(workshop): collections keep their declared game version; unknown …
PyXiion Oct 6, 2026
023fdc8
feat(workshop): installed state on collection banners; unboxed detail…
PyXiion Oct 6, 2026
9e5522f
fix(catalog): return stored featured ranks for picked collections
PyXiion Oct 6, 2026
f434150
chore(release): prepare v0.3.0
PyXiion Oct 6, 2026
eb64d8a
fix(ci): exclude binary artwork from Codacy SARIF inputs
PyXiion Oct 6, 2026
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
7 changes: 6 additions & 1 deletion .codacy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,9 @@ exclude_paths:
- "pnpm-lock.yaml"
- "rimsort-original/**"
- "tests/**"
- "**/qwebchannel.js"
- "**/qwebchannel.js"
# SARIF formatting reads reported files as UTF-8; binary artwork is not source.
- "**/*.png"
- "**/*.ico"
- "**/*.icns"
- ".codacy/**"
171 changes: 88 additions & 83 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ permissions:
jobs:
build-linux:
runs-on: ubuntu-22.04
strategy:
fail-fast: false
matrix:
workshop: [NoWorkshop, SteamWorkshop, NativeWorkshop]
env:
UV_NO_SYNC: '1'
steps:
- uses: actions/checkout@v7

Expand Down Expand Up @@ -43,10 +49,10 @@ jobs:
chmod +x /usr/local/bin/appimagetool

- name: Install Dependencies
run: uv sync --locked --dev --extra steam
run: uv sync --locked --dev ${{ matrix.workshop == 'NoWorkshop' && '--no-install-package pxsteamdl' || '--extra steam' }}

- name: Build with Nuitka
run: just build-release-bundled
run: just build-release-bundled --workshop ${{ matrix.workshop }}

- name: Verify glibc floor
run: |
Expand All @@ -58,35 +64,51 @@ jobs:
test "$(printf '%s\nGLIBC_2.35\n' "$max" | sort -V | tail -1)" = "GLIBC_2.35"

- name: Package tar.gz
run: tar -czf PxModRim-Linux.tar.gz -C dist entrypoint.dist/
run: tar -czf PxModRim-Linux-${{ matrix.workshop }}.tar.gz -C dist entrypoint.dist/

- name: Rename Linux packages
run: |
set -euo pipefail
mv dist/PxModRim-x86_64.AppImage dist/PxModRim-Linux-x86_64-${{ matrix.workshop }}.AppImage
for f in dist/*.deb; do
[ -f "$f" ] || continue
mv "$f" "${f%.deb}-${{ matrix.workshop }}.deb"
done
for f in dist/*.rpm; do
[ -f "$f" ] || continue
mv "$f" "${f%.rpm}-${{ matrix.workshop }}.rpm"
done

- name: Upload artifact (tar.gz)
uses: actions/upload-artifact@v7
with:
name: PxModRim-Linux
path: PxModRim-Linux.tar.gz
name: PxModRim-Linux-${{ matrix.workshop }}
path: PxModRim-Linux-${{ matrix.workshop }}.tar.gz

- name: Upload artifact (AppImage)
uses: actions/upload-artifact@v7
with:
name: PxModRim-Linux-AppImage
path: dist/PxModRim-x86_64.AppImage
name: PxModRim-Linux-AppImage-${{ matrix.workshop }}
path: dist/PxModRim-Linux-x86_64-${{ matrix.workshop }}.AppImage

- name: Upload artifact (deb)
uses: actions/upload-artifact@v7
with:
name: PxModRim-Linux-Deb
path: dist/*.deb
name: PxModRim-Linux-Deb-${{ matrix.workshop }}
path: dist/*-${{ matrix.workshop }}.deb

- name: Upload artifact (rpm)
uses: actions/upload-artifact@v7
with:
name: PxModRim-Linux-RPM
path: dist/*.rpm

name: PxModRim-Linux-RPM-${{ matrix.workshop }}
path: dist/*-${{ matrix.workshop }}.rpm
build-flatpak:
needs: [build-linux]
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
workshop: [NoWorkshop, SteamWorkshop, NativeWorkshop]
container:
image: ghcr.io/flathub-infra/flatpak-github-actions:freedesktop-24.08
options: --privileged
Expand All @@ -96,25 +118,30 @@ jobs:
- name: Download Linux dist
uses: actions/download-artifact@v8
with:
name: PxModRim-Linux
name: PxModRim-Linux-${{ matrix.workshop }}
path: dist-archive

- name: Extract Linux dist
run: |
mkdir -p dist
tar -xzf dist-archive/PxModRim-Linux.tar.gz -C dist
tar -xzf dist-archive/PxModRim-Linux-${{ matrix.workshop }}.tar.gz -C dist

- name: Build Flatpak bundle
uses: flatpak/flatpak-github-actions/flatpak-builder@v6
with:
manifest-path: packaging/flatpak/com.github.PyXiion.PxModRim.yaml
bundle: com.github.PyXiion.PxModRim.flatpak
bundle: PxModRim-Linux-${{ matrix.workshop }}.flatpak
build-bundle: true
upload-artifact: true
artifact-name: PxModRim-Flatpak

artifact-name: PxModRim-Flatpak-${{ matrix.workshop }}
build-windows:
runs-on: windows-latest
strategy:
fail-fast: false
matrix:
workshop: [NoWorkshop, SteamWorkshop, NativeWorkshop]
env:
UV_NO_SYNC: '1'
steps:
- uses: actions/checkout@v7

Expand All @@ -139,7 +166,7 @@ jobs:
tool: just@1.40.0

- name: Install Dependencies
run: uv sync --locked --dev --extra steam
run: uv sync --locked --dev ${{ matrix.workshop == 'NoWorkshop' && '--no-install-package pxsteamdl' || '--extra steam' }}
- name: Install NSIS
shell: pwsh
run: |
Expand All @@ -154,23 +181,31 @@ jobs:
& $makensis /VERSION

- name: Build with Nuitka
run: just build-release-bundled
run: just build-release-bundled --workshop ${{ matrix.workshop }}

- name: Rename Windows installer
shell: bash
run: mv dist/PxModRim-Setup.exe dist/PxModRim-Windows-${{ matrix.workshop }}-Setup.exe

- name: Upload artifact (dist)
uses: actions/upload-artifact@v7
with:
name: PxModRim-Windows
name: PxModRim-Windows-${{ matrix.workshop }}
path: dist/entrypoint.dist/*

- name: Upload artifact (installer)
uses: actions/upload-artifact@v7
with:
name: PxModRim-Windows-Installer
path: dist/PxModRim-Setup.exe

name: PxModRim-Windows-Installer-${{ matrix.workshop }}
path: dist/PxModRim-Windows-${{ matrix.workshop }}-Setup.exe
build-macos:
runs-on: macos-latest
strategy:
fail-fast: false
matrix:
workshop: [NoWorkshop, SteamWorkshop, NativeWorkshop]
env:
UV_NO_SYNC: '1'
MACOS_SIGNING_CONFIGURED: >-
${{ secrets.MACOS_CERT != '' &&
secrets.MACOS_CERT_PASSWORD != '' &&
Expand Down Expand Up @@ -202,10 +237,10 @@ jobs:
tool: just@1.40.0

- name: Install Dependencies
run: uv sync --locked --dev --extra steam
run: uv sync --locked --dev ${{ matrix.workshop == 'NoWorkshop' && '--no-install-package pxsteamdl' || '--extra steam' }}

- name: Build with Nuitka
run: just build-release-bundled
run: just build-release-bundled --workshop ${{ matrix.workshop }}

- name: Sign and notarize macOS app
if: env.MACOS_SIGNING_CONFIGURED == 'true'
Expand Down Expand Up @@ -258,82 +293,52 @@ jobs:
security delete-keychain "$keychain_path"
rm -f "$certificate" "$notary_zip"

- name: Rename macOS packages
run: |
set -euo pipefail
mv dist/PxModRim-macOS.zip dist/PxModRim-macOS-${{ matrix.workshop }}.zip
mv dist/PxModRim-macOS.dmg dist/PxModRim-macOS-${{ matrix.workshop }}.dmg

- name: Upload artifact (dmg)
uses: actions/upload-artifact@v7
with:
name: PxModRim-macOS-DMG
path: dist/PxModRim-macOS.dmg
name: PxModRim-macOS-DMG-${{ matrix.workshop }}
path: dist/PxModRim-macOS-${{ matrix.workshop }}.dmg

- name: Upload artifact (zip)
uses: actions/upload-artifact@v7
with:
name: PxModRim-macOS-ZIP
path: dist/PxModRim-macOS.zip

name: PxModRim-macOS-ZIP-${{ matrix.workshop }}
path: dist/PxModRim-macOS-${{ matrix.workshop }}.zip
release:
needs: [build-linux, build-flatpak, build-windows, build-macos]
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/')
steps:
- uses: actions/download-artifact@v8
with:
name: PxModRim-Linux
path: linux-targz

- uses: actions/download-artifact@v8
with:
name: PxModRim-Linux-AppImage
path: linux-appimage

- uses: actions/download-artifact@v8
with:
name: PxModRim-Linux-Deb
path: linux-deb

- uses: actions/download-artifact@v8
with:
name: PxModRim-Linux-RPM
path: linux-rpm

- uses: actions/download-artifact@v8
with:
name: PxModRim-Flatpak
path: flatpak

- uses: actions/download-artifact@v8
with:
name: PxModRim-Windows
path: windows

- uses: actions/download-artifact@v8
with:
name: PxModRim-Windows-Installer
path: windows-installer

- uses: actions/download-artifact@v8
with:
name: PxModRim-macOS-DMG
path: macos-dmg

- uses: actions/download-artifact@v8
- name: Download all artifacts
uses: actions/download-artifact@v8
with:
name: PxModRim-macOS-ZIP
path: macos-zip
pattern: PxModRim-*
path: artifacts
merge-multiple: false

- name: Package Windows release
run: 7z a PxModRim-Windows.zip ./windows/*
run: |
for variant in NoWorkshop SteamWorkshop NativeWorkshop; do
7z a "PxModRim-Windows-${variant}.zip" "./artifacts/PxModRim-Windows-${variant}/*"
done

- name: Create Release
uses: softprops/action-gh-release@v3
with:
files: |
linux-targz/PxModRim-Linux.tar.gz
linux-appimage/PxModRim-x86_64.AppImage
linux-deb/*.deb
linux-rpm/*.rpm
flatpak/com.github.PyXiion.PxModRim.flatpak
PxModRim-Windows.zip
windows-installer/PxModRim-Setup.exe
macos-dmg/PxModRim-macOS.dmg
macos-zip/PxModRim-macOS.zip
artifacts/PxModRim-Linux-*/*.tar.gz
artifacts/PxModRim-Linux-AppImage-*/*.AppImage
artifacts/PxModRim-Linux-Deb-*/*.deb
artifacts/PxModRim-Linux-RPM-*/*.rpm
artifacts/PxModRim-Flatpak-*/*.flatpak
PxModRim-Windows-*.zip
artifacts/PxModRim-Windows-Installer-*/*-Setup.exe
artifacts/PxModRim-macOS-DMG-*/*.dmg
artifacts/PxModRim-macOS-ZIP-*/*.zip
generate_release_notes: true
19 changes: 19 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,22 @@ jobs:
export QT_QPA_PLATFORM=offscreen
fi
just test

workshop-backend:
runs-on: ubuntu-latest
defaults:
run:
working-directory: cf-workers
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: "22"
cache: npm
cache-dependency-path: cf-workers/package-lock.json
- name: Install Worker dependencies
run: npm ci
- name: Check TypeScript
run: npm run check
- name: Exercise bundled Worker with D1
run: npm test
17 changes: 14 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,21 @@ src/pxmodrim/
## Download plugins
Downloads are plugins; the UI only talks to `core/downloads/manager.py` (`DownloadManager`, always present, plugin name `downloads`).
- Sources subclass `core.downloads.Downloader` (`core/downloads/steam.py` = `steam_downloader`, the only one today) and register with the manager in `setup()`. Third-party imports stay inside the source's own module; `_app.py` imports it lazily and skips it if disabled or missing.
- UI plugins: `downloads_ui` (Downloads tab, source-agnostic, shown when any downloader exists), `steamworkshop` (Steam WebView, needs `steam_downloader`).
- Disable at runtime: `PX_DISABLED_PLUGINS=steam_downloader,steamworkshop,downloads_ui` (comma-separated).
- Catalog: `workshop_catalog` consumes the read-only catalog API with constructor-injected configuration/client factory; it resolves transitive download plans and delegates files to `DownloadManager`, without activating mods. `AppConfig.workshop_catalog_url` defaults to `https://api.modrim.pyxiion.dev`; an empty string disables catalog network requests. Defaulted config fields load from older files without a schema migration.
- UI plugins: `downloads_ui` (Downloads tab, source-agnostic, shown when any downloader exists), `steamworkshop` (Steam WebView, needs `steam_downloader`), `workshop_ui` (native QML Workshop browser, needs `workshop_catalog` and `downloads`).
- Disable at runtime: `PX_DISABLED_PLUGINS=steam_downloader,steamworkshop,downloads_ui,workshop_catalog,workshop_ui` (comma-separated). Disabling the catalog also omits its UI.
- Installed catalog state uses discovered mods' `published_file_id` (About/PublishedFileId.txt or numeric folder); only downloader-managed local copies have known sync times. Steam-client Workshop copies are installed, but cannot be marked outdated without a known sync timestamp. Downloading places mods in Local mods and never subscribes or activates them.
- Catalog requests batch installed metadata by 100 IDs and resolution by 50 mod roots / 10 collection roots; large Update all selections retain dependency-first order and deduplicate shared dependencies across requests.
- New source: subclass `Downloader`, register in `_app.py`, add its name to the `downloaders` list; UI needs no change.

## Plugin API
Core and the shell (`ui/window`, `ui/panels`) never name a concrete plugin; only `_app.py` (composition root) does.
- Settings: `PluginConfig(ctx.config_service, name, Struct, legacy={old_config_json_key: field})` in `setup()` → `plugins/<name>.json`; subscribe to `.changed`.
- Settings UI: a UI plugin calls `app_ctx.add_settings_section(factory)` (one Settings tab each); subclass `ui.settings_section.PluginConfigSection` with `title` + `source` (QML whose root has `required property QtObject section`, reading `section.initial` and calling `section.set(key, value)`). Edits apply on Save only.
- Views: `RailView` subclasses on `app_ctx.add_rail_view`; `open_route(path)` handles `modrim://<view_id>/…`; `lists_downloads = True` marks the download queue.
- `steam_downloader_ui` holds the Steam download settings section.
- Build variants: `packaging/build.py --workshop {NoWorkshop,SteamWorkshop,NativeWorkshop}` bakes `workshop-variant.txt` into the package, which `_app.py` turns into disabled plugins, and `_variant_exclusions` leaves the matching modules out of the Nuitka build. `NoWorkshop` omits PxSteamDL, the Downloads tab and both browsers (CI syncs it with `--no-install-package pxsteamdl`); `SteamWorkshop` omits the native browser; `NativeWorkshop` omits the web browser and WebEngine. CI builds all three on every OS. Build jobs set `UV_NO_SYNC=1` so `uv run` preserves the explicitly synced variant environment; both Workshop variants sync with `--extra steam`.

## Key conventions
- `from __future__ import annotations` in every `.py` file
- No comments unless explaining _why_. _What_ comments allowed for long blocks.
Expand All @@ -58,7 +69,7 @@ Output `inject.js` is gitignored; always build before running.

## Layer dependency checker
`just check-deps` (`scripts/check-deps.py`) enforces strict import rules via pydeps.
Defines 24 groups with explicit ALLOWED dependency matrices.
Defines groups with explicit ALLOWED dependency matrices, including `core.workshop`.
If a new import violates boundaries, update `scripts/check-deps.py`'s `ALLOWED` dict.

## CI matrix
Expand Down
Loading
Loading