From 50065871239b9fe9d2d3d78837af78d3337231d5 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Oct 2026 04:40:01 +0000 Subject: [PATCH 1/5] App: privacy link in the app, iOS privacy manifest, approximate location only, notes for App Review - More links the privacy policy and help pages (Apple 5.1.1 wants the policy reachable in the app) - PrivacyInfo.xcprivacy: no tracking, coarse location for app functionality, file timestamps (C617.1) - ACCESS_FINE_LOCATION only up to Android 11; from 12 on the dialog offers approximate only - store/listing.en.md: the text for the reviewers' notes Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01JKfrmz9N5BvxMtYJYzBjEF --- app/android/app/src/main/AndroidManifest.xml | 5 ++- app/ios/App/App.xcodeproj/project.pbxproj | 4 ++ app/ios/App/App/PrivacyInfo.xcprivacy | 42 ++++++++++++++++++++ app/locales/de.json | 2 + app/locales/en.json | 2 + app/pet.html | 9 +++++ store/listing.en.md | 24 +++++++++++ 7 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 app/ios/App/App/PrivacyInfo.xcprivacy diff --git a/app/android/app/src/main/AndroidManifest.xml b/app/android/app/src/main/AndroidManifest.xml index e2dd896..24f4b05 100644 --- a/app/android/app/src/main/AndroidManifest.xml +++ b/app/android/app/src/main/AndroidManifest.xml @@ -38,7 +38,10 @@ - + + diff --git a/app/ios/App/App.xcodeproj/project.pbxproj b/app/ios/App/App.xcodeproj/project.pbxproj index 7dd74a3..659c492 100644 --- a/app/ios/App/App.xcodeproj/project.pbxproj +++ b/app/ios/App/App.xcodeproj/project.pbxproj @@ -16,6 +16,7 @@ 504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC3101FED79650016851F /* LaunchScreen.storyboard */; }; 50B271D11FEDC1A000F3C39B /* public in Resources */ = {isa = PBXBuildFile; fileRef = 50B271D01FEDC1A000F3C39B /* public */; }; 9582B6832FE993A70072D4E8 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9582B6822FE993A50072D4E8 /* SceneDelegate.swift */; }; + B10B1100B10B1100B10B1201 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = B10B1100B10B1100B10B1202 /* PrivacyInfo.xcprivacy */; }; B10B1100B10B1100B10B1101 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = B10B1100B10B1100B10B1102 /* InfoPlist.strings */; }; /* End PBXBuildFile section */ @@ -31,6 +32,7 @@ 50B271D01FEDC1A000F3C39B /* public */ = {isa = PBXFileReference; lastKnownFileType = folder; path = public; sourceTree = ""; }; 9582B6822FE993A50072D4E8 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 958DCC722DB07C7200EA8C5F /* debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = debug.xcconfig; path = ../debug.xcconfig; sourceTree = SOURCE_ROOT; }; + B10B1100B10B1100B10B1202 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; B10B1100B10B1100B10B1103 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/InfoPlist.strings; sourceTree = ""; }; /* End PBXFileReference section */ @@ -74,6 +76,7 @@ 504EC3101FED79650016851F /* LaunchScreen.storyboard */, 504EC3131FED79650016851F /* Info.plist */, B10B1100B10B1100B10B1102 /* InfoPlist.strings */, + B10B1100B10B1100B10B1202 /* PrivacyInfo.xcprivacy */, 2FAD9762203C412B000D30F8 /* config.xml */, 50B271D01FEDC1A000F3C39B /* public */, ); @@ -153,6 +156,7 @@ 504EC30D1FED79650016851F /* Main.storyboard in Resources */, 2FAD9763203C412B000D30F8 /* config.xml in Resources */, B10B1100B10B1100B10B1101 /* InfoPlist.strings in Resources */, + B10B1100B10B1100B10B1201 /* PrivacyInfo.xcprivacy in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/app/ios/App/App/PrivacyInfo.xcprivacy b/app/ios/App/App/PrivacyInfo.xcprivacy new file mode 100644 index 0000000..a068a59 --- /dev/null +++ b/app/ios/App/App/PrivacyInfo.xcprivacy @@ -0,0 +1,42 @@ + + + + + + NSPrivacyTracking + + NSPrivacyTrackingDomains + + NSPrivacyCollectedDataTypes + + + NSPrivacyCollectedDataType + NSPrivacyCollectedDataTypeCoarseLocation + NSPrivacyCollectedDataTypeLinked + + NSPrivacyCollectedDataTypeTracking + + NSPrivacyCollectedDataTypePurposes + + NSPrivacyCollectedDataTypePurposeAppFunctionality + + + + NSPrivacyAccessedAPITypes + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryFileTimestamp + NSPrivacyAccessedAPITypeReasons + + C617.1 + + + + + diff --git a/app/locales/de.json b/app/locales/de.json index 4ca2a71..3178c83 100644 --- a/app/locales/de.json +++ b/app/locales/de.json @@ -629,6 +629,8 @@ "location_set": "Wetter für {latitude}, {longitude}.", "locate_failed": "Dein Ort ließ sich nicht herausfinden.", "connect_note": "Blobling gibt es auch als LED-Display zum Selberbauen.", + "privacy": "Datenschutz", + "support": "Hilfe", "connect": "Mit dem Display verbinden", "connect_prompt": "Adresse des Displays, z. B. raspberrypi.local oder blobling.local", "connect_ok": "Verbinden", diff --git a/app/locales/en.json b/app/locales/en.json index 2365b76..b2f227e 100644 --- a/app/locales/en.json +++ b/app/locales/en.json @@ -629,6 +629,8 @@ "location_set": "Weather for {latitude}, {longitude}.", "locate_failed": "Couldn't find out where you are.", "connect_note": "Blobling also comes as an LED display to build yourself.", + "privacy": "Privacy policy", + "support": "Help", "connect": "Connect to the display", "connect_prompt": "Address of the display, e.g. raspberrypi.local or blobling.local", "connect_ok": "Connect", diff --git a/app/pet.html b/app/pet.html index 618eb6c..7448736 100644 --- a/app/pet.html +++ b/app/pet.html @@ -496,6 +496,7 @@

+ @@ -2211,6 +2212,14 @@

$("build-link").hidden = $("connect").hidden = !!app.connectedTo; $("disconnect").hidden = $("offline-leave").hidden = !app.connectedTo; } +// The app from the stores links its privacy policy and help (docs/ on GitHub Pages), as the stores ask +if (app.native) { + const docs = "https://mlampir.github.io/blobling/"; + const de = LANG.startsWith("de") ? ".de" : ""; + $("privacy-link").href = `${docs}privacy${de}`; + $("support-link").href = `${docs}support${de}`; + $("legal").hidden = false; +} // The Pi's own page shows the display part only for its update if (!app.native) $("display-note").hidden = $("connect").hidden = $("disconnect").hidden = true; diff --git a/store/listing.en.md b/store/listing.en.md index 4896fd5..43f8653 100644 --- a/store/listing.en.md +++ b/store/listing.en.md @@ -21,6 +21,30 @@ Meet Blobling, a little pixel pet that lives on your phone. Hatch it, look after virtual pet,pixel,cute,slime,egg,hatch,raise,care,adventure,retro,cozy,chiptune,led,offline +## App Store: notes for App Review + +(App Store Connect, the version's *App Review Information → Notes*; the same +text fits Google Play's *App access*, with "All functionality is available +without special access".) Reviewers write in English, so this exists in +English only. + +Blobling needs no account, no login and no hardware: the whole game runs on +the phone. A new pet starts as an egg that hatches within about an hour +(stroking it keeps it warm and makes it sooner). Then drag food from the +tray onto the pet and stroke it. The "More" tab has the name, a new egg, +sound and the optional weather. + +The section "Own LED display" in "More" is optional. It connects the app to +a self-built LED panel (a Raspberry Pi on the home network, instructions at +https://github.com/mLampir/blobling). That is why the app asks for local +network access and talks plain HTTP to private addresses only. Without such +a display, the search finds nothing, asks for an address, and the pet stays +on the phone. + +Location is optional and only used for the weather in the pet's world: the +app rounds it to about 1 km and sends it to Open-Meteo. Notifications are +local only (the pet asks for you when it needs you); there is no server. + ## Description (Google Play full description and App Store description) Blobling is a little pixel pet that lives on your phone. It hatches from an egg, wants your attention a few times a day, and grows up depending on how you raise it. From eea4d059b046eff3fd56adcee15879889d79768d Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Oct 2026 04:47:45 +0000 Subject: [PATCH 2/5] App: location through Capacitor's Geolocation, approximate only The WebView's navigator.geolocation made iOS ask a second time on its own and Android ask for the precise location. The plugin asks once, through the system, for the approximate location; ACCESS_FINE_LOCATION is gone from the manifest. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01JKfrmz9N5BvxMtYJYzBjEF --- app/android/app/capacitor.build.gradle | 1 + app/android/app/src/main/AndroidManifest.xml | 8 +++--- app/android/capacitor.settings.gradle | 3 +++ app/ios/App/CapApp-SPM/Package.swift | 2 ++ app/src/native.ts | 26 +++++++++----------- package-lock.json | 13 ++++++++++ package.json | 1 + 7 files changed, 35 insertions(+), 19 deletions(-) diff --git a/app/android/app/capacitor.build.gradle b/app/android/app/capacitor.build.gradle index 3ff4091..9d12fcc 100644 --- a/app/android/app/capacitor.build.gradle +++ b/app/android/app/capacitor.build.gradle @@ -10,6 +10,7 @@ android { apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle" dependencies { implementation project(':capacitor-filesystem') + implementation project(':capacitor-geolocation') implementation project(':capacitor-haptics') implementation project(':capacitor-local-notifications') diff --git a/app/android/app/src/main/AndroidManifest.xml b/app/android/app/src/main/AndroidManifest.xml index 24f4b05..27f913f 100644 --- a/app/android/app/src/main/AndroidManifest.xml +++ b/app/android/app/src/main/AndroidManifest.xml @@ -36,12 +36,10 @@ - + - - diff --git a/app/android/capacitor.settings.gradle b/app/android/capacitor.settings.gradle index e129225..10007e5 100644 --- a/app/android/capacitor.settings.gradle +++ b/app/android/capacitor.settings.gradle @@ -5,6 +5,9 @@ project(':capacitor-android').projectDir = new File('../../node_modules/@capacit include ':capacitor-filesystem' project(':capacitor-filesystem').projectDir = new File('../../node_modules/@capacitor/filesystem/android') +include ':capacitor-geolocation' +project(':capacitor-geolocation').projectDir = new File('../../node_modules/@capacitor/geolocation/android') + include ':capacitor-haptics' project(':capacitor-haptics').projectDir = new File('../../node_modules/@capacitor/haptics/android') diff --git a/app/ios/App/CapApp-SPM/Package.swift b/app/ios/App/CapApp-SPM/Package.swift index 5974f4b..571cbd1 100644 --- a/app/ios/App/CapApp-SPM/Package.swift +++ b/app/ios/App/CapApp-SPM/Package.swift @@ -13,6 +13,7 @@ let package = Package( dependencies: [ .package(url: "https://github.com/ionic-team/capacitor-swift-pm.git", exact: "8.5.3"), .package(name: "CapacitorFilesystem", path: "../../../../node_modules/@capacitor/filesystem"), + .package(name: "CapacitorGeolocation", path: "../../../../node_modules/@capacitor/geolocation"), .package(name: "CapacitorHaptics", path: "../../../../node_modules/@capacitor/haptics"), .package(name: "CapacitorLocalNotifications", path: "../../../../node_modules/@capacitor/local-notifications") ], @@ -23,6 +24,7 @@ let package = Package( .product(name: "Capacitor", package: "capacitor-swift-pm"), .product(name: "Cordova", package: "capacitor-swift-pm"), .product(name: "CapacitorFilesystem", package: "CapacitorFilesystem"), + .product(name: "CapacitorGeolocation", package: "CapacitorGeolocation"), .product(name: "CapacitorHaptics", package: "CapacitorHaptics"), .product(name: "CapacitorLocalNotifications", package: "CapacitorLocalNotifications") ] diff --git a/app/src/native.ts b/app/src/native.ts index 260409a..41278e0 100644 --- a/app/src/native.ts +++ b/app/src/native.ts @@ -4,22 +4,20 @@ */ import { Capacitor } from "@capacitor/core"; +import { Geolocation } from "@capacitor/geolocation"; /** Whether the page runs inside the app from the stores. */ export const native = Capacitor.isNativePlatform(); -/** Where the phone is, as [latitude, longitude]. Throws if it can't tell (or may not). */ -export function locate(): Promise<[number, number]> { - return new Promise((resolve, reject) => { - if (!navigator.geolocation) { - reject(new Error("no geolocation")); - return; - } - // coarse is enough for the weather, and quicker - navigator.geolocation.getCurrentPosition( - ({ coords }) => resolve([coords.latitude, coords.longitude]), - (error) => reject(new Error(error.message)), - { enableHighAccuracy: false, timeout: 15_000, maximumAge: 3600_000 }, - ); - }); +/** + * Where the phone is, as [latitude, longitude]. Throws if it can't tell (or may not). + * + * In the app through Capacitor's Geolocation, not the WebView's navigator.geolocation: on iOS the + * WebView would ask a second time on its own ("localhost would like to use your location"), and + * on Android it would also ask for the precise location. Approximate is enough for the weather. + */ +export async function locate(): Promise<[number, number]> { + const { coords } = await Geolocation.getCurrentPosition( + { enableHighAccuracy: false, timeout: 15_000, maximumAge: 3600_000 }); + return [coords.latitude, coords.longitude]; } diff --git a/package-lock.json b/package-lock.json index cec476a..db8a5d1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "dependencies": { "@capacitor/core": "8.5.3", "@capacitor/filesystem": "8.1.4", + "@capacitor/geolocation": "8.2.4", "@capacitor/haptics": "8.0.2", "@capacitor/local-notifications": "8.3.1" }, @@ -90,6 +91,18 @@ "@capacitor/core": ">=8.0.0" } }, + "node_modules/@capacitor/geolocation": { + "version": "8.2.4", + "resolved": "https://registry.npmjs.org/@capacitor/geolocation/-/geolocation-8.2.4.tgz", + "integrity": "sha512-FDcUiPQc2AXSTAV61QSDPXml11guKBx5HPEkdWvUqw7kMqrc2Ucp0bsWngeLyRs/01a5aWBI0hEuvetuF3PsrQ==", + "license": "MIT", + "dependencies": { + "@capacitor/synapse": "^1.0.4" + }, + "peerDependencies": { + "@capacitor/core": ">=8.0.0" + } + }, "node_modules/@capacitor/haptics": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@capacitor/haptics/-/haptics-8.0.2.tgz", diff --git a/package.json b/package.json index b0848f4..6a5ac04 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "dependencies": { "@capacitor/core": "8.5.3", "@capacitor/filesystem": "8.1.4", + "@capacitor/geolocation": "8.2.4", "@capacitor/haptics": "8.0.2", "@capacitor/local-notifications": "8.3.1" } From d4924f5586e653357471d3f4ab282cd2f3924a01 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Oct 2026 04:52:42 +0000 Subject: [PATCH 3/5] CONTRIBUTING.md: contributions come with permission to offer the app in the stores App store terms add restrictions the GPL alone does not allow; without this permission, code from others could not ship in the App Store. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01JKfrmz9N5BvxMtYJYzBjEF --- CONTRIBUTING.md | 38 ++++++++++++++++++++++++++++++++++++++ README.md | 4 +++- 2 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..0ee9ff3 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,38 @@ +# Contributing to Blobling + +Ideas, bug reports and pull requests are welcome. Ideas and the roadmap live in +the [issues](https://github.com/mLampir/blobling/issues); how the game works is +in [docs/gameplay.md](docs/gameplay.md), and the notes in [CLAUDE.md](CLAUDE.md) +list the rules the project keeps (English only, texts in the language files, +sprites from the script, and so on). + +Before you open a pull request, run the tests and the type check: + +```bash +npm test && npm run check +``` + +## The license of your contribution + +Blobling is licensed under the GNU General Public License, version 2 or (at +your option) any later version ([LICENSE](LICENSE)). The phone app is also +offered in Apple's App Store and Google Play, whose terms of use add +restrictions the GPL alone does not allow. So the project's own code carries +one additional permission: + +> **App store permission.** As an additional permission to the GNU General +> Public License, the copyright holders of Blobling allow Lampir +> () to distribute the Blobling app, built from this +> code, through app stores (such as Apple's App Store and Google Play) under +> the terms of use those stores require, even where those terms restrict the +> recipients' rights under the GPL. The source code stays available under the +> GPL in this repository. + +**By opening a pull request, you agree that your contribution is licensed +under the GPL version 2 or later together with this permission**, and that you +have the right to license it that way (it is your own work, or under a license +that allows it). Please don't submit code you copied from elsewhere unless you +say where it comes from and under which license. + +The sprites in `app/pet/assets/` are under their own license +([app/pet/assets/LICENSE.txt](app/pet/assets/LICENSE.txt)). diff --git a/README.md b/README.md index ea72285..d6dc33c 100644 --- a/README.md +++ b/README.md @@ -483,6 +483,7 @@ npm run package -- --version v1.2.0 --repo me/fork # also dist/install.sh for | Path | Purpose | |---|---| | `setup.sh` | Sets up the Pi completely (`--app`: update the app only) | +| `CONTRIBUTING.md` | How to contribute, and the license (with the app store permission) a contribution comes under | | `core/` | The pet: game logic, scene and sprites (`src/`) and their tests (`test/`). It runs anywhere and knows nothing about files or the panel; `api.ts` is the pet's JSON API, `weather.ts` asks Open-Meteo, `notify.ts` sees its next need coming for the app's notifications | | `pi/src/main.ts` | Entry point on the Pi: panel driver, display loop, web server | | `pi/src/display.ts` | The display loop (pet, off, brightness, night mode) | @@ -519,7 +520,8 @@ Copyright (C) 2026 Lampir () - **Code:** GNU General Public License, version 2 or (at your option) any later version, see [LICENSE](LICENSE). The project comes without any - warranty. + warranty. Contributions come with an additional permission that lets the + app be offered in the app stores, see [CONTRIBUTING.md](CONTRIBUTING.md). - **Sprites** in `app/pet/assets/`: [CC BY-NC 4.0](https://creativecommons.org/licenses/by-nc/4.0/), see [app/pet/assets/LICENSE.txt](app/pet/assets/LICENSE.txt). Use, change and From e5a16d87c9f0a0998357db77e8f3529e5ce459a3 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Oct 2026 04:57:09 +0000 Subject: [PATCH 4/5] App: choose the language under More Until one is chosen, the app keeps taking the phone's language. The Pi's own page keeps its language from the settings page. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01JKfrmz9N5BvxMtYJYzBjEF --- app/locales/de.json | 3 ++- app/locales/en.json | 3 ++- app/pet.html | 18 ++++++++++++++++++ app/src/main.ts | 28 +++++++++++++++++++++++++++- docs/translating.md | 4 +++- 5 files changed, 52 insertions(+), 4 deletions(-) diff --git a/app/locales/de.json b/app/locales/de.json index 3178c83..2f12a6d 100644 --- a/app/locales/de.json +++ b/app/locales/de.json @@ -727,7 +727,8 @@ "sound_on": "An", "sound_off": "Aus", "pet": "Pet", - "new_egg_note": "Deine Funde bleiben." + "new_egg_note": "Deine Funde bleiben.", + "language": "Sprache" }, "gestures": { "bow": "nach unten", diff --git a/app/locales/en.json b/app/locales/en.json index b2f227e..1a215ce 100644 --- a/app/locales/en.json +++ b/app/locales/en.json @@ -727,7 +727,8 @@ "sound_on": "On", "sound_off": "Off", "pet": "Pet", - "new_egg_note": "Your finds stay." + "new_egg_note": "Your finds stay.", + "language": "Language" }, "gestures": { "bow": "down", diff --git a/app/pet.html b/app/pet.html index 7448736..8bf13f6 100644 --- a/app/pet.html +++ b/app/pet.html @@ -479,6 +479,10 @@

+ - @@ -2203,20 +2199,6 @@

})); showSound(); -// The language of the page (the Pi's own page has it on the settings page instead) -const languages = app.languages(); -if (languages) { - for (const [code, name] of Object.entries(languages)) { - const button = document.createElement("button"); - button.textContent = name; // each in its own language, so it can be found in any - button.lang = code; - button.setAttribute("aria-pressed", code === app.language); - button.addEventListener("click", () => { if (code !== app.language) app.setLanguage(code); }); - $("languages").append(button); - } - $("language-choice").hidden = false; -} - if (app.mode === "local") { // The pet lives on the phone: "More" has where it lives (for the weather and the sun) $("on-phone").hidden = false; @@ -2236,6 +2218,8 @@

const de = LANG.startsWith("de") ? ".de" : ""; $("privacy-link").href = `${docs}privacy${de}`; $("support-link").href = `${docs}support${de}`; + $("licenses-link").href = `${docs}licenses`; + if (app.version) $("version").textContent = ` · ${t("page.app.version", { version: app.version })}`; $("legal").hidden = false; } // The Pi's own page shows the display part only for its update diff --git a/app/src/main.ts b/app/src/main.ts index 3cf34b8..8669642 100644 --- a/app/src/main.ts +++ b/app/src/main.ts @@ -29,7 +29,6 @@ import { pageSounds, type SoundName } from "./sound"; const PI_KEY = "pi"; // the address of the Pi the app is connected to const LIVE_KEY = "location.live"; // "1": the pet lives wherever the phone is -const LANGUAGE_KEY = "language"; // the language chosen under "More"; without one, the phone's const LIVE_EVERY_MS = 30 * 60_000; // how often the live location is looked up again, at most function stored(key: string): string | null { @@ -47,11 +46,8 @@ const servedByPi = root.dataset.host === "pi"; const connectedTo = !servedByPi && native ? stored(PI_KEY) : null; const mode = servedByPi || connectedTo ? "pi" : "local"; -// The Pi's page comes in the Pi's language (its answers are in that one too); otherwise the one -// chosen under "More", or the phone's -const chosenLanguage = stored(LANGUAGE_KEY); +// The Pi's page comes in the Pi's language (its answers are in that one too); otherwise the phone's const language = servedByPi ? root.lang - : chosenLanguage && chosenLanguage in locales ? chosenLanguage : preferredLanguage(navigator.languages?.join(",") || navigator.language, Object.keys(locales)) ?? DEFAULT_LANGUAGE; const i18n = new I18n(locales, language); @@ -84,6 +80,8 @@ const app = { mode, /** The page is the app from the stores. */ native, + /** The app's version in a build for the stores, "" otherwise. */ + version: __APP_VERSION__, /** The Pi's address, if the app is connected to one. */ connectedTo, /** All texts in the page's language, {key: text}. */ @@ -106,28 +104,6 @@ const app = { sounds.play(name); }, - /** The languages the page can be in, as {code: the language's own name}; null on the Pi's page, which has its own. */ - languages(): Record | null { - if (servedByPi) { - return null; - } - return Object.fromEntries(Object.entries(locales).map(([code, texts]) => - [code, (texts as { language?: { name?: string } }).language?.name ?? code])); - }, - - /** The page's language. */ - language: i18n.language, - - /** Keeps `code` as the page's language from now on and shows the page in it. */ - setLanguage(code: string): void { - try { - localStorage.setItem(LANGUAGE_KEY, code); - } catch { - return; // it would not last: stay as it is - } - location.reload(); - }, - /** Whether the sounds are on. */ soundOn(): boolean { return sounds.on; diff --git a/app/src/virtual.d.ts b/app/src/virtual.d.ts index b8486c5..61e92b4 100644 --- a/app/src/virtual.d.ts +++ b/app/src/virtual.d.ts @@ -17,3 +17,6 @@ declare module "blobling:sounds" { const sounds: Record; export default sounds; } + +/** The app's version ("1.2.0") in a build for the stores, "" otherwise (tools/build.mjs). */ +declare const __APP_VERSION__: string; diff --git a/app/test/languages.test.ts b/app/test/languages.test.ts new file mode 100644 index 0000000..89e441a --- /dev/null +++ b/app/test/languages.test.ts @@ -0,0 +1,22 @@ +// The phone's settings offer the app's languages from two lists of their own; both have to name +// every language file in app/locales/. + +import assert from "node:assert/strict"; +import { readFileSync, readdirSync } from "node:fs"; +import { test } from "node:test"; + +const files = readdirSync("app/locales").filter((name) => name.endsWith(".json")) + .map((name) => name.slice(0, -".json".length)).sort(); + +test("Android's locales_config.xml names every language file", () => { + const config = readFileSync("app/android/app/src/main/res/xml/locales_config.xml", "utf-8"); + const names = [...config.matchAll(/ match[1]).sort(); + assert.deepEqual(names, files); +}); + +test("iOS's CFBundleLocalizations names every language file", () => { + const plist = readFileSync("app/ios/App/App/Info.plist", "utf-8"); + const list = plist.match(/CFBundleLocalizations<\/key>\s*([\s\S]*?)<\/array>/)?.[1] ?? ""; + const names = [...list.matchAll(/([\w-]+)<\/string>/g)].map((match) => match[1]).sort(); + assert.deepEqual(names, files); +}); diff --git a/docs/licenses.md b/docs/licenses.md new file mode 100644 index 0000000..fa5664e --- /dev/null +++ b/docs/licenses.md @@ -0,0 +1,63 @@ +# Blobling – Licenses + +Blobling is free software under the GNU General Public License, version 2 or +(at your option) any later version. Its source code, the sprites and the +sounds are at ; the sprites are under +CC BY-NC 4.0 (see `app/pet/assets/LICENSE.txt` there). + +The app from the stores is built with the following open-source parts, under +their own licenses. + +## Weather data + +Weather data by [Open-Meteo.com](https://open-meteo.com/), under +[CC BY 4.0](https://creativecommons.org/licenses/by/4.0/). + +## MIT License + +- **Capacitor** (`@capacitor/core`, `@capacitor/android`, `@capacitor/ios`): + Copyright (c) 2017-present Drifty Co. +- **Capacitor plugins** Filesystem, Geolocation, Haptics and Local + Notifications: Copyright (c) 2020-present Ionic +- **ion-android-filesystem, ion-ios-filesystem, ion-android-geolocation, + ion-ios-geolocation**: Copyright (c) OutSystems / Ionic + +``` +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +``` + +## Apache License 2.0 + +- **Apache Cordova** (Android framework, part of Capacitor's Cordova + support): Copyright The Apache Software Foundation +- **AndroidX** libraries (AppCompat, Core, Activity, Fragment, + CoordinatorLayout, WebKit, SplashScreen): Copyright The Android Open Source + Project +- **Kotlin** standard library and **kotlinx.coroutines**: Copyright JetBrains + s.r.o. and Kotlin Programming Language contributors +- **Gson**: Copyright Google LLC + +Licensed under the Apache License, Version 2.0: +. Distributed on an "AS IS" +basis, without warranties or conditions of any kind. + +## Google Play services (Android) + +The Android app uses Google Play services for the location, under the +[Android Software Development Kit License](https://developer.android.com/studio/terms). diff --git a/docs/translating.md b/docs/translating.md index 91a1e9b..085a679 100644 --- a/docs/translating.md +++ b/docs/translating.md @@ -20,9 +20,10 @@ language = de Times and dates on the pet page follow the phone's own settings, not this option. The app on the phone takes the phone's language, if there is a file -for it, until a language is chosen under *More*; the pet page the Pi serves -comes in the Pi's language. A new file shows up under *More* by itself, with -the name from its `language.name`. +for it, or the language chosen for the app in the phone's settings (Android +13 and later: *Settings → Apps → Blobling → Language*; iOS: *Settings → +Blobling → Language*, shown once the phone has more than one language in its +list). The pet page the Pi serves comes in the Pi's language. ## Adding a language @@ -34,7 +35,10 @@ the name from its `language.name`. 3. Restart the app (`sudo systemctl restart led-matrix`), then choose the language on the settings page. -No code changes are needed. A few things to know while translating: +For the app from the stores, also add the code to the two lists the phones' +settings take the app's languages from: `app/android/app/src/main/res/xml/locales_config.xml` +and `CFBundleLocalizations` in `app/ios/App/App/Info.plist` (a test checks +both). No other code changes are needed. A few things to know while translating: - **Placeholders** in curly braces, such as `{name}` or `{time}`, are filled in by the app. Keep them, but move them wherever your language needs them. diff --git a/tools/build.mjs b/tools/build.mjs index bf276d1..4c083f6 100644 --- a/tools/build.mjs +++ b/tools/build.mjs @@ -66,7 +66,9 @@ await build({ ...shared, entryPoints: ["pi/src/main.ts"], platform: "node", targ outfile: "dist/pi/server.mjs" }); await build({ ...shared, entryPoints: ["app/src/main.ts"], platform: "browser", format: "iife", minify: true, sourcemap: false, // what the phones of the last few years run (Android WebView, iOS Safari 15) - target: ["es2020", "chrome90", "safari15"], plugins: [appFiles], outfile: "dist/app/app.js" }); + target: ["es2020", "chrome90", "safari15"], plugins: [appFiles], outfile: "dist/app/app.js", + // the version from the release tag when Codemagic builds the app for the stores (codemagic.yaml), else "" + define: { __APP_VERSION__: JSON.stringify(process.env.VERSION ?? "") } }); mkdirSync("dist/app", { recursive: true }); copyFileSync("app/pet.html", "dist/app/index.html"); await build({ ...core, platform: "node", target: "node22", outfile: "dist/core.node.mjs" });