# Contributing

## Getting Started

1. Fork and clone the repository
2. Run `flutter pub get` to install dependencies
3. Run `scripts/codegen.sh` to generate translations and Dart model code
4. Start developing!

## Development

- Follow Dart/Flutter conventions
- Run `dart format .` to format Dart code (note: generated files like `*.g.dart` are excluded from CI checks)
- Run `scripts/format_native.sh --fix` to format Kotlin, Swift, C++, C, Objective-C, and native headers
- Run `flutter analyze` before submitting to check for issues
- Run `scripts/run_tests.sh` to run the test suite (same as `flutter test`, but scaled to your core count)
- Test your changes thoroughly

### Code Quality Checks

The project includes automated CI checks that run on all pull requests:

1. **Code Formatting**: Ensures code follows Dart and native formatting standards
   - Run locally: `dart format .` to format Dart files
   - Run locally: `scripts/format_native.sh --fix` to format native files
   - Note: CI only checks non-generated files (excludes `.g.dart`, `.freezed.dart`)
   - Generated files are reformatted automatically by build tools

2. **Static Analysis**: Checks for code issues and potential bugs
   - Run locally: `flutter analyze`
   - Note: CI excludes generated files from analysis (configured in `analysis_options.yaml`)

3. **Generated Code**: Ensures generated translations and model files are current
   - Run locally: `scripts/codegen.sh --check`

4. **Tests**: Runs unit and widget tests (when available)
   - Run locally: `scripts/run_tests.sh`
   - This is `flutter test` with `-j` set to the core count. The default is half your cores, which
     leaves most of the machine idle because the suite is dominated by per-file compilation.
     Arguments are forwarded, so `scripts/run_tests.sh test/widgets/some_test.dart` works.

All these checks must pass before your changes can be merged.

### Maestro end-to-end tests

Android E2E tests use [Maestro](https://maestro.mobile.dev/) against a disposable, pre-seeded Jellyfin container.

Prerequisites: Java 17, Flutter and Android SDK/platform tools, a running Android emulator, Docker, and the
[Maestro CLI](https://docs.maestro.dev/getting-started/installing-maestro).

Run the suites from the repository root (`py -3` can replace `python3` on Windows):

```bash
python3 scripts/run_maestro.py basic    # Basic user flows
python3 scripts/run_maestro.py catalog  # Catalog and music flows
python3 scripts/run_maestro.py media    # Codec playback and track selection
```

Run one flow with `--flow`:

```bash
python3 scripts/run_maestro.py basic --flow .maestro/flows/04_search.yaml
```

Use `--skip-build` to reuse the debug APK and `--skip-jellyfin-build` to reuse the Jellyfin image. Set
`--device <adb-serial>` when multiple devices are connected; physical devices also require `--adb-reverse`.

Top-level flows live in `.maestro/flows/`, shared setup in `.maestro/subflows/`, and focused regressions in
`.maestro/regression_flows/`. Automatic PR groups are declared in `scripts/run_maestro_ci.py::GROUPS`. Every top-level
regression flow must be registered either there or in `DESTRUCTIVE_MANUAL_TARGETS`; reusable subflows are not
independent tests. A manual-only classification must state why the flow cannot run automatically and must not be
described as CI coverage. `.github/workflows/e2e.yml` runs only automatic targets and uploads diagnostics on failure.

The profile-isolation and profile-teardown regressions create and remove profile connections, so they are a destructive
manual target rather than automatic PR coverage. Run them only against the pre-seeded Jellyfin fixture and a disposable
emulator, using the required opt-in:

```bash
python3 scripts/run_maestro_ci.py profile-regressions --disposable-emulator
```

The target refuses to start without `--disposable-emulator`. Each profile flow writes to its own Jellyfin log and
diagnostics directory under `build/maestro-profile-regressions/`.

### Production container image updates

Production images in `server/Dockerfile` and `server/docker-compose.yml` use a readable version or source-revision tag
plus an authoritative multi-platform index digest. The adjacent `Platforms` declaration records the supported
`linux/amd64` and `linux/arm64` variants. Never replace these references with a mutable tag or a single-platform child
manifest.

Update a production image only through a reviewed change:

1. For the Bugs service, first record the running container's image ID, repository digest, platform, and OCI source
   revision without printing its environment. Prefer that reviewed running identity; selecting anything else is a
   service upgrade, not a routine pin refresh.
2. Review the upstream source revision and changelog, provenance, vulnerability results, and manifest contents. Resolve
   the readable tag and digest-qualified reference independently and confirm they identify the same OCI index in two
   clean caches. The index must contain both declared platforms; provenance/attestation descriptors do not count as
   runnable platforms.
3. Change the readable tag, full `sha256` index digest, and adjacent platform declaration together. Include the old and
   new identities, manifest/platform evidence, review findings, smoke results, and rollback notes in the change.
4. Before changing the Bugs digest, exercise it with non-production configuration and a disposable volume. Review
   migrations, take a restorable `bugs_data` backup, then validate a cloned volume. A forward-only migration rolls back
   with the prior digest and pre-change backup, not by changing the image reference alone.
5. Run `python3 scripts/check_container_image_pins.py`, `python3 scripts/test_check_container_image_pins.py`, and
   `(cd server && go test ./...)`. Inspect the rendered Compose configuration and rebuilt images locally without
   exposing configuration values. Do not publish or deploy from a review checkout, and never fall back to `latest` when
   a digest is unavailable.

## Internationalization (i18n)

This project uses `slang` for internationalization with JSON files.

### Adding New Strings

1. Add your string to `lib/i18n/strings.i18n.json`:
   ```json
   {
     "section": {
       "myNewString": "My new text"
     }
   }
   ```

2. Run `dart run slang` to regenerate translation files

3. Use in your code:
   ```dart
   Text(t.section.myNewString)
   ```

### Adding New Languages

1. Create new JSON file: `lib/i18n/[locale].i18n.json`
2. Copy structure from `en.i18n.json` and translate values
3. Run `dart run slang` to regenerate files

### Guidelines

- Organize strings logically in nested objects
- Use camelCase for keys
- Keep strings concise and clear
- Always run `dart run slang` after changes
