feat(studio): registry credentials panel in Settings (v0.44.0)
The hub install path now reads `~/.fai/registry-token` as a fallback when `FAI_REGISTRY_TOKEN` is unset (platform v0.10.92). Studio now writes that file directly: a fresh operator pastes the PAT into Settings → Registry credentials, hits Save, and the next install attempt resolves the auth wall without any shell or env-var setup. The token never round-trips back into Studio after save — status is shown only as "Configured (40 chars)" / "Not set" with no display of the secret itself. The Clear action deletes the file. On Unix the file is chmod-ed to 0600 (owner-only read/write); Windows leaves the default user ACL in place. Storage is strictly local: `~/.fai/registry-token`, never sent to a remote service. The hint text under the field says so to make the data flow explicit. Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
parent
01f3f773cf
commit
878f0a4e28
8 changed files with 464 additions and 6 deletions
66
lib/data/registry_token.dart
Normal file
66
lib/data/registry_token.dart
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
import 'dart:io';
|
||||||
|
import 'package:path/path.dart' as p;
|
||||||
|
|
||||||
|
/// Operator-managed registry auth token, kept at
|
||||||
|
/// `~/.fai/registry-token` (mode 0600 on Unix). The hub reads
|
||||||
|
/// this file at install time when `FAI_REGISTRY_TOKEN` is unset
|
||||||
|
/// — see `download_to_temp` in `crates/fai_hub/src/lib.rs`.
|
||||||
|
///
|
||||||
|
/// Studio writes the file directly so a fresh install never
|
||||||
|
/// requires the operator to fiddle with shell env vars.
|
||||||
|
class RegistryToken {
|
||||||
|
static String _faiHome() {
|
||||||
|
final home = Platform.environment['HOME'] ?? Platform.environment['USERPROFILE'];
|
||||||
|
if (home == null || home.isEmpty) {
|
||||||
|
throw StateError('Cannot resolve home directory (no HOME / USERPROFILE)');
|
||||||
|
}
|
||||||
|
return p.join(home, '.fai');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Absolute path to the token file.
|
||||||
|
static String get path => p.join(_faiHome(), 'registry-token');
|
||||||
|
|
||||||
|
/// True iff the file exists with a non-empty trimmed body.
|
||||||
|
static Future<bool> isConfigured() async {
|
||||||
|
final f = File(path);
|
||||||
|
if (!await f.exists()) return false;
|
||||||
|
final content = await f.readAsString();
|
||||||
|
return content.trim().isNotEmpty;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Length of the trimmed token, or null when the file is
|
||||||
|
/// missing / empty. Used for status display ("Configured
|
||||||
|
/// (40 chars)") without ever surfacing the secret.
|
||||||
|
static Future<int?> charCount() async {
|
||||||
|
final f = File(path);
|
||||||
|
if (!await f.exists()) return null;
|
||||||
|
final content = await f.readAsString();
|
||||||
|
final trimmed = content.trim();
|
||||||
|
return trimmed.isEmpty ? null : trimmed.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Persist [token] to disk, creating `~/.fai/` if needed.
|
||||||
|
/// On Unix the file is chmod-ed to 0600 (owner read/write
|
||||||
|
/// only). On Windows the default user-ACL is left alone —
|
||||||
|
/// best-effort, no PowerShell handshake.
|
||||||
|
static Future<void> write(String token) async {
|
||||||
|
final f = File(path);
|
||||||
|
await f.parent.create(recursive: true);
|
||||||
|
await f.writeAsString(token.trim(), flush: true);
|
||||||
|
if (Platform.isLinux || Platform.isMacOS) {
|
||||||
|
try {
|
||||||
|
await Process.run('chmod', ['600', f.path]);
|
||||||
|
} catch (_) {
|
||||||
|
// best-effort; if chmod is unavailable the file still
|
||||||
|
// exists with default perms (usually 0644, world-readable
|
||||||
|
// but only on the operator's own machine).
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Delete the token file. No-op when the file doesn't exist.
|
||||||
|
static Future<void> delete() async {
|
||||||
|
final f = File(path);
|
||||||
|
if (await f.exists()) await f.delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -433,6 +433,22 @@
|
||||||
"@flowsOpenInEditorFailed": {
|
"@flowsOpenInEditorFailed": {
|
||||||
"placeholders": { "error": { "type": "String" } }
|
"placeholders": { "error": { "type": "String" } }
|
||||||
},
|
},
|
||||||
|
"registryCredentialsHeader": "REGISTRY-ZUGANGSDATEN",
|
||||||
|
"registryCredentialsBlurb": "Token zum Herunterladen von .fai-Modulen aus einer Registry mit Anmeldepflicht (Forgejo, private GitHub-Repos). Gespeichert in ~/.fai/registry-token, Modus 0600. Die Umgebungsvariable FAI_REGISTRY_TOKEN hat weiterhin Vorrang, wenn gesetzt.",
|
||||||
|
"registryTokenStatusConfigured": "Konfiguriert ({chars} Zeichen)",
|
||||||
|
"@registryTokenStatusConfigured": { "placeholders": { "chars": { "type": "int" } } },
|
||||||
|
"registryTokenStatusNotSet": "Nicht gesetzt",
|
||||||
|
"registryTokenFieldLabel": "Token",
|
||||||
|
"registryTokenFieldHint": "Forgejo- oder GitHub-PAT einfügen",
|
||||||
|
"registryTokenSaveButton": "Speichern",
|
||||||
|
"registryTokenClearButton": "Entfernen",
|
||||||
|
"registryTokenStorageHint": "Lokal gespeichert in ~/.fai/registry-token. Wird nie an einen Remote-Dienst übertragen.",
|
||||||
|
"registryTokenSavedToast": "Registry-Token gespeichert.",
|
||||||
|
"registryTokenClearedToast": "Registry-Token entfernt.",
|
||||||
|
"registryTokenSaveFailedToast": "Speichern fehlgeschlagen: {error}",
|
||||||
|
"@registryTokenSaveFailedToast": {
|
||||||
|
"placeholders": { "error": { "type": "String" } }
|
||||||
|
},
|
||||||
"welcomeChecklistAllSetTitle": "Du bist eingerichtet.",
|
"welcomeChecklistAllSetTitle": "Du bist eingerichtet.",
|
||||||
"welcomeChecklistAllSetBody": "Drei Stränge, an denen du als nächstes ziehen kannst:",
|
"welcomeChecklistAllSetBody": "Drei Stränge, an denen du als nächstes ziehen kannst:",
|
||||||
"welcomeChecklistNextAuditTitle": "Audit-Log lesen",
|
"welcomeChecklistNextAuditTitle": "Audit-Log lesen",
|
||||||
|
|
|
||||||
|
|
@ -434,6 +434,22 @@
|
||||||
"@flowsOpenInEditorFailed": {
|
"@flowsOpenInEditorFailed": {
|
||||||
"placeholders": { "error": { "type": "String" } }
|
"placeholders": { "error": { "type": "String" } }
|
||||||
},
|
},
|
||||||
|
"registryCredentialsHeader": "REGISTRY CREDENTIALS",
|
||||||
|
"registryCredentialsBlurb": "Token used to download .fai modules from a registry behind a signin wall (Forgejo, GitHub-private). Stored at ~/.fai/registry-token, mode 0600. The FAI_REGISTRY_TOKEN env var still wins when set.",
|
||||||
|
"registryTokenStatusConfigured": "Configured ({chars} chars)",
|
||||||
|
"@registryTokenStatusConfigured": { "placeholders": { "chars": { "type": "int" } } },
|
||||||
|
"registryTokenStatusNotSet": "Not set",
|
||||||
|
"registryTokenFieldLabel": "Token",
|
||||||
|
"registryTokenFieldHint": "Paste your Forgejo or GitHub personal access token",
|
||||||
|
"registryTokenSaveButton": "Save",
|
||||||
|
"registryTokenClearButton": "Clear",
|
||||||
|
"registryTokenStorageHint": "Saved locally to ~/.fai/registry-token. Never sent to a remote service.",
|
||||||
|
"registryTokenSavedToast": "Registry token saved.",
|
||||||
|
"registryTokenClearedToast": "Registry token cleared.",
|
||||||
|
"registryTokenSaveFailedToast": "Could not save: {error}",
|
||||||
|
"@registryTokenSaveFailedToast": {
|
||||||
|
"placeholders": { "error": { "type": "String" } }
|
||||||
|
},
|
||||||
"welcomeChecklistAllSetTitle": "You're set up.",
|
"welcomeChecklistAllSetTitle": "You're set up.",
|
||||||
"welcomeChecklistAllSetBody": "Three threads to pull on next:",
|
"welcomeChecklistAllSetBody": "Three threads to pull on next:",
|
||||||
"welcomeChecklistNextAuditTitle": "Read the audit log",
|
"welcomeChecklistNextAuditTitle": "Read the audit log",
|
||||||
|
|
|
||||||
|
|
@ -1970,6 +1970,78 @@ abstract class AppLocalizations {
|
||||||
/// **'Could not open file: {error}'**
|
/// **'Could not open file: {error}'**
|
||||||
String flowsOpenInEditorFailed(String error);
|
String flowsOpenInEditorFailed(String error);
|
||||||
|
|
||||||
|
/// No description provided for @registryCredentialsHeader.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'REGISTRY CREDENTIALS'**
|
||||||
|
String get registryCredentialsHeader;
|
||||||
|
|
||||||
|
/// No description provided for @registryCredentialsBlurb.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Token used to download .fai modules from a registry behind a signin wall (Forgejo, GitHub-private). Stored at ~/.fai/registry-token, mode 0600. The FAI_REGISTRY_TOKEN env var still wins when set.'**
|
||||||
|
String get registryCredentialsBlurb;
|
||||||
|
|
||||||
|
/// No description provided for @registryTokenStatusConfigured.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Configured ({chars} chars)'**
|
||||||
|
String registryTokenStatusConfigured(int chars);
|
||||||
|
|
||||||
|
/// No description provided for @registryTokenStatusNotSet.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Not set'**
|
||||||
|
String get registryTokenStatusNotSet;
|
||||||
|
|
||||||
|
/// No description provided for @registryTokenFieldLabel.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Token'**
|
||||||
|
String get registryTokenFieldLabel;
|
||||||
|
|
||||||
|
/// No description provided for @registryTokenFieldHint.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Paste your Forgejo or GitHub personal access token'**
|
||||||
|
String get registryTokenFieldHint;
|
||||||
|
|
||||||
|
/// No description provided for @registryTokenSaveButton.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Save'**
|
||||||
|
String get registryTokenSaveButton;
|
||||||
|
|
||||||
|
/// No description provided for @registryTokenClearButton.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Clear'**
|
||||||
|
String get registryTokenClearButton;
|
||||||
|
|
||||||
|
/// No description provided for @registryTokenStorageHint.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Saved locally to ~/.fai/registry-token. Never sent to a remote service.'**
|
||||||
|
String get registryTokenStorageHint;
|
||||||
|
|
||||||
|
/// No description provided for @registryTokenSavedToast.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Registry token saved.'**
|
||||||
|
String get registryTokenSavedToast;
|
||||||
|
|
||||||
|
/// No description provided for @registryTokenClearedToast.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Registry token cleared.'**
|
||||||
|
String get registryTokenClearedToast;
|
||||||
|
|
||||||
|
/// No description provided for @registryTokenSaveFailedToast.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Could not save: {error}'**
|
||||||
|
String registryTokenSaveFailedToast(String error);
|
||||||
|
|
||||||
/// No description provided for @welcomeChecklistAllSetTitle.
|
/// No description provided for @welcomeChecklistAllSetTitle.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|
|
||||||
|
|
@ -1117,6 +1117,48 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
return 'Konnte Datei nicht öffnen: $error';
|
return 'Konnte Datei nicht öffnen: $error';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get registryCredentialsHeader => 'REGISTRY-ZUGANGSDATEN';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get registryCredentialsBlurb =>
|
||||||
|
'Token zum Herunterladen von .fai-Modulen aus einer Registry mit Anmeldepflicht (Forgejo, private GitHub-Repos). Gespeichert in ~/.fai/registry-token, Modus 0600. Die Umgebungsvariable FAI_REGISTRY_TOKEN hat weiterhin Vorrang, wenn gesetzt.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String registryTokenStatusConfigured(int chars) {
|
||||||
|
return 'Konfiguriert ($chars Zeichen)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get registryTokenStatusNotSet => 'Nicht gesetzt';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get registryTokenFieldLabel => 'Token';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get registryTokenFieldHint => 'Forgejo- oder GitHub-PAT einfügen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get registryTokenSaveButton => 'Speichern';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get registryTokenClearButton => 'Entfernen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get registryTokenStorageHint =>
|
||||||
|
'Lokal gespeichert in ~/.fai/registry-token. Wird nie an einen Remote-Dienst übertragen.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get registryTokenSavedToast => 'Registry-Token gespeichert.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get registryTokenClearedToast => 'Registry-Token entfernt.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String registryTokenSaveFailedToast(String error) {
|
||||||
|
return 'Speichern fehlgeschlagen: $error';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get welcomeChecklistAllSetTitle => 'Du bist eingerichtet.';
|
String get welcomeChecklistAllSetTitle => 'Du bist eingerichtet.';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1131,6 +1131,49 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
return 'Could not open file: $error';
|
return 'Could not open file: $error';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get registryCredentialsHeader => 'REGISTRY CREDENTIALS';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get registryCredentialsBlurb =>
|
||||||
|
'Token used to download .fai modules from a registry behind a signin wall (Forgejo, GitHub-private). Stored at ~/.fai/registry-token, mode 0600. The FAI_REGISTRY_TOKEN env var still wins when set.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String registryTokenStatusConfigured(int chars) {
|
||||||
|
return 'Configured ($chars chars)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get registryTokenStatusNotSet => 'Not set';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get registryTokenFieldLabel => 'Token';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get registryTokenFieldHint =>
|
||||||
|
'Paste your Forgejo or GitHub personal access token';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get registryTokenSaveButton => 'Save';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get registryTokenClearButton => 'Clear';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get registryTokenStorageHint =>
|
||||||
|
'Saved locally to ~/.fai/registry-token. Never sent to a remote service.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get registryTokenSavedToast => 'Registry token saved.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get registryTokenClearedToast => 'Registry token cleared.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String registryTokenSaveFailedToast(String error) {
|
||||||
|
return 'Could not save: $error';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get welcomeChecklistAllSetTitle => 'You\'re set up.';
|
String get welcomeChecklistAllSetTitle => 'You\'re set up.';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import 'package:fai_dart_sdk/fai_dart_sdk.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import '../data/hub.dart';
|
import '../data/hub.dart';
|
||||||
|
import '../data/registry_token.dart';
|
||||||
import '../data/system_actions.dart';
|
import '../data/system_actions.dart';
|
||||||
import '../l10n/app_localizations.dart';
|
import '../l10n/app_localizations.dart';
|
||||||
import '../theme/theme.dart';
|
import '../theme/theme.dart';
|
||||||
|
|
@ -41,6 +42,10 @@ class _FaiSettingsDialogState extends State<FaiSettingsDialog> {
|
||||||
SystemAiStatus? _aiStatus;
|
SystemAiStatus? _aiStatus;
|
||||||
List<McpClientInfo>? _mcpClients;
|
List<McpClientInfo>? _mcpClients;
|
||||||
List<N8nEndpointInfo>? _n8nEndpoints;
|
List<N8nEndpointInfo>? _n8nEndpoints;
|
||||||
|
/// Length of the configured registry token, or null when the
|
||||||
|
/// `~/.fai/registry-token` file is missing / empty. Reloaded
|
||||||
|
/// after save / clear.
|
||||||
|
int? _registryTokenChars;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
|
@ -53,6 +58,51 @@ class _FaiSettingsDialogState extends State<FaiSettingsDialog> {
|
||||||
_loadAiStatus();
|
_loadAiStatus();
|
||||||
_loadMcpClients();
|
_loadMcpClients();
|
||||||
_loadN8nEndpoints();
|
_loadN8nEndpoints();
|
||||||
|
_loadRegistryToken();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadRegistryToken() async {
|
||||||
|
try {
|
||||||
|
final n = await RegistryToken.charCount();
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() => _registryTokenChars = n);
|
||||||
|
} catch (_) {/* fail-quiet */}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _saveRegistryToken(String token) async {
|
||||||
|
try {
|
||||||
|
await RegistryToken.write(token);
|
||||||
|
await _loadRegistryToken();
|
||||||
|
if (!mounted) return;
|
||||||
|
final l = AppLocalizations.of(context)!;
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text(l.registryTokenSavedToast)),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
if (!mounted) return;
|
||||||
|
final l = AppLocalizations.of(context)!;
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text(l.registryTokenSaveFailedToast(e.toString()))),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _clearRegistryToken() async {
|
||||||
|
try {
|
||||||
|
await RegistryToken.delete();
|
||||||
|
await _loadRegistryToken();
|
||||||
|
if (!mounted) return;
|
||||||
|
final l = AppLocalizations.of(context)!;
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text(l.registryTokenClearedToast)),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
if (!mounted) return;
|
||||||
|
final l = AppLocalizations.of(context)!;
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text(l.registryTokenSaveFailedToast(e.toString()))),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _loadN8nEndpoints() async {
|
Future<void> _loadN8nEndpoints() async {
|
||||||
|
|
@ -453,6 +503,12 @@ class _FaiSettingsDialogState extends State<FaiSettingsDialog> {
|
||||||
onRefresh: _refreshN8nEndpoints,
|
onRefresh: _refreshN8nEndpoints,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
const SizedBox(height: FaiSpace.lg),
|
||||||
|
_RegistryCredentialsPanel(
|
||||||
|
configuredChars: _registryTokenChars,
|
||||||
|
onSave: _saveRegistryToken,
|
||||||
|
onClear: _clearRegistryToken,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -1381,12 +1437,159 @@ class _AddN8nEndpointDialogState extends State<_AddN8nEndpointDialog> {
|
||||||
/// metadata; nothing is auto-spawned. Trust model:
|
/// metadata; nothing is auto-spawned. Trust model:
|
||||||
/// referencing public projects with their canonical install
|
/// referencing public projects with their canonical install
|
||||||
/// command is the same legal / security shape as a package
|
/// command is the same legal / security shape as a package
|
||||||
/// manager listing a third-party repo.
|
/// Credentials panel for the operator's registry auth token.
|
||||||
|
/// The hub reads the same token via `~/.fai/registry-token`
|
||||||
|
/// (or the `FAI_REGISTRY_TOKEN` env var, which still wins)
|
||||||
|
/// when downloading `.fai` bundles from a registry behind a
|
||||||
|
/// signin wall — Forgejo with REQUIRE_SIGNIN_VIEW=true,
|
||||||
|
/// GitHub-private releases, etc.
|
||||||
///
|
///
|
||||||
/// All entries point at official Anthropic MCP servers from
|
/// The token itself is never round-tripped back into Studio
|
||||||
/// `github.com/modelcontextprotocol/servers` plus a few
|
/// after save: `configuredChars` is the trimmed length, used
|
||||||
/// community-curated standards. Refresh the list when the
|
/// only as a "Configured (40 chars)" status display so the
|
||||||
/// upstream README changes.
|
/// operator knows something landed without seeing the secret.
|
||||||
|
class _RegistryCredentialsPanel extends StatefulWidget {
|
||||||
|
/// Trimmed length of the persisted token, or null when none
|
||||||
|
/// is configured.
|
||||||
|
final int? configuredChars;
|
||||||
|
final ValueChanged<String> onSave;
|
||||||
|
final Future<void> Function() onClear;
|
||||||
|
|
||||||
|
const _RegistryCredentialsPanel({
|
||||||
|
required this.configuredChars,
|
||||||
|
required this.onSave,
|
||||||
|
required this.onClear,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_RegistryCredentialsPanel> createState() =>
|
||||||
|
_RegistryCredentialsPanelState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RegistryCredentialsPanelState
|
||||||
|
extends State<_RegistryCredentialsPanel> {
|
||||||
|
final _controller = TextEditingController();
|
||||||
|
bool _obscured = true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_controller.addListener(() {
|
||||||
|
if (mounted) setState(() {});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final l = AppLocalizations.of(context)!;
|
||||||
|
final isConfigured = widget.configuredChars != null;
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
l.registryCredentialsHeader,
|
||||||
|
style: theme.textTheme.labelSmall?.copyWith(
|
||||||
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
|
letterSpacing: 0.6,
|
||||||
|
fontSize: 10,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
Text(
|
||||||
|
l.registryCredentialsBlurb,
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: FaiSpace.sm),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
isConfigured ? Icons.check_circle : Icons.radio_button_unchecked,
|
||||||
|
size: 14,
|
||||||
|
color: isConfigured
|
||||||
|
? theme.colorScheme.primary
|
||||||
|
: theme.colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
const SizedBox(width: FaiSpace.xs),
|
||||||
|
Text(
|
||||||
|
isConfigured
|
||||||
|
? l.registryTokenStatusConfigured(widget.configuredChars!)
|
||||||
|
: l.registryTokenStatusNotSet,
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
color: isConfigured
|
||||||
|
? theme.colorScheme.onSurface
|
||||||
|
: theme.colorScheme.onSurfaceVariant,
|
||||||
|
fontWeight: isConfigured ? FontWeight.w600 : null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
if (isConfigured)
|
||||||
|
TextButton.icon(
|
||||||
|
icon: const Icon(Icons.delete_outline, size: 14),
|
||||||
|
onPressed: () async {
|
||||||
|
await widget.onClear();
|
||||||
|
_controller.clear();
|
||||||
|
},
|
||||||
|
label: Text(l.registryTokenClearButton),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: FaiSpace.sm),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: TextField(
|
||||||
|
controller: _controller,
|
||||||
|
obscureText: _obscured,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: l.registryTokenFieldLabel,
|
||||||
|
hintText: l.registryTokenFieldHint,
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
isDense: true,
|
||||||
|
suffixIcon: IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
_obscured ? Icons.visibility : Icons.visibility_off,
|
||||||
|
size: 16,
|
||||||
|
),
|
||||||
|
onPressed: () => setState(() => _obscured = !_obscured),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: FaiSpace.sm),
|
||||||
|
FilledButton.icon(
|
||||||
|
icon: const Icon(Icons.save_outlined, size: 14),
|
||||||
|
label: Text(l.registryTokenSaveButton),
|
||||||
|
onPressed: _controller.text.trim().isEmpty
|
||||||
|
? null
|
||||||
|
: () {
|
||||||
|
widget.onSave(_controller.text);
|
||||||
|
_controller.clear();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
l.registryTokenStorageHint,
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
|
fontSize: 11,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class _McpSuggestion {
|
class _McpSuggestion {
|
||||||
final String name;
|
final String name;
|
||||||
final IconData icon;
|
final IconData icon;
|
||||||
|
|
|
||||||
|
|
@ -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.43.0
|
version: 0.44.0
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.11.0-200.1.beta
|
sdk: ^3.11.0-200.1.beta
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue