feat(settings): About section — vendor, versions, license, contact
Some checks are pending
Security / Security check (push) Waiting to run
Some checks are pending
Security / Security check (push) Waiting to run
Procurement personas found no vendor, version, license, or support information anywhere in the app (a hard checklist fail for regulated buyers). Settings gains an About category: product name, Studio + running-hub version, vendor Flemming.AI, author, Apache 2.0 license, contact address, and the docs URL — every value selectable and one-click copyable. Studio version constant moved to data/about_info.dart so sidebar tag and About can never drift. Bumps Studio to 0.71.0. Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
parent
c436e12601
commit
c253f5d23e
9 changed files with 352 additions and 5 deletions
14
lib/data/about_info.dart
Normal file
14
lib/data/about_info.dart
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
// Product identity shown in Settings → About and the sidebar
|
||||
// version tag. One place to bump so procurement-facing facts
|
||||
// (vendor, license, contact) can never drift between surfaces.
|
||||
|
||||
/// Studio's own build version. Bump on every UI release so the
|
||||
/// running app self-identifies.
|
||||
const String kStudioVersion = '0.71.0';
|
||||
|
||||
const String kProductName = 'Ch∆In Studio';
|
||||
const String kVendorName = 'Flemming.AI (F∆I)';
|
||||
const String kAuthorName = 'Dr. Stefan Flemming';
|
||||
const String kLicenseName = 'Apache 2.0';
|
||||
const String kContactEmail = 'chain@flemming.ai';
|
||||
const String kDocsUrl = 'https://chain.flemming.ai';
|
||||
|
|
@ -1187,6 +1187,22 @@ class HubService {
|
|||
}
|
||||
|
||||
/// Composite "doctor" snapshot. One round-trip per piece, run
|
||||
/// Version string of the RUNNING hub (from CheckUpdate's
|
||||
/// local_version — a local RPC field; the manifest fetch result
|
||||
/// is ignored). Empty when the hub is unreachable. Used by
|
||||
/// Settings → About.
|
||||
Future<String> hubVersion() async {
|
||||
try {
|
||||
final r = await _client.checkUpdate().timeout(
|
||||
const Duration(seconds: 2),
|
||||
onTimeout: () => CheckUpdateResponse(),
|
||||
);
|
||||
return r.localVersion;
|
||||
} catch (_) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/// in parallel so the page populates fast.
|
||||
Future<DoctorSnapshot> doctor() async {
|
||||
final results = await Future.wait([
|
||||
|
|
|
|||
|
|
@ -1549,6 +1549,21 @@
|
|||
"doctorPathStudioErrors": "Studio-Fehler",
|
||||
"settingsCategoryGeneral": "Allgemein",
|
||||
"settingsCategoryAppearance": "Darstellung",
|
||||
"settingsCategoryAbout": "Über",
|
||||
"aboutPanelTitle": "Über Ch∆In",
|
||||
"aboutPanelBody": "Produkt-, Hersteller- und Lizenzangaben dieser Installation — alle Werte sind kopierbar.",
|
||||
"aboutProduct": "Produkt",
|
||||
"aboutStudioVersion": "Studio-Version",
|
||||
"aboutHubVersion": "Hub-Version",
|
||||
"aboutHubVersionUnknown": "nicht verbunden",
|
||||
"aboutVendor": "Hersteller",
|
||||
"aboutAuthor": "Autor",
|
||||
"aboutLicense": "Lizenz",
|
||||
"aboutContact": "Kontakt",
|
||||
"aboutDocs": "Dokumentation",
|
||||
"aboutOpenLink": "Im Browser öffnen",
|
||||
"aboutCopyTooltip": "Kopieren",
|
||||
"aboutCopiedToast": "In die Zwischenablage kopiert.",
|
||||
"sidebarSearchLabel": "Suchen & Befehle",
|
||||
"settingsSidebarPinnedTitle": "Navigation immer ausgeklappt",
|
||||
"settingsSidebarPinnedBody": "Zeigt die Beschriftungen der Seitenleiste dauerhaft an, statt sie nur beim Überfahren mit der Maus einzublenden.",
|
||||
|
|
|
|||
|
|
@ -1573,6 +1573,21 @@
|
|||
"doctorPathStudioErrors": "Studio errors",
|
||||
"settingsCategoryGeneral": "General",
|
||||
"settingsCategoryAppearance": "Appearance",
|
||||
"settingsCategoryAbout": "About",
|
||||
"aboutPanelTitle": "About Ch∆In",
|
||||
"aboutPanelBody": "Product, vendor, and license details of this installation — every value is copyable.",
|
||||
"aboutProduct": "Product",
|
||||
"aboutStudioVersion": "Studio version",
|
||||
"aboutHubVersion": "Hub version",
|
||||
"aboutHubVersionUnknown": "not connected",
|
||||
"aboutVendor": "Vendor",
|
||||
"aboutAuthor": "Author",
|
||||
"aboutLicense": "License",
|
||||
"aboutContact": "Contact",
|
||||
"aboutDocs": "Documentation",
|
||||
"aboutOpenLink": "Open in browser",
|
||||
"aboutCopyTooltip": "Copy",
|
||||
"aboutCopiedToast": "Copied to the clipboard.",
|
||||
"sidebarSearchLabel": "Search & commands",
|
||||
"settingsSidebarPinnedTitle": "Keep the navigation expanded",
|
||||
"settingsSidebarPinnedBody": "Shows the sidebar labels permanently instead of only while hovering with the mouse.",
|
||||
|
|
|
|||
|
|
@ -4544,6 +4544,96 @@ abstract class AppLocalizations {
|
|||
/// **'Appearance'**
|
||||
String get settingsCategoryAppearance;
|
||||
|
||||
/// No description provided for @settingsCategoryAbout.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'About'**
|
||||
String get settingsCategoryAbout;
|
||||
|
||||
/// No description provided for @aboutPanelTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'About Ch∆In'**
|
||||
String get aboutPanelTitle;
|
||||
|
||||
/// No description provided for @aboutPanelBody.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Product, vendor, and license details of this installation — every value is copyable.'**
|
||||
String get aboutPanelBody;
|
||||
|
||||
/// No description provided for @aboutProduct.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Product'**
|
||||
String get aboutProduct;
|
||||
|
||||
/// No description provided for @aboutStudioVersion.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Studio version'**
|
||||
String get aboutStudioVersion;
|
||||
|
||||
/// No description provided for @aboutHubVersion.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Hub version'**
|
||||
String get aboutHubVersion;
|
||||
|
||||
/// No description provided for @aboutHubVersionUnknown.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'not connected'**
|
||||
String get aboutHubVersionUnknown;
|
||||
|
||||
/// No description provided for @aboutVendor.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Vendor'**
|
||||
String get aboutVendor;
|
||||
|
||||
/// No description provided for @aboutAuthor.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Author'**
|
||||
String get aboutAuthor;
|
||||
|
||||
/// No description provided for @aboutLicense.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'License'**
|
||||
String get aboutLicense;
|
||||
|
||||
/// No description provided for @aboutContact.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Contact'**
|
||||
String get aboutContact;
|
||||
|
||||
/// No description provided for @aboutDocs.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Documentation'**
|
||||
String get aboutDocs;
|
||||
|
||||
/// No description provided for @aboutOpenLink.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Open in browser'**
|
||||
String get aboutOpenLink;
|
||||
|
||||
/// No description provided for @aboutCopyTooltip.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Copy'**
|
||||
String get aboutCopyTooltip;
|
||||
|
||||
/// No description provided for @aboutCopiedToast.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Copied to the clipboard.'**
|
||||
String get aboutCopiedToast;
|
||||
|
||||
/// No description provided for @sidebarSearchLabel.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
|
|||
|
|
@ -2666,6 +2666,52 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
@override
|
||||
String get settingsCategoryAppearance => 'Darstellung';
|
||||
|
||||
@override
|
||||
String get settingsCategoryAbout => 'Über';
|
||||
|
||||
@override
|
||||
String get aboutPanelTitle => 'Über Ch∆In';
|
||||
|
||||
@override
|
||||
String get aboutPanelBody =>
|
||||
'Produkt-, Hersteller- und Lizenzangaben dieser Installation — alle Werte sind kopierbar.';
|
||||
|
||||
@override
|
||||
String get aboutProduct => 'Produkt';
|
||||
|
||||
@override
|
||||
String get aboutStudioVersion => 'Studio-Version';
|
||||
|
||||
@override
|
||||
String get aboutHubVersion => 'Hub-Version';
|
||||
|
||||
@override
|
||||
String get aboutHubVersionUnknown => 'nicht verbunden';
|
||||
|
||||
@override
|
||||
String get aboutVendor => 'Hersteller';
|
||||
|
||||
@override
|
||||
String get aboutAuthor => 'Autor';
|
||||
|
||||
@override
|
||||
String get aboutLicense => 'Lizenz';
|
||||
|
||||
@override
|
||||
String get aboutContact => 'Kontakt';
|
||||
|
||||
@override
|
||||
String get aboutDocs => 'Dokumentation';
|
||||
|
||||
@override
|
||||
String get aboutOpenLink => 'Im Browser öffnen';
|
||||
|
||||
@override
|
||||
String get aboutCopyTooltip => 'Kopieren';
|
||||
|
||||
@override
|
||||
String get aboutCopiedToast => 'In die Zwischenablage kopiert.';
|
||||
|
||||
@override
|
||||
String get sidebarSearchLabel => 'Suchen & Befehle';
|
||||
|
||||
|
|
|
|||
|
|
@ -2668,6 +2668,52 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
@override
|
||||
String get settingsCategoryAppearance => 'Appearance';
|
||||
|
||||
@override
|
||||
String get settingsCategoryAbout => 'About';
|
||||
|
||||
@override
|
||||
String get aboutPanelTitle => 'About Ch∆In';
|
||||
|
||||
@override
|
||||
String get aboutPanelBody =>
|
||||
'Product, vendor, and license details of this installation — every value is copyable.';
|
||||
|
||||
@override
|
||||
String get aboutProduct => 'Product';
|
||||
|
||||
@override
|
||||
String get aboutStudioVersion => 'Studio version';
|
||||
|
||||
@override
|
||||
String get aboutHubVersion => 'Hub version';
|
||||
|
||||
@override
|
||||
String get aboutHubVersionUnknown => 'not connected';
|
||||
|
||||
@override
|
||||
String get aboutVendor => 'Vendor';
|
||||
|
||||
@override
|
||||
String get aboutAuthor => 'Author';
|
||||
|
||||
@override
|
||||
String get aboutLicense => 'License';
|
||||
|
||||
@override
|
||||
String get aboutContact => 'Contact';
|
||||
|
||||
@override
|
||||
String get aboutDocs => 'Documentation';
|
||||
|
||||
@override
|
||||
String get aboutOpenLink => 'Open in browser';
|
||||
|
||||
@override
|
||||
String get aboutCopyTooltip => 'Copy';
|
||||
|
||||
@override
|
||||
String get aboutCopiedToast => 'Copied to the clipboard.';
|
||||
|
||||
@override
|
||||
String get sidebarSearchLabel => 'Search & commands';
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import 'package:flutter/foundation.dart' show defaultTargetPlatform;
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'data/about_info.dart';
|
||||
import 'data/chain_log.dart';
|
||||
import 'data/error_presentation.dart';
|
||||
import 'data/hub.dart';
|
||||
|
|
@ -33,10 +34,9 @@ import 'theme/tokens.dart';
|
|||
import 'widgets/chain_search_palette.dart';
|
||||
import 'widgets/widgets.dart';
|
||||
|
||||
/// Studio's own build version. Bump on every UI commit so the
|
||||
/// running app self-identifies — visible in the sidebar header
|
||||
/// and quick-glance proof that you're seeing the current build.
|
||||
const String kStudioVersion = '0.70.0';
|
||||
/// Studio's own build version — canonical value lives in
|
||||
/// data/about_info.dart (shared with Settings → About).
|
||||
export 'data/about_info.dart' show kStudioVersion;
|
||||
|
||||
/// Run one pre-frame restore step, absorbing any failure. Everything
|
||||
/// before `runApp` is a black window waiting to happen: an exception
|
||||
|
|
|
|||
|
|
@ -4,7 +4,9 @@
|
|||
|
||||
import 'package:chain_client_sdk/chain_client_sdk.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart' show Clipboard, ClipboardData;
|
||||
|
||||
import '../data/about_info.dart';
|
||||
import '../data/error_presentation.dart';
|
||||
import '../data/hub.dart';
|
||||
import '../data/hub_auth_token.dart';
|
||||
|
|
@ -41,7 +43,15 @@ class ChainSettingsDialog extends StatefulWidget {
|
|||
/// Sidebar categories — every Settings panel belongs to exactly
|
||||
/// one. Order here drives the sidebar order and the
|
||||
/// first-opened category.
|
||||
enum _Category { general, appearance, ai, integrations, security, maintenance }
|
||||
enum _Category {
|
||||
general,
|
||||
appearance,
|
||||
ai,
|
||||
integrations,
|
||||
security,
|
||||
maintenance,
|
||||
about,
|
||||
}
|
||||
|
||||
class _FaiSettingsDialogState extends State<ChainSettingsDialog> {
|
||||
late final TextEditingController _host;
|
||||
|
|
@ -70,6 +80,10 @@ class _FaiSettingsDialogState extends State<ChainSettingsDialog> {
|
|||
/// auth-policy panel below re-queries with the new credentials.
|
||||
int _authPolicyReloadTick = 0;
|
||||
|
||||
/// Running hub's version for the About panel. Null while
|
||||
/// loading, empty string when the hub is unreachable.
|
||||
String? _hubVersion;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
|
@ -83,6 +97,13 @@ class _FaiSettingsDialogState extends State<ChainSettingsDialog> {
|
|||
_loadN8nEndpoints();
|
||||
_loadRegistryToken();
|
||||
_loadHubAuthToken();
|
||||
_loadHubVersion();
|
||||
}
|
||||
|
||||
Future<void> _loadHubVersion() async {
|
||||
final v = await HubService.instance.hubVersion();
|
||||
if (!mounted) return;
|
||||
setState(() => _hubVersion = v);
|
||||
}
|
||||
|
||||
Future<void> _loadHubAuthToken() async {
|
||||
|
|
@ -392,6 +413,7 @@ class _FaiSettingsDialogState extends State<ChainSettingsDialog> {
|
|||
_Category.integrations => _integrationsPanel(l, theme),
|
||||
_Category.security => _securityPanel(l, theme),
|
||||
_Category.maintenance => _maintenancePanel(l, theme),
|
||||
_Category.about => _aboutPanel(l, theme),
|
||||
};
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
|
|
@ -621,6 +643,39 @@ class _FaiSettingsDialogState extends State<ChainSettingsDialog> {
|
|||
];
|
||||
}
|
||||
|
||||
List<Widget> _aboutPanel(AppLocalizations l, ThemeData theme) {
|
||||
// Procurement-facing identity: who makes this, which
|
||||
// versions run, under which license, whom to contact.
|
||||
// Every value is selectable AND has a copy affordance.
|
||||
return [
|
||||
_panelTitle(l.aboutPanelTitle, l.aboutPanelBody, theme),
|
||||
_AboutRow(label: l.aboutProduct, value: kProductName),
|
||||
_AboutRow(label: l.aboutStudioVersion, value: kStudioVersion),
|
||||
_AboutRow(
|
||||
label: l.aboutHubVersion,
|
||||
value: _hubVersion == null
|
||||
? '…'
|
||||
: _hubVersion!.isEmpty
|
||||
? l.aboutHubVersionUnknown
|
||||
: _hubVersion!,
|
||||
),
|
||||
_AboutRow(label: l.aboutVendor, value: kVendorName),
|
||||
_AboutRow(label: l.aboutAuthor, value: kAuthorName),
|
||||
_AboutRow(label: l.aboutLicense, value: kLicenseName),
|
||||
_AboutRow(label: l.aboutContact, value: kContactEmail),
|
||||
_AboutRow(
|
||||
label: l.aboutDocs,
|
||||
value: kDocsUrl,
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Icons.open_in_new, size: 16),
|
||||
tooltip: l.aboutOpenLink,
|
||||
visualDensity: VisualDensity.compact,
|
||||
onPressed: () => SystemActions.openInOs(kDocsUrl),
|
||||
),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
List<Widget> _aiPanel(AppLocalizations l, ThemeData theme) {
|
||||
return [
|
||||
_panelTitle(
|
||||
|
|
@ -779,6 +834,55 @@ class _FaiSettingsDialogState extends State<ChainSettingsDialog> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Label/value row of the About panel. The value is selectable
|
||||
/// and carries an explicit copy button (procurement reviewers
|
||||
/// paste these into checklists).
|
||||
class _AboutRow extends StatelessWidget {
|
||||
final String label;
|
||||
final String value;
|
||||
final Widget? trailing;
|
||||
|
||||
const _AboutRow({required this.label, required this.value, this.trailing});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final l = AppLocalizations.of(context)!;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: ChainSpace.xs),
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 150,
|
||||
child: Text(
|
||||
label,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: SelectableText(value, style: theme.textTheme.bodyMedium),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.copy_outlined, size: 14),
|
||||
tooltip: l.aboutCopyTooltip,
|
||||
visualDensity: VisualDensity.compact,
|
||||
onPressed: () async {
|
||||
await Clipboard.setData(ClipboardData(text: value));
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(l.aboutCopiedToast)),
|
||||
);
|
||||
},
|
||||
),
|
||||
?trailing,
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ChannelRow extends StatelessWidget {
|
||||
final ChannelInfo channel;
|
||||
final bool active;
|
||||
|
|
@ -2635,6 +2739,7 @@ class _Sidebar extends StatelessWidget {
|
|||
Icons.build_outlined,
|
||||
l.settingsCategoryMaintenance,
|
||||
),
|
||||
(_Category.about, Icons.info_outline, l.settingsCategoryAbout),
|
||||
];
|
||||
return SizedBox(
|
||||
width: 220,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue