feat: customize quick pills

This commit is contained in:
cogwheel0
2025-09-07 14:40:20 +05:30
parent 9cb835861a
commit 0d175b1e0a
4 changed files with 320 additions and 75 deletions

View File

@@ -21,6 +21,8 @@ class SettingsService {
static const String _voiceAutoSendKey = 'voice_auto_send_final';
// Realtime transport preference
static const String _socketTransportModeKey = 'socket_transport_mode'; // 'auto' or 'ws'
// Quick pill visibility selections (max 2)
static const String _quickPillsKey = 'quick_pills'; // StringList of identifiers e.g. ['web','image','tools']
/// Get reduced motion preference
static Future<bool> getReduceMotion() async {
@@ -136,6 +138,7 @@ class SettingsService {
voiceHoldToTalk: await getVoiceHoldToTalk(),
voiceAutoSendFinal: await getVoiceAutoSendFinal(),
socketTransportMode: await getSocketTransportMode(),
quickPills: await getQuickPills(),
);
}
@@ -154,6 +157,7 @@ class SettingsService {
setVoiceHoldToTalk(settings.voiceHoldToTalk),
setVoiceAutoSendFinal(settings.voiceAutoSendFinal),
setSocketTransportMode(settings.socketTransportMode),
setQuickPills(settings.quickPills),
]);
}
@@ -204,6 +208,21 @@ class SettingsService {
await prefs.setString(_socketTransportModeKey, mode);
}
// Quick Pills (visibility)
static Future<List<String>> getQuickPills() async {
final prefs = await SharedPreferences.getInstance();
final list = prefs.getStringList(_quickPillsKey);
// Default: none selected
if (list == null) return const [];
// Enforce max 2; accept arbitrary tool IDs plus 'web' and 'image'
return list.take(2).toList();
}
static Future<void> setQuickPills(List<String> pills) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setStringList(_quickPillsKey, pills.take(2).toList());
}
/// Get effective animation duration considering all settings
static Duration getEffectiveAnimationDuration(
BuildContext context,
@@ -258,6 +277,7 @@ class AppSettings {
final bool voiceHoldToTalk;
final bool voiceAutoSendFinal;
final String socketTransportMode; // 'auto' or 'ws'
final List<String> quickPills; // e.g., ['web','image']
const AppSettings({
this.reduceMotion = false,
@@ -272,6 +292,7 @@ class AppSettings {
this.voiceHoldToTalk = false,
this.voiceAutoSendFinal = false,
this.socketTransportMode = 'auto',
this.quickPills = const [],
});
AppSettings copyWith({
@@ -287,6 +308,7 @@ class AppSettings {
bool? voiceHoldToTalk,
bool? voiceAutoSendFinal,
String? socketTransportMode,
List<String>? quickPills,
}) {
return AppSettings(
reduceMotion: reduceMotion ?? this.reduceMotion,
@@ -301,6 +323,7 @@ class AppSettings {
voiceHoldToTalk: voiceHoldToTalk ?? this.voiceHoldToTalk,
voiceAutoSendFinal: voiceAutoSendFinal ?? this.voiceAutoSendFinal,
socketTransportMode: socketTransportMode ?? this.socketTransportMode,
quickPills: quickPills ?? this.quickPills,
);
}
@@ -318,7 +341,8 @@ class AppSettings {
other.omitProviderInModelName == omitProviderInModelName &&
other.voiceLocaleId == voiceLocaleId &&
other.voiceHoldToTalk == voiceHoldToTalk &&
other.voiceAutoSendFinal == voiceAutoSendFinal;
other.voiceAutoSendFinal == voiceAutoSendFinal &&
_listEquals(other.quickPills, quickPills);
// socketTransportMode intentionally not included in == to avoid frequent rebuilds
}
@@ -337,10 +361,20 @@ class AppSettings {
voiceHoldToTalk,
voiceAutoSendFinal,
socketTransportMode,
Object.hashAllUnordered(quickPills),
);
}
}
bool _listEquals(List<String> a, List<String> b) {
if (identical(a, b)) return true;
if (a.length != b.length) return false;
for (int i = 0; i < a.length; i++) {
if (a[i] != b[i]) return false;
}
return true;
}
/// Provider for app settings
final appSettingsProvider =
StateNotifierProvider<AppSettingsNotifier, AppSettings>(
@@ -417,6 +451,13 @@ class AppSettingsNotifier extends StateNotifier<AppSettings> {
await SettingsService.setSocketTransportMode(mode);
}
Future<void> setQuickPills(List<String> pills) async {
// Enforce max 2; accept arbitrary server tool IDs plus built-ins
final filtered = pills.take(2).toList();
state = state.copyWith(quickPills: filtered);
await SettingsService.setQuickPills(filtered);
}
Future<void> resetToDefaults() async {
const defaultSettings = AppSettings();
await SettingsService.saveSettings(defaultSettings);

View File

@@ -26,7 +26,15 @@ class SocketService {
_socket?.dispose();
} catch (_) {}
final base = serverConfig.url.replaceFirst(RegExp(r'/+$'), '');
String base = serverConfig.url.replaceFirst(RegExp(r'/+$'), '');
// Normalize accidental ":0" ports or invalid port values in stored URL
try {
final u = Uri.parse(base);
if (u.hasPort && u.port == 0) {
// Drop the explicit :0 to fall back to scheme default (80/443)
base = '${u.scheme}://${u.host}${u.path.isEmpty ? '' : u.path}';
}
} catch (_) {}
final path = '/ws/socket.io';
final builder = io.OptionBuilder()