Merge pull request #117 from cogwheel0/allow-unlimited-quick-pill-selections
feat(settings): Allow unlimited quick pill selections
This commit is contained in:
@@ -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 {
|
||||||
|
|||||||
@@ -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)
|
|
||||||
: baseBackground.withValues(
|
|
||||||
alpha: brightness == Brightness.dark ? 0.18 : 0.12,
|
|
||||||
);
|
|
||||||
final Color outline = isActive
|
|
||||||
? context.conduitTheme.buttonPrimary.withValues(alpha: 0.8)
|
|
||||||
: context.conduitTheme.cardBorder.withValues(alpha: 0.6);
|
|
||||||
final Color contentColor = isActive
|
|
||||||
? context.conduitTheme.buttonPrimary
|
|
||||||
: context.conduitTheme.textPrimary.withValues(
|
|
||||||
alpha: enabled ? Alpha.strong : Alpha.disabled,
|
|
||||||
);
|
|
||||||
|
|
||||||
return Material(
|
// Enhanced color scheme for active state
|
||||||
color: Colors.transparent,
|
final Color activeBackground = isActive
|
||||||
child: InkWell(
|
? theme.buttonPrimary.withValues(alpha: brightness == Brightness.dark ? 0.22 : 0.14)
|
||||||
borderRadius: BorderRadius.circular(AppBorderRadius.input),
|
: Colors.transparent;
|
||||||
onTap: onTap == null
|
|
||||||
? null
|
final Color inactiveBackground = brightness == Brightness.dark
|
||||||
: () {
|
? theme.cardBackground.withValues(alpha: 0.25)
|
||||||
HapticFeedback.selectionClick();
|
: theme.cardBackground.withValues(alpha: 0.08);
|
||||||
onTap();
|
|
||||||
},
|
final Color background = isActive ? activeBackground : inactiveBackground;
|
||||||
child: Container(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
// Enhanced border styling
|
||||||
horizontal: Spacing.sm,
|
final Color activeBorder = theme.buttonPrimary.withValues(
|
||||||
vertical: Spacing.xs,
|
alpha: brightness == Brightness.dark ? 0.85 : 0.75,
|
||||||
),
|
);
|
||||||
decoration: BoxDecoration(
|
final Color inactiveBorder = theme.cardBorder.withValues(
|
||||||
color: background,
|
alpha: brightness == Brightness.dark ? 0.4 : 0.25,
|
||||||
borderRadius: BorderRadius.circular(AppBorderRadius.input),
|
);
|
||||||
border: Border.all(color: outline, width: BorderWidth.thin),
|
final Color borderColor = isActive ? activeBorder : inactiveBorder;
|
||||||
),
|
|
||||||
child: Row(
|
// Enhanced content colors
|
||||||
mainAxisSize: MainAxisSize.min,
|
final Color activeTextColor = theme.buttonPrimary;
|
||||||
children: [
|
final Color inactiveTextColor = theme.textPrimary.withValues(
|
||||||
Icon(icon, size: IconSize.medium, color: contentColor),
|
alpha: enabled ? (brightness == Brightness.dark ? 0.85 : 0.75) : Alpha.disabled,
|
||||||
const SizedBox(width: Spacing.xs),
|
);
|
||||||
Text(
|
final Color textColor = isActive ? activeTextColor : inactiveTextColor;
|
||||||
label,
|
|
||||||
maxLines: 1,
|
final Color iconColor = isActive
|
||||||
overflow: TextOverflow.ellipsis,
|
? activeTextColor
|
||||||
style: AppTypography.labelStyle.copyWith(color: contentColor),
|
: inactiveTextColor;
|
||||||
|
|
||||||
|
return AnimatedContainer(
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
curve: Curves.easeOutCubic,
|
||||||
|
child: Material(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: InkWell(
|
||||||
|
borderRadius: BorderRadius.circular(AppBorderRadius.round),
|
||||||
|
onTap: onTap == null
|
||||||
|
? null
|
||||||
|
: () {
|
||||||
|
HapticFeedback.mediumImpact();
|
||||||
|
onTap();
|
||||||
|
},
|
||||||
|
child: AnimatedContainer(
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
curve: Curves.easeOutCubic,
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: Spacing.md,
|
||||||
|
vertical: Spacing.sm - 2,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: background,
|
||||||
|
borderRadius: BorderRadius.circular(AppBorderRadius.round),
|
||||||
|
border: Border.all(
|
||||||
|
color: borderColor,
|
||||||
|
width: isActive ? BorderWidth.medium : BorderWidth.thin,
|
||||||
),
|
),
|
||||||
],
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in New Issue
Block a user