Merge pull request #117 from cogwheel0/allow-unlimited-quick-pill-selections

feat(settings): Allow unlimited quick pill selections
This commit is contained in:
cogwheel
2025-10-30 22:44:58 +05:30
committed by GitHub
4 changed files with 135 additions and 72 deletions

View File

@@ -168,7 +168,7 @@ class SettingsService {
_voiceHoldToTalkKey: settings.voiceHoldToTalk, _voiceHoldToTalkKey: settings.voiceHoldToTalk,
_voiceAutoSendKey: settings.voiceAutoSendFinal, _voiceAutoSendKey: settings.voiceAutoSendFinal,
_socketTransportModeKey: settings.socketTransportMode, _socketTransportModeKey: settings.socketTransportMode,
_quickPillsKey: settings.quickPills.take(2).toList(), _quickPillsKey: settings.quickPills.toList(),
_sendOnEnterKey: settings.sendOnEnter, _sendOnEnterKey: settings.sendOnEnter,
PreferenceKeys.ttsSpeechRate: settings.ttsSpeechRate, PreferenceKeys.ttsSpeechRate: settings.ttsSpeechRate,
PreferenceKeys.ttsPitch: settings.ttsPitch, PreferenceKeys.ttsPitch: settings.ttsPitch,
@@ -287,11 +287,11 @@ class SettingsService {
if (stored == null) { if (stored == null) {
return Future.value(const []); return Future.value(const []);
} }
return Future.value(List<String>.from(stored.take(2))); return Future.value(List<String>.from(stored));
} }
static Future<void> setQuickPills(List<String> pills) { static Future<void> setQuickPills(List<String> pills) {
return _preferencesBox().put(_quickPillsKey, pills.take(2).toList()); return _preferencesBox().put(_quickPillsKey, pills.toList());
} }
// Chat input behavior // Chat input behavior
@@ -592,10 +592,10 @@ class AppSettingsNotifier extends _$AppSettingsNotifier {
} }
Future<void> setQuickPills(List<String> pills) async { Future<void> setQuickPills(List<String> pills) async {
// Enforce max 2; accept arbitrary server tool IDs plus built-ins // Accept arbitrary server tool IDs plus built-ins
final filtered = pills.take(2).toList(); // Platform-specific limits are enforced in the UI layer
state = state.copyWith(quickPills: filtered); state = state.copyWith(quickPills: pills);
await SettingsService.setQuickPills(filtered); await SettingsService.setQuickPills(pills);
} }
Future<void> setSendOnEnter(bool value) async { Future<void> setSendOnEnter(bool value) async {

View File

@@ -1553,53 +1553,109 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
}) { }) {
final bool enabled = onTap != null; final bool enabled = onTap != null;
final Brightness brightness = Theme.of(context).brightness; final Brightness brightness = Theme.of(context).brightness;
final Color baseBackground = context.conduitTheme.cardBackground; final theme = context.conduitTheme;
final Color background = isActive
? context.conduitTheme.buttonPrimary.withValues(alpha: 0.16) // Enhanced color scheme for active state
: baseBackground.withValues( final Color activeBackground = isActive
alpha: brightness == Brightness.dark ? 0.18 : 0.12, ? theme.buttonPrimary.withValues(alpha: brightness == Brightness.dark ? 0.22 : 0.14)
); : Colors.transparent;
final Color outline = isActive
? context.conduitTheme.buttonPrimary.withValues(alpha: 0.8) final Color inactiveBackground = brightness == Brightness.dark
: context.conduitTheme.cardBorder.withValues(alpha: 0.6); ? theme.cardBackground.withValues(alpha: 0.25)
final Color contentColor = isActive : theme.cardBackground.withValues(alpha: 0.08);
? context.conduitTheme.buttonPrimary
: context.conduitTheme.textPrimary.withValues( final Color background = isActive ? activeBackground : inactiveBackground;
alpha: enabled ? Alpha.strong : Alpha.disabled,
); // Enhanced border styling
final Color activeBorder = theme.buttonPrimary.withValues(
alpha: brightness == Brightness.dark ? 0.85 : 0.75,
);
final Color inactiveBorder = theme.cardBorder.withValues(
alpha: brightness == Brightness.dark ? 0.4 : 0.25,
);
final Color borderColor = isActive ? activeBorder : inactiveBorder;
// Enhanced content colors
final Color activeTextColor = theme.buttonPrimary;
final Color inactiveTextColor = theme.textPrimary.withValues(
alpha: enabled ? (brightness == Brightness.dark ? 0.85 : 0.75) : Alpha.disabled,
);
final Color textColor = isActive ? activeTextColor : inactiveTextColor;
final Color iconColor = isActive
? activeTextColor
: inactiveTextColor;
return Material( return AnimatedContainer(
color: Colors.transparent, duration: const Duration(milliseconds: 200),
child: InkWell( curve: Curves.easeOutCubic,
borderRadius: BorderRadius.circular(AppBorderRadius.input), child: Material(
onTap: onTap == null color: Colors.transparent,
? null child: InkWell(
: () { borderRadius: BorderRadius.circular(AppBorderRadius.round),
HapticFeedback.selectionClick(); onTap: onTap == null
onTap(); ? null
}, : () {
child: Container( HapticFeedback.mediumImpact();
padding: const EdgeInsets.symmetric( onTap();
horizontal: Spacing.sm, },
vertical: Spacing.xs, child: AnimatedContainer(
), duration: const Duration(milliseconds: 200),
decoration: BoxDecoration( curve: Curves.easeOutCubic,
color: background, padding: const EdgeInsets.symmetric(
borderRadius: BorderRadius.circular(AppBorderRadius.input), horizontal: Spacing.md,
border: Border.all(color: outline, width: BorderWidth.thin), vertical: Spacing.sm - 2,
), ),
child: Row( decoration: BoxDecoration(
mainAxisSize: MainAxisSize.min, color: background,
children: [ borderRadius: BorderRadius.circular(AppBorderRadius.round),
Icon(icon, size: IconSize.medium, color: contentColor), border: Border.all(
const SizedBox(width: Spacing.xs), color: borderColor,
Text( width: isActive ? BorderWidth.medium : BorderWidth.thin,
label,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: AppTypography.labelStyle.copyWith(color: contentColor),
), ),
], boxShadow: isActive
? [
BoxShadow(
color: theme.buttonPrimary.withValues(
alpha: brightness == Brightness.dark ? 0.25 : 0.15,
),
blurRadius: 8,
spreadRadius: 0,
offset: const Offset(0, 2),
),
]
: [],
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
AnimatedContainer(
duration: const Duration(milliseconds: 200),
curve: Curves.easeOutCubic,
child: Icon(
icon,
size: IconSize.small + 1,
color: iconColor,
),
),
const SizedBox(width: Spacing.xs + 1),
AnimatedDefaultTextStyle(
duration: const Duration(milliseconds: 200),
curve: Curves.easeOutCubic,
style: AppTypography.labelStyle.copyWith(
color: textColor,
fontWeight: isActive ? FontWeight.w600 : FontWeight.w500,
fontSize: 13,
letterSpacing: -0.1,
),
child: Text(
label,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
),
), ),
), ),
), ),

View File

@@ -283,6 +283,9 @@ class AppCustomizationPage extends ConsumerWidget {
WidgetRef ref, WidgetRef ref,
AppSettings settings, AppSettings settings,
) { ) {
// Allow unlimited selections on all platforms
final maxPills = 999;
final selectedRaw = ref.watch( final selectedRaw = ref.watch(
appSettingsProvider.select((s) => s.quickPills), appSettingsProvider.select((s) => s.quickPills),
); );
@@ -295,7 +298,7 @@ class AppCustomizationPage extends ConsumerWidget {
final selected = selectedRaw final selected = selectedRaw
.where((id) => allowed.contains(id)) .where((id) => allowed.contains(id))
.take(2) .take(maxPills)
.toList(); .toList();
if (selected.length != selectedRaw.length) { if (selected.length != selectedRaw.length) {
Future.microtask( Future.microtask(
@@ -310,7 +313,7 @@ class AppCustomizationPage extends ConsumerWidget {
if (next.contains(id)) { if (next.contains(id)) {
next.remove(id); next.remove(id);
} else { } else {
if (next.length >= 2) return; if (next.length >= maxPills) return;
next.add(id); next.add(id);
} }
await ref.read(appSettingsProvider.notifier).setQuickPills(next); await ref.read(appSettingsProvider.notifier).setQuickPills(next);
@@ -319,7 +322,7 @@ class AppCustomizationPage extends ConsumerWidget {
List<Widget> buildToolChips() { List<Widget> buildToolChips() {
return tools.map((tool) { return tools.map((tool) {
final isSelected = selected.contains(tool.id); final isSelected = selected.contains(tool.id);
final canSelect = selectedCount < 2 || isSelected; final canSelect = selectedCount < maxPills || isSelected;
return ConduitChip( return ConduitChip(
label: tool.name, label: tool.name,
icon: Icons.extension, icon: Icons.extension,
@@ -344,19 +347,6 @@ class AppCustomizationPage extends ConsumerWidget {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
if (selected.isNotEmpty)
Padding(
padding: const EdgeInsets.only(bottom: Spacing.md),
child: Align(
alignment: Alignment.centerRight,
child: TextButton(
onPressed: () => ref
.read(appSettingsProvider.notifier)
.setQuickPills(const []),
child: Text(l10n.clear),
),
),
),
Wrap( Wrap(
spacing: Spacing.sm, spacing: Spacing.sm,
runSpacing: Spacing.sm, runSpacing: Spacing.sm,
@@ -365,7 +355,7 @@ class AppCustomizationPage extends ConsumerWidget {
label: l10n.web, label: l10n.web,
icon: Platform.isIOS ? CupertinoIcons.search : Icons.search, icon: Platform.isIOS ? CupertinoIcons.search : Icons.search,
isSelected: selected.contains('web'), isSelected: selected.contains('web'),
onTap: (selectedCount < 2 || selected.contains('web')) onTap: (selectedCount < maxPills || selected.contains('web'))
? () => toggle('web') ? () => toggle('web')
: null, : null,
), ),
@@ -373,11 +363,20 @@ class AppCustomizationPage extends ConsumerWidget {
label: l10n.imageGen, label: l10n.imageGen,
icon: Platform.isIOS ? CupertinoIcons.photo : Icons.image, icon: Platform.isIOS ? CupertinoIcons.photo : Icons.image,
isSelected: selected.contains('image'), isSelected: selected.contains('image'),
onTap: (selectedCount < 2 || selected.contains('image')) onTap: (selectedCount < maxPills || selected.contains('image'))
? () => toggle('image') ? () => toggle('image')
: null, : null,
), ),
...buildToolChips(), ...buildToolChips(),
if (selected.isNotEmpty)
ConduitChip(
label: l10n.clear,
icon: Platform.isIOS ? CupertinoIcons.xmark : Icons.close,
isSelected: false,
onTap: () => ref
.read(appSettingsProvider.notifier)
.setQuickPills(const []),
),
], ],
), ),
], ],
@@ -501,7 +500,15 @@ class AppCustomizationPage extends ConsumerWidget {
color: theme.buttonPrimary, color: theme.buttonPrimary,
), ),
const SizedBox(width: Spacing.sm), const SizedBox(width: Spacing.sm),
const Text('Engine'), Text(
'Engine',
style:
theme.bodyMedium?.copyWith(
color: theme.sidebarForeground,
fontWeight: FontWeight.w500,
) ??
TextStyle(color: theme.sidebarForeground, fontSize: 14),
),
const Spacer(), const Spacer(),
Wrap( Wrap(
spacing: Spacing.sm, spacing: Spacing.sm,

View File

@@ -654,7 +654,7 @@ class ConduitChip extends StatelessWidget {
? context.conduitTheme.buttonPrimary.withValues( ? context.conduitTheme.buttonPrimary.withValues(
alpha: Alpha.standard, alpha: Alpha.standard,
) )
: context.conduitTheme.dividerColor, : context.conduitTheme.cardBorder,
width: BorderWidth.standard, width: BorderWidth.standard,
), ),
), ),