fix image preview and add option for automatically stop auto screenshoting when idle

This commit is contained in:
iceBear67
2026-08-07 19:00:20 +00:00
parent 014b18f1ba
commit 30d124fd8b
22 changed files with 641 additions and 32 deletions
+141
View File
@@ -0,0 +1,141 @@
# Working on PhotoSync
A Fabric client mod that uploads Minecraft screenshots to Immich and browses the
album in game, built from one source tree for nine Minecraft versions
(1.20 → 26.2). Read [`README.md`](README.md) for what it does and
[`docs/PORTING.md`](docs/PORTING.md) before touching anything that imports
`net.minecraft`.
This file is the orientation an agent needs before its first edit: where code
goes, what may import what, and which oddities are deliberate.
## Commands
```sh
./gradlew test # the shared modules' unit tests
./gradlew :platform:1.21.8:build # one bucket, remapped jar included
./gradlew buildAllPlatforms # all nine — the real check before finishing
./gradlew :platform:1.21.8:runClient # dev client for one bucket
```
`buildAllPlatforms` takes a few minutes and is the only thing that proves a
change compiles everywhere. A change that only touches `shared/` still has to
pass it, because each bucket compiles the shared sources at its own bytecode
level. If the machine has no network but a populated Gradle cache, add
`--offline`.
Two probes answer "what does this method look like on every version", by running
`javap` across the cached Minecraft jars:
```sh
python3 tools/probe-api.py # the fixed list of members the bridge depends on
python3 tools/probe-class.py net.minecraft.client.gui.GuiGraphics '(?i)blit'
```
Use them instead of recalling a signature. Every table in `docs/PORTING.md` was
produced this way, and the ones that were not were wrong.
## The module map
```
shared/core no Minecraft, no GUI. Config, uploads, providers, timeline,
ThumbHash. Where behaviour that can be unit-tested lives.
shared/mc-api the seam: interfaces the game must implement, no bodies.
shared/ui every screen and widget, drawn through mc-api only.
shared/client wiring — screenshot becomes upload becomes notification.
platform/common a source root, not a project: the adapter code that is
textually identical on all nine buckets.
platform/<v> one bucket's adapters (dev.photosync.platform.impl) and mixins.
```
The dependency arrow points one way: `platform``client``ui``mc-api`
`core`. Each module's `package-info.java` states its own rule and is worth
reading before adding a file to it. The two that get violated by accident:
- **Nothing under `shared/` may import `net.minecraft`, `net.fabricmc` or
`org.lwjgl`.** If shared code needs something from the game, the answer is a
new method on an `mc-api` interface plus nine adapter implementations — never
an import. Adding to that seam is expensive on purpose, so first check whether
the thing can be computed from what `RenderBridge` already exposes.
- **`shared/` compiles to Java 17 bytecode** (`shared_java` in
`gradle.properties`), because 1.201.20.4 run on a Java 17 JVM. No virtual
threads, no Java 21 pattern matching, no `HttpClient.close()`. Platform modules
compile at their own bucket's level, which is why the same idiom can be legal
in `platform/26.2` and rejected in `shared/core`.
## Where a change goes
| You want to | Put it in |
| --- | --- |
| change upload, retry or queue behaviour | `shared/core/upload` |
| add a setting | `shared/core/config` (a record + `normalized()`), then `SettingsScreen` and `en_us.json` |
| support another photo service | `shared/core/provider/<name>`, implementing `PhotoProvider` + `ProviderFactory`, added to the list in `PhotoSync`'s one-argument constructor. No UI or platform change needed — the settings screen builds its fields from `ProviderDescriptor` |
| change a screen | `shared/ui/screen` |
| add a drawing primitive | `RenderBridge` **and all nine** `RenderAdapter`s. Read `docs/PORTING.md` §3 first |
| fix something on one Minecraft version | that bucket's `platform/<v>/…/impl` |
| fix something on every Minecraft version | `platform/common` — but only if it compiles on all nine |
The `platform/common` rule has no third case: a class lives there while all nine
compilations accept it, and the day one of them stops, it moves down into all
nine copies of `impl`. Do not add a version check to keep it in `common`.
## Conventions
The compiler does not enforce these; reviewers do.
- **Lombok with fluent accessors.** `lombok.config` at the repo root sets
`lombok.accessors.fluent = true`, so a `@Getter` on `kind` generates `kind()`,
not `getKind()`. Records are the default for data; `@Builder(toBuilder = true)`
for config records.
- **No utility classes, no scattered constants.** A constant belongs to the class
that uses it, as a `private static final` next to that use. If a static helper
is tempting, it usually means the behaviour belongs on one of the objects.
- **No new dependencies.** Gson and SLF4J are `compileOnly` because Minecraft and
Fabric Loader already ship them; nothing is shaded and nothing is jar-in-jar'd,
which is why each jar is ~270 KB. Do not write a JSON parser, an HTTP client or
a base64 encoder — the platform has all three.
- **Comments explain why, not what.** The existing ones record measurements and
decisions ("Immich's default thumbnail format is WebP, which the game's decoder
cannot read"). Match that; a comment restating the code is noise.
- **Don't over-abstract.** One provider interface exists because a second provider
is a stated goal. An interface with one implementation and no second in sight is
not the house style.
- **User-visible strings are translation keys**, in
`shared/client/src/main/resources/assets/photosync/lang/en_us.json`. Adding keys
is safe on every version; renaming them is not.
## Things that are the way they are on purpose
Verify before "fixing" any of these — each one cost a measurement.
- **No refmap.** Loom's non-legacy mixin remapping rewrites annotations in place.
There is no `refmap.json` and none is needed; `docs/PORTING.md` §6 shows how to
confirm it with `javap -p -v` on a built jar.
- **`CaptureMixin` targets `method = "*"`** because the private helper that writes
the screenshot has three different names across the range, and it redirects the
`File` overload rather than the `Path` one because the two delegate — redirecting
both publishes every capture twice.
- **26.x uses a generated 47-byte identity mappings jar** (root `build.gradle`),
because those Minecraft jars ship deobfuscated and Loom still demands a mappings
artifact.
- **The UI draws its own widgets.** Minecraft's drawing primitives have been stable
since 1.20; its widget constructors have not.
- **Immich renditions are a ladder, not a constant** (`ImmichProvider.thumbnail`):
the grid asks for `thumbnail` then `preview`, an opened photo asks for `fullsize`
then `preview`, and a rendition that answers with WebP, a 404 or a 403 is retired
for the session. Minecraft decodes PNG and JPEG only — stb_image reads neither
WebP nor AVIF — so a rendition's format is a correctness concern, not a
preference.
- **`ThumbnailCache` floors capacity at 1, not 8.** The grid's real floor comes
from `BrowserSettings.normalized()`; the detail cache wants two, because
full-resolution textures are tens of megabytes each.
## Testing
`shared/core` is the only module with meaningful test coverage, and it is thin —
`ThumbHashTest` is what exists. Anything you add to `core` that can be tested
without a game should come with tests; `ui` and `platform` are verified by
compiling all nine buckets and running a dev client.
There is no test that catches a broken adapter. If you change anything under
`platform/`, run `runClient` on that bucket and look at the screen.