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
|
|
@ -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