Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ This repository follows the same working agreements as [LycheeOrg/Lychee](https:
- Commit keystores, `.p8`/`.p12` files, provisioning profiles, service-account JSON or any password (they are git-ignored; keep it that way).
- Print secrets in workflow logs, or pass them to steps that do not need them.
- Run `fastlane … alpha|production`, `match` in write mode, or anything that uploads to TestFlight/App Store Connect/Google Play from a local machine without the owner's explicit approval in the current session.
- Change the app identifier (`com.mlapaglia.lycheemobile`, ADR-0003) without an ADR.
- Change the app identifier (`dev.lycheeorg.mobile`, ADR-0004) without an ADR.

### Releases
- Alpha ships automatically from `main`; production ships when the owner publishes a GitHub release tagged `vX.Y.Z` ([runbook-releases.md](docs/specs/5-operations/runbook-releases.md), ADR-0002).
Expand Down
4 changes: 2 additions & 2 deletions android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ apply plugin: 'com.android.application'
def releaseKeystore = System.getenv("ANDROID_KEYSTORE_PATH")

android {
namespace = "com.mlapaglia.lycheemobile"
namespace = "dev.lycheeorg.mobile"
compileSdk = rootProject.ext.compileSdkVersion
defaultConfig {
applicationId "com.mlapaglia.lycheemobile"
applicationId "dev.lycheeorg.mobile"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode((System.getenv("VERSION_CODE") ?: "1") as Integer)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.mlapaglia.lycheemobile;
package dev.lycheeorg.mobile;

import android.os.Bundle;
import com.getcapacitor.BridgeActivity;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Copyright (c) 2026 Lychee-Mobile contributors.
*/

package com.mlapaglia.lycheemobile;
package dev.lycheeorg.mobile;

import android.Manifest;
import android.app.Activity;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Copyright (c) 2026 Lychee-Mobile contributors.
*/

package com.mlapaglia.lycheemobile;
package dev.lycheeorg.mobile;

import android.Manifest;
import android.content.Intent;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Copyright (c) 2026 Lychee-Mobile contributors.
*/

package com.mlapaglia.lycheemobile;
package dev.lycheeorg.mobile;

import android.app.Notification;
import android.app.NotificationChannel;
Expand Down
4 changes: 2 additions & 2 deletions android/app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@
<resources>
<string name="app_name">Lychee</string>
<string name="title_activity_main">Lychee</string>
<string name="package_name">com.mlapaglia.lycheemobile</string>
<string name="custom_url_scheme">com.mlapaglia.lycheemobile</string>
<string name="package_name">dev.lycheeorg.mobile</string>
<string name="custom_url_scheme">dev.lycheeorg.mobile</string>
</resources>
7 changes: 2 additions & 5 deletions capacitor.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,9 @@

import type { CapacitorConfig } from "@capacitor/cli";

/**
* The app identifier is a personal one for the proof of concept (ADR-0003);
* it changes when the app moves to LycheeOrg's store accounts.
*/
/** LycheeOrg's app identifier, from its domain lycheeorg.dev (ADR-0004). */
const config: CapacitorConfig = {
appId: "com.mlapaglia.lycheemobile",
appId: "dev.lycheeorg.mobile",
appName: "Lychee",
webDir: "dist",
// NFR-006-01: the bridge's debug logging prints plugin results, which
Expand Down
2 changes: 1 addition & 1 deletion docs/specs/0-overview/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Lychee-Mobile is an iOS and Android client for a self-hosted [Lychee](https://github.com/LycheeOrg/Lychee) photo gallery: browse albums, view photos and upload from the phone.

- **Status:** proof of concept under `mlapaglia/Lychee-Mobile`, to move to LycheeOrg once it works (ADR-0003).
- **Status:** hosted at [`LycheeOrg/Lychee-Mobile`](https://github.com/LycheeOrg/Lychee-Mobile), transferred from `mlapaglia/Lychee-Mobile` on 2026-10-20; the store identities are still the proof of concept's (ADR-0003).
- **Stack:** Capacitor 8 + Ionic Vue 9 on Vue 3 and TypeScript, built with Lychee's toolchain (ADR-0001).
- **Delivery:** alpha builds to TestFlight and Google Play internal testing on every push to `main`; production on published GitHub releases (ADR-0002).

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ A user who may upload picks photos and videos on the phone and uploads them, unc
|----|---------------------|-----------|
| NC-005-01 | `@capawesome/capacitor-file-picker` `8.1.0` `pickMedia` | Native photo pickers; iOS `PHPickerViewController` with `skipTranscoding` copies the original to a temporary file; Android's photo picker returns `content://` URIs. |
| NC-005-02 | Android `ACCESS_MEDIA_LOCATION` (manifest + runtime) | Unredacted GPS in picked photos (NFR-005-04). |
| NC-005-03 | App-local Capacitor plugin `OriginalMedia` (`android/app/src/main/java/com/mlapaglia/lycheemobile/OriginalMediaPlugin.java`) | `pick()` → `{ files }`: launches `MediaStore.ACTION_PICK_IMAGES` with `EXTRA_PICK_IMAGES_MAX` (the platform limit), image and video types, `EXTRA_ACCEPT_ORIGINAL_MEDIA_FORMAT` and the undocumented `android.provider.extra.REQUEST_LOCATION_METADATA_ACCESS` (found in the picker's code, Q-005-05); returns each URI with `name`, `mimeType`, `size`, `modifiedAt` (date taken) read from the picker's columns; rejects with `CANCELLED` or `UNAVAILABLE` (no system photo picker). `prepare({ uri })` → `{ path }`: copies `MediaStore.setRequireOriginal(uri)` to `cacheDir/uploads/`; returns `{ path, location: "present" | "missing" | "unknown" }`; when MediaStore refuses the original (`UnsupportedOperationException`/`SecurityException`, "Include location" off) it copies the URI without `requireOriginal` and reports `missing`; other failures reject with `UNREADABLE`. `release({ path })` deletes a copy in that folder. Leftover copies are deleted when the plugin loads. |
| NC-005-03 | App-local Capacitor plugin `OriginalMedia` (`android/app/src/main/java/dev/lycheeorg/mobile/OriginalMediaPlugin.java`) | `pick()` → `{ files }`: launches `MediaStore.ACTION_PICK_IMAGES` with `EXTRA_PICK_IMAGES_MAX` (the platform limit), image and video types, `EXTRA_ACCEPT_ORIGINAL_MEDIA_FORMAT` and the undocumented `android.provider.extra.REQUEST_LOCATION_METADATA_ACCESS` (found in the picker's code, Q-005-05); returns each URI with `name`, `mimeType`, `size`, `modifiedAt` (date taken) read from the picker's columns; rejects with `CANCELLED` or `UNAVAILABLE` (no system photo picker). `prepare({ uri })` → `{ path }`: copies `MediaStore.setRequireOriginal(uri)` to `cacheDir/uploads/`; returns `{ path, location: "present" | "missing" | "unknown" }`; when MediaStore refuses the original (`UnsupportedOperationException`/`SecurityException`, "Include location" off) it copies the URI without `requireOriginal` and reports `missing`; other failures reject with `UNREADABLE`. `release({ path })` deletes a copy in that folder. Leftover copies are deleted when the plugin loads. |

### UI States
| ID | State | Trigger / Expected outcome |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ Open questions for [Feature 007](spec.md). Log every high- and medium-impact que

### ~~Q-007-07~~ – Where Android saves downloads

**Context:** `@capacitor-community/media` 9.1.0 on Android requires `albumIdentifier`, a folder under `getExternalMediaDirs()[0]`, i.e. `Android/media/com.mlapaglia.lycheemobile/<album>/`. It byte-copies the file there and broadcasts a media scan, so the photo shows in Google Photos; it needs no permission. Android treats `Android/media/<package>` as app-specific storage and deletes it when the app is uninstalled (to confirm on the Pixel), taking the downloaded photos with it; `getExternalMediaDirs()` is also deprecated since API 30. The app already owns an Android plugin, `OriginalMediaPlugin` (Feature 005). Saving through `MediaStore` into `Pictures/` or `Movies/` needs no permission on Android 10+; Android 7–9 needs `WRITE_EXTERNAL_STORAGE` (`maxSdkVersion="28"`).
**Context:** `@capacitor-community/media` 9.1.0 on Android requires `albumIdentifier`, a folder under `getExternalMediaDirs()[0]`, i.e. `Android/media/<application ID>/<album>/`. It byte-copies the file there and broadcasts a media scan, so the photo shows in Google Photos; it needs no permission. Android treats `Android/media/<package>` as app-specific storage and deletes it when the app is uninstalled (to confirm on the Pixel), taking the downloaded photos with it; `getExternalMediaDirs()` is also deprecated since API 30. The app already owns an Android plugin, `OriginalMediaPlugin` (Feature 005). Saving through `MediaStore` into `Pictures/` or `Movies/` needs no permission on Android 10+; Android 7–9 needs `WRITE_EXTERNAL_STORAGE` (`maxSdkVersion="28"`).

- **Option A (recommended, chosen):** add `saveToGallery({ path, name, mimeType })` to `OriginalMediaPlugin`: `MediaStore` insert into `Pictures/Lychee` or `Movies/Lychee` with `IS_PENDING`, byte copy; on Android 7–9 copy into the public folder and scan it after asking for `WRITE_EXTERNAL_STORAGE`. The media plugin is used on iOS only.
- ✅ Downloads survive uninstalling the app and sit in standard folders.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -239,4 +239,4 @@ When Lychee converts a RAW upload, it stores a JPEG as the `ORIGINAL` variant an
### Library findings (read from the published packages, 2026-10-20)
- `@capacitor/file-transfer` 2.0.6: `downloadFile({ url, path, headers, progress, connectTimeout, readTimeout })` → `{ path }`; `path` must be a full file path ("use `@capacitor/filesystem`"); no response headers on success; HTTP errors reject with `error.data.httpStatus`; Android asks for storage permission only for public-directory targets. No cookie handling of its own.
- `@capacitor-community/media` 9.1.0, iOS: `savePhoto` loads the file through SDWebImage's downloader and adds the raw data with `PHAssetCreationRequest.addResource(.photo, data:)`; `saveVideo` uses `creationRequestForAssetFromVideo(atFileURL:)`; add-only authorisation when no album is given.
- `@capacitor-community/media` 9.1.0, Android: `albumIdentifier` is required and must be a folder under `getExternalMediaDirs()[0]` (`Android/media/com.mlapaglia.lycheemobile/`); the file is byte-copied there and a media scan is broadcast; no permission outside "gallery mode". Android removes that folder when the app is uninstalled (Q-007-07).
- `@capacitor-community/media` 9.1.0, Android: `albumIdentifier` is required and must be a folder under `getExternalMediaDirs()[0]` (`Android/media/<application ID>/`); the file is byte-copied there and a media scan is broadcast; no permission outside "gallery mode". Android removes that folder when the app is uninstalled (Q-007-07).
2 changes: 1 addition & 1 deletion docs/specs/4-architecture/knowledge-map.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ How the pieces of Lychee-Mobile relate. Update it whenever modules, services, st
- `src/utils/photoActionMessages.ts`: selection title, delete confirmation and failures, download progress and summary (Feature 007).
- `src/utils/photoVariant.ts` (`displayVariant()`, `zoomVariant()`, `videoSource()`, `videoPoster()`), `viewerSlides.ts` (`counterText()`, `shouldLoadMore()`), `pressAndHold.ts`, `singleTap.ts` (Feature 004).
- `src/theme/variables.css`: Ionic colour variables.
- **Native shells**: Capacitor 8 (`capacitor.config.ts`, app ID `com.mlapaglia.lycheemobile`, ADR-0003).
- **Native shells**: Capacitor 8 (`capacitor.config.ts`, app ID `dev.lycheeorg.mobile`, ADR-0004).
- `android/`: Gradle project; `android/app/build.gradle` reads `VERSION_NAME`, `VERSION_CODE` and the `ANDROID_KEY*` variables for release builds.
- `ios/`: Xcode project using Swift Package Manager (`ios/App/CapApp-SPM`); app-local plugin `OriginalCameraPlugin.swift` (camera, metadata, GPS via `CLLocationManager`, `Application Support/captures/`) registered by `MainViewController` (the storyboard's bridge view controller) (Feature 011, ADR-011-01); `UploadKeepAlivePlugin.swift` (`beginBackgroundTask` around a run) (Feature 012); usage strings for camera, location when in use and adding photos.
- Libraries beyond the starter: `swiper` `14.3.0` (ADR-004-01), `@capawesome/capacitor-file-picker` `8.1.0` (ADR-005-01), `@aparajita/capacitor-secure-storage` `8.0.1` (ADR-006-01), `@capacitor/file-transfer` `2.0.6`, `@capacitor-community/media` `9.1.0` (iOS only), `@capacitor/filesystem` `8.1.5` (ADR-007-01), all pinned.
Expand Down
4 changes: 2 additions & 2 deletions docs/specs/5-operations/runbook-releases.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ Until the secrets below exist, each release job prints a notice and skips, so th

### Apple (iOS)
1. Apple Developer Program membership; note the **Team ID** → secret `APPLE_TEAM_ID`.
2. App Store Connect → Apps → New App with bundle ID `com.mlapaglia.lycheemobile` (register the identifier first under Certificates, Identifiers & Profiles).
2. App Store Connect → Apps → New App with bundle ID `dev.lycheeorg.mobile` (ADR-0004; register the identifier first under Certificates, Identifiers & Profiles).
3. App Store Connect → Users and Access → Integrations → App Store Connect API → new key with the **App Manager** role:
- `ASC_KEY_ID` (Key ID), `ASC_ISSUER_ID` (Issuer ID)
- `ASC_KEY_P8_BASE64`: `base64 -i AuthKey_XXXX.p8 | pbcopy`
Expand All @@ -35,7 +35,7 @@ Until the secrets below exist, each release job prints a notice and skips, so th
5. TestFlight → Internal Testing: create a group and add testers.

### Google (Android)
1. Google Play Console developer account; create the app with package `com.mlapaglia.lycheemobile`.
1. Google Play Console developer account; create the app with package `dev.lycheeorg.mobile` (ADR-0004).
2. Upload keystore (keep a backup outside the repository):
```sh
keytool -genkeypair -v -keystore upload.keystore -alias upload -keyalg RSA -keysize 4096 -validity 10000
Expand Down
8 changes: 7 additions & 1 deletion docs/specs/6-decisions/ADR-0003-proof-of-concept-identity.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# ADR-0003: Proof-of-concept identity

- **Status:** Accepted
- **Status:** Accepted; repository transferred to LycheeOrg on 2026-10-20 (see Update); app identifier superseded by ADR-0004
- **Date:** 2026-10-19
- **Related features/specs:** repository setup (no feature)
- **Related open questions:** none (decided by the owner when creating the repository)
Expand Down Expand Up @@ -38,6 +38,12 @@ None beyond ADR-0002's secret handling.

Migration checklist (when the PoC is accepted): transfer the repository, move or recreate the store listings, change `appId` in `capacitor.config.ts` and the native projects if the identifier changes, rotate secrets into the organisation.

## Update – 2026-10-20

The repository was transferred to [`LycheeOrg/Lychee-Mobile`](https://github.com/LycheeOrg/Lychee-Mobile); history, issues and pull requests moved with it, and the old URL redirects. Local clones point `origin` at the new URL (`git remote set-url origin https://github.com/LycheeOrg/Lychee-Mobile.git`).

The app identifier changed to `dev.lycheeorg.mobile` before any store release (ADR-0004). Still open on the migration checklist above: which Apple and Google accounts hold the listings, the certificates repository used by fastlane match, and moving the release secrets into the organisation.

## Links

- Related ADRs: ADR-0001, ADR-0002
44 changes: 44 additions & 0 deletions docs/specs/6-decisions/ADR-0004-lycheeorg-app-identifier.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# ADR-0004: LycheeOrg app identifier

- **Status:** Accepted
- **Date:** 2026-10-20
- **Related features/specs:** repository setup (no feature); supersedes the app identifier of ADR-0003
- **Related open questions:** none (decided by the owner after the move to LycheeOrg)

## Context

ADR-0003 gave the proof of concept a personal identifier, `com.mlapaglia.lycheemobile`, to keep LycheeOrg's name out of store identities until the organisation agreed. The repository has since moved to `LycheeOrg/Lychee-Mobile` (ADR-0003 Update), and the app has not been published to either store, so the identifier can still change without stranding users or listings.

## Decision

- **App identifier:** `dev.lycheeorg.mobile` (iOS bundle ID and Android application ID), the reverse of LycheeOrg's domain `lycheeorg.dev`. Chosen by the owner ("use lycheeorg"); the form is the one ADR-0003 listed as the LycheeOrg-style alternative.
- Changed in `capacitor.config.ts`, the Android `namespace`/`applicationId` (Java sources moved to `dev/lycheeorg/mobile`), `strings.xml`, the iOS `PRODUCT_BUNDLE_IDENTIFIER`, and fastlane's `Appfile`, `Fastfile` and `Matchfile`.
- The display name stays "Lychee". Changing the identifier again needs a new ADR (AGENTS.md).

## Consequences

### Positive
- The store identity matches the organisation that now owns the code.
- No migration for users: nothing was published yet.

### Negative
- Test installs under the old identifier are separate apps: they stay on devices until removed, and their settings and stored credentials don't carry over.
- Before the release workflow can ship again: register `dev.lycheeorg.mobile` in Apple Developer (Identifiers) and create the App Store Connect app, run `fastlane match appstore` once for the new identifier (owner only), and create the Google Play app with the new package; the Play upload key and the Play listing for the old package are not reused. Until then the alpha lanes on `main` fail.
- Which developer accounts hold the listings (the owner's or LycheeOrg's) is still open (ADR-0003 migration checklist).

## Alternatives Considered

- **Keep `com.mlapaglia.lycheemobile` until the first store release:** no setup now, but the identifier would be fixed forever once published.
- **`org.lycheeorg.mobile`:** LycheeOrg's site is `lycheeorg.dev`, so `dev.lycheeorg` is the matching reverse domain.

## Security / Privacy Impact

None. Android's `FileProvider` authority and the iOS Keychain items follow the identifier; nothing carries over from the old app.

## Operational Impact

The release runbook's store setup uses the new identifier; secrets that embed the identifier (none in this repository) would need updating.

## Links

- Related ADRs: ADR-0003 (identifier superseded), ADR-0002 (release pipeline)
3 changes: 2 additions & 1 deletion docs/specs/6-decisions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ Numbering convention (same as LycheeOrg/Lychee): a decision raised by a feature
|-----|----------|------------------------------------|
| [ADR-0001](ADR-0001-capacitor-ionic-vue.md) | Accepted | Capacitor + Ionic Vue, with Lychee's frontend toolchain (Vite+, Oxlint, Oxfmt, Vitest, vue-tsc) |
| [ADR-0002](ADR-0002-release-pipeline.md) | Accepted | fastlane on GitHub Actions: alpha to TestFlight and Google Play internal testing on every push to `main`, production on published GitHub releases |
| [ADR-0003](ADR-0003-proof-of-concept-identity.md) | Accepted | Proof of concept under a personal account: public MIT repository, app ID `com.mlapaglia.lycheemobile`, moved to LycheeOrg after the PoC |
| [ADR-0003](ADR-0003-proof-of-concept-identity.md) | Accepted | Proof of concept under a personal account: public MIT repository (transferred to `LycheeOrg/Lychee-Mobile` on 2026-10-20); its app ID `com.mlapaglia.lycheemobile` is superseded by ADR-0004 |
| [ADR-0004](ADR-0004-lycheeorg-app-identifier.md) | Accepted | App identifier `dev.lycheeorg.mobile` (LycheeOrg), changed before any store release; supersedes ADR-0003's identifier |
| [ADR-001-01](ADR-001-01-native-http-transport.md) | Accepted | CapacitorHttp with the native cookie store; Lychee calls load `{server}/` for the session and `XSRF-TOKEN`, then send JSON headers and `X-XSRF-TOKEN` |
| [ADR-001-02](ADR-001-02-local-network-plain-http.md) | Accepted | Plain http only for local-network hosts, enforced by the app; iOS `NSAllowsLocalNetworking`, Android cleartext allowed at OS level |
| [ADR-002-01](ADR-002-01-session-cookie-sign-in.md) | Superseded by ADR-006-01 | Stay signed in through Lychee's session and remember cookies in the native cookie store; no stored password or API token |
Expand Down
6 changes: 3 additions & 3 deletions fastlane/Appfile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# SPDX-License-Identifier: MIT
# Copyright (c) 2026 Lychee-Mobile contributors.

# Proof-of-concept identifier (ADR-0003).
app_identifier("com.mlapaglia.lycheemobile")
package_name("com.mlapaglia.lycheemobile")
# LycheeOrg app identifier (ADR-0004).
app_identifier("dev.lycheeorg.mobile")
package_name("dev.lycheeorg.mobile")
team_id(ENV["APPLE_TEAM_ID"]) if ENV["APPLE_TEAM_ID"]
2 changes: 1 addition & 1 deletion fastlane/Fastfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

default_platform(:android)

APP_ID = "com.mlapaglia.lycheemobile"
APP_ID = "dev.lycheeorg.mobile"

def version_name
ENV.fetch("VERSION_NAME")
Expand Down
2 changes: 1 addition & 1 deletion fastlane/Matchfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@
git_url(ENV["MATCH_GIT_URL"]) if ENV["MATCH_GIT_URL"]
storage_mode("git")
type("appstore")
app_identifier(["com.mlapaglia.lycheemobile"])
app_identifier(["dev.lycheeorg.mobile"])
Loading
Loading