feat(studio): inline README + per-module update badge in Store (v0.19.0)
Two Phase-2 Store improvements that operators feel immediately: - Per-module update badge: store cards compare the operator's installed version against the index's `best_version`, raise an "update" pill + "Update to vX.Y.Z" button when newer is available. Result: a single glance at the Store reveals what's behind. The page now fetches `listModules` in parallel with the search so the comparison is on the current snapshot. - Inline README rendering: detail sheet has a new Documentation section. "Load documentation" button (lazy — no network until clicked) calls FetchModuleDocs and renders the markdown via flutter_markdown, with the operator's theme colors and link-tap routed through SystemActions. Auth / not-found / network failures fall back to a friendly fix-hint panel + "Open repository in browser" button so the operator never hits a dead end. Adds flutter_markdown ^0.7.7 to pubspec; the package is maintenance-only upstream but is the only mature option for Material-themed markdown right now. Easy to swap to markdown_widget if/when we need GitHub-flavored extras. Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
parent
93926741b1
commit
04cc12bc54
5 changed files with 335 additions and 16 deletions
|
|
@ -326,6 +326,19 @@ class HubService {
|
||||||
return (name: r.name, version: r.version);
|
return (name: r.name, version: r.version);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Fetch a module's README markdown via the hub. Errors come
|
||||||
|
/// back as `errorKind` strings (not exceptions) so callers can
|
||||||
|
/// render fallback UI without try/catch ceremony.
|
||||||
|
Future<({String errorKind, String text, String sourceUrl})>
|
||||||
|
fetchModuleDocs(String name) async {
|
||||||
|
final r = await _client.fetchModuleDocs(name);
|
||||||
|
return (
|
||||||
|
errorKind: r.errorKind,
|
||||||
|
text: r.text,
|
||||||
|
sourceUrl: r.sourceUrl,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// Active channel + per-channel daemon status snapshot.
|
/// Active channel + per-channel daemon status snapshot.
|
||||||
Future<ChannelStatusSnapshot> channelStatus() async {
|
Future<ChannelStatusSnapshot> channelStatus() async {
|
||||||
final r = await _client.channelStatus();
|
final r = await _client.channelStatus();
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ import 'widgets/widgets.dart';
|
||||||
/// Studio's own build version. Bump on every UI commit so the
|
/// Studio's own build version. Bump on every UI commit so the
|
||||||
/// running app self-identifies — visible in the sidebar header
|
/// running app self-identifies — visible in the sidebar header
|
||||||
/// and quick-glance proof that you're seeing the current build.
|
/// and quick-glance proof that you're seeing the current build.
|
||||||
const String kStudioVersion = '0.18.0';
|
const String kStudioVersion = '0.19.0';
|
||||||
|
|
||||||
Future<void> main() async {
|
Future<void> main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
// repository link.
|
// repository link.
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||||
|
|
||||||
import '../data/hub.dart';
|
import '../data/hub.dart';
|
||||||
import '../data/system_actions.dart';
|
import '../data/system_actions.dart';
|
||||||
|
|
@ -40,15 +41,26 @@ class _StorePageState extends State<StorePage> {
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Map<String, String> _installedVersions = const {};
|
||||||
|
|
||||||
Future<List<StoreItem>> _load() async {
|
Future<List<StoreItem>> _load() async {
|
||||||
final all = await HubService.instance.searchStore(
|
// Fetch the store entries and the installed modules in
|
||||||
|
// parallel so the page paints in one round-trip. The
|
||||||
|
// installed-version map drives the per-card "Update"
|
||||||
|
// badge — store entries only carry `best_version` and
|
||||||
|
// `installed: bool`, never the installed version itself.
|
||||||
|
final results = await Future.wait([
|
||||||
|
HubService.instance.searchStore(
|
||||||
query: _queryCtrl.text.trim(),
|
query: _queryCtrl.text.trim(),
|
||||||
category: _category,
|
category: _category,
|
||||||
status: _status,
|
status: _status,
|
||||||
// Pull a generous page so the local "installed only"
|
|
||||||
// filter does not bite into early results.
|
|
||||||
limit: 200,
|
limit: 200,
|
||||||
);
|
),
|
||||||
|
HubService.instance.listModules().catchError((_) => <ModuleSummary>[]),
|
||||||
|
]);
|
||||||
|
final all = results[0] as List<StoreItem>;
|
||||||
|
final mods = results[1] as List<ModuleSummary>;
|
||||||
|
_installedVersions = {for (final m in mods) m.name: m.version};
|
||||||
return _installedOnly ? all.where((e) => e.installed).toList() : all;
|
return _installedOnly ? all.where((e) => e.installed).toList() : all;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -189,6 +201,7 @@ class _StorePageState extends State<StorePage> {
|
||||||
return _StoreGrid(
|
return _StoreGrid(
|
||||||
items: items,
|
items: items,
|
||||||
locale: _locale,
|
locale: _locale,
|
||||||
|
installedVersions: _installedVersions,
|
||||||
onTap: _openDetail,
|
onTap: _openDetail,
|
||||||
onInstall: _install,
|
onInstall: _install,
|
||||||
);
|
);
|
||||||
|
|
@ -402,12 +415,17 @@ class _LocaleToggle extends StatelessWidget {
|
||||||
class _StoreGrid extends StatelessWidget {
|
class _StoreGrid extends StatelessWidget {
|
||||||
final List<StoreItem> items;
|
final List<StoreItem> items;
|
||||||
final String locale;
|
final String locale;
|
||||||
|
/// Map of installed module name → installed version. Cards
|
||||||
|
/// compare against `bestVersion` to render an "Update" badge
|
||||||
|
/// when the store offers something newer.
|
||||||
|
final Map<String, String> installedVersions;
|
||||||
final ValueChanged<StoreItem> onTap;
|
final ValueChanged<StoreItem> onTap;
|
||||||
final ValueChanged<StoreItem> onInstall;
|
final ValueChanged<StoreItem> onInstall;
|
||||||
|
|
||||||
const _StoreGrid({
|
const _StoreGrid({
|
||||||
required this.items,
|
required this.items,
|
||||||
required this.locale,
|
required this.locale,
|
||||||
|
required this.installedVersions,
|
||||||
required this.onTap,
|
required this.onTap,
|
||||||
required this.onInstall,
|
required this.onInstall,
|
||||||
});
|
});
|
||||||
|
|
@ -430,6 +448,7 @@ class _StoreGrid extends StatelessWidget {
|
||||||
itemBuilder: (context, i) => _StoreCard(
|
itemBuilder: (context, i) => _StoreCard(
|
||||||
item: items[i],
|
item: items[i],
|
||||||
locale: locale,
|
locale: locale,
|
||||||
|
installedVersion: installedVersions[items[i].name],
|
||||||
onTap: () => onTap(items[i]),
|
onTap: () => onTap(items[i]),
|
||||||
onInstall: () => onInstall(items[i]),
|
onInstall: () => onInstall(items[i]),
|
||||||
),
|
),
|
||||||
|
|
@ -442,16 +461,31 @@ class _StoreGrid extends StatelessWidget {
|
||||||
class _StoreCard extends StatelessWidget {
|
class _StoreCard extends StatelessWidget {
|
||||||
final StoreItem item;
|
final StoreItem item;
|
||||||
final String locale;
|
final String locale;
|
||||||
|
/// Installed version for this name, when the operator has it.
|
||||||
|
/// `null` for not-installed entries.
|
||||||
|
final String? installedVersion;
|
||||||
final VoidCallback onTap;
|
final VoidCallback onTap;
|
||||||
final VoidCallback onInstall;
|
final VoidCallback onInstall;
|
||||||
|
|
||||||
const _StoreCard({
|
const _StoreCard({
|
||||||
required this.item,
|
required this.item,
|
||||||
required this.locale,
|
required this.locale,
|
||||||
|
required this.installedVersion,
|
||||||
required this.onTap,
|
required this.onTap,
|
||||||
required this.onInstall,
|
required this.onInstall,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// True iff the operator has this module installed AND the
|
||||||
|
/// store-index offers a strictly-newer best_version. Uses a
|
||||||
|
/// dotted-numeric semver-light compare so 0.10.5 > 0.9.99.
|
||||||
|
bool get _hasUpdate {
|
||||||
|
final installed = installedVersion;
|
||||||
|
final best = item.bestVersion;
|
||||||
|
if (installed == null || best.isEmpty) return false;
|
||||||
|
if (installed == best) return false;
|
||||||
|
return _versionCmp(best, installed) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
|
|
@ -504,7 +538,17 @@ class _StoreCard extends StatelessWidget {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (item.installed)
|
if (_hasUpdate)
|
||||||
|
Tooltip(
|
||||||
|
message:
|
||||||
|
'Installed ${installedVersion ?? "?"} — store has v${item.bestVersion}',
|
||||||
|
child: const FaiPill(
|
||||||
|
label: 'update',
|
||||||
|
tone: FaiPillTone.warning,
|
||||||
|
icon: Icons.system_update_alt,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else if (item.installed)
|
||||||
const FaiPill(
|
const FaiPill(
|
||||||
label: 'installed',
|
label: 'installed',
|
||||||
tone: FaiPillTone.success,
|
tone: FaiPillTone.success,
|
||||||
|
|
@ -536,7 +580,16 @@ class _StoreCard extends StatelessWidget {
|
||||||
monospace: true,
|
monospace: true,
|
||||||
),
|
),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
if (item.installed)
|
if (_hasUpdate)
|
||||||
|
FilledButton.icon(
|
||||||
|
onPressed: onInstall,
|
||||||
|
icon: const Icon(Icons.system_update_alt, size: 14),
|
||||||
|
label: Text('Update to ${item.bestVersion}'),
|
||||||
|
style: FilledButton.styleFrom(
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else if (item.installed)
|
||||||
TextButton.icon(
|
TextButton.icon(
|
||||||
onPressed: onTap,
|
onPressed: onTap,
|
||||||
icon: const Icon(Icons.info_outline, size: 14),
|
icon: const Icon(Icons.info_outline, size: 14),
|
||||||
|
|
@ -605,6 +658,8 @@ class _StoreDetailSheetState extends State<_StoreDetailSheet> {
|
||||||
late String _locale = widget.locale;
|
late String _locale = widget.locale;
|
||||||
bool _busy = false;
|
bool _busy = false;
|
||||||
String? _toast;
|
String? _toast;
|
||||||
|
Future<({String errorKind, String text, String sourceUrl})>? _docsFuture;
|
||||||
|
bool _docsLoaded = false;
|
||||||
|
|
||||||
Future<void> _install() async {
|
Future<void> _install() async {
|
||||||
setState(() {
|
setState(() {
|
||||||
|
|
@ -686,6 +741,15 @@ class _StoreDetailSheetState extends State<_StoreDetailSheet> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _loadDocs() {
|
||||||
|
if (_docsLoaded) return;
|
||||||
|
setState(() {
|
||||||
|
_docsLoaded = true;
|
||||||
|
_docsFuture =
|
||||||
|
HubService.instance.fetchModuleDocs(widget.item.name);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
|
|
@ -859,6 +923,14 @@ class _StoreDetailSheetState extends State<_StoreDetailSheet> {
|
||||||
const SizedBox(height: FaiSpace.lg),
|
const SizedBox(height: FaiSpace.lg),
|
||||||
],
|
],
|
||||||
if (item.repository.isNotEmpty) ...[
|
if (item.repository.isNotEmpty) ...[
|
||||||
|
_SectionHeader('Documentation'),
|
||||||
|
const SizedBox(height: FaiSpace.sm),
|
||||||
|
_DocsPanel(
|
||||||
|
future: _docsFuture,
|
||||||
|
onLoad: _loadDocs,
|
||||||
|
onOpenRepo: _openRepository,
|
||||||
|
),
|
||||||
|
const SizedBox(height: FaiSpace.lg),
|
||||||
_SectionHeader('Source'),
|
_SectionHeader('Source'),
|
||||||
const SizedBox(height: FaiSpace.sm),
|
const SizedBox(height: FaiSpace.sm),
|
||||||
InkWell(
|
InkWell(
|
||||||
|
|
@ -908,7 +980,7 @@ class _StoreDetailSheetState extends State<_StoreDetailSheet> {
|
||||||
label: const Text('Read docs'),
|
label: const Text('Read docs'),
|
||||||
),
|
),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
if (item.installed)
|
if (item.installed) ...[
|
||||||
OutlinedButton.icon(
|
OutlinedButton.icon(
|
||||||
onPressed: _busy ? null : _uninstall,
|
onPressed: _busy ? null : _uninstall,
|
||||||
icon: _busy
|
icon: _busy
|
||||||
|
|
@ -927,8 +999,8 @@ class _StoreDetailSheetState extends State<_StoreDetailSheet> {
|
||||||
.withValues(alpha: 0.5),
|
.withValues(alpha: 0.5),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
),
|
||||||
else if (installable)
|
] else if (installable)
|
||||||
FilledButton.icon(
|
FilledButton.icon(
|
||||||
onPressed: _busy ? null : _install,
|
onPressed: _busy ? null : _install,
|
||||||
icon: _busy
|
icon: _busy
|
||||||
|
|
@ -1118,3 +1190,219 @@ IconData _iconForCategory(String category) {
|
||||||
return Icons.extension_outlined;
|
return Icons.extension_outlined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Lazy-loaded inline README renderer. Shows a "Load
|
||||||
|
/// documentation" button until the operator clicks it (so we
|
||||||
|
/// don't blow the network unprompted), then a markdown view of
|
||||||
|
/// the result. Auth / not-found errors come back with friendly
|
||||||
|
/// fix-hint copy and a fallback "View on repository" button.
|
||||||
|
class _DocsPanel extends StatelessWidget {
|
||||||
|
final Future<({String errorKind, String text, String sourceUrl})>? future;
|
||||||
|
final VoidCallback onLoad;
|
||||||
|
final VoidCallback onOpenRepo;
|
||||||
|
|
||||||
|
const _DocsPanel({
|
||||||
|
required this.future,
|
||||||
|
required this.onLoad,
|
||||||
|
required this.onOpenRepo,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.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'),
|
||||||
|
),
|
||||||
|
const SizedBox(width: FaiSpace.sm),
|
||||||
|
Text(
|
||||||
|
'README rendered inline; uses the hub\'s registry token when needed.',
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return FutureBuilder<({String errorKind, String text, String sourceUrl})>(
|
||||||
|
future: future,
|
||||||
|
builder: (context, snap) {
|
||||||
|
if (snap.connectionState == ConnectionState.waiting) {
|
||||||
|
return const Padding(
|
||||||
|
padding: EdgeInsets.symmetric(vertical: FaiSpace.md),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: 14,
|
||||||
|
height: 14,
|
||||||
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
|
),
|
||||||
|
SizedBox(width: FaiSpace.sm),
|
||||||
|
Text('Fetching README…'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (snap.hasError) {
|
||||||
|
return _DocsErrorPanel(
|
||||||
|
message: snap.error.toString(),
|
||||||
|
onOpenRepo: onOpenRepo,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
final r = snap.data!;
|
||||||
|
if (r.errorKind.isNotEmpty) {
|
||||||
|
return _DocsErrorPanel(
|
||||||
|
message: _friendlyDocsError(r.errorKind, r.text),
|
||||||
|
onOpenRepo: onOpenRepo,
|
||||||
|
sourceUrl: r.sourceUrl,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Container(
|
||||||
|
width: double.infinity,
|
||||||
|
constraints: const BoxConstraints(maxHeight: 480),
|
||||||
|
padding: const EdgeInsets.all(FaiSpace.md),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: theme.colorScheme.surfaceContainerHigh,
|
||||||
|
borderRadius: BorderRadius.circular(FaiRadius.sm),
|
||||||
|
border: Border.all(color: theme.colorScheme.outlineVariant),
|
||||||
|
),
|
||||||
|
child: Markdown(
|
||||||
|
data: r.text,
|
||||||
|
shrinkWrap: true,
|
||||||
|
selectable: true,
|
||||||
|
onTapLink: (text, href, title) async {
|
||||||
|
if (href == null || href.isEmpty) return;
|
||||||
|
await SystemActions.openInOs(href);
|
||||||
|
},
|
||||||
|
styleSheet: MarkdownStyleSheet.fromTheme(theme).copyWith(
|
||||||
|
p: theme.textTheme.bodyMedium,
|
||||||
|
code: FaiTheme.mono(
|
||||||
|
size: 12,
|
||||||
|
color: theme.colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
codeblockDecoration: BoxDecoration(
|
||||||
|
color: theme.colorScheme.surfaceContainer,
|
||||||
|
borderRadius: BorderRadius.circular(FaiRadius.sm),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DocsErrorPanel extends StatelessWidget {
|
||||||
|
final String message;
|
||||||
|
final VoidCallback onOpenRepo;
|
||||||
|
final String? sourceUrl;
|
||||||
|
|
||||||
|
const _DocsErrorPanel({
|
||||||
|
required this.message,
|
||||||
|
required this.onOpenRepo,
|
||||||
|
this.sourceUrl,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
return Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.all(FaiSpace.md),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: theme.colorScheme.surfaceContainerHigh,
|
||||||
|
borderRadius: BorderRadius.circular(FaiRadius.sm),
|
||||||
|
border: Border.all(color: theme.colorScheme.outlineVariant),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.info_outline,
|
||||||
|
size: 16,
|
||||||
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
const SizedBox(width: FaiSpace.sm),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
message,
|
||||||
|
style: theme.textTheme.bodySmall,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (sourceUrl != null && sourceUrl!.isNotEmpty) ...[
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
SelectableText(
|
||||||
|
sourceUrl!,
|
||||||
|
style: FaiTheme.mono(
|
||||||
|
size: 10,
|
||||||
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
const SizedBox(height: FaiSpace.sm),
|
||||||
|
OutlinedButton.icon(
|
||||||
|
onPressed: onOpenRepo,
|
||||||
|
icon: const Icon(Icons.open_in_new, size: 14),
|
||||||
|
label: const Text('Open repository in browser'),
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lightweight semver compare used by the "update available"
|
||||||
|
/// badge. Returns negative if a < b, 0 if equal, positive if
|
||||||
|
/// a > b. Splits on '.' and compares integer chunks; falls back
|
||||||
|
/// to lexicographic compare on non-numeric segments so
|
||||||
|
/// `0.10.0-rc.1` still sorts sensibly. Good enough for the
|
||||||
|
/// store's coarse "is there a newer publish" question — the
|
||||||
|
/// hub does its own strict resolution at install time.
|
||||||
|
int _versionCmp(String a, String b) {
|
||||||
|
final aParts = a.split('.');
|
||||||
|
final bParts = b.split('.');
|
||||||
|
final n = aParts.length > bParts.length ? aParts.length : bParts.length;
|
||||||
|
for (var i = 0; i < n; i++) {
|
||||||
|
final ap = i < aParts.length ? aParts[i] : '0';
|
||||||
|
final bp = i < bParts.length ? bParts[i] : '0';
|
||||||
|
final ai = int.tryParse(ap);
|
||||||
|
final bi = int.tryParse(bp);
|
||||||
|
if (ai != null && bi != null) {
|
||||||
|
if (ai != bi) return ai.compareTo(bi);
|
||||||
|
} else {
|
||||||
|
final cmp = ap.compareTo(bp);
|
||||||
|
if (cmp != 0) return cmp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
String _friendlyDocsError(String kind, String detail) {
|
||||||
|
switch (kind) {
|
||||||
|
case 'not_found':
|
||||||
|
return 'This module is not in the hub\'s store index.';
|
||||||
|
case 'no_docs':
|
||||||
|
return 'No repository URL is recorded for this module.';
|
||||||
|
case 'auth':
|
||||||
|
return 'The registry requires authentication to fetch this README. '
|
||||||
|
'Set FAI_REGISTRY_TOKEN in the daemon\'s environment '
|
||||||
|
'and restart the hub.';
|
||||||
|
case 'network':
|
||||||
|
return 'Network error while fetching the README. $detail';
|
||||||
|
case 'parse':
|
||||||
|
return 'README is not valid UTF-8.';
|
||||||
|
default:
|
||||||
|
return detail.isEmpty ? 'Could not load README.' : detail;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
18
pubspec.lock
18
pubspec.lock
|
|
@ -79,7 +79,7 @@ packages:
|
||||||
path: "../fai_dart_sdk"
|
path: "../fai_dart_sdk"
|
||||||
relative: true
|
relative: true
|
||||||
source: path
|
source: path
|
||||||
version: "0.9.0"
|
version: "0.10.0"
|
||||||
fake_async:
|
fake_async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -125,6 +125,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.0.0"
|
version: "6.0.0"
|
||||||
|
flutter_markdown:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: flutter_markdown
|
||||||
|
sha256: "08fb8315236099ff8e90cb87bb2b935e0a724a3af1623000a9cec930468e0f27"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.7.7+1"
|
||||||
flutter_test:
|
flutter_test:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description: flutter
|
description: flutter
|
||||||
|
|
@ -279,6 +287,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.0"
|
version: "1.3.0"
|
||||||
|
markdown:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: markdown
|
||||||
|
sha256: ee85086ad7698b42522c6ad42fe195f1b9898e4d974a1af4576c1a3a176cada9
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "7.3.1"
|
||||||
matcher:
|
matcher:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
name: fai_studio
|
name: fai_studio
|
||||||
description: "F∆I Studio — desktop GUI for the F∆I hub"
|
description: "F∆I Studio — desktop GUI for the F∆I hub"
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
version: 0.18.0
|
version: 0.19.0
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.11.0-200.1.beta
|
sdk: ^3.11.0-200.1.beta
|
||||||
|
|
@ -16,6 +16,8 @@ dependencies:
|
||||||
path: ../fai_dart_sdk
|
path: ../fai_dart_sdk
|
||||||
google_fonts: ^8.1.0
|
google_fonts: ^8.1.0
|
||||||
shared_preferences: ^2.5.5
|
shared_preferences: ^2.5.5
|
||||||
|
# Inline README rendering inside the Store detail sheet.
|
||||||
|
flutter_markdown: ^0.7.7
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue