feat: ask ai (#936)
* feat: ask ai in ssh terminal Fixes #934 * new(ask_ai): settings * fix: app hot reload * new: l10n * chore: deps. * opt.
This commit is contained in:
95
lib/view/page/setting/entries/ai.dart
Normal file
95
lib/view/page/setting/entries/ai.dart
Normal file
@@ -0,0 +1,95 @@
|
||||
part of '../entry.dart';
|
||||
|
||||
extension _AI on _AppSettingsPageState {
|
||||
Widget _buildAskAiConfig() {
|
||||
final l10n = context.l10n;
|
||||
return ExpandTile(
|
||||
leading: const Icon(LineAwesome.robot_solid, size: _kIconSize),
|
||||
title: TipText(l10n.askAi, l10n.askAiUsageHint),
|
||||
children: [
|
||||
_setting.askAiBaseUrl.listenable().listenVal((val) {
|
||||
final display = val.isEmpty ? libL10n.empty : val;
|
||||
return ListTile(
|
||||
leading: const Icon(MingCute.link_2_line),
|
||||
title: Text(l10n.askAiBaseUrl),
|
||||
subtitle: Text(display, style: UIs.textGrey, maxLines: 2, overflow: TextOverflow.ellipsis),
|
||||
onTap: () => _showAskAiFieldDialog(
|
||||
prop: _setting.askAiBaseUrl,
|
||||
title: l10n.askAiBaseUrl,
|
||||
hint: 'https://api.openai.com',
|
||||
),
|
||||
);
|
||||
}),
|
||||
_setting.askAiModel.listenable().listenVal((val) {
|
||||
final display = val.isEmpty ? libL10n.empty : val;
|
||||
return ListTile(
|
||||
leading: const Icon(Icons.view_module),
|
||||
title: Text(l10n.askAiModel),
|
||||
subtitle: Text(display, style: UIs.textGrey),
|
||||
onTap: () => _showAskAiFieldDialog(
|
||||
prop: _setting.askAiModel,
|
||||
title: l10n.askAiModel,
|
||||
hint: 'gpt-4o-mini',
|
||||
),
|
||||
);
|
||||
}),
|
||||
_setting.askAiApiKey.listenable().listenVal((val) {
|
||||
final hasKey = val.isNotEmpty;
|
||||
return ListTile(
|
||||
leading: const Icon(MingCute.key_2_line),
|
||||
title: Text(l10n.askAiApiKey),
|
||||
subtitle: Text(hasKey ? '••••••••' : libL10n.empty, style: UIs.textGrey),
|
||||
onTap: () => _showAskAiFieldDialog(
|
||||
prop: _setting.askAiApiKey,
|
||||
title: l10n.askAiApiKey,
|
||||
hint: 'sk-...',
|
||||
obscure: true,
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
).cardx;
|
||||
}
|
||||
|
||||
|
||||
Future<void> _showAskAiFieldDialog({
|
||||
required HiveProp<String> prop,
|
||||
required String title,
|
||||
required String hint,
|
||||
bool obscure = false,
|
||||
}) async {
|
||||
return withTextFieldController((ctrl) async {
|
||||
final fetched = prop.fetch();
|
||||
if (fetched != null && fetched.isNotEmpty) ctrl.text = fetched;
|
||||
|
||||
void onSave() {
|
||||
prop.put(ctrl.text.trim());
|
||||
context.pop();
|
||||
}
|
||||
|
||||
await context.showRoundDialog(
|
||||
title: title,
|
||||
child: Input(
|
||||
controller: ctrl,
|
||||
autoFocus: true,
|
||||
label: title,
|
||||
hint: hint,
|
||||
icon: obscure ? MingCute.key_2_line : Icons.edit,
|
||||
obscureText: obscure,
|
||||
suggestion: !obscure,
|
||||
onSubmitted: (_) => onSave(),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
prop.delete();
|
||||
context.pop();
|
||||
},
|
||||
child: Text(libL10n.clear),
|
||||
),
|
||||
TextButton(onPressed: onSave, child: Text(libL10n.ok)),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -92,37 +92,37 @@ extension _App on _AppSettingsPageState {
|
||||
trailing: _setting.colorSeed.listenable().listenVal((_) {
|
||||
return ClipOval(child: Container(color: UIs.primaryColor, height: 27, width: 27));
|
||||
}),
|
||||
onTap: () async {
|
||||
final ctrl = TextEditingController(text: UIs.primaryColor.toHex);
|
||||
await context.showRoundDialog(
|
||||
title: libL10n.primaryColorSeed,
|
||||
child: StatefulBuilder(
|
||||
builder: (context, setState) {
|
||||
final children = <Widget>[
|
||||
/// Plugin [dynamic_color] is not supported on iOS
|
||||
if (!isIOS)
|
||||
ListTile(
|
||||
title: Text(l10n.followSystem),
|
||||
trailing: StoreSwitch(
|
||||
prop: _setting.useSystemPrimaryColor,
|
||||
callback: (_) => setState(() {}),
|
||||
onTap: () {
|
||||
withTextFieldController((ctrl) async {
|
||||
await context.showRoundDialog(
|
||||
title: libL10n.primaryColorSeed,
|
||||
child: StatefulBuilder(
|
||||
builder: (context, setState) {
|
||||
final children = <Widget>[
|
||||
/// Plugin [dynamic_color] is not supported on iOS
|
||||
if (!isIOS)
|
||||
ListTile(
|
||||
title: Text(l10n.followSystem),
|
||||
trailing: StoreSwitch(
|
||||
prop: _setting.useSystemPrimaryColor,
|
||||
callback: (_) => setState(() {}),
|
||||
),
|
||||
),
|
||||
),
|
||||
];
|
||||
if (!_setting.useSystemPrimaryColor.fetch()) {
|
||||
children.add(
|
||||
ColorPicker(
|
||||
color: Color(_setting.colorSeed.fetch()),
|
||||
onColorChanged: (c) => ctrl.text = c.toHex,
|
||||
),
|
||||
);
|
||||
}
|
||||
return Column(mainAxisSize: MainAxisSize.min, children: children);
|
||||
},
|
||||
),
|
||||
actions: Btn.ok(onTap: () => _onSaveColor(ctrl.text)).toList,
|
||||
);
|
||||
ctrl.dispose();
|
||||
];
|
||||
if (!_setting.useSystemPrimaryColor.fetch()) {
|
||||
children.add(
|
||||
ColorPicker(
|
||||
color: Color(_setting.colorSeed.fetch()),
|
||||
onColorChanged: (c) => ctrl.text = c.toHex,
|
||||
),
|
||||
);
|
||||
}
|
||||
return Column(mainAxisSize: MainAxisSize.min, children: children);
|
||||
},
|
||||
),
|
||||
actions: Btn.ok(onTap: () => _onSaveColor(ctrl.text)).toList,
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -44,28 +44,28 @@ extension _SFTP on _AppSettingsPageState {
|
||||
leading: const Icon(MingCute.edit_fill),
|
||||
title: TipText(libL10n.editor, l10n.sftpEditorTip),
|
||||
trailing: Text(val.isEmpty ? l10n.inner : val, style: UIs.text15),
|
||||
onTap: () async {
|
||||
final ctrl = TextEditingController(text: val);
|
||||
void onSave() {
|
||||
final s = ctrl.text.trim();
|
||||
_setting.sftpEditor.put(s);
|
||||
context.pop();
|
||||
}
|
||||
onTap: () {
|
||||
withTextFieldController((ctrl) async {
|
||||
void onSave() {
|
||||
final s = ctrl.text.trim();
|
||||
_setting.sftpEditor.put(s);
|
||||
context.pop();
|
||||
}
|
||||
|
||||
await context.showRoundDialog<bool>(
|
||||
title: libL10n.select,
|
||||
child: Input(
|
||||
controller: ctrl,
|
||||
autoFocus: true,
|
||||
label: libL10n.editor,
|
||||
hint: '\$EDITOR / vim / nano ...',
|
||||
icon: Icons.edit,
|
||||
suggestion: false,
|
||||
onSubmitted: (_) => onSave(),
|
||||
),
|
||||
actions: Btn.ok(onTap: onSave).toList,
|
||||
);
|
||||
ctrl.dispose();
|
||||
await context.showRoundDialog<bool>(
|
||||
title: libL10n.select,
|
||||
child: Input(
|
||||
controller: ctrl,
|
||||
autoFocus: true,
|
||||
label: libL10n.editor,
|
||||
hint: '\$EDITOR / vim / nano ...',
|
||||
icon: Icons.edit,
|
||||
suggestion: false,
|
||||
onSubmitted: (_) => onSave(),
|
||||
),
|
||||
actions: Btn.ok(onTap: onSave).toList,
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -116,27 +116,28 @@ extension _SSH on _AppSettingsPageState {
|
||||
leading: const Icon(Icons.terminal),
|
||||
title: TipText(l10n.terminal, l10n.desktopTerminalTip),
|
||||
trailing: Text(val, style: UIs.text15, maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||
onTap: () async {
|
||||
final ctrl = TextEditingController(text: val);
|
||||
void onSave() {
|
||||
_setting.desktopTerminal.put(ctrl.text.trim());
|
||||
context.pop();
|
||||
}
|
||||
onTap: () {
|
||||
withTextFieldController((ctrl) async {
|
||||
ctrl.text = val;
|
||||
void onSave() {
|
||||
_setting.desktopTerminal.put(ctrl.text.trim());
|
||||
context.pop();
|
||||
}
|
||||
|
||||
await context.showRoundDialog<bool>(
|
||||
title: libL10n.select,
|
||||
child: Input(
|
||||
controller: ctrl,
|
||||
autoFocus: true,
|
||||
label: l10n.terminal,
|
||||
hint: 'x-terminal-emulator / gnome-terminal',
|
||||
icon: Icons.edit,
|
||||
suggestion: false,
|
||||
onSubmitted: (_) => onSave(),
|
||||
),
|
||||
actions: Btn.ok(onTap: onSave).toList,
|
||||
);
|
||||
ctrl.dispose();
|
||||
await context.showRoundDialog<bool>(
|
||||
title: libL10n.select,
|
||||
child: Input(
|
||||
controller: ctrl,
|
||||
autoFocus: true,
|
||||
label: l10n.terminal,
|
||||
hint: 'x-terminal-emulator / gnome-terminal',
|
||||
icon: Icons.edit,
|
||||
suggestion: false,
|
||||
onSubmitted: (_) => onSave(),
|
||||
),
|
||||
actions: Btn.ok(onTap: onSave).toList,
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -35,6 +35,7 @@ part 'entries/full_screen.dart';
|
||||
part 'entries/server.dart';
|
||||
part 'entries/sftp.dart';
|
||||
part 'entries/ssh.dart';
|
||||
part 'entries/ai.dart';
|
||||
|
||||
const _kIconSize = 23.0;
|
||||
|
||||
@@ -120,7 +121,7 @@ final class _AppSettingsPageState extends ConsumerState<AppSettingsPage> {
|
||||
Widget build(BuildContext context) {
|
||||
return MultiList(
|
||||
children: [
|
||||
[const CenterGreyTitle('App'), _buildApp()],
|
||||
[const CenterGreyTitle('App'), _buildApp(), const CenterGreyTitle('AI'), _buildAskAiConfig()],
|
||||
[CenterGreyTitle(l10n.server), _buildServer()],
|
||||
[const CenterGreyTitle('SSH'), _buildSSH(), const CenterGreyTitle('SFTP'), _buildSFTP()],
|
||||
[CenterGreyTitle(l10n.container), _buildContainer(), CenterGreyTitle(libL10n.editor), _buildEditor()],
|
||||
|
||||
Reference in New Issue
Block a user