diff --git a/analysis_options.yaml b/analysis_options.yaml index f4b4b3b..666a6d9 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -22,6 +22,7 @@ linter: # producing the lint. rules: use_build_context_synchronously: false + constant_identifier_names: false # avoid_print: false # Uncomment to disable the `avoid_print` rule # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule diff --git a/lib/core/ext/context/dialog.dart b/lib/core/ext/context/dialog.dart index 9d6b9ee..5b059ae 100644 --- a/lib/core/ext/context/dialog.dart +++ b/lib/core/ext/context/dialog.dart @@ -52,20 +52,28 @@ extension DialogX on BuildContext { } } + /// [noConfirm] Return value immediately without confirmation, + /// only valid if [multi] is false Future?> showPickDialog({ - required List items, + required List items, String Function(T)? name, bool multi = true, List? initial, - bool clearable = false, + bool clearable = true, List? actions, - - /// Return value immediately without confirmation, only valid for non-multi bool noConfirm = true, String? title, }) async { assert(!noConfirm || !multi); var vals = initial ?? []; + final actions_ = [ + if (actions != null) ...actions, + if (!noConfirm) + TextButton( + onPressed: () => pop(true), + child: Text(l10n.ok), + ), + ]; final sure = await showRoundDialog( title: title ?? l10n.choose, child: SingleChildScrollView( @@ -80,14 +88,13 @@ extension DialogX on BuildContext { items.length, (index) { final item = items[index]; - if (item == null) return UIs.placeholder; return ChoiceChipX( label: name?.call(item) ?? item.toString(), state: state, value: item, onSelected: noConfirm ? (_) { - state.onSelected(item); + vals = [item]; pop(true); } : null, @@ -98,15 +105,7 @@ extension DialogX on BuildContext { }, ), ), - actions: noConfirm - ? null - : [ - if (actions != null) ...actions, - TextButton( - onPressed: () => pop(true), - child: Text(l10n.ok), - ), - ], + actions: actions_.isEmpty ? null : actions_, ); if (sure == true && vals.isNotEmpty) { return vals; @@ -115,10 +114,9 @@ extension DialogX on BuildContext { } Future showPickSingleDialog({ - required List items, + required List items, String Function(T)? name, T? initial, - bool clearable = false, List? actions, /// Return value immediately without confirmation @@ -134,6 +132,7 @@ extension DialogX on BuildContext { actions: actions, ); if (vals != null && vals.isNotEmpty) { + assert(vals.length == 1); return vals.first; } return null; diff --git a/lib/data/res/openai.dart b/lib/data/res/openai.dart index 040d681..596b513 100644 --- a/lib/data/res/openai.dart +++ b/lib/data/res/openai.dart @@ -6,29 +6,35 @@ import 'package:flutter_chatgpt/data/store/all.dart'; abstract final class OpenAICfg { static final nameNotifier = ValueNotifier(_cfg.name); - static ChatConfig _cfg = - Stores.config.fetch(ChatConfig.defaultId) ?? ChatConfig.defaultOne; + + static ChatConfig _cfg = () { + final selectedKey = Stores.config.selectedKey.fetch(); + final selected = Stores.config.fetch(selectedKey); + return selected ?? ChatConfig.defaultOne; + }(); + static ChatConfig get current => _cfg; static set current(ChatConfig config) { _cfg = config; apply(); config.save(); + Stores.config.selectedKey.put(config.id); nameNotifier.value = config.name; } static void apply() { + Loggers.app.info('Switch profile [${_cfg.name}]'); OpenAI.apiKey = _cfg.key; OpenAI.baseUrl = _cfg.url; } - static bool switchTo(String id) { - final cfg = Stores.config.fetch(id); + static void switchToDefault() { + final cfg = Stores.config.fetch(ChatConfig.defaultId); if (cfg != null) { - _cfg = cfg; - apply(); - return true; + current = cfg; + } else { + current = ChatConfig.defaultOne; + Loggers.app.warning('Default config not found'); } - Loggers.app.warning('Config not found: $id'); - return false; } } diff --git a/lib/data/store/config.dart b/lib/data/store/config.dart index 6afbcb3..f55d272 100644 --- a/lib/data/store/config.dart +++ b/lib/data/store/config.dart @@ -5,6 +5,9 @@ import 'package:flutter_chatgpt/data/model/chat/config.dart'; final class ConfigStore extends Store { ConfigStore() : super('config'); + static const SELECTED_KEY = 'selectedKey'; + late final selectedKey = property(SELECTED_KEY, ChatConfig.defaultId); + ChatConfig? fetch(String id) { final val = box.get(id) as ChatConfig?; if (val == null && id == ChatConfig.defaultId) { @@ -29,6 +32,7 @@ final class ConfigStore extends Store { final map = {}; var errCount = 0; for (final key in box.keys) { + if (key == SELECTED_KEY) continue; final item = box.get(key); if (item != null) { if (item is ChatConfig) { diff --git a/lib/view/page/home/enum.dart b/lib/view/page/home/enum.dart index d057588..aeef6f6 100644 --- a/lib/view/page/home/enum.dart +++ b/lib/view/page/home/enum.dart @@ -44,19 +44,25 @@ enum HomePageEnum { final items = [ IconButton( onPressed: () async { - final profiles = Stores.config.fetchAll().values.toList(); final select = await context.showPickSingleDialog( title: l10n.profile, - items: profiles, + items: Stores.config.fetchAll().values.toList(), name: (p0) => p0.name.isEmpty ? l10n.defaulT : p0.name, initial: OpenAICfg.current, ); if (select == null) return; - OpenAICfg.switchTo(select.id); + OpenAICfg.current = select; }, icon: const Icon(Iconsax.profile_2user_bold), tooltip: l10n.profile, ), + IconButton( + onPressed: () => showSearch( + context: context, + delegate: _ChatSearchDelegate(), + ), + icon: const Icon(Icons.search), + ) ]; /// Put it here, or it's l10n string won't rebuild every time @@ -67,15 +73,6 @@ enum HomePageEnum { onTap: () => _onShareChat(context), onHomePage: [HomePageEnum.chat], ), - _MoreAction( - title: l10n.search, - icon: Icons.search, - onHomePage: [HomePageEnum.history], - onTap: () => showSearch( - context: context, - delegate: _ChatSearchDelegate(), - ), - ), ]) { if (!_isWide.value && !item.onHomePage.contains(this)) { continue; diff --git a/lib/view/page/home/history.dart b/lib/view/page/home/history.dart index 4297455..ef8e89c 100644 --- a/lib/view/page/home/history.dart +++ b/lib/view/page/home/history.dart @@ -66,7 +66,7 @@ class _HistoryPageState extends State<_HistoryPage> ); }, ), - contentPadding: const EdgeInsets.only(left: 17, right: 13), + contentPadding: const EdgeInsets.only(left: 17, right: 15), // trailing: Row( // mainAxisSize: MainAxisSize.min, // children: [ diff --git a/lib/view/page/setting.dart b/lib/view/page/setting.dart index 754dd0f..5846047 100644 --- a/lib/view/page/setting.dart +++ b/lib/view/page/setting.dart @@ -99,6 +99,7 @@ class _SettingPageState extends State { title: Text(l10n.themeMode), onTap: () async { final result = await context.showPickSingleDialog( + title: l10n.themeMode, items: ThemeMode.values, name: (e) => e.name, initial: ThemeMode.values[val], @@ -183,9 +184,10 @@ class _SettingPageState extends State { ), onTap: () async { final result = await context.showPickSingleDialog( + title: l10n.lang, items: AppLocalizations.supportedLocales, name: (e) => e.toLanguageTag(), - initial: val.toLocale, + initial: val.toLocale ?? l10n.localeName.toLocale, ); if (result != null) { final newLocaleStr = result.toLanguageTag(); @@ -261,7 +263,7 @@ class _SettingPageState extends State { _cfgRN.build(); context.pop(); if (cfg.id == value.id) { - OpenAICfg.switchTo(ChatConfig.defaultId); + OpenAICfg.switchToDefault(); _cfgRN.build(); } }, @@ -275,14 +277,14 @@ class _SettingPageState extends State { value: cfg.id == value.id, onChanged: (val) { if (val != true) return; - OpenAICfg.switchTo(value.id); + OpenAICfg.current = value; _cfgRN.build(); }, ), title: Text(value.name.isEmpty ? l10n.defaulT : value.name), onTap: () { if (cfg.id == value.id) return; - OpenAICfg.switchTo(value.id); + OpenAICfg.current = value; _cfgRN.build(); }, trailing: value.id != ChatConfig.defaultId ? delBtn : null, @@ -305,7 +307,7 @@ class _SettingPageState extends State { id: shortid.generate(), name: name, )..save(); - OpenAICfg.switchTo(cfg.id); + OpenAICfg.current = cfg; _cfgRN.build(); }, title: Text(l10n.add), @@ -691,11 +693,7 @@ class _SettingPageState extends State { Widget _buildReplay() { return ListTile( leading: const Icon(Icons.replay), - title: Text(l10n.replay), - subtitle: Text( - '${l10n.attention}: experimental feature', - style: UIs.text13Grey, - ), + title: Text('${l10n.replay} (experimental)'), trailing: StoreSwitch(prop: _store.replay), ); }