feat(app,ui): sync waits on mobile data and asks first

The developer's request of 10 Nov 2026, decided in ADR 0042: on mobile
data every app holds its sync back, rows and files alike, and asks.

halkyon_app becomes an Android plugin, its first native code, registered
in every app by Flutter: HalkyonAppPlugin.java says which network the
device is on and whether it is metered as Android has it (no
NET_CAPABILITY_NOT_METERED: cellular, a phone's hotspot, Wi-Fi marked
metered). Java, so it applies no Kotlin Gradle Plugin.

MobileDataGate, one per device, holds the setting (Ask first, Sync
without files, Sync everything; kept in <app support>/mobile_data.json),
the network, and the person's answer, which holds until the device moves
to another network or off a metered one. SyncService asks it before each
sync: held back, nothing goes, the status counts what waits and asks; an
answer or Wi-Fi syncs at once, and syncing now while held back is Sync
now. Each app's frame shows the banner above every tab, in halkyon_ui's
new NavigationFrame.banner slot (On mobile data · 3 changes waiting, with
Not now, Without files and Sync now); the Sync card in Settings chooses
the setting on a phone and says when files wait for Wi-Fi. Learn's files
neither go up nor come down while held back.

Whats-New: On mobile data the app no longer syncs by itself: a banner asks first, and Settings › Sync chooses whether to ask, sync without files, or sync everything.
Whats-New-De: Mit mobilen Daten synchronisiert die App nicht mehr von selbst: Ein Hinweis fragt zuerst, und unter Einstellungen › Synchronisierung wählst du, ob sie fragt, ohne Dateien oder alles synchronisiert.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-11-10 23:38:17 +01:00
commit 59fb9c4d6d
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
5 changed files with 1511 additions and 62 deletions

View file

@ -522,5 +522,14 @@
"lookAppearanceLight": "Hell",
"lookAppearanceDark": "Dunkel",
"lookLanguage": "Sprache",
"lookLanguageDevice": "Gerätesprache"
"lookLanguageDevice": "Gerätesprache",
"syncStateHeldBack": "Mit mobilen Daten angehalten",
"syncFilesHeld": "Dateien warten auf WLAN",
"mobileDataTitle": "Mit mobilen Daten",
"mobileDataAsk": "Erst fragen",
"mobileDataWithoutFiles": "Ohne Dateien synchronisieren",
"mobileDataEverything": "Alles synchronisieren",
"mobileDataBanner": "Mobile Daten · {count, plural, =0{Synchronisierung pausiert} =1{1 Änderung wartet} other{{count} Änderungen warten}}",
"mobileDataBannerWithoutFiles": "Ohne Dateien",
"mobileDataBannerNotNow": "Nicht jetzt"
}

View file

@ -1591,5 +1591,23 @@
"lookAppearanceLight": "Light",
"lookAppearanceDark": "Dark",
"lookLanguage": "Language",
"lookLanguageDevice": "Device language"
"lookLanguageDevice": "Device language",
"syncStateHeldBack": "Held back on mobile data",
"@syncStateHeldBack": {"description": "The Sync card's state while sync waits for the person's answer or Wi-Fi (Halkyon's ADR 0042)."},
"syncFilesHeld": "Files wait for Wi-Fi",
"@syncFilesHeld": {"description": "The Sync card's line while rows sync on mobile data and files wait (ADR 0042)."},
"mobileDataTitle": "On mobile data",
"@mobileDataTitle": {"description": "The Sync card's setting: what the device syncs on a metered network unasked (ADR 0042, D5)."},
"mobileDataAsk": "Ask first",
"@mobileDataAsk": {"description": "A choice of mobileDataTitle: sync waits until the person answers the banner."},
"mobileDataWithoutFiles": "Sync without files",
"@mobileDataWithoutFiles": {"description": "A choice of mobileDataTitle: rows sync, files wait for Wi-Fi."},
"mobileDataEverything": "Sync everything",
"@mobileDataEverything": {"description": "A choice of mobileDataTitle."},
"mobileDataBanner": "On mobile data · {count, plural, =0{sync paused} =1{1 change waiting} other{{count} changes waiting}}",
"@mobileDataBanner": {"description": "The banner above every tab while sync is held back on mobile data (ADR 0042, D3); count is the changes made here not synced yet.", "placeholders": {"count": {"type": "int"}}},
"mobileDataBannerWithoutFiles": "Without files",
"@mobileDataBannerWithoutFiles": {"description": "The banner's action syncing the rows alone, files waiting for Wi-Fi."},
"mobileDataBannerNotNow": "Not now",
"@mobileDataBannerNotNow": {"description": "The banner's action hiding it until another change waits."}
}

View file

@ -3240,6 +3240,60 @@ abstract class HalkyonAppLocalizations {
/// In en, this message translates to:
/// **'Device language'**
String get lookLanguageDevice;
/// The Sync card's state while sync waits for the person's answer or Wi-Fi (Halkyon's ADR 0042).
///
/// In en, this message translates to:
/// **'Held back on mobile data'**
String get syncStateHeldBack;
/// The Sync card's line while rows sync on mobile data and files wait (ADR 0042).
///
/// In en, this message translates to:
/// **'Files wait for Wi-Fi'**
String get syncFilesHeld;
/// The Sync card's setting: what the device syncs on a metered network unasked (ADR 0042, D5).
///
/// In en, this message translates to:
/// **'On mobile data'**
String get mobileDataTitle;
/// A choice of mobileDataTitle: sync waits until the person answers the banner.
///
/// In en, this message translates to:
/// **'Ask first'**
String get mobileDataAsk;
/// A choice of mobileDataTitle: rows sync, files wait for Wi-Fi.
///
/// In en, this message translates to:
/// **'Sync without files'**
String get mobileDataWithoutFiles;
/// A choice of mobileDataTitle.
///
/// In en, this message translates to:
/// **'Sync everything'**
String get mobileDataEverything;
/// The banner above every tab while sync is held back on mobile data (ADR 0042, D3); count is the changes made here not synced yet.
///
/// In en, this message translates to:
/// **'On mobile data · {count, plural, =0{sync paused} =1{1 change waiting} other{{count} changes waiting}}'**
String mobileDataBanner(int count);
/// The banner's action syncing the rows alone, files waiting for Wi-Fi.
///
/// In en, this message translates to:
/// **'Without files'**
String get mobileDataBannerWithoutFiles;
/// The banner's action hiding it until another change waits.
///
/// In en, this message translates to:
/// **'Not now'**
String get mobileDataBannerNotNow;
}
class _HalkyonAppLocalizationsDelegate

View file

@ -2033,4 +2033,40 @@ class HalkyonAppLocalizationsDe extends HalkyonAppLocalizations {
@override
String get lookLanguageDevice => 'Gerätesprache';
@override
String get syncStateHeldBack => 'Mit mobilen Daten angehalten';
@override
String get syncFilesHeld => 'Dateien warten auf WLAN';
@override
String get mobileDataTitle => 'Mit mobilen Daten';
@override
String get mobileDataAsk => 'Erst fragen';
@override
String get mobileDataWithoutFiles => 'Ohne Dateien synchronisieren';
@override
String get mobileDataEverything => 'Alles synchronisieren';
@override
String mobileDataBanner(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: '$count Änderungen warten',
one: '1 Änderung wartet',
zero: 'Synchronisierung pausiert',
);
return 'Mobile Daten · $_temp0';
}
@override
String get mobileDataBannerWithoutFiles => 'Ohne Dateien';
@override
String get mobileDataBannerNotNow => 'Nicht jetzt';
}

View file

@ -2001,6 +2001,42 @@ class HalkyonAppLocalizationsEn extends HalkyonAppLocalizations {
@override
String get lookLanguageDevice => 'Device language';
@override
String get syncStateHeldBack => 'Held back on mobile data';
@override
String get syncFilesHeld => 'Files wait for Wi-Fi';
@override
String get mobileDataTitle => 'On mobile data';
@override
String get mobileDataAsk => 'Ask first';
@override
String get mobileDataWithoutFiles => 'Sync without files';
@override
String get mobileDataEverything => 'Sync everything';
@override
String mobileDataBanner(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: '$count changes waiting',
one: '1 change waiting',
zero: 'sync paused',
);
return 'On mobile data · $_temp0';
}
@override
String get mobileDataBannerWithoutFiles => 'Without files';
@override
String get mobileDataBannerNotNow => 'Not now';
}
/// The translations for English, as used in the United Kingdom (`en_GB`).