feat(studio): Store + Cmd+K palette i18n (v0.27.0)
- Localize Store page: search hint, category and status filter
chips (All / Published / Alpha / Planned / Installed),
pluralized result count, hub-unreachable / no-matches empty
states, featured strip header, store-card pills (installed /
update / version), Install / Update / Details / Read docs /
Uninstall / Not installable buttons, install-progress dialog,
uninstall confirm dialog, install / uninstall toasts,
open-browser failure toast.
- Localize detail-sheet sections: Tags, Required capabilities,
Required host services, Screenshots, Documentation, Source.
- Localize docs panel: Load documentation button, fetching
spinner copy, Open repository in browser button, friendly
error mapping (not_found / no_docs / auth / network / parse).
- Localize Cmd+K palette: search hint, group labels (Pages /
Modules / Store / Flows), keyboard hint footer, indexing /
no-matches states, dynamic-hit copy ("installed module ·
v{version}", "saved flow", "uncategorized"). Static page
hits now flow through the localized "Pages" group; Settings
entry uses the localized label and hint.
Status pill values ("published", "alpha", "planned") stay as
data-derived English strings since they map directly to the
store-index `status:` enum.
Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
parent
d25b4c87ae
commit
e2b2639a86
9 changed files with 990 additions and 109 deletions
|
|
@ -99,10 +99,11 @@ class _StorePageState extends State<StorePage> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final l = AppLocalizations.of(context)!;
|
||||
return Scaffold(
|
||||
backgroundColor: theme.scaffoldBackgroundColor,
|
||||
appBar: AppBar(
|
||||
title: const Text('Store'),
|
||||
title: Text(l.searchGroupStore),
|
||||
actions: [
|
||||
// The DE/EN toggle now lives in the sidebar footer
|
||||
// and flips the entire app's locale. The Store
|
||||
|
|
@ -110,7 +111,7 @@ class _StorePageState extends State<StorePage> {
|
|||
// _locale reads from `Localizations.localeOf`.
|
||||
IconButton(
|
||||
icon: const Icon(Icons.refresh, size: 18),
|
||||
tooltip: 'Reload',
|
||||
tooltip: l.storeReloadTooltip,
|
||||
onPressed: _runSearch,
|
||||
),
|
||||
const SizedBox(width: FaiSpace.sm),
|
||||
|
|
@ -181,11 +182,11 @@ class _StorePageState extends State<StorePage> {
|
|||
return FaiEmptyState(
|
||||
icon: Icons.cloud_off_outlined,
|
||||
iconColor: theme.colorScheme.error,
|
||||
title: 'Hub unreachable',
|
||||
hint: 'Start the hub with `fai daemon start`.',
|
||||
title: l.hubUnreachable,
|
||||
hint: l.hubUnreachableHint,
|
||||
action: FilledButton.tonal(
|
||||
onPressed: _runSearch,
|
||||
child: const Text('Retry'),
|
||||
child: Text(l.buttonRetry),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -193,9 +194,8 @@ class _StorePageState extends State<StorePage> {
|
|||
if (items.isEmpty) {
|
||||
return FaiEmptyState(
|
||||
icon: Icons.search_off,
|
||||
title: 'No matches',
|
||||
hint:
|
||||
'Adjust the filters or clear the search to see all entries.',
|
||||
title: l.storeNoMatches,
|
||||
hint: l.storeNoMatchesHint,
|
||||
action: FilledButton.tonal(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
|
|
@ -206,7 +206,7 @@ class _StorePageState extends State<StorePage> {
|
|||
});
|
||||
_runSearch();
|
||||
},
|
||||
child: const Text('Clear filters'),
|
||||
child: Text(l.storeClearFilters),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -293,6 +293,7 @@ class _SearchField extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l = AppLocalizations.of(context)!;
|
||||
return ValueListenableBuilder<TextEditingValue>(
|
||||
valueListenable: controller,
|
||||
builder: (_, value, _) {
|
||||
|
|
@ -300,7 +301,7 @@ class _SearchField extends StatelessWidget {
|
|||
controller: controller,
|
||||
decoration: InputDecoration(
|
||||
prefixIcon: const Icon(Icons.search, size: 20),
|
||||
hintText: 'Search modules — name, tagline, tag, capability…',
|
||||
hintText: l.storeSearchHint,
|
||||
border: const OutlineInputBorder(),
|
||||
isDense: true,
|
||||
suffixIcon: value.text.isEmpty
|
||||
|
|
@ -334,6 +335,7 @@ class _CategoryStrip extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l = AppLocalizations.of(context)!;
|
||||
if (categories.isEmpty) return const SizedBox.shrink();
|
||||
return SizedBox(
|
||||
height: 36,
|
||||
|
|
@ -341,7 +343,7 @@ class _CategoryStrip extends StatelessWidget {
|
|||
scrollDirection: Axis.horizontal,
|
||||
children: [
|
||||
_ChoiceChip(
|
||||
label: 'All',
|
||||
label: l.storeStatusAll,
|
||||
selected: selected.isEmpty,
|
||||
onSelected: () => onSelected(''),
|
||||
),
|
||||
|
|
@ -375,38 +377,39 @@ class _FilterRow extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final l = AppLocalizations.of(context)!;
|
||||
return Row(
|
||||
children: [
|
||||
_ChoiceChip(
|
||||
label: 'All',
|
||||
label: l.storeStatusAll,
|
||||
selected: status.isEmpty,
|
||||
onSelected: () => onStatus(''),
|
||||
),
|
||||
_ChoiceChip(
|
||||
label: 'Published',
|
||||
label: l.storeStatusPublished,
|
||||
selected: status == 'published',
|
||||
onSelected: () => onStatus('published'),
|
||||
),
|
||||
_ChoiceChip(
|
||||
label: 'Alpha',
|
||||
label: l.storeStatusAlpha,
|
||||
selected: status == 'alpha',
|
||||
onSelected: () => onStatus('alpha'),
|
||||
),
|
||||
_ChoiceChip(
|
||||
label: 'Planned',
|
||||
label: l.storeStatusPlanned,
|
||||
selected: status == 'planned',
|
||||
onSelected: () => onStatus('planned'),
|
||||
),
|
||||
const SizedBox(width: FaiSpace.md),
|
||||
FilterChip(
|
||||
label: const Text('Installed'),
|
||||
label: Text(l.storeInstalledOnly),
|
||||
selected: installedOnly,
|
||||
onSelected: onInstalledOnly,
|
||||
showCheckmark: true,
|
||||
),
|
||||
const Spacer(),
|
||||
Text(
|
||||
'$resultCount result${resultCount == 1 ? "" : "s"}',
|
||||
l.storeNResults(resultCount),
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
|
|
@ -512,6 +515,7 @@ class _FeaturedStrip extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final l = AppLocalizations.of(context)!;
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
|
|
@ -520,7 +524,7 @@ class _FeaturedStrip extends StatelessWidget {
|
|||
Icon(Icons.star, size: 14, color: FaiColors.success),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
'FEATURED',
|
||||
l.storeFeatured,
|
||||
style: theme.textTheme.labelSmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
letterSpacing: 0.6,
|
||||
|
|
@ -529,7 +533,7 @@ class _FeaturedStrip extends StatelessWidget {
|
|||
),
|
||||
const SizedBox(width: FaiSpace.sm),
|
||||
Text(
|
||||
'curated picks · click for details',
|
||||
l.storeFeaturedHint,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
|
|
@ -582,6 +586,7 @@ class _FeaturedTile extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final l = AppLocalizations.of(context)!;
|
||||
final tagline = locale == 'de' && item.taglineDe.isNotEmpty
|
||||
? item.taglineDe
|
||||
: item.taglineEn;
|
||||
|
|
@ -649,7 +654,7 @@ class _FeaturedTile extends StatelessWidget {
|
|||
const SizedBox(height: FaiSpace.md),
|
||||
Expanded(
|
||||
child: Text(
|
||||
tagline.isEmpty ? '(no tagline)' : tagline,
|
||||
tagline.isEmpty ? l.storeNoTagline : tagline,
|
||||
style: theme.textTheme.bodyLarge,
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
|
|
@ -672,8 +677,8 @@ class _FeaturedTile extends StatelessWidget {
|
|||
),
|
||||
const Spacer(),
|
||||
if (item.installed)
|
||||
const FaiPill(
|
||||
label: 'installed',
|
||||
FaiPill(
|
||||
label: l.storePillInstalled,
|
||||
tone: FaiPillTone.success,
|
||||
icon: Icons.check,
|
||||
)
|
||||
|
|
@ -681,7 +686,7 @@ class _FeaturedTile extends StatelessWidget {
|
|||
FilledButton.icon(
|
||||
onPressed: onInstall,
|
||||
icon: const Icon(Icons.download, size: 16),
|
||||
label: const Text('Install'),
|
||||
label: Text(l.buttonInstall),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -724,6 +729,7 @@ class _StoreCard extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final l = AppLocalizations.of(context)!;
|
||||
final tagline = locale == 'de' && item.taglineDe.isNotEmpty
|
||||
? item.taglineDe
|
||||
: item.taglineEn;
|
||||
|
|
@ -775,17 +781,19 @@ class _StoreCard extends StatelessWidget {
|
|||
),
|
||||
if (_hasUpdate)
|
||||
Tooltip(
|
||||
message:
|
||||
'Installed ${installedVersion ?? "?"} — store has v${item.bestVersion}',
|
||||
child: const FaiPill(
|
||||
label: 'update',
|
||||
message: l.storeUpdateTooltip(
|
||||
installedVersion ?? '?',
|
||||
item.bestVersion,
|
||||
),
|
||||
child: FaiPill(
|
||||
label: l.storePillUpdate,
|
||||
tone: FaiPillTone.warning,
|
||||
icon: Icons.system_update_alt,
|
||||
),
|
||||
)
|
||||
else if (item.installed)
|
||||
const FaiPill(
|
||||
label: 'installed',
|
||||
FaiPill(
|
||||
label: l.storePillInstalled,
|
||||
tone: FaiPillTone.success,
|
||||
icon: Icons.check,
|
||||
)
|
||||
|
|
@ -799,7 +807,7 @@ class _StoreCard extends StatelessWidget {
|
|||
const SizedBox(height: FaiSpace.sm),
|
||||
Expanded(
|
||||
child: Text(
|
||||
tagline.isEmpty ? '(no tagline)' : tagline,
|
||||
tagline.isEmpty ? l.storeNoTagline : tagline,
|
||||
style: theme.textTheme.bodySmall,
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
|
|
@ -819,7 +827,7 @@ class _StoreCard extends StatelessWidget {
|
|||
FilledButton.icon(
|
||||
onPressed: onInstall,
|
||||
icon: const Icon(Icons.system_update_alt, size: 14),
|
||||
label: Text('Update to ${item.bestVersion}'),
|
||||
label: Text(l.storeUpdateButton(item.bestVersion)),
|
||||
style: FilledButton.styleFrom(
|
||||
visualDensity: VisualDensity.compact,
|
||||
),
|
||||
|
|
@ -828,13 +836,13 @@ class _StoreCard extends StatelessWidget {
|
|||
TextButton.icon(
|
||||
onPressed: onTap,
|
||||
icon: const Icon(Icons.info_outline, size: 14),
|
||||
label: const Text('Details'),
|
||||
label: Text(l.buttonDetails),
|
||||
)
|
||||
else if (installable)
|
||||
FilledButton.icon(
|
||||
onPressed: onInstall,
|
||||
icon: const Icon(Icons.download, size: 14),
|
||||
label: const Text('Install'),
|
||||
label: Text(l.buttonInstall),
|
||||
style: FilledButton.styleFrom(
|
||||
visualDensity: VisualDensity.compact,
|
||||
),
|
||||
|
|
@ -843,7 +851,7 @@ class _StoreCard extends StatelessWidget {
|
|||
OutlinedButton.icon(
|
||||
onPressed: onTap,
|
||||
icon: const Icon(Icons.info_outline, size: 14),
|
||||
label: const Text('Details'),
|
||||
label: Text(l.buttonDetails),
|
||||
style: OutlinedButton.styleFrom(
|
||||
visualDensity: VisualDensity.compact,
|
||||
),
|
||||
|
|
@ -906,33 +914,33 @@ class _StoreDetailSheetState extends State<_StoreDetailSheet> {
|
|||
try {
|
||||
final r = await HubService.instance.installModule(source: widget.item.name);
|
||||
if (!mounted) return;
|
||||
final l = AppLocalizations.of(context)!;
|
||||
setState(() {
|
||||
_busy = false;
|
||||
_toast = '${r.name} v${r.version} installed.';
|
||||
_toast = l.storeInstalledToast(r.name, r.version);
|
||||
});
|
||||
Navigator.pop(context, true);
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
final l = AppLocalizations.of(context)!;
|
||||
setState(() {
|
||||
_busy = false;
|
||||
_toast = 'Install failed: $e';
|
||||
_toast = l.storeInstallFailedToast(e.toString());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _uninstall() async {
|
||||
final l = AppLocalizations.of(context)!;
|
||||
final ok = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: const Text('Uninstall module?'),
|
||||
content: Text(
|
||||
'Removes ${widget.item.name} from this hub. Flows that '
|
||||
'reference it will fail at the next run.',
|
||||
),
|
||||
title: Text(l.storeUninstallTitle),
|
||||
content: Text(l.storeUninstallBody(widget.item.name)),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx, false),
|
||||
child: const Text('Cancel'),
|
||||
child: Text(l.buttonCancel),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.pop(ctx, true),
|
||||
|
|
@ -940,7 +948,7 @@ class _StoreDetailSheetState extends State<_StoreDetailSheet> {
|
|||
backgroundColor: Theme.of(ctx).colorScheme.error,
|
||||
foregroundColor: Theme.of(ctx).colorScheme.onError,
|
||||
),
|
||||
child: const Text('Uninstall'),
|
||||
child: Text(l.buttonUninstall),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -953,16 +961,18 @@ class _StoreDetailSheetState extends State<_StoreDetailSheet> {
|
|||
try {
|
||||
final r = await HubService.instance.uninstallModule(widget.item.name);
|
||||
if (!mounted) return;
|
||||
final l2 = AppLocalizations.of(context)!;
|
||||
setState(() {
|
||||
_busy = false;
|
||||
_toast = '${r.name} v${r.version} uninstalled.';
|
||||
_toast = l2.storeUninstalledToast(r.name, r.version);
|
||||
});
|
||||
Navigator.pop(context, true);
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
final l2 = AppLocalizations.of(context)!;
|
||||
setState(() {
|
||||
_busy = false;
|
||||
_toast = 'Uninstall failed: $e';
|
||||
_toast = l2.storeUninstallFailedToast(e.toString());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -972,8 +982,9 @@ class _StoreDetailSheetState extends State<_StoreDetailSheet> {
|
|||
final r = await SystemActions.openInOs(widget.item.repository);
|
||||
if (!mounted) return;
|
||||
if (!r.ok) {
|
||||
final l = AppLocalizations.of(context)!;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Could not open browser: ${r.stderr}')),
|
||||
SnackBar(content: Text(l.storeOpenBrowserFailed(r.stderr))),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -990,6 +1001,7 @@ class _StoreDetailSheetState extends State<_StoreDetailSheet> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final l = AppLocalizations.of(context)!;
|
||||
final item = widget.item;
|
||||
final tagline = _locale == 'de' && item.taglineDe.isNotEmpty
|
||||
? item.taglineDe
|
||||
|
|
@ -1069,8 +1081,8 @@ class _StoreDetailSheetState extends State<_StoreDetailSheet> {
|
|||
tone: FaiPillTone.neutral,
|
||||
),
|
||||
if (item.installed)
|
||||
const FaiPill(
|
||||
label: 'installed',
|
||||
FaiPill(
|
||||
label: l.storePillInstalled,
|
||||
tone: FaiPillTone.success,
|
||||
icon: Icons.check,
|
||||
),
|
||||
|
|
@ -1107,7 +1119,7 @@ class _StoreDetailSheetState extends State<_StoreDetailSheet> {
|
|||
const SizedBox(height: FaiSpace.xl),
|
||||
],
|
||||
if (item.tags.isNotEmpty) ...[
|
||||
_SectionHeader('Tags'),
|
||||
_SectionHeader(l.storeSectionTags),
|
||||
const SizedBox(height: FaiSpace.sm),
|
||||
Wrap(
|
||||
spacing: FaiSpace.xs,
|
||||
|
|
@ -1120,7 +1132,7 @@ class _StoreDetailSheetState extends State<_StoreDetailSheet> {
|
|||
const SizedBox(height: FaiSpace.lg),
|
||||
],
|
||||
if (item.requiresCapabilities.isNotEmpty) ...[
|
||||
_SectionHeader('Required capabilities'),
|
||||
_SectionHeader(l.storeSectionRequiredCapabilities),
|
||||
const SizedBox(height: FaiSpace.sm),
|
||||
Wrap(
|
||||
spacing: FaiSpace.xs,
|
||||
|
|
@ -1137,7 +1149,7 @@ class _StoreDetailSheetState extends State<_StoreDetailSheet> {
|
|||
const SizedBox(height: FaiSpace.lg),
|
||||
],
|
||||
if (item.requiresServices.isNotEmpty) ...[
|
||||
_SectionHeader('Required host services'),
|
||||
_SectionHeader(l.storeSectionRequiredHostServices),
|
||||
const SizedBox(height: FaiSpace.sm),
|
||||
Wrap(
|
||||
spacing: FaiSpace.xs,
|
||||
|
|
@ -1154,7 +1166,7 @@ class _StoreDetailSheetState extends State<_StoreDetailSheet> {
|
|||
const SizedBox(height: FaiSpace.lg),
|
||||
],
|
||||
if (item.screenshotUrls.isNotEmpty) ...[
|
||||
_SectionHeader('Screenshots'),
|
||||
_SectionHeader(l.storeSectionScreenshots),
|
||||
const SizedBox(height: FaiSpace.sm),
|
||||
SizedBox(
|
||||
height: 220,
|
||||
|
|
@ -1171,7 +1183,7 @@ class _StoreDetailSheetState extends State<_StoreDetailSheet> {
|
|||
const SizedBox(height: FaiSpace.lg),
|
||||
],
|
||||
if (item.repository.isNotEmpty || item.docsUrl.isNotEmpty) ...[
|
||||
_SectionHeader('Documentation'),
|
||||
_SectionHeader(l.storeSectionDocumentation),
|
||||
const SizedBox(height: FaiSpace.sm),
|
||||
_DocsPanel(
|
||||
future: _docsFuture,
|
||||
|
|
@ -1179,7 +1191,7 @@ class _StoreDetailSheetState extends State<_StoreDetailSheet> {
|
|||
onOpenRepo: _openRepository,
|
||||
),
|
||||
const SizedBox(height: FaiSpace.lg),
|
||||
_SectionHeader('Source'),
|
||||
_SectionHeader(l.storeSectionSource),
|
||||
const SizedBox(height: FaiSpace.sm),
|
||||
InkWell(
|
||||
onTap: _openRepository,
|
||||
|
|
@ -1225,7 +1237,7 @@ class _StoreDetailSheetState extends State<_StoreDetailSheet> {
|
|||
OutlinedButton.icon(
|
||||
onPressed: _openRepository,
|
||||
icon: const Icon(Icons.menu_book_outlined, size: 16),
|
||||
label: const Text('Read docs'),
|
||||
label: Text(l.buttonReadDocs),
|
||||
),
|
||||
const Spacer(),
|
||||
if (item.installed) ...[
|
||||
|
|
@ -1239,7 +1251,7 @@ class _StoreDetailSheetState extends State<_StoreDetailSheet> {
|
|||
CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Icon(Icons.delete_outline, size: 16),
|
||||
label: const Text('Uninstall'),
|
||||
label: Text(l.buttonUninstall),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: theme.colorScheme.error,
|
||||
side: BorderSide(
|
||||
|
|
@ -1259,15 +1271,16 @@ class _StoreDetailSheetState extends State<_StoreDetailSheet> {
|
|||
CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Icon(Icons.download, size: 16),
|
||||
label: Text(_busy ? 'Installing…' : 'Install'),
|
||||
label: Text(
|
||||
_busy ? l.storeInstallingButton : l.buttonInstall,
|
||||
),
|
||||
)
|
||||
else
|
||||
Tooltip(
|
||||
message:
|
||||
'Status "${item.status}" — install path not yet wired.',
|
||||
child: const FilledButton(
|
||||
message: l.storeNotInstallableTooltip(item.status),
|
||||
child: FilledButton(
|
||||
onPressed: null,
|
||||
child: Text('Not installable'),
|
||||
child: Text(l.storeNotInstallable),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
@ -1324,8 +1337,9 @@ class _InstallProgressDialogState extends State<_InstallProgressDialog> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final l = AppLocalizations.of(context)!;
|
||||
return AlertDialog(
|
||||
title: Text('Installing ${widget.item.name}'),
|
||||
title: Text(l.storeInstallProgressTitle(widget.item.name)),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(FaiRadius.md),
|
||||
),
|
||||
|
|
@ -1342,7 +1356,7 @@ class _InstallProgressDialogState extends State<_InstallProgressDialog> {
|
|||
const CircularProgressIndicator(),
|
||||
const SizedBox(height: FaiSpace.md),
|
||||
Text(
|
||||
'Fetching, verifying, unpacking…',
|
||||
l.storeInstallProgressBody,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
|
|
@ -1384,7 +1398,7 @@ class _InstallProgressDialogState extends State<_InstallProgressDialog> {
|
|||
),
|
||||
const SizedBox(height: FaiSpace.md),
|
||||
Text(
|
||||
'${r.name} v${r.version} installed.',
|
||||
l.storeInstalledToast(r.name, r.version),
|
||||
style: theme.textTheme.titleMedium,
|
||||
),
|
||||
],
|
||||
|
|
@ -1395,7 +1409,7 @@ class _InstallProgressDialogState extends State<_InstallProgressDialog> {
|
|||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
child: const Text('Close'),
|
||||
child: Text(l.buttonClose),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
|
@ -1458,17 +1472,18 @@ class _DocsPanel extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final l = AppLocalizations.of(context)!;
|
||||
if (future == null) {
|
||||
return Row(
|
||||
children: [
|
||||
OutlinedButton.icon(
|
||||
onPressed: onLoad,
|
||||
icon: const Icon(Icons.menu_book_outlined, size: 16),
|
||||
label: const Text('Load documentation'),
|
||||
label: Text(l.storeLoadDocs),
|
||||
),
|
||||
const SizedBox(width: FaiSpace.sm),
|
||||
Text(
|
||||
'README rendered inline; uses the hub\'s registry token when needed.',
|
||||
l.storeDocsHint,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
|
|
@ -1480,17 +1495,17 @@ class _DocsPanel extends StatelessWidget {
|
|||
future: future,
|
||||
builder: (context, snap) {
|
||||
if (snap.connectionState == ConnectionState.waiting) {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: FaiSpace.md),
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: FaiSpace.md),
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
const SizedBox(
|
||||
width: 14,
|
||||
height: 14,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
),
|
||||
SizedBox(width: FaiSpace.sm),
|
||||
Text('Fetching README…'),
|
||||
const SizedBox(width: FaiSpace.sm),
|
||||
Text(l.storeDocsFetching),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
@ -1504,7 +1519,7 @@ class _DocsPanel extends StatelessWidget {
|
|||
final r = snap.data!;
|
||||
if (r.errorKind.isNotEmpty) {
|
||||
return _DocsErrorPanel(
|
||||
message: _friendlyDocsError(r.errorKind, r.text),
|
||||
message: _friendlyDocsError(context, r.errorKind, r.text),
|
||||
onOpenRepo: onOpenRepo,
|
||||
sourceUrl: r.sourceUrl,
|
||||
);
|
||||
|
|
@ -1599,7 +1614,7 @@ class _DocsErrorPanel extends StatelessWidget {
|
|||
OutlinedButton.icon(
|
||||
onPressed: onOpenRepo,
|
||||
icon: const Icon(Icons.open_in_new, size: 14),
|
||||
label: const Text('Open repository in browser'),
|
||||
label: Text(AppLocalizations.of(context)!.storeOpenRepoButton),
|
||||
style: OutlinedButton.styleFrom(
|
||||
visualDensity: VisualDensity.compact,
|
||||
),
|
||||
|
|
@ -1636,22 +1651,21 @@ int _versionCmp(String a, String b) {
|
|||
return 0;
|
||||
}
|
||||
|
||||
String _friendlyDocsError(String kind, String detail) {
|
||||
String _friendlyDocsError(BuildContext context, String kind, String detail) {
|
||||
final l = AppLocalizations.of(context)!;
|
||||
switch (kind) {
|
||||
case 'not_found':
|
||||
return 'This module is not in the hub\'s store index.';
|
||||
return l.storeDocsErrorNotFound;
|
||||
case 'no_docs':
|
||||
return 'No repository URL is recorded for this module.';
|
||||
return l.storeDocsErrorNoDocs;
|
||||
case 'auth':
|
||||
return 'The registry requires authentication to fetch this README. '
|
||||
'Set FAI_REGISTRY_TOKEN in the daemon\'s environment '
|
||||
'and restart the hub.';
|
||||
return l.storeDocsErrorAuth;
|
||||
case 'network':
|
||||
return 'Network error while fetching the README. $detail';
|
||||
return l.storeDocsErrorNetwork(detail);
|
||||
case 'parse':
|
||||
return 'README is not valid UTF-8.';
|
||||
return l.storeDocsErrorParse;
|
||||
default:
|
||||
return detail.isEmpty ? 'Could not load README.' : detail;
|
||||
return detail.isEmpty ? l.storeDocsErrorGeneric : detail;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue