Merge branch 'main' into spike/block2-derisking
# Conflicts: # apps/app_seeds/lib/i18n/strings.g.dart
This commit is contained in:
commit
1fb8e37536
25 changed files with 2396 additions and 36 deletions
|
|
@ -360,8 +360,11 @@ class _AutoBackupTileState extends State<_AutoBackupTile> {
|
|||
final subtitle = last == null
|
||||
? t.backup.autoBackupNone
|
||||
: t.backup.autoBackupLast(
|
||||
// Format via the Localizations locale, not the raw app locale:
|
||||
// Asturian (`ast`) has no `intl` date symbols and would throw,
|
||||
// but it's mapped to Spanish for the framework (see app.dart).
|
||||
date: DateFormat.yMMMd(
|
||||
LocaleSettings.currentLocale.languageCode,
|
||||
Localizations.localeOf(context).languageCode,
|
||||
).format(last),
|
||||
);
|
||||
return ListTile(
|
||||
|
|
|
|||
|
|
@ -240,26 +240,95 @@ class MarketBody extends StatelessWidget {
|
|||
),
|
||||
);
|
||||
}
|
||||
final cubit = context.read<OffersCubit>();
|
||||
// Pull-to-refresh re-runs discovery for the current area (keeping the
|
||||
// text filter), so the list is never stuck on a stale scan.
|
||||
Future<void> refresh() => cubit.discover(state.areaGeohash);
|
||||
|
||||
// Nothing discovered at all: keep the friendly empty state, but make it
|
||||
// pull-to-refreshable so a swipe re-scans.
|
||||
if (state.offers.isEmpty) {
|
||||
return _EmptyState(
|
||||
icon: Icons.grass_outlined,
|
||||
title: t.market.empty,
|
||||
body: '',
|
||||
return RefreshIndicator(
|
||||
onRefresh: refresh,
|
||||
child: ListView(
|
||||
// AlwaysScrollable so the pull gesture works with a single child.
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
children: [
|
||||
SizedBox(
|
||||
height: MediaQuery.of(context).size.height * 0.6,
|
||||
child: _EmptyState(
|
||||
icon: Icons.grass_outlined,
|
||||
title: t.market.empty,
|
||||
body: '',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
return ListView.separated(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: state.offers.length,
|
||||
separatorBuilder: (_, _) => const SizedBox(height: 12),
|
||||
itemBuilder: (context, i) {
|
||||
final o = state.offers[i];
|
||||
final mine = o.authorPubkeyHex == selfPubkey;
|
||||
return _OfferCard(
|
||||
offer: o,
|
||||
mine: mine,
|
||||
onContact: mine ? null : () => context.push('/chat/${o.authorPubkeyHex}'),
|
||||
);
|
||||
},
|
||||
|
||||
final visible = state.visibleOffers;
|
||||
return Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12, 12, 12, 4),
|
||||
child: TextField(
|
||||
key: const Key('market.search'),
|
||||
decoration: InputDecoration(
|
||||
hintText: t.market.searchHint,
|
||||
prefixIcon: const Icon(Icons.search, color: seedMuted),
|
||||
hintStyle: const TextStyle(color: seedMuted),
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
isDense: true,
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 14),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(28),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
),
|
||||
onChanged: cubit.search,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: RefreshIndicator(
|
||||
onRefresh: refresh,
|
||||
child: visible.isEmpty
|
||||
// Discovered offers exist, but the search filtered them out.
|
||||
? ListView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
children: [
|
||||
SizedBox(
|
||||
height: MediaQuery.of(context).size.height * 0.5,
|
||||
child: _EmptyState(
|
||||
icon: Icons.search_off,
|
||||
title: t.market.noMatches,
|
||||
body: '',
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: ListView.separated(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: visible.length,
|
||||
separatorBuilder: (_, _) => const SizedBox(height: 12),
|
||||
itemBuilder: (context, i) {
|
||||
final o = visible[i];
|
||||
final mine = o.authorPubkeyHex == selfPubkey;
|
||||
return _OfferCard(
|
||||
offer: o,
|
||||
mine: mine,
|
||||
onContact: mine
|
||||
? null
|
||||
: () =>
|
||||
context.push('/chat/${o.authorPubkeyHex}'),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,23 +1,30 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../di/injector.dart';
|
||||
import '../i18n/strings.g.dart';
|
||||
import '../services/export_import_service.dart';
|
||||
import '../services/locale_store.dart';
|
||||
import 'backup_section.dart';
|
||||
import 'theme.dart';
|
||||
|
||||
/// Basic settings: app language, backup (export/import) and an "about"
|
||||
/// section. [exportImport] is injectable so widget tests can pass a fake;
|
||||
/// the app resolves it lazily from the service locator.
|
||||
/// section. [exportImport] and [localeStore] are injectable so widget tests can
|
||||
/// pass fakes; the app resolves them lazily from the service locator.
|
||||
class SettingsScreen extends StatelessWidget {
|
||||
const SettingsScreen({this.exportImport, super.key});
|
||||
const SettingsScreen({this.exportImport, this.localeStore, super.key});
|
||||
|
||||
final ExportImportService? exportImport;
|
||||
final LocaleStore? localeStore;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final t = context.t;
|
||||
final current = TranslationProvider.of(context).flutterLocale.languageCode;
|
||||
// Compare the full locale (not just the language code) so `es` and the
|
||||
// Asturian `es-AST` — which share the `es` language — are told apart.
|
||||
final current = TranslationProvider.of(context).locale;
|
||||
void pick(AppLocale locale) =>
|
||||
(localeStore ?? getIt<LocaleStore>()).setLocale(locale);
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text(t.menu.settings)),
|
||||
body: ListView(
|
||||
|
|
@ -25,23 +32,28 @@ class SettingsScreen extends StatelessWidget {
|
|||
_SectionHeader(t.settings.language),
|
||||
_LanguageTile(
|
||||
label: t.settings.langEs,
|
||||
selected: current == 'es',
|
||||
onTap: () => LocaleSettings.setLocale(AppLocale.es),
|
||||
selected: current == AppLocale.es,
|
||||
onTap: () => pick(AppLocale.es),
|
||||
),
|
||||
_LanguageTile(
|
||||
label: t.settings.langAst,
|
||||
selected: current == AppLocale.ast,
|
||||
onTap: () => pick(AppLocale.ast),
|
||||
),
|
||||
_LanguageTile(
|
||||
label: t.settings.langEn,
|
||||
selected: current == 'en',
|
||||
onTap: () => LocaleSettings.setLocale(AppLocale.en),
|
||||
selected: current == AppLocale.en,
|
||||
onTap: () => pick(AppLocale.en),
|
||||
),
|
||||
_LanguageTile(
|
||||
label: t.settings.langPt,
|
||||
selected: current == 'pt',
|
||||
onTap: () => LocaleSettings.setLocale(AppLocale.pt),
|
||||
selected: current == AppLocale.pt,
|
||||
onTap: () => pick(AppLocale.pt),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.smartphone_outlined),
|
||||
title: Text(t.settings.systemLanguage),
|
||||
onTap: () => LocaleSettings.useDeviceLocale(),
|
||||
onTap: () => (localeStore ?? getIt<LocaleStore>()).useDeviceLocale(),
|
||||
),
|
||||
const Divider(),
|
||||
_SectionHeader(t.backup.title),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue