refactor: settings pages

This commit is contained in:
cogwheel0
2025-09-20 23:02:59 +05:30
parent 8d89fd79b1
commit 3db5a8b760
11 changed files with 998 additions and 895 deletions

View File

@@ -34,24 +34,63 @@ class AppCustomizationPage extends ConsumerWidget {
return AppLocalizations.of(context)!.currentlyUsingLightTheme;
}();
final locale = ref.watch(localeProvider);
final currentLanguageCode = locale?.languageCode ?? 'system';
final languageLabel = _resolveLanguageLabel(context, currentLanguageCode);
return Scaffold(
backgroundColor: context.conduitTheme.surfaceBackground,
appBar: AppBar(
appBar: _buildAppBar(context),
body: SafeArea(
child: ListView(
physics: const BouncingScrollPhysics(
parent: AlwaysScrollableScrollPhysics(),
),
padding: const EdgeInsets.symmetric(
horizontal: Spacing.pagePadding,
vertical: Spacing.pagePadding,
),
children: [
_buildDisplaySection(
context,
ref,
themeMode,
themeDescription,
currentLanguageCode,
languageLabel,
settings,
),
const SizedBox(height: Spacing.sectionGap),
_buildQuickPillsSection(context, ref, settings),
const SizedBox(height: Spacing.sectionGap),
_buildChatSection(context, ref, settings),
],
),
),
);
}
PreferredSizeWidget _buildAppBar(BuildContext context) {
final canPop = ModalRoute.of(context)?.canPop ?? false;
return AppBar(
backgroundColor: context.conduitTheme.surfaceBackground,
surfaceTintColor: Colors.transparent,
elevation: Elevation.none,
toolbarHeight: kToolbarHeight - 8,
leading: IconButton(
toolbarHeight: kToolbarHeight,
automaticallyImplyLeading: false,
leading: canPop
? IconButton(
icon: Icon(
UiUtils.platformIcon(
ios: CupertinoIcons.back,
android: Icons.arrow_back,
),
color: context.conduitTheme.textPrimary,
color: context.conduitTheme.iconPrimary,
),
onPressed: () => Navigator.of(context).maybePop(),
tooltip: AppLocalizations.of(context)!.back,
),
)
: null,
titleSpacing: 0,
title: Text(
AppLocalizations.of(context)!.appCustomization,
style: AppTypography.headlineSmallStyle.copyWith(
@@ -60,537 +99,402 @@ class AppCustomizationPage extends ConsumerWidget {
),
),
centerTitle: true,
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(Spacing.pagePadding),
child: Column(
);
}
Widget _buildDisplaySection(
BuildContext context,
WidgetRef ref,
ThemeMode themeMode,
String themeDescription,
String currentLanguageCode,
String languageLabel,
AppSettings settings,
) {
final theme = context.conduitTheme;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
AppLocalizations.of(context)!.display,
style: context.conduitTheme.headingSmall?.copyWith(
color: context.conduitTheme.textPrimary,
),
style:
theme.headingSmall?.copyWith(color: theme.textPrimary) ??
TextStyle(color: theme.textPrimary, fontSize: 18),
),
const SizedBox(height: Spacing.sm),
_buildThemeSelector(context, ref, themeMode, themeDescription),
const SizedBox(height: Spacing.md),
ConduitCard(
padding: EdgeInsets.zero,
child: Column(
children: [
// Theme mode dropdown
ListTile(
contentPadding: const EdgeInsets.symmetric(
horizontal: Spacing.listItemPadding,
vertical: Spacing.sm,
),
leading: Container(
padding: const EdgeInsets.all(Spacing.sm),
decoration: BoxDecoration(
color: context.conduitTheme.buttonPrimary.withValues(
alpha: Alpha.highlight,
),
borderRadius: BorderRadius.circular(
AppBorderRadius.small,
),
),
child: Icon(
UiUtils.platformIcon(
ios: CupertinoIcons.moon_stars,
android: Icons.dark_mode,
),
color: context.conduitTheme.buttonPrimary,
size: IconSize.medium,
),
),
title: Text(
AppLocalizations.of(context)!.darkMode,
style: context.conduitTheme.bodyLarge?.copyWith(
color: context.conduitTheme.textPrimary,
fontWeight: FontWeight.w500,
),
),
subtitle: Text(
themeDescription,
style: context.conduitTheme.bodySmall?.copyWith(
color: context.conduitTheme.textSecondary,
),
),
trailing: DropdownButtonHideUnderline(
child: DropdownButton<ThemeMode>(
value: themeMode,
onChanged: (mode) {
if (mode == null) return;
ref.read(themeModeProvider.notifier).setTheme(mode);
},
items: [
DropdownMenuItem(
value: ThemeMode.system,
child: Text(AppLocalizations.of(context)!.system),
),
DropdownMenuItem(
value: ThemeMode.light,
child: Text(
AppLocalizations.of(context)!.themeLight,
),
),
DropdownMenuItem(
value: ThemeMode.dark,
child: Text(
AppLocalizations.of(context)!.themeDark,
),
),
],
),
),
),
Divider(color: context.conduitTheme.dividerColor, height: 1),
// App language selector
Builder(
builder: (context) {
final currentCode = locale?.languageCode ?? 'system';
final label = () {
switch (currentCode) {
case 'en':
return 'English';
case 'de':
return 'Deutsch';
case 'fr':
return 'Français';
case 'it':
return 'Italiano';
default:
return 'System';
}
}();
return ListTile(
contentPadding: const EdgeInsets.symmetric(
horizontal: Spacing.listItemPadding,
vertical: Spacing.sm,
),
leading: Container(
padding: const EdgeInsets.all(Spacing.sm),
decoration: BoxDecoration(
color: context.conduitTheme.buttonPrimary
.withValues(alpha: Alpha.highlight),
borderRadius: BorderRadius.circular(
AppBorderRadius.small,
),
),
child: Icon(
_CustomizationTile(
leading: _buildIconBadge(
context,
UiUtils.platformIcon(
ios: CupertinoIcons.globe,
android: Icons.language,
),
color: context.conduitTheme.buttonPrimary,
size: IconSize.medium,
),
),
title: Text(
AppLocalizations.of(context)!.appLanguage,
style: context.conduitTheme.bodyLarge?.copyWith(
color: context.conduitTheme.textPrimary,
fontWeight: FontWeight.w500,
),
),
subtitle: Text(
label,
style: context.conduitTheme.bodySmall?.copyWith(
color: context.conduitTheme.textSecondary,
),
),
trailing: Icon(
UiUtils.platformIcon(
ios: CupertinoIcons.chevron_right,
android: Icons.chevron_right,
),
color: context.conduitTheme.iconSecondary,
size: IconSize.small,
color: theme.buttonPrimary,
),
title: AppLocalizations.of(context)!.appLanguage,
subtitle: languageLabel,
onTap: () async {
final selected = await _showLanguageSelector(
context,
currentCode,
currentLanguageCode,
);
if (selected != null) {
if (selected == null) return;
if (selected == 'system') {
await ref
.read(localeProvider.notifier)
.setLocale(null);
await ref.read(localeProvider.notifier).setLocale(null);
} else {
await ref
.read(localeProvider.notifier)
.setLocale(Locale(selected));
}
}
},
);
},
),
Divider(color: context.conduitTheme.dividerColor, height: 1),
SwitchListTile.adaptive(
contentPadding: const EdgeInsets.symmetric(
horizontal: Spacing.listItemPadding,
vertical: Spacing.sm,
const SizedBox(height: Spacing.md),
_CustomizationTile(
leading: _buildIconBadge(
context,
Platform.isIOS ? CupertinoIcons.textformat : Icons.text_fields,
color: theme.buttonPrimary,
),
// Use platform defaults for switch colors to match theme
value: settings.omitProviderInModelName,
title: Text(
AppLocalizations.of(context)!.hideProviderInModelNames,
style: context.conduitTheme.bodyLarge?.copyWith(
color: context.conduitTheme.textPrimary,
fontWeight: FontWeight.w500,
),
),
subtitle: Text(
AppLocalizations.of(
title: AppLocalizations.of(context)!.hideProviderInModelNames,
subtitle: AppLocalizations.of(
context,
)!.hideProviderInModelNamesDescription,
style: context.conduitTheme.bodySmall?.copyWith(
color: context.conduitTheme.textSecondary,
),
),
onChanged: (v) {
ref
trailing: Switch.adaptive(
value: settings.omitProviderInModelName,
onChanged: (v) => ref
.read(appSettingsProvider.notifier)
.setOmitProviderInModelName(v);
},
secondary: Container(
padding: const EdgeInsets.all(Spacing.sm),
decoration: BoxDecoration(
color: context.conduitTheme.buttonPrimary.withValues(
alpha: Alpha.highlight,
.setOmitProviderInModelName(v),
),
borderRadius: BorderRadius.circular(
AppBorderRadius.small,
),
),
child: Icon(
Platform.isIOS
? CupertinoIcons.textformat
: Icons.text_fields,
color: context.conduitTheme.buttonPrimary,
size: IconSize.medium,
showChevron: false,
onTap: () => ref
.read(appSettingsProvider.notifier)
.setOmitProviderInModelName(!settings.omitProviderInModelName),
),
],
);
}
Widget _buildThemeSelector(
BuildContext context,
WidgetRef ref,
ThemeMode themeMode,
String themeDescription,
) {
final theme = context.conduitTheme;
return ConduitCard(
padding: const EdgeInsets.all(Spacing.cardPadding),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildIconBadge(
context,
UiUtils.platformIcon(
ios: CupertinoIcons.moon_stars,
android: Icons.dark_mode,
),
color: theme.buttonPrimary,
),
const SizedBox(width: Spacing.md),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
AppLocalizations.of(context)!.darkMode,
style:
theme.bodyLarge?.copyWith(
color: theme.textPrimary,
fontWeight: FontWeight.w600,
) ??
TextStyle(
color: theme.textPrimary,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: Spacing.textSpacing),
Text(
themeDescription,
style:
theme.bodySmall?.copyWith(
color: theme.textSecondary,
) ??
TextStyle(color: theme.textSecondary),
),
],
),
),
const SizedBox(height: Spacing.lg),
// Quick pills (Web / Image Gen)
Text(
AppLocalizations.of(context)!.onboardQuickTitle,
style: context.conduitTheme.headingSmall?.copyWith(
color: context.conduitTheme.textPrimary,
),
],
),
const SizedBox(height: Spacing.md),
Consumer(
builder: (context, ref, _) {
Wrap(
spacing: Spacing.sm,
runSpacing: Spacing.sm,
children: [
_buildThemeChip(
context,
ref,
mode: ThemeMode.system,
isSelected: themeMode == ThemeMode.system,
label: AppLocalizations.of(context)!.system,
icon: UiUtils.platformIcon(
ios: CupertinoIcons.sparkles,
android: Icons.auto_mode,
),
),
_buildThemeChip(
context,
ref,
mode: ThemeMode.light,
isSelected: themeMode == ThemeMode.light,
label: AppLocalizations.of(context)!.themeLight,
icon: UiUtils.platformIcon(
ios: CupertinoIcons.sun_max,
android: Icons.light_mode,
),
),
_buildThemeChip(
context,
ref,
mode: ThemeMode.dark,
isSelected: themeMode == ThemeMode.dark,
label: AppLocalizations.of(context)!.themeDark,
icon: UiUtils.platformIcon(
ios: CupertinoIcons.moon_fill,
android: Icons.dark_mode,
),
),
],
),
],
),
);
}
Widget _buildThemeChip(
BuildContext context,
WidgetRef ref, {
required ThemeMode mode,
required bool isSelected,
required String label,
required IconData icon,
}) {
return ConduitChip(
label: label,
icon: icon,
isSelected: isSelected,
onTap: () => ref.read(themeModeProvider.notifier).setTheme(mode),
);
}
Widget _buildQuickPillsSection(
BuildContext context,
WidgetRef ref,
AppSettings settings,
) {
final theme = context.conduitTheme;
final selectedRaw = ref.watch(
appSettingsProvider.select((s) => s.quickPills),
);
final toolsAsync = ref.watch(toolsListProvider);
final tools = toolsAsync.maybeWhen(
data: (t) => t,
data: (value) => value,
orElse: () => const <Tool>[],
);
final allowed = <String>{
'web',
'image',
...tools.map((t) => t.id),
};
// Sanitize persisted selection
final allowed = <String>{'web', 'image', ...tools.map((t) => t.id)};
final selected = selectedRaw
.where((id) => allowed.contains(id))
.take(2)
.toList();
if (selected.length != selectedRaw.length) {
// Persist sanitized list asynchronously
Future.microtask(
() => ref
.read(appSettingsProvider.notifier)
.setQuickPills(selected),
() => ref.read(appSettingsProvider.notifier).setQuickPills(selected),
);
}
final int selectedCount = selected.length;
void toggle(String id) async {
final current = List<String>.from(selected);
if (current.contains(id)) {
current.remove(id);
final selectedCount = selected.length;
Future<void> toggle(String id) async {
final next = List<String>.from(selected);
if (next.contains(id)) {
next.remove(id);
} else {
if (current.length >= 2) return; // enforce max 2
current.add(id);
if (next.length >= 2) return;
next.add(id);
}
await ref
.read(appSettingsProvider.notifier)
.setQuickPills(current);
await ref.read(appSettingsProvider.notifier).setQuickPills(next);
}
// Build dynamic tool chips list once
final List<Widget> dynamicToolChips = ref
.watch(toolsListProvider)
.maybeWhen<List<Widget>>(
data: (tools) => tools.map((Tool t) {
final isSel = selected.contains(t.id);
final canSelect = selectedCount < 2 || isSel;
List<Widget> buildToolChips() {
return tools.map((tool) {
final isSelected = selected.contains(tool.id);
final canSelect = selectedCount < 2 || isSelected;
return ConduitChip(
label: t.name,
label: tool.name,
icon: Icons.extension,
isSelected: isSel,
onTap: canSelect ? () => toggle(t.id) : null,
);
}).toList(),
orElse: () => const <Widget>[],
isSelected: isSelected,
onTap: canSelect ? () => toggle(tool.id) : null,
);
}).toList();
}
return ConduitCard(
padding: const EdgeInsets.symmetric(
horizontal: Spacing.listItemPadding,
vertical: Spacing.sm,
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
AppLocalizations.of(context)!.onboardQuickTitle,
style:
theme.headingSmall?.copyWith(color: theme.textPrimary) ??
TextStyle(color: theme.textPrimary, fontSize: 18),
),
const SizedBox(height: Spacing.sm),
ConduitCard(
padding: const EdgeInsets.all(Spacing.cardPadding),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildIconBadge(
context,
UiUtils.platformIcon(
ios: CupertinoIcons.bolt,
android: Icons.flash_on,
),
color: theme.buttonPrimary,
),
const SizedBox(width: Spacing.md),
Expanded(
child: Text(
AppLocalizations.of(
context,
)!.appCustomizationSubtitle,
style: context.conduitTheme.bodySmall?.copyWith(
color: context.conduitTheme.textSecondary,
),
AppLocalizations.of(context)!.quickActionsDescription,
style:
theme.bodySmall?.copyWith(
color: theme.textSecondary,
) ??
TextStyle(color: theme.textSecondary),
),
),
TextButton(
onPressed: selected.isEmpty
? null
: () async {
await ref
: () => ref
.read(appSettingsProvider.notifier)
.setQuickPills(const []);
},
.setQuickPills(const []),
child: Text(AppLocalizations.of(context)!.clear),
),
],
),
const SizedBox(height: Spacing.sm),
const SizedBox(height: Spacing.md),
Wrap(
spacing: Spacing.sm,
runSpacing: Spacing.sm,
children: [
ConduitChip(
label: AppLocalizations.of(context)!.web,
icon: Platform.isIOS
? CupertinoIcons.search
: Icons.search,
icon: Platform.isIOS ? CupertinoIcons.search : Icons.search,
isSelected: selected.contains('web'),
onTap:
(selectedCount < 2 || selected.contains('web'))
onTap: (selectedCount < 2 || selected.contains('web'))
? () => toggle('web')
: null,
),
ConduitChip(
label: AppLocalizations.of(context)!.imageGen,
icon: Platform.isIOS
? CupertinoIcons.photo
: Icons.image,
icon: Platform.isIOS ? CupertinoIcons.photo : Icons.image,
isSelected: selected.contains('image'),
onTap:
(selectedCount < 2 ||
selected.contains('image'))
onTap: (selectedCount < 2 || selected.contains('image'))
? () => toggle('image')
: null,
),
// Dynamic tools from server
...dynamicToolChips,
...buildToolChips(),
],
),
],
),
),
],
);
},
),
}
const SizedBox(height: Spacing.lg),
// Chat input behavior
Text(
'Chat',
style: context.conduitTheme.headingSmall?.copyWith(
color: context.conduitTheme.textPrimary,
),
),
const SizedBox(height: Spacing.md),
ConduitCard(
padding: EdgeInsets.zero,
child: Column(
children: [
ListTile(
contentPadding: const EdgeInsets.symmetric(
horizontal: Spacing.listItemPadding,
vertical: Spacing.sm,
),
leading: Container(
padding: const EdgeInsets.all(Spacing.sm),
decoration: BoxDecoration(
color: context.conduitTheme.buttonPrimary.withValues(
alpha: Alpha.highlight,
),
borderRadius: BorderRadius.circular(
AppBorderRadius.small,
),
),
child: Icon(
Platform.isIOS
? CupertinoIcons.paperplane
: Icons.keyboard_return,
color: context.conduitTheme.buttonPrimary,
size: IconSize.medium,
),
),
title: Text(
'Send on Enter',
style: context.conduitTheme.bodyLarge?.copyWith(
color: context.conduitTheme.textPrimary,
fontWeight: FontWeight.w500,
),
),
subtitle: Text(
'Enter sends (soft keyboard). Cmd/Ctrl+Enter also available',
style: context.conduitTheme.bodySmall?.copyWith(
color: context.conduitTheme.textSecondary,
),
),
trailing: Switch.adaptive(
value: settings.sendOnEnter,
onChanged: (v) => ref
.read(appSettingsProvider.notifier)
.setSendOnEnter(v),
),
),
],
),
),
const SizedBox(height: Spacing.lg),
Text(
AppLocalizations.of(context)!.realtime,
style: context.conduitTheme.headingSmall?.copyWith(
color: context.conduitTheme.textPrimary,
),
),
const SizedBox(height: Spacing.md),
ConduitCard(
padding: EdgeInsets.zero,
child: Column(
Widget _buildChatSection(
BuildContext context,
WidgetRef ref,
AppSettings settings,
) {
final theme = context.conduitTheme;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ListTile(
contentPadding: const EdgeInsets.symmetric(
horizontal: Spacing.listItemPadding,
vertical: Spacing.sm,
Text(
'Chat',
style:
theme.headingSmall?.copyWith(color: theme.textPrimary) ??
TextStyle(color: theme.textPrimary, fontSize: 18),
),
leading: Container(
padding: const EdgeInsets.all(Spacing.sm),
decoration: BoxDecoration(
color: context.conduitTheme.buttonPrimary.withValues(
alpha: Alpha.highlight,
const SizedBox(height: Spacing.sm),
_CustomizationTile(
leading: _buildIconBadge(
context,
Platform.isIOS ? CupertinoIcons.paperplane : Icons.keyboard_return,
color: theme.buttonPrimary,
),
borderRadius: BorderRadius.circular(
AppBorderRadius.small,
title: 'Send on Enter',
subtitle:
'Enter sends (soft keyboard). Cmd/Ctrl+Enter also available',
trailing: Switch.adaptive(
value: settings.sendOnEnter,
onChanged: (value) =>
ref.read(appSettingsProvider.notifier).setSendOnEnter(value),
),
),
child: Icon(
Platform.isIOS
? CupertinoIcons.waveform
: Icons.sync_alt,
color: context.conduitTheme.buttonPrimary,
size: IconSize.medium,
),
),
title: Text(
AppLocalizations.of(context)!.transportMode,
style: context.conduitTheme.bodyLarge?.copyWith(
color: context.conduitTheme.textPrimary,
fontWeight: FontWeight.w600,
),
),
subtitle: Text(
AppLocalizations.of(context)!.transportModeDescription,
style: context.conduitTheme.bodySmall?.copyWith(
color: context.conduitTheme.textSecondary,
),
),
),
Padding(
padding: const EdgeInsets.fromLTRB(
Spacing.listItemPadding,
0,
Spacing.listItemPadding,
Spacing.md,
),
child: DropdownButtonFormField<String>(
initialValue: settings.socketTransportMode,
onChanged: (v) async {
if (v == null) return;
await ref
showChevron: false,
onTap: () => ref
.read(appSettingsProvider.notifier)
.setSocketTransportMode(v);
},
items: [
DropdownMenuItem(
value: 'auto',
child: Text(
AppLocalizations.of(context)!.transportModeAuto,
),
),
DropdownMenuItem(
value: 'ws',
child: Text(
AppLocalizations.of(context)!.transportModeWs,
),
),
],
decoration: InputDecoration(
labelText: AppLocalizations.of(context)!.mode,
border: const OutlineInputBorder(),
isDense: true,
contentPadding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 10,
),
),
),
),
Padding(
padding: const EdgeInsets.fromLTRB(
Spacing.listItemPadding,
0,
Spacing.listItemPadding,
Spacing.md,
),
child: Text(
settings.socketTransportMode == 'auto'
? AppLocalizations.of(context)!.transportModeAutoInfo
: AppLocalizations.of(context)!.transportModeWsInfo,
style: context.conduitTheme.caption?.copyWith(
color: context.conduitTheme.textSecondary,
),
),
),
],
),
.setSendOnEnter(!settings.sendOnEnter),
),
],
);
}
String _resolveLanguageLabel(BuildContext context, String code) {
switch (code) {
case 'en':
return AppLocalizations.of(context)!.english;
case 'de':
return AppLocalizations.of(context)!.deutsch;
case 'fr':
return AppLocalizations.of(context)!.francais;
case 'it':
return AppLocalizations.of(context)!.italiano;
default:
return AppLocalizations.of(context)!.system;
}
}
Widget _buildIconBadge(
BuildContext context,
IconData icon, {
required Color color,
}) {
return Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: color.withValues(alpha: Alpha.highlight),
borderRadius: BorderRadius.circular(AppBorderRadius.medium),
border: Border.all(
color: color.withValues(alpha: 0.2),
width: BorderWidth.thin,
),
),
alignment: Alignment.center,
child: Icon(icon, color: color, size: IconSize.large),
);
}
@@ -646,3 +550,80 @@ class AppCustomizationPage extends ConsumerWidget {
);
}
}
class _CustomizationTile extends StatelessWidget {
const _CustomizationTile({
required this.leading,
required this.title,
required this.subtitle,
this.trailing,
this.onTap,
this.showChevron = true,
});
final Widget leading;
final String title;
final String subtitle;
final Widget? trailing;
final VoidCallback? onTap;
final bool showChevron;
@override
Widget build(BuildContext context) {
final theme = context.conduitTheme;
return ConduitCard(
padding: const EdgeInsets.symmetric(
horizontal: Spacing.listItemPadding,
vertical: Spacing.md,
),
onTap: onTap,
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
leading,
const SizedBox(width: Spacing.md),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style:
theme.bodyLarge?.copyWith(
color: theme.textPrimary,
fontWeight: FontWeight.w600,
) ??
TextStyle(
color: theme.textPrimary,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: Spacing.textSpacing),
Text(
subtitle,
style:
theme.bodySmall?.copyWith(color: theme.textSecondary) ??
TextStyle(color: theme.textSecondary),
),
],
),
),
if (trailing != null) ...[
const SizedBox(width: Spacing.md),
trailing!,
] else if (showChevron && onTap != null) ...[
const SizedBox(width: Spacing.md),
Icon(
UiUtils.platformIcon(
ios: CupertinoIcons.chevron_right,
android: Icons.chevron_right,
),
color: theme.iconSecondary,
size: IconSize.small,
),
],
],
),
);
}
}

View File

@@ -43,129 +43,24 @@ class ProfilePage extends ConsumerWidget {
return ErrorBoundary(
child: user.when(
data: (userData) => Scaffold(
backgroundColor: context.conduitTheme.surfaceBackground,
appBar: AppBar(
backgroundColor: context.conduitTheme.surfaceBackground,
elevation: Elevation.none,
toolbarHeight: kToolbarHeight - 8,
automaticallyImplyLeading: false,
leading: IconButton(
icon: Icon(
UiUtils.platformIcon(
ios: CupertinoIcons.back,
android: Icons.arrow_back,
data: (userData) => _buildScaffold(
context,
body: _buildProfileBody(context, ref, userData, api),
),
color: context.conduitTheme.textPrimary,
),
onPressed: () => Navigator.of(context).maybePop(),
tooltip: AppLocalizations.of(context)!.back,
),
// keep reduced height only once
titleSpacing: 0.0,
title: Text(
AppLocalizations.of(context)!.you,
style: AppTypography.headlineSmallStyle.copyWith(
color: context.conduitTheme.textPrimary,
fontWeight: FontWeight.w600,
),
),
centerTitle: true,
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(Spacing.pagePadding),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Profile Header - Enhanced with better spacing and animations
_buildProfileHeader(context, userData, api)
.animate()
.fadeIn(duration: AnimationDuration.pageTransition)
.slideY(
begin: 0.1,
end: 0,
curve: AnimationCurves.pageTransition,
),
const SizedBox(height: Spacing.sectionGap),
// Account Section - Enhanced with improved spacing
_buildAccountSection(context, ref)
.animate()
.fadeIn(
delay: AnimationDelay.short,
duration: AnimationDuration.pageTransition,
)
.slideY(
begin: 0.1,
end: 0,
curve: AnimationCurves.pageTransition,
),
],
),
),
),
loading: () => Scaffold(
backgroundColor: context.conduitTheme.surfaceBackground,
appBar: AppBar(
backgroundColor: context.conduitTheme.surfaceBackground,
elevation: Elevation.none,
toolbarHeight: kToolbarHeight - 8,
automaticallyImplyLeading: false,
leading: IconButton(
icon: Icon(
UiUtils.platformIcon(
ios: CupertinoIcons.back,
android: Icons.arrow_back,
),
color: context.conduitTheme.textPrimary,
),
onPressed: () => Navigator.of(context).maybePop(),
tooltip: AppLocalizations.of(context)!.back,
),
title: Text(
AppLocalizations.of(context)!.you,
style: AppTypography.headlineSmallStyle.copyWith(
color: context.conduitTheme.textPrimary,
fontWeight: FontWeight.w600,
),
),
centerTitle: true,
),
body: Center(
child: ImprovedLoadingState(
loading: () => _buildScaffold(
context,
body: _buildCenteredState(
context,
ImprovedLoadingState(
message: AppLocalizations.of(context)!.loadingProfile,
),
),
),
error: (error, stack) => Scaffold(
backgroundColor: context.conduitTheme.surfaceBackground,
appBar: AppBar(
backgroundColor: context.conduitTheme.surfaceBackground,
elevation: Elevation.none,
toolbarHeight: kToolbarHeight - 8,
automaticallyImplyLeading: false,
leading: IconButton(
icon: Icon(
UiUtils.platformIcon(
ios: CupertinoIcons.back,
android: Icons.arrow_back,
),
color: context.conduitTheme.textPrimary,
),
onPressed: () => Navigator.of(context).maybePop(),
tooltip: AppLocalizations.of(context)!.back,
),
title: Text(
AppLocalizations.of(context)!.you,
style: AppTypography.headlineSmallStyle.copyWith(
color: context.conduitTheme.textPrimary,
fontWeight: FontWeight.w600,
),
),
centerTitle: true,
),
body: Center(
child: ImprovedEmptyState(
error: (error, stack) => _buildScaffold(
context,
body: _buildCenteredState(
context,
ImprovedEmptyState(
title: AppLocalizations.of(context)!.unableToLoadProfile,
subtitle: AppLocalizations.of(context)!.pleaseCheckConnection,
icon: UiUtils.platformIcon(
@@ -179,6 +74,97 @@ class ProfilePage extends ConsumerWidget {
);
}
Scaffold _buildScaffold(BuildContext context, {required Widget body}) {
return Scaffold(
backgroundColor: context.conduitTheme.surfaceBackground,
appBar: _buildAppBar(context),
body: body,
);
}
PreferredSizeWidget _buildAppBar(BuildContext context) {
final canPop = ModalRoute.of(context)?.canPop ?? false;
return AppBar(
backgroundColor: context.conduitTheme.surfaceBackground,
surfaceTintColor: Colors.transparent,
elevation: Elevation.none,
toolbarHeight: kToolbarHeight,
automaticallyImplyLeading: false,
leading: canPop
? IconButton(
icon: Icon(
UiUtils.platformIcon(
ios: CupertinoIcons.back,
android: Icons.arrow_back,
),
color: context.conduitTheme.iconPrimary,
),
onPressed: () => Navigator.of(context).maybePop(),
tooltip: AppLocalizations.of(context)!.back,
)
: null,
titleSpacing: 0,
title: Text(
AppLocalizations.of(context)!.you,
style: AppTypography.headlineSmallStyle.copyWith(
color: context.conduitTheme.textPrimary,
fontWeight: FontWeight.w600,
),
),
centerTitle: true,
);
}
Widget _buildCenteredState(BuildContext context, Widget child) {
return SafeArea(
child: Padding(
padding: const EdgeInsets.all(Spacing.pagePadding),
child: Center(child: child),
),
);
}
Widget _buildProfileBody(
BuildContext context,
WidgetRef ref,
dynamic userData,
ApiService? api,
) {
return SafeArea(
child: ListView(
physics: const BouncingScrollPhysics(
parent: AlwaysScrollableScrollPhysics(),
),
padding: const EdgeInsets.symmetric(
horizontal: Spacing.pagePadding,
vertical: Spacing.pagePadding,
),
children: [
_buildProfileHeader(context, userData, api)
.animate()
.fadeIn(duration: AnimationDuration.pageTransition)
.slideY(
begin: 0.1,
end: 0,
curve: AnimationCurves.pageTransition,
),
const SizedBox(height: Spacing.sectionGap),
_buildAccountSection(context, ref)
.animate()
.fadeIn(
delay: AnimationDelay.short,
duration: AnimationDuration.pageTransition,
)
.slideY(
begin: 0.08,
end: 0,
curve: AnimationCurves.pageTransition,
),
],
),
);
}
Widget _buildProfileHeader(
BuildContext context,
dynamic user,
@@ -212,23 +198,68 @@ class ProfilePage extends ConsumerWidget {
}
final email = extractEmail(user) ?? 'No email';
final theme = context.conduitTheme;
final accent = theme.buttonPrimary;
return ConduitCard(
return Container(
padding: const EdgeInsets.all(Spacing.cardPadding),
child: Row(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
accent.withValues(alpha: 0.22),
accent.withValues(alpha: 0.06),
],
),
borderRadius: BorderRadius.circular(AppBorderRadius.extraLarge),
border: Border.all(
color: accent.withValues(alpha: 0.18),
width: BorderWidth.thin,
),
boxShadow: ConduitShadows.medium,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.symmetric(
horizontal: Spacing.sm,
vertical: Spacing.xs,
),
decoration: BoxDecoration(
color: theme.surfaceBackground.withValues(alpha: 0.7),
borderRadius: BorderRadius.circular(AppBorderRadius.pill),
),
child: Text(
AppLocalizations.of(context)!.account,
style:
theme.caption?.copyWith(
color: theme.textSecondary,
fontWeight: FontWeight.w600,
) ??
TextStyle(
color: theme.textSecondary,
fontWeight: FontWeight.w600,
),
),
),
const SizedBox(height: Spacing.lg),
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(AppBorderRadius.avatar),
boxShadow: ConduitShadows.card,
boxShadow: ConduitShadows.high,
),
child: UserAvatar(
size: IconSize.avatar,
size: IconSize.huge,
imageUrl: avatarUrl,
fallbackText: initial,
),
),
const SizedBox(width: Spacing.md),
const SizedBox(width: Spacing.lg),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -236,33 +267,97 @@ class ProfilePage extends ConsumerWidget {
Text(
displayName,
style:
context.conduitTheme.headingMedium?.copyWith(
color: context.conduitTheme.textPrimary,
fontWeight: FontWeight.w600,
theme.headingMedium?.copyWith(
color: theme.textPrimary,
fontWeight: FontWeight.w700,
) ??
TextStyle(
color: context.conduitTheme.textPrimary,
fontWeight: FontWeight.w600,
color: theme.textPrimary,
fontWeight: FontWeight.w700,
fontSize: 22,
),
),
const SizedBox(height: Spacing.sm),
Text(
Container(
padding: const EdgeInsets.symmetric(
horizontal: Spacing.md,
vertical: Spacing.xs,
),
decoration: BoxDecoration(
color: theme.surfaceBackground.withValues(alpha: 0.75),
borderRadius: BorderRadius.circular(
AppBorderRadius.round,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
UiUtils.platformIcon(
ios: CupertinoIcons.envelope,
android: Icons.mail_outline,
),
size: IconSize.small,
color: theme.textSecondary,
),
const SizedBox(width: Spacing.xs),
Flexible(
child: Text(
email,
style:
context.conduitTheme.bodyMedium?.copyWith(
color: context.conduitTheme.textSecondary,
theme.bodySmall?.copyWith(
color: theme.textSecondary,
) ??
TextStyle(color: context.conduitTheme.textSecondary),
TextStyle(color: theme.textSecondary),
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
),
],
),
),
],
),
),
],
),
],
),
);
}
Widget _buildAccountSection(BuildContext context, WidgetRef ref) {
final items = [
_buildDefaultModelTile(context, ref),
_buildAccountOption(
context,
icon: UiUtils.platformIcon(
ios: CupertinoIcons.slider_horizontal_3,
android: Icons.tune,
),
title: AppLocalizations.of(context)!.appCustomization,
subtitle: AppLocalizations.of(context)!.appCustomizationSubtitle,
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const AppCustomizationPage()),
);
},
),
_buildAboutTile(context),
_buildAccountOption(
context,
icon: UiUtils.platformIcon(
ios: CupertinoIcons.square_arrow_left,
android: Icons.logout,
),
title: AppLocalizations.of(context)!.signOut,
subtitle: AppLocalizations.of(context)!.endYourSession,
onTap: () => _signOut(context, ref),
isDestructive: true,
showChevron: false,
),
];
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -272,106 +367,42 @@ class ProfilePage extends ConsumerWidget {
color: context.conduitTheme.textPrimary,
),
),
const SizedBox(height: Spacing.md),
ConduitCard(
padding: EdgeInsets.zero,
child: Column(
children: [
_buildDefaultModelTile(context, ref),
Divider(color: context.conduitTheme.dividerColor, height: 1),
_buildAccountOption(
icon: UiUtils.platformIcon(
ios: CupertinoIcons.slider_horizontal_3,
android: Icons.tune,
),
title: AppLocalizations.of(context)!.appCustomization,
subtitle: AppLocalizations.of(
context,
)!.appCustomizationSubtitle,
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => const AppCustomizationPage(),
),
);
},
),
Divider(color: context.conduitTheme.dividerColor, height: 1),
_buildAboutTile(context),
Divider(color: context.conduitTheme.dividerColor, height: 1),
_buildAccountOption(
icon: UiUtils.platformIcon(
ios: CupertinoIcons.square_arrow_left,
android: Icons.logout,
),
title: AppLocalizations.of(context)!.signOut,
subtitle: AppLocalizations.of(context)!.endYourSession,
onTap: () => _signOut(context, ref),
isDestructive: true,
),
const SizedBox(height: Spacing.sm),
for (var i = 0; i < items.length; i++) ...[
items[i],
if (i != items.length - 1) const SizedBox(height: Spacing.md),
],
),
),
],
);
}
Widget _buildAccountOption({
Widget _buildAccountOption(
BuildContext context, {
required IconData icon,
required String title,
required String subtitle,
required VoidCallback onTap,
bool isDestructive = false,
bool showChevron = true,
}) {
return Builder(
builder: (context) => ListTile(
contentPadding: const EdgeInsets.symmetric(
horizontal: Spacing.listItemPadding,
vertical: Spacing.sm,
),
leading: Container(
padding: const EdgeInsets.all(Spacing.sm),
decoration: BoxDecoration(
color: isDestructive
? context.conduitTheme.error.withValues(alpha: Alpha.highlight)
: context.conduitTheme.buttonPrimary.withValues(
alpha: Alpha.highlight,
),
borderRadius: BorderRadius.circular(AppBorderRadius.small),
),
child: Icon(
icon,
color: isDestructive
? context.conduitTheme.error
: context.conduitTheme.buttonPrimary,
size: IconSize.medium,
),
),
title: Text(
title,
style: context.conduitTheme.bodyLarge?.copyWith(
color: isDestructive
? context.conduitTheme.error
: context.conduitTheme.textPrimary,
fontWeight: FontWeight.w500,
),
),
subtitle: Text(
subtitle,
style: context.conduitTheme.bodySmall?.copyWith(
color: context.conduitTheme.textSecondary,
),
),
trailing: Icon(
final theme = context.conduitTheme;
final color = isDestructive ? theme.error : theme.buttonPrimary;
return _ProfileSettingTile(
onTap: onTap,
isDestructive: isDestructive,
leading: _buildIconBadge(context, icon, color: color),
title: title,
subtitle: subtitle,
trailing: showChevron
? Icon(
UiUtils.platformIcon(
ios: CupertinoIcons.chevron_right,
android: Icons.chevron_right,
),
color: context.conduitTheme.iconSecondary,
color: theme.iconSecondary,
size: IconSize.small,
),
onTap: onTap,
),
)
: null,
);
}
@@ -400,142 +431,125 @@ class ProfilePage extends ConsumerWidget {
? currentModel.name
: AppLocalizations.of(context)!.autoSelect;
final theme = context.conduitTheme;
Widget leading;
if (selectedModelExplicit) {
leading = ModelAvatar(
leading = Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: theme.surfaceBackground.withValues(alpha: 0.85),
borderRadius: BorderRadius.circular(AppBorderRadius.medium),
border: Border.all(
color: theme.cardBorder,
width: BorderWidth.thin,
),
),
alignment: Alignment.center,
child: ModelAvatar(
size: 32,
imageUrl: modelIconUrl,
label: currentModel.name,
),
);
} else {
leading = Container(
padding: const EdgeInsets.all(Spacing.sm),
decoration: BoxDecoration(
color: context.conduitTheme.buttonPrimary.withValues(
alpha: Alpha.highlight,
),
borderRadius: BorderRadius.circular(AppBorderRadius.small),
),
child: Icon(
leading = _buildIconBadge(
context,
UiUtils.platformIcon(
ios: CupertinoIcons.wand_stars,
android: Icons.auto_awesome,
),
color: context.conduitTheme.buttonPrimary,
size: IconSize.medium,
),
color: theme.buttonPrimary,
);
}
return ListTile(
contentPadding: const EdgeInsets.symmetric(
horizontal: Spacing.listItemPadding,
vertical: Spacing.sm,
),
return _ProfileSettingTile(
leading: leading,
title: Text(
AppLocalizations.of(context)!.defaultModel,
style: context.conduitTheme.bodyLarge?.copyWith(
color: context.conduitTheme.textPrimary,
fontWeight: FontWeight.w500,
),
),
subtitle: Text(
modelLabel,
style: context.conduitTheme.bodySmall?.copyWith(
color: context.conduitTheme.textSecondary,
),
),
trailing: Icon(
UiUtils.platformIcon(
ios: CupertinoIcons.chevron_right,
android: Icons.chevron_right,
),
color: context.conduitTheme.iconSecondary,
size: IconSize.small,
),
title: AppLocalizations.of(context)!.defaultModel,
subtitle: modelLabel,
onTap: () => _showModelSelector(context, ref, models),
);
},
loading: () => ListTile(
contentPadding: const EdgeInsets.symmetric(
horizontal: Spacing.listItemPadding,
vertical: Spacing.sm,
),
leading: Container(
padding: const EdgeInsets.all(Spacing.sm),
decoration: BoxDecoration(
color: context.conduitTheme.buttonPrimary.withValues(
alpha: Alpha.highlight,
),
borderRadius: BorderRadius.circular(AppBorderRadius.small),
),
child: Icon(
loading: () => _ProfileSettingTile(
leading: _buildIconBadge(
context,
UiUtils.platformIcon(
ios: CupertinoIcons.cube_box,
android: Icons.psychology,
),
color: context.conduitTheme.buttonPrimary,
size: IconSize.medium,
),
),
title: Text(
AppLocalizations.of(context)!.defaultModel,
style: context.conduitTheme.bodyLarge?.copyWith(
color: context.conduitTheme.textPrimary,
fontWeight: FontWeight.w500,
),
),
subtitle: Text(
AppLocalizations.of(context)!.loadingModels,
style: context.conduitTheme.bodySmall?.copyWith(
color: context.conduitTheme.textSecondary,
title: AppLocalizations.of(context)!.defaultModel,
subtitle: AppLocalizations.of(context)!.loadingModels,
showChevron: false,
trailing: SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(
context.conduitTheme.buttonPrimary,
),
),
),
error: (error, stack) => ListTile(
contentPadding: const EdgeInsets.symmetric(
horizontal: Spacing.listItemPadding,
vertical: Spacing.sm,
),
leading: Container(
padding: const EdgeInsets.all(Spacing.sm),
decoration: BoxDecoration(
color: context.conduitTheme.error.withValues(
alpha: Alpha.highlight,
),
borderRadius: BorderRadius.circular(AppBorderRadius.small),
),
child: Icon(
error: (error, stack) => _ProfileSettingTile(
leading: _buildIconBadge(
context,
UiUtils.platformIcon(
ios: CupertinoIcons.exclamationmark_triangle,
android: Icons.error_outline,
),
color: context.conduitTheme.error,
size: IconSize.medium,
),
title: AppLocalizations.of(context)!.defaultModel,
subtitle: AppLocalizations.of(context)!.failedToLoadModels,
isDestructive: true,
showChevron: false,
onTap: () => ref.invalidate(modelsProvider),
trailing: IconButton(
onPressed: () => ref.invalidate(modelsProvider),
tooltip: AppLocalizations.of(context)!.retry,
icon: Icon(
UiUtils.platformIcon(
ios: CupertinoIcons.refresh,
android: Icons.refresh,
),
title: Text(
AppLocalizations.of(context)!.defaultModel,
style: context.conduitTheme.bodyLarge?.copyWith(
color: context.conduitTheme.textPrimary,
fontWeight: FontWeight.w500,
),
),
subtitle: Text(
AppLocalizations.of(context)!.failedToLoadModels,
style: context.conduitTheme.bodySmall?.copyWith(
color: context.conduitTheme.error,
size: IconSize.small,
),
),
),
);
}
Widget _buildIconBadge(
BuildContext context,
IconData icon, {
required Color color,
}) {
return Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: color.withValues(alpha: Alpha.highlight),
borderRadius: BorderRadius.circular(AppBorderRadius.medium),
border: Border.all(
color: color.withValues(alpha: 0.2),
width: BorderWidth.thin,
),
),
alignment: Alignment.center,
child: Icon(icon, color: color, size: IconSize.large),
);
}
// Theme and language controls moved to AppCustomizationPage.
Widget _buildAboutTile(BuildContext context) {
return _buildAccountOption(
context,
icon: UiUtils.platformIcon(
ios: CupertinoIcons.info,
android: Icons.info_outline,
@@ -666,6 +680,87 @@ class ProfilePage extends ConsumerWidget {
}
}
class _ProfileSettingTile extends StatelessWidget {
const _ProfileSettingTile({
required this.leading,
required this.title,
required this.subtitle,
this.onTap,
this.trailing,
this.isDestructive = false,
this.showChevron = true,
});
final Widget leading;
final String title;
final String subtitle;
final VoidCallback? onTap;
final Widget? trailing;
final bool isDestructive;
final bool showChevron;
@override
Widget build(BuildContext context) {
final theme = context.conduitTheme;
final textColor = isDestructive ? theme.error : theme.textPrimary;
final subtitleColor = isDestructive
? theme.error.withValues(alpha: 0.85)
: theme.textSecondary;
return ConduitCard(
padding: const EdgeInsets.symmetric(
horizontal: Spacing.listItemPadding,
vertical: Spacing.md,
),
onTap: onTap,
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
leading,
const SizedBox(width: Spacing.md),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style:
theme.bodyLarge?.copyWith(
color: textColor,
fontWeight: FontWeight.w600,
) ??
TextStyle(color: textColor, fontWeight: FontWeight.w600),
),
const SizedBox(height: Spacing.textSpacing),
Text(
subtitle,
style:
theme.bodySmall?.copyWith(color: subtitleColor) ??
TextStyle(color: subtitleColor),
),
],
),
),
if (trailing != null) ...[
const SizedBox(width: Spacing.md),
trailing!,
] else if (showChevron && onTap != null) ...[
const SizedBox(width: Spacing.md),
Icon(
UiUtils.platformIcon(
ios: CupertinoIcons.chevron_right,
android: Icons.chevron_right,
),
color: theme.iconSecondary,
size: IconSize.small,
),
],
],
),
);
}
}
class _DefaultModelBottomSheet extends ConsumerStatefulWidget {
final List<Model> models;
final String? currentDefaultModelId;

View File

@@ -265,6 +265,7 @@
,
"appCustomization": "App-Anpassung",
"appCustomizationSubtitle": "Personalisieren, wie Namen und UI angezeigt werden",
"quickActionsDescription": "Wähle bis zu zwei Schnellzugriffe, die neben dem Eingabefeld angepinnt werden",
"display": "Anzeige",
"realtime": "Echtzeit",
"hideProviderInModelNames": "Anbieter in Modellnamen ausblenden",

View File

@@ -532,6 +532,8 @@
"@appCustomization": {"description": "Title of the customization settings page."},
"appCustomizationSubtitle": "Personalize how names and UI display",
"@appCustomizationSubtitle": {"description": "Subtitle shown under App Customization tile and page header."},
"quickActionsDescription": "Pick up to two shortcuts to pin near the composer",
"@quickActionsDescription": {"description": "Helper text explaining quick action pill selection in customization."},
"display": "Display",
"@display": {"description": "Section header for visual and layout related settings."},
"realtime": "Realtime",

View File

@@ -265,6 +265,7 @@
,
"appCustomization": "Personnalisation de l'app",
"appCustomizationSubtitle": "Personnalisez l'affichage des noms et de l'UI",
"quickActionsDescription": "Choisissez jusqu'à deux raccourcis à épingler près du champ de saisie",
"display": "Affichage",
"realtime": "Temps réel",
"hideProviderInModelNames": "Masquer le fournisseur dans les noms de modèles",

View File

@@ -265,6 +265,7 @@
,
"appCustomization": "Personalizzazione app",
"appCustomizationSubtitle": "Personalizza la visualizzazione dei nomi e dell'UI",
"quickActionsDescription": "Scegli fino a due scorciatoie da fissare vicino al campo di input",
"display": "Schermo",
"realtime": "Tempo reale",
"hideProviderInModelNames": "Nascondi provider nei nomi dei modelli",

View File

@@ -1506,6 +1506,12 @@ abstract class AppLocalizations {
/// **'Personalize how names and UI display'**
String get appCustomizationSubtitle;
/// Helper text explaining quick action pill selection in customization.
///
/// In en, this message translates to:
/// **'Pick up to two shortcuts to pin near the composer'**
String get quickActionsDescription;
/// Section header for visual and layout related settings.
///
/// In en, this message translates to:

View File

@@ -784,6 +784,10 @@ class AppLocalizationsDe extends AppLocalizations {
String get appCustomizationSubtitle =>
'Personalisieren, wie Namen und UI angezeigt werden';
@override
String get quickActionsDescription =>
'Wähle bis zu zwei Schnellzugriffe, die neben dem Eingabefeld angepinnt werden';
@override
String get display => 'Anzeige';

View File

@@ -777,6 +777,10 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get appCustomizationSubtitle => 'Personalize how names and UI display';
@override
String get quickActionsDescription =>
'Pick up to two shortcuts to pin near the composer';
@override
String get display => 'Display';

View File

@@ -792,6 +792,10 @@ class AppLocalizationsFr extends AppLocalizations {
String get appCustomizationSubtitle =>
'Personnalisez l\'affichage des noms et de l\'UI';
@override
String get quickActionsDescription =>
'Choisissez jusqu\'à deux raccourcis à épingler près du champ de saisie';
@override
String get display => 'Affichage';

View File

@@ -781,6 +781,10 @@ class AppLocalizationsIt extends AppLocalizations {
String get appCustomizationSubtitle =>
'Personalizza la visualizzazione dei nomi e dell\'UI';
@override
String get quickActionsDescription =>
'Scegli fino a due scorciatoie da fissare vicino al campo di input';
@override
String get display => 'Schermo';