fix(block2): sharing config UX + discovery + labels (raw feedback batch)

From on-device feedback:
- Area is no longer shown as a raw geohash 'name'. 'Use my location' is the
  primary action; a human status line says whether your area is set (coarse,
  never exact). The geohash code + the relay servers moved under 'Advanced'
  (code field labelled 'a code like sp3e9 — not a place name'), which also
  brings back node/relay selection that had been fully hidden.
- Location denial no longer yanks you into system settings — returns quietly
  with an actionable inline message.
- Discovery no longer spins forever when nobody's sharing nearby: a timeout
  resolves 'searching' to the empty state.
- Home card 'Open market' -> 'Market' + 'Discover and share seeds nearby'
  (the word 'open' confused).

i18n en/es/pt; analyzer clean; the use-my-location test asserts the status flip.
This commit is contained in:
vjrj 2026-07-10 13:32:11 +02:00
parent d7136ec2c2
commit 5f87ad2d56
11 changed files with 208 additions and 90 deletions

View file

@ -12,8 +12,8 @@
}, },
"home": { "home": {
"tagline": "Share and grow local seeds", "tagline": "Share and grow local seeds",
"openMarket": "Open market", "openMarket": "Market",
"openMarketSubtitle": "Browse and exchange", "openMarketSubtitle": "Discover and share seeds nearby",
"yourInventory": "Your inventory", "yourInventory": "Your inventory",
"yourInventorySubtitle": "Manage your seeds" "yourInventorySubtitle": "Manage your seeds"
}, },
@ -348,8 +348,13 @@
"setupIntro": "Sharing with people nearby is optional. Just set your rough area — you're already connected to shared community servers so people can find what you offer, with no company in the middle.", "setupIntro": "Sharing with people nearby is optional. Just set your rough area — you're already connected to shared community servers so people can find what you offer, with no company in the middle.",
"areaLabel": "Your area", "areaLabel": "Your area",
"areaHelp": "Kept rough on purpose — your zone, never an exact spot.", "areaHelp": "Kept rough on purpose — your zone, never an exact spot.",
"areaSet": "Your area is set — kept coarse, never your exact spot",
"areaNotSet": "Area not set yet — use your location, or add a code under Advanced",
"advanced": "Advanced",
"areaCodeLabel": "Area code",
"areaCodeHint": "A short code like sp3e9 — not a place name",
"serversLabel": "Community servers", "serversLabel": "Community servers",
"serversHelp": "Pick one or more shared servers. Anyone can use them — or add your own community's below.", "serversHelp": "One address per line (wss://…). Leave the defaults if unsure.",
"serversAdvanced": "Add another server", "serversAdvanced": "Add another server",
"save": "Save", "save": "Save",
"saved": "Saved", "saved": "Saved",

View file

@ -12,8 +12,8 @@
}, },
"home": { "home": {
"tagline": "Comparte y cultiva semillas locales", "tagline": "Comparte y cultiva semillas locales",
"openMarket": "Mercado abierto", "openMarket": "Mercado",
"openMarketSubtitle": "Explora e intercambia", "openMarketSubtitle": "Descubre y comparte semillas cerca",
"yourInventory": "Tu inventario", "yourInventory": "Tu inventario",
"yourInventorySubtitle": "Gestiona tus semillas" "yourInventorySubtitle": "Gestiona tus semillas"
}, },
@ -348,8 +348,13 @@
"setupIntro": "Compartir con gente cercana es opcional. Solo indica tu zona aproximada — ya estás conectado a servidores comunitarios compartidos para que otras personas encuentren lo que ofreces, sin ninguna empresa en medio.", "setupIntro": "Compartir con gente cercana es opcional. Solo indica tu zona aproximada — ya estás conectado a servidores comunitarios compartidos para que otras personas encuentren lo que ofreces, sin ninguna empresa en medio.",
"areaLabel": "Tu zona", "areaLabel": "Tu zona",
"areaHelp": "Se mantiene aproximado a propósito — tu zona, nunca un punto exacto.", "areaHelp": "Se mantiene aproximado a propósito — tu zona, nunca un punto exacto.",
"areaSet": "Tu zona está puesta — aproximada, nunca tu punto exacto",
"areaNotSet": "Zona sin definir — usa tu ubicación, o añade un código en Avanzado",
"advanced": "Avanzado",
"areaCodeLabel": "Código de zona",
"areaCodeHint": "Un código corto como sp3e9 — no un nombre de lugar",
"serversLabel": "Servidores de la comunidad", "serversLabel": "Servidores de la comunidad",
"serversHelp": "Elige uno o más servidores compartidos. Cualquiera puede usarlos — o añade abajo el de tu comunidad.", "serversHelp": "Una dirección por línea (wss://…). Déjalo así si no sabes.",
"serversAdvanced": "Añadir otro servidor", "serversAdvanced": "Añadir otro servidor",
"save": "Guardar", "save": "Guardar",
"saved": "Guardado", "saved": "Guardado",

View file

@ -12,8 +12,8 @@
}, },
"home": { "home": {
"tagline": "Partilha e cultiva sementes locais", "tagline": "Partilha e cultiva sementes locais",
"openMarket": "Mercado aberto", "openMarket": "Mercado",
"openMarketSubtitle": "Descobre e troca", "openMarketSubtitle": "Descobre e partilha sementes por perto",
"yourInventory": "O teu inventário", "yourInventory": "O teu inventário",
"yourInventorySubtitle": "Gere as tuas sementes" "yourInventorySubtitle": "Gere as tuas sementes"
}, },
@ -344,8 +344,13 @@
"setupIntro": "Partilhar com pessoas por perto é opcional. Basta indicar a tua zona aproximada — já estás ligado a servidores comunitários partilhados para que outras pessoas encontrem o que ofereces, sem nenhuma empresa no meio.", "setupIntro": "Partilhar com pessoas por perto é opcional. Basta indicar a tua zona aproximada — já estás ligado a servidores comunitários partilhados para que outras pessoas encontrem o que ofereces, sem nenhuma empresa no meio.",
"areaLabel": "A tua zona", "areaLabel": "A tua zona",
"areaHelp": "Mantém-se aproximado de propósito — a tua zona, nunca um ponto exato.", "areaHelp": "Mantém-se aproximado de propósito — a tua zona, nunca um ponto exato.",
"areaSet": "A tua zona está definida — aproximada, nunca o teu ponto exato",
"areaNotSet": "Zona por definir — usa a tua localização, ou adiciona um código em Avançado",
"advanced": "Avançado",
"areaCodeLabel": "Código de zona",
"areaCodeHint": "Um código curto como sp3e9 — não um nome de lugar",
"serversLabel": "Servidores da comunidade", "serversLabel": "Servidores da comunidade",
"serversHelp": "Escolhe um ou mais servidores partilhados. Qualquer pessoa os pode usar — ou adiciona abaixo o da tua comunidade.", "serversHelp": "Um endereço por linha (wss://…). Deixa assim se não souberes.",
"serversAdvanced": "Adicionar outro servidor", "serversAdvanced": "Adicionar outro servidor",
"save": "Guardar", "save": "Guardar",
"saved": "Guardado", "saved": "Guardado",

View file

@ -4,9 +4,9 @@
/// To regenerate, run: `dart run slang` /// To regenerate, run: `dart run slang`
/// ///
/// Locales: 3 /// Locales: 3
/// Strings: 1043 (347 per locale) /// Strings: 1058 (352 per locale)
/// ///
/// Built on 2026-07-10 at 11:05 UTC /// Built on 2026-07-10 at 11:31 UTC
// coverage:ignore-file // coverage:ignore-file
// ignore_for_file: type=lint, unused_import // ignore_for_file: type=lint, unused_import

View file

@ -126,11 +126,11 @@ class Translations$home$en {
/// en: 'Share and grow local seeds' /// en: 'Share and grow local seeds'
String get tagline => 'Share and grow local seeds'; String get tagline => 'Share and grow local seeds';
/// en: 'Open market' /// en: 'Market'
String get openMarket => 'Open market'; String get openMarket => 'Market';
/// en: 'Browse and exchange' /// en: 'Discover and share seeds nearby'
String get openMarketSubtitle => 'Browse and exchange'; String get openMarketSubtitle => 'Discover and share seeds nearby';
/// en: 'Your inventory' /// en: 'Your inventory'
String get yourInventory => 'Your inventory'; String get yourInventory => 'Your inventory';
@ -1107,11 +1107,26 @@ class Translations$market$en {
/// en: 'Kept rough on purpose — your zone, never an exact spot.' /// en: 'Kept rough on purpose — your zone, never an exact spot.'
String get areaHelp => 'Kept rough on purpose — your zone, never an exact spot.'; String get areaHelp => 'Kept rough on purpose — your zone, never an exact spot.';
/// en: 'Your area is set — kept coarse, never your exact spot'
String get areaSet => 'Your area is set — kept coarse, never your exact spot';
/// en: 'Area not set yet — use your location, or add a code under Advanced'
String get areaNotSet => 'Area not set yet — use your location, or add a code under Advanced';
/// en: 'Advanced'
String get advanced => 'Advanced';
/// en: 'Area code'
String get areaCodeLabel => 'Area code';
/// en: 'A short code like sp3e9 — not a place name'
String get areaCodeHint => 'A short code like sp3e9 — not a place name';
/// en: 'Community servers' /// en: 'Community servers'
String get serversLabel => 'Community servers'; String get serversLabel => 'Community servers';
/// en: 'Pick one or more shared servers. Anyone can use them — or add your own community's below.' /// en: 'One address per line (wss://…). Leave the defaults if unsure.'
String get serversHelp => 'Pick one or more shared servers. Anyone can use them — or add your own community\'s below.'; String get serversHelp => 'One address per line (wss://…). Leave the defaults if unsure.';
/// en: 'Add another server' /// en: 'Add another server'
String get serversAdvanced => 'Add another server'; String get serversAdvanced => 'Add another server';
@ -1726,8 +1741,8 @@ extension on Translations {
'common.type' => 'Type', 'common.type' => 'Type',
'common.comingSoon' => 'Coming soon', 'common.comingSoon' => 'Coming soon',
'home.tagline' => 'Share and grow local seeds', 'home.tagline' => 'Share and grow local seeds',
'home.openMarket' => 'Open market', 'home.openMarket' => 'Market',
'home.openMarketSubtitle' => 'Browse and exchange', 'home.openMarketSubtitle' => 'Discover and share seeds nearby',
'home.yourInventory' => 'Your inventory', 'home.yourInventory' => 'Your inventory',
'home.yourInventorySubtitle' => 'Manage your seeds', 'home.yourInventorySubtitle' => 'Manage your seeds',
'photo.camera' => 'Take a photo', 'photo.camera' => 'Take a photo',
@ -2028,8 +2043,13 @@ extension on Translations {
'market.setupIntro' => 'Sharing with people nearby is optional. Just set your rough area — you\'re already connected to shared community servers so people can find what you offer, with no company in the middle.', 'market.setupIntro' => 'Sharing with people nearby is optional. Just set your rough area — you\'re already connected to shared community servers so people can find what you offer, with no company in the middle.',
'market.areaLabel' => 'Your area', 'market.areaLabel' => 'Your area',
'market.areaHelp' => 'Kept rough on purpose — your zone, never an exact spot.', 'market.areaHelp' => 'Kept rough on purpose — your zone, never an exact spot.',
'market.areaSet' => 'Your area is set — kept coarse, never your exact spot',
'market.areaNotSet' => 'Area not set yet — use your location, or add a code under Advanced',
'market.advanced' => 'Advanced',
'market.areaCodeLabel' => 'Area code',
'market.areaCodeHint' => 'A short code like sp3e9 — not a place name',
'market.serversLabel' => 'Community servers', 'market.serversLabel' => 'Community servers',
'market.serversHelp' => 'Pick one or more shared servers. Anyone can use them — or add your own community\'s below.', 'market.serversHelp' => 'One address per line (wss://…). Leave the defaults if unsure.',
'market.serversAdvanced' => 'Add another server', 'market.serversAdvanced' => 'Add another server',
'market.save' => 'Save', 'market.save' => 'Save',
'market.saved' => 'Saved', 'market.saved' => 'Saved',

View file

@ -108,8 +108,8 @@ class _Translations$home$es extends Translations$home$en {
// Translations // Translations
@override String get tagline => 'Comparte y cultiva semillas locales'; @override String get tagline => 'Comparte y cultiva semillas locales';
@override String get openMarket => 'Mercado abierto'; @override String get openMarket => 'Mercado';
@override String get openMarketSubtitle => 'Explora e intercambia'; @override String get openMarketSubtitle => 'Descubre y comparte semillas cerca';
@override String get yourInventory => 'Tu inventario'; @override String get yourInventory => 'Tu inventario';
@override String get yourInventorySubtitle => 'Gestiona tus semillas'; @override String get yourInventorySubtitle => 'Gestiona tus semillas';
} }
@ -622,8 +622,13 @@ class _Translations$market$es extends Translations$market$en {
@override String get setupIntro => 'Compartir con gente cercana es opcional. Solo indica tu zona aproximada — ya estás conectado a servidores comunitarios compartidos para que otras personas encuentren lo que ofreces, sin ninguna empresa en medio.'; @override String get setupIntro => 'Compartir con gente cercana es opcional. Solo indica tu zona aproximada — ya estás conectado a servidores comunitarios compartidos para que otras personas encuentren lo que ofreces, sin ninguna empresa en medio.';
@override String get areaLabel => 'Tu zona'; @override String get areaLabel => 'Tu zona';
@override String get areaHelp => 'Se mantiene aproximado a propósito — tu zona, nunca un punto exacto.'; @override String get areaHelp => 'Se mantiene aproximado a propósito — tu zona, nunca un punto exacto.';
@override String get areaSet => 'Tu zona está puesta — aproximada, nunca tu punto exacto';
@override String get areaNotSet => 'Zona sin definir — usa tu ubicación, o añade un código en Avanzado';
@override String get advanced => 'Avanzado';
@override String get areaCodeLabel => 'Código de zona';
@override String get areaCodeHint => 'Un código corto como sp3e9 — no un nombre de lugar';
@override String get serversLabel => 'Servidores de la comunidad'; @override String get serversLabel => 'Servidores de la comunidad';
@override String get serversHelp => 'Elige uno o más servidores compartidos. Cualquiera puede usarlos — o añade abajo el de tu comunidad.'; @override String get serversHelp => 'Una dirección por línea (wss://…). Déjalo así si no sabes.';
@override String get serversAdvanced => 'Añadir otro servidor'; @override String get serversAdvanced => 'Añadir otro servidor';
@override String get save => 'Guardar'; @override String get save => 'Guardar';
@override String get saved => 'Guardado'; @override String get saved => 'Guardado';
@ -1048,8 +1053,8 @@ extension on TranslationsEs {
'common.type' => 'Tipo', 'common.type' => 'Tipo',
'common.comingSoon' => 'Pronto', 'common.comingSoon' => 'Pronto',
'home.tagline' => 'Comparte y cultiva semillas locales', 'home.tagline' => 'Comparte y cultiva semillas locales',
'home.openMarket' => 'Mercado abierto', 'home.openMarket' => 'Mercado',
'home.openMarketSubtitle' => 'Explora e intercambia', 'home.openMarketSubtitle' => 'Descubre y comparte semillas cerca',
'home.yourInventory' => 'Tu inventario', 'home.yourInventory' => 'Tu inventario',
'home.yourInventorySubtitle' => 'Gestiona tus semillas', 'home.yourInventorySubtitle' => 'Gestiona tus semillas',
'photo.camera' => 'Hacer una foto', 'photo.camera' => 'Hacer una foto',
@ -1350,8 +1355,13 @@ extension on TranslationsEs {
'market.setupIntro' => 'Compartir con gente cercana es opcional. Solo indica tu zona aproximada — ya estás conectado a servidores comunitarios compartidos para que otras personas encuentren lo que ofreces, sin ninguna empresa en medio.', 'market.setupIntro' => 'Compartir con gente cercana es opcional. Solo indica tu zona aproximada — ya estás conectado a servidores comunitarios compartidos para que otras personas encuentren lo que ofreces, sin ninguna empresa en medio.',
'market.areaLabel' => 'Tu zona', 'market.areaLabel' => 'Tu zona',
'market.areaHelp' => 'Se mantiene aproximado a propósito — tu zona, nunca un punto exacto.', 'market.areaHelp' => 'Se mantiene aproximado a propósito — tu zona, nunca un punto exacto.',
'market.areaSet' => 'Tu zona está puesta — aproximada, nunca tu punto exacto',
'market.areaNotSet' => 'Zona sin definir — usa tu ubicación, o añade un código en Avanzado',
'market.advanced' => 'Avanzado',
'market.areaCodeLabel' => 'Código de zona',
'market.areaCodeHint' => 'Un código corto como sp3e9 — no un nombre de lugar',
'market.serversLabel' => 'Servidores de la comunidad', 'market.serversLabel' => 'Servidores de la comunidad',
'market.serversHelp' => 'Elige uno o más servidores compartidos. Cualquiera puede usarlos — o añade abajo el de tu comunidad.', 'market.serversHelp' => 'Una dirección por línea (wss://…). Déjalo así si no sabes.',
'market.serversAdvanced' => 'Añadir otro servidor', 'market.serversAdvanced' => 'Añadir otro servidor',
'market.save' => 'Guardar', 'market.save' => 'Guardar',
'market.saved' => 'Guardado', 'market.saved' => 'Guardado',

View file

@ -108,8 +108,8 @@ class _Translations$home$pt extends Translations$home$en {
// Translations // Translations
@override String get tagline => 'Partilha e cultiva sementes locais'; @override String get tagline => 'Partilha e cultiva sementes locais';
@override String get openMarket => 'Mercado aberto'; @override String get openMarket => 'Mercado';
@override String get openMarketSubtitle => 'Descobre e troca'; @override String get openMarketSubtitle => 'Descobre e partilha sementes por perto';
@override String get yourInventory => 'O teu inventário'; @override String get yourInventory => 'O teu inventário';
@override String get yourInventorySubtitle => 'Gere as tuas sementes'; @override String get yourInventorySubtitle => 'Gere as tuas sementes';
} }
@ -618,8 +618,13 @@ class _Translations$market$pt extends Translations$market$en {
@override String get setupIntro => 'Partilhar com pessoas por perto é opcional. Basta indicar a tua zona aproximada — já estás ligado a servidores comunitários partilhados para que outras pessoas encontrem o que ofereces, sem nenhuma empresa no meio.'; @override String get setupIntro => 'Partilhar com pessoas por perto é opcional. Basta indicar a tua zona aproximada — já estás ligado a servidores comunitários partilhados para que outras pessoas encontrem o que ofereces, sem nenhuma empresa no meio.';
@override String get areaLabel => 'A tua zona'; @override String get areaLabel => 'A tua zona';
@override String get areaHelp => 'Mantém-se aproximado de propósito — a tua zona, nunca um ponto exato.'; @override String get areaHelp => 'Mantém-se aproximado de propósito — a tua zona, nunca um ponto exato.';
@override String get areaSet => 'A tua zona está definida — aproximada, nunca o teu ponto exato';
@override String get areaNotSet => 'Zona por definir — usa a tua localização, ou adiciona um código em Avançado';
@override String get advanced => 'Avançado';
@override String get areaCodeLabel => 'Código de zona';
@override String get areaCodeHint => 'Um código curto como sp3e9 — não um nome de lugar';
@override String get serversLabel => 'Servidores da comunidade'; @override String get serversLabel => 'Servidores da comunidade';
@override String get serversHelp => 'Escolhe um ou mais servidores partilhados. Qualquer pessoa os pode usar — ou adiciona abaixo o da tua comunidade.'; @override String get serversHelp => 'Um endereço por linha (wss://…). Deixa assim se não souberes.';
@override String get serversAdvanced => 'Adicionar outro servidor'; @override String get serversAdvanced => 'Adicionar outro servidor';
@override String get save => 'Guardar'; @override String get save => 'Guardar';
@override String get saved => 'Guardado'; @override String get saved => 'Guardado';
@ -1044,8 +1049,8 @@ extension on TranslationsPt {
'common.type' => 'Tipo', 'common.type' => 'Tipo',
'common.comingSoon' => 'Em breve', 'common.comingSoon' => 'Em breve',
'home.tagline' => 'Partilha e cultiva sementes locais', 'home.tagline' => 'Partilha e cultiva sementes locais',
'home.openMarket' => 'Mercado aberto', 'home.openMarket' => 'Mercado',
'home.openMarketSubtitle' => 'Descobre e troca', 'home.openMarketSubtitle' => 'Descobre e partilha sementes por perto',
'home.yourInventory' => 'O teu inventário', 'home.yourInventory' => 'O teu inventário',
'home.yourInventorySubtitle' => 'Gere as tuas sementes', 'home.yourInventorySubtitle' => 'Gere as tuas sementes',
'photo.camera' => 'Tirar uma foto', 'photo.camera' => 'Tirar uma foto',
@ -1342,8 +1347,13 @@ extension on TranslationsPt {
'market.setupIntro' => 'Partilhar com pessoas por perto é opcional. Basta indicar a tua zona aproximada — já estás ligado a servidores comunitários partilhados para que outras pessoas encontrem o que ofereces, sem nenhuma empresa no meio.', 'market.setupIntro' => 'Partilhar com pessoas por perto é opcional. Basta indicar a tua zona aproximada — já estás ligado a servidores comunitários partilhados para que outras pessoas encontrem o que ofereces, sem nenhuma empresa no meio.',
'market.areaLabel' => 'A tua zona', 'market.areaLabel' => 'A tua zona',
'market.areaHelp' => 'Mantém-se aproximado de propósito — a tua zona, nunca um ponto exato.', 'market.areaHelp' => 'Mantém-se aproximado de propósito — a tua zona, nunca um ponto exato.',
'market.areaSet' => 'A tua zona está definida — aproximada, nunca o teu ponto exato',
'market.areaNotSet' => 'Zona por definir — usa a tua localização, ou adiciona um código em Avançado',
'market.advanced' => 'Avançado',
'market.areaCodeLabel' => 'Código de zona',
'market.areaCodeHint' => 'Um código curto como sp3e9 — não um nome de lugar',
'market.serversLabel' => 'Servidores da comunidade', 'market.serversLabel' => 'Servidores da comunidade',
'market.serversHelp' => 'Escolhe um ou mais servidores partilhados. Qualquer pessoa os pode usar — ou adiciona abaixo o da tua comunidade.', 'market.serversHelp' => 'Um endereço por linha (wss://…). Deixa assim se não souberes.',
'market.serversAdvanced' => 'Adicionar outro servidor', 'market.serversAdvanced' => 'Adicionar outro servidor',
'market.save' => 'Guardar', 'market.save' => 'Guardar',
'market.saved' => 'Guardado', 'market.saved' => 'Guardado',

View file

@ -20,16 +20,13 @@ class GeolocatorCoarseLocation implements CoarseLocationProvider {
if (permission == LocationPermission.denied) { if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission(); permission = await Geolocator.requestPermission();
} }
// Give the user a way out of a permanent denial. // Denied (or turned off): return null quietly the UI shows an actionable
if (permission == LocationPermission.deniedForever) { // message. We don't yank the user into system settings.
await Geolocator.openAppSettings(); if (permission == LocationPermission.denied ||
return null; permission == LocationPermission.deniedForever) {
}
if (permission == LocationPermission.denied) return null;
if (!await Geolocator.isLocationServiceEnabled()) {
await Geolocator.openLocationSettings();
return null; return null;
} }
if (!await Geolocator.isLocationServiceEnabled()) return null;
// A fresh coarse fix can take a while (or never come indoors); fall back // A fresh coarse fix can take a while (or never come indoors); fall back
// to the last known position so the button stays responsive. // to the last known position so the button stays responsive.

View file

@ -76,6 +76,7 @@ class OffersCubit extends Cubit<OffersState> {
/// (null in tests, where the transport is a fake with nothing to close). /// (null in tests, where the transport is a fake with nothing to close).
final Future<void> Function()? _onDispose; final Future<void> Function()? _onDispose;
StreamSubscription<Offer>? _sub; StreamSubscription<Offer>? _sub;
Timer? _searchTimeout;
/// Whether a live transport is available (relay configured and reachable). /// Whether a live transport is available (relay configured and reachable).
bool get isOnline => _transport != null; bool get isOnline => _transport != null;
@ -88,6 +89,7 @@ class OffersCubit extends Cubit<OffersState> {
return; return;
} }
await _sub?.cancel(); await _sub?.cancel();
_searchTimeout?.cancel();
emit(OffersState( emit(OffersState(
areaGeohash: geohashPrefix, areaGeohash: geohashPrefix,
searching: true, searching: true,
@ -99,6 +101,14 @@ class OffersCubit extends Cubit<OffersState> {
onError: (Object e) => onError: (Object e) =>
emit(state.copyWith(searching: false, error: () => '$e')), emit(state.copyWith(searching: false, error: () => '$e')),
); );
// The discover stream stays open for live offers and never signals "done",
// so stop the spinner after a beat: no results show the empty state, not
// an endless "searching".
_searchTimeout = Timer(const Duration(seconds: 6), () {
if (!isClosed && state.searching) {
emit(state.copyWith(searching: false));
}
});
} }
/// Publishes [offer]; returns the transport's verdict. No-op result when /// Publishes [offer]; returns the transport's verdict. No-op result when
@ -157,6 +167,7 @@ class OffersCubit extends Cubit<OffersState> {
@override @override
Future<void> close() async { Future<void> close() async {
_searchTimeout?.cancel();
await _sub?.cancel(); await _sub?.cancel();
await _onDispose?.call(); await _onDispose?.call();
return super.close(); return super.close();

View file

@ -431,6 +431,7 @@ class _ConfigSheet extends StatefulWidget {
class _ConfigSheetState extends State<_ConfigSheet> { class _ConfigSheetState extends State<_ConfigSheet> {
final _area = TextEditingController(); final _area = TextEditingController();
final _servers = TextEditingController();
bool _loading = true; bool _loading = true;
bool _locationBusy = false; bool _locationBusy = false;
String? _locationError; String? _locationError;
@ -443,13 +444,13 @@ class _ConfigSheetState extends State<_ConfigSheet> {
Future<void> _load() async { Future<void> _load() async {
_area.text = await widget.settings.areaGeohash(); _area.text = await widget.settings.areaGeohash();
_servers.text = (await widget.settings.relayUrls()).join('\n');
if (mounted) setState(() => _loading = false); if (mounted) setState(() => _loading = false);
} }
Future<void> _save() async { Future<void> _save() async {
// Relays are managed automatically (default community servers, dead ones
// skipped by the pool) the user only sets their area here.
await widget.settings.setAreaGeohash(_area.text); await widget.settings.setAreaGeohash(_area.text);
await widget.settings.setRelayUrls(_servers.text.split('\n'));
if (mounted) Navigator.of(context).pop(true); if (mounted) Navigator.of(context).pop(true);
} }
@ -477,12 +478,14 @@ class _ConfigSheetState extends State<_ConfigSheet> {
@override @override
void dispose() { void dispose() {
_area.dispose(); _area.dispose();
_servers.dispose();
super.dispose(); super.dispose();
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final t = context.t; final t = context.t;
final hasArea = _area.text.trim().isNotEmpty;
return Padding( return Padding(
padding: EdgeInsets.only( padding: EdgeInsets.only(
left: 20, left: 20,
@ -491,40 +494,34 @@ class _ConfigSheetState extends State<_ConfigSheet> {
bottom: MediaQuery.of(context).viewInsets.bottom + 20, bottom: MediaQuery.of(context).viewInsets.bottom + 20,
), ),
child: _loading child: _loading
? const SizedBox(height: 120, child: Center(child: CircularProgressIndicator())) ? const SizedBox(
height: 120, child: Center(child: CircularProgressIndicator()))
: SingleChildScrollView( : SingleChildScrollView(
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
Text( Text(
t.market.configTitle, t.market.configTitle,
style: const TextStyle( style: const TextStyle(
fontSize: 18, fontSize: 18,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: seedTitle, color: seedTitle,
),
), ),
), const SizedBox(height: 8),
const SizedBox(height: 8), Text(
Text( t.market.setupIntro,
t.market.setupIntro, style: const TextStyle(
style: const TextStyle(color: seedMuted, fontSize: 13, height: 1.4), color: seedMuted, fontSize: 13, height: 1.4),
),
const SizedBox(height: 16),
TextField(
key: const Key('market.area'),
controller: _area,
decoration: InputDecoration(
labelText: t.market.areaLabel,
helperText: t.market.areaHelp,
helperMaxLines: 3,
), ),
), const SizedBox(height: 18),
if (widget.location != null) // Setting your area from device location is the human path;
Align( // typing a code is the advanced fallback.
alignment: AlignmentDirectional.centerStart, if (widget.location != null)
child: TextButton.icon( FilledButton.tonalIcon(
key: const Key('market.useLocation'), key: const Key('market.useLocation'),
onPressed: _locationBusy ? null : _useLocation,
icon: _locationBusy icon: _locationBusy
? const SizedBox( ? const SizedBox(
width: 16, width: 16,
@ -533,26 +530,85 @@ class _ConfigSheetState extends State<_ConfigSheet> {
) )
: const Icon(Icons.my_location, size: 18), : const Icon(Icons.my_location, size: 18),
label: Text(t.market.useLocation), label: Text(t.market.useLocation),
onPressed: _locationBusy ? null : _useLocation, ),
if (_locationError != null)
Padding(
padding: const EdgeInsets.only(top: 6),
child: Text(
_locationError!,
style: const TextStyle(
color: Color(0xFFB3261E), fontSize: 12),
),
),
const SizedBox(height: 12),
Row(
children: [
Icon(
hasArea
? Icons.check_circle
: Icons.pending_outlined,
size: 18,
color: hasArea ? seedGreen : seedMuted,
),
const SizedBox(width: 8),
Expanded(
child: Text(
hasArea ? t.market.areaSet : t.market.areaNotSet,
style: TextStyle(
color: hasArea ? seedOnSurface : seedMuted,
fontSize: 13,
),
),
),
],
),
const SizedBox(height: 8),
Theme(
data: Theme.of(context)
.copyWith(dividerColor: Colors.transparent),
child: ExpansionTile(
key: const Key('market.advanced'),
tilePadding: EdgeInsets.zero,
childrenPadding: const EdgeInsets.only(bottom: 8),
title: Text(
t.market.advanced,
style: const TextStyle(fontSize: 13, color: seedMuted),
),
children: [
TextField(
key: const Key('market.area'),
controller: _area,
onChanged: (_) => setState(() {}),
decoration: InputDecoration(
labelText: t.market.areaCodeLabel,
helperText: t.market.areaCodeHint,
helperMaxLines: 2,
),
),
const SizedBox(height: 14),
TextField(
key: const Key('market.servers'),
controller: _servers,
minLines: 1,
maxLines: 3,
decoration: InputDecoration(
labelText: t.market.serversLabel,
helperText: t.market.serversHelp,
helperMaxLines: 2,
),
),
],
), ),
), ),
if (_locationError != null) const SizedBox(height: 20),
Padding( FilledButton(
padding: const EdgeInsets.only(top: 4), key: const Key('market.save'),
child: Text( onPressed: _save,
_locationError!, child: Text(t.market.save),
style: const TextStyle(color: Color(0xFFB3261E), fontSize: 12),
),
), ),
const SizedBox(height: 20), ],
FilledButton( ),
key: const Key('market.save'),
onPressed: _save,
child: Text(t.market.save),
),
],
), ),
),
); );
} }
} }

View file

@ -107,8 +107,7 @@ void main() {
await tester.tap(find.byKey(const Key('market.useLocation'))); await tester.tap(find.byKey(const Key('market.useLocation')));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
final area = tester.widget<TextField>(find.byKey(const Key('market.area'))); // The area status flips to "set" once a coarse location is captured.
expect(area.controller!.text, hasLength(5)); expect(find.textContaining('Your area is set'), findsOneWidget);
expect(area.controller!.text, matches(RegExp(r'^[0-9a-z]+$')));
}); });
} }