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; return AppLocalizations.of(context)!.currentlyUsingLightTheme;
}(); }();
final locale = ref.watch(localeProvider); final locale = ref.watch(localeProvider);
final currentLanguageCode = locale?.languageCode ?? 'system';
final languageLabel = _resolveLanguageLabel(context, currentLanguageCode);
return Scaffold( return Scaffold(
backgroundColor: context.conduitTheme.surfaceBackground, 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, backgroundColor: context.conduitTheme.surfaceBackground,
surfaceTintColor: Colors.transparent,
elevation: Elevation.none, elevation: Elevation.none,
toolbarHeight: kToolbarHeight - 8, toolbarHeight: kToolbarHeight,
leading: IconButton( automaticallyImplyLeading: false,
leading: canPop
? IconButton(
icon: Icon( icon: Icon(
UiUtils.platformIcon( UiUtils.platformIcon(
ios: CupertinoIcons.back, ios: CupertinoIcons.back,
android: Icons.arrow_back, android: Icons.arrow_back,
), ),
color: context.conduitTheme.textPrimary, color: context.conduitTheme.iconPrimary,
), ),
onPressed: () => Navigator.of(context).maybePop(), onPressed: () => Navigator.of(context).maybePop(),
tooltip: AppLocalizations.of(context)!.back, tooltip: AppLocalizations.of(context)!.back,
), )
: null,
titleSpacing: 0,
title: Text( title: Text(
AppLocalizations.of(context)!.appCustomization, AppLocalizations.of(context)!.appCustomization,
style: AppTypography.headlineSmallStyle.copyWith( style: AppTypography.headlineSmallStyle.copyWith(
@@ -60,537 +99,402 @@ class AppCustomizationPage extends ConsumerWidget {
), ),
), ),
centerTitle: true, 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, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
AppLocalizations.of(context)!.display, AppLocalizations.of(context)!.display,
style: context.conduitTheme.headingSmall?.copyWith( style:
color: context.conduitTheme.textPrimary, 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), const SizedBox(height: Spacing.md),
ConduitCard( _CustomizationTile(
padding: EdgeInsets.zero, leading: _buildIconBadge(
child: Column( context,
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(
UiUtils.platformIcon( UiUtils.platformIcon(
ios: CupertinoIcons.globe, ios: CupertinoIcons.globe,
android: Icons.language, android: Icons.language,
), ),
color: context.conduitTheme.buttonPrimary, color: theme.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,
), ),
title: AppLocalizations.of(context)!.appLanguage,
subtitle: languageLabel,
onTap: () async { onTap: () async {
final selected = await _showLanguageSelector( final selected = await _showLanguageSelector(
context, context,
currentCode, currentLanguageCode,
); );
if (selected != null) { if (selected == null) return;
if (selected == 'system') { if (selected == 'system') {
await ref await ref.read(localeProvider.notifier).setLocale(null);
.read(localeProvider.notifier)
.setLocale(null);
} else { } else {
await ref await ref
.read(localeProvider.notifier) .read(localeProvider.notifier)
.setLocale(Locale(selected)); .setLocale(Locale(selected));
} }
}
},
);
}, },
), ),
Divider(color: context.conduitTheme.dividerColor, height: 1), const SizedBox(height: Spacing.md),
_CustomizationTile(
SwitchListTile.adaptive( leading: _buildIconBadge(
contentPadding: const EdgeInsets.symmetric( context,
horizontal: Spacing.listItemPadding, Platform.isIOS ? CupertinoIcons.textformat : Icons.text_fields,
vertical: Spacing.sm, color: theme.buttonPrimary,
), ),
// Use platform defaults for switch colors to match theme title: AppLocalizations.of(context)!.hideProviderInModelNames,
value: settings.omitProviderInModelName, subtitle: AppLocalizations.of(
title: Text(
AppLocalizations.of(context)!.hideProviderInModelNames,
style: context.conduitTheme.bodyLarge?.copyWith(
color: context.conduitTheme.textPrimary,
fontWeight: FontWeight.w500,
),
),
subtitle: Text(
AppLocalizations.of(
context, context,
)!.hideProviderInModelNamesDescription, )!.hideProviderInModelNamesDescription,
style: context.conduitTheme.bodySmall?.copyWith( trailing: Switch.adaptive(
color: context.conduitTheme.textSecondary, value: settings.omitProviderInModelName,
), onChanged: (v) => ref
),
onChanged: (v) {
ref
.read(appSettingsProvider.notifier) .read(appSettingsProvider.notifier)
.setOmitProviderInModelName(v); .setOmitProviderInModelName(v),
},
secondary: Container(
padding: const EdgeInsets.all(Spacing.sm),
decoration: BoxDecoration(
color: context.conduitTheme.buttonPrimary.withValues(
alpha: Alpha.highlight,
), ),
borderRadius: BorderRadius.circular( showChevron: false,
AppBorderRadius.small, onTap: () => ref
), .read(appSettingsProvider.notifier)
), .setOmitProviderInModelName(!settings.omitProviderInModelName),
child: Icon( ),
Platform.isIOS ],
? CupertinoIcons.textformat );
: Icons.text_fields, }
color: context.conduitTheme.buttonPrimary,
size: IconSize.medium, 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), const SizedBox(height: Spacing.md),
Consumer( Wrap(
builder: (context, ref, _) { 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( final selectedRaw = ref.watch(
appSettingsProvider.select((s) => s.quickPills), appSettingsProvider.select((s) => s.quickPills),
); );
final toolsAsync = ref.watch(toolsListProvider); final toolsAsync = ref.watch(toolsListProvider);
final tools = toolsAsync.maybeWhen( final tools = toolsAsync.maybeWhen(
data: (t) => t, data: (value) => value,
orElse: () => const <Tool>[], orElse: () => const <Tool>[],
); );
final allowed = <String>{ final allowed = <String>{'web', 'image', ...tools.map((t) => t.id)};
'web',
'image',
...tools.map((t) => t.id),
};
// Sanitize persisted selection
final selected = selectedRaw final selected = selectedRaw
.where((id) => allowed.contains(id)) .where((id) => allowed.contains(id))
.take(2) .take(2)
.toList(); .toList();
if (selected.length != selectedRaw.length) { if (selected.length != selectedRaw.length) {
// Persist sanitized list asynchronously
Future.microtask( Future.microtask(
() => ref () => ref.read(appSettingsProvider.notifier).setQuickPills(selected),
.read(appSettingsProvider.notifier)
.setQuickPills(selected),
); );
} }
final int selectedCount = selected.length;
void toggle(String id) async { final selectedCount = selected.length;
final current = List<String>.from(selected);
if (current.contains(id)) { Future<void> toggle(String id) async {
current.remove(id); final next = List<String>.from(selected);
if (next.contains(id)) {
next.remove(id);
} else { } else {
if (current.length >= 2) return; // enforce max 2 if (next.length >= 2) return;
current.add(id); next.add(id);
} }
await ref await ref.read(appSettingsProvider.notifier).setQuickPills(next);
.read(appSettingsProvider.notifier)
.setQuickPills(current);
} }
// Build dynamic tool chips list once List<Widget> buildToolChips() {
final List<Widget> dynamicToolChips = ref return tools.map((tool) {
.watch(toolsListProvider) final isSelected = selected.contains(tool.id);
.maybeWhen<List<Widget>>( final canSelect = selectedCount < 2 || isSelected;
data: (tools) => tools.map((Tool t) {
final isSel = selected.contains(t.id);
final canSelect = selectedCount < 2 || isSel;
return ConduitChip( return ConduitChip(
label: t.name, label: tool.name,
icon: Icons.extension, icon: Icons.extension,
isSelected: isSel, isSelected: isSelected,
onTap: canSelect ? () => toggle(t.id) : null, onTap: canSelect ? () => toggle(tool.id) : null,
);
}).toList(),
orElse: () => const <Widget>[],
); );
}).toList();
}
return ConduitCard( return Column(
padding: const EdgeInsets.symmetric( crossAxisAlignment: CrossAxisAlignment.start,
horizontal: Spacing.listItemPadding, children: [
vertical: Spacing.sm, 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( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Row( Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
_buildIconBadge(
context,
UiUtils.platformIcon(
ios: CupertinoIcons.bolt,
android: Icons.flash_on,
),
color: theme.buttonPrimary,
),
const SizedBox(width: Spacing.md),
Expanded( Expanded(
child: Text( child: Text(
AppLocalizations.of( AppLocalizations.of(context)!.quickActionsDescription,
context, style:
)!.appCustomizationSubtitle, theme.bodySmall?.copyWith(
style: context.conduitTheme.bodySmall?.copyWith( color: theme.textSecondary,
color: context.conduitTheme.textSecondary, ) ??
), TextStyle(color: theme.textSecondary),
), ),
), ),
TextButton( TextButton(
onPressed: selected.isEmpty onPressed: selected.isEmpty
? null ? null
: () async { : () => ref
await ref
.read(appSettingsProvider.notifier) .read(appSettingsProvider.notifier)
.setQuickPills(const []); .setQuickPills(const []),
},
child: Text(AppLocalizations.of(context)!.clear), child: Text(AppLocalizations.of(context)!.clear),
), ),
], ],
), ),
const SizedBox(height: Spacing.sm), const SizedBox(height: Spacing.md),
Wrap( Wrap(
spacing: Spacing.sm, spacing: Spacing.sm,
runSpacing: Spacing.sm, runSpacing: Spacing.sm,
children: [ children: [
ConduitChip( ConduitChip(
label: AppLocalizations.of(context)!.web, label: AppLocalizations.of(context)!.web,
icon: Platform.isIOS icon: Platform.isIOS ? CupertinoIcons.search : Icons.search,
? CupertinoIcons.search
: Icons.search,
isSelected: selected.contains('web'), isSelected: selected.contains('web'),
onTap: onTap: (selectedCount < 2 || selected.contains('web'))
(selectedCount < 2 || selected.contains('web'))
? () => toggle('web') ? () => toggle('web')
: null, : null,
), ),
ConduitChip( ConduitChip(
label: AppLocalizations.of(context)!.imageGen, label: AppLocalizations.of(context)!.imageGen,
icon: Platform.isIOS icon: Platform.isIOS ? CupertinoIcons.photo : Icons.image,
? CupertinoIcons.photo
: Icons.image,
isSelected: selected.contains('image'), isSelected: selected.contains('image'),
onTap: onTap: (selectedCount < 2 || selected.contains('image'))
(selectedCount < 2 ||
selected.contains('image'))
? () => toggle('image') ? () => toggle('image')
: null, : null,
), ),
// Dynamic tools from server ...buildToolChips(),
...dynamicToolChips,
], ],
), ),
], ],
), ),
),
],
); );
}, }
),
const SizedBox(height: Spacing.lg), Widget _buildChatSection(
// Chat input behavior BuildContext context,
Text( WidgetRef ref,
'Chat', AppSettings settings,
style: context.conduitTheme.headingSmall?.copyWith( ) {
color: context.conduitTheme.textPrimary, final theme = context.conduitTheme;
), return Column(
),
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(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
ListTile( Text(
contentPadding: const EdgeInsets.symmetric( 'Chat',
horizontal: Spacing.listItemPadding, style:
vertical: Spacing.sm, theme.headingSmall?.copyWith(color: theme.textPrimary) ??
TextStyle(color: theme.textPrimary, fontSize: 18),
), ),
leading: Container( const SizedBox(height: Spacing.sm),
padding: const EdgeInsets.all(Spacing.sm), _CustomizationTile(
decoration: BoxDecoration( leading: _buildIconBadge(
color: context.conduitTheme.buttonPrimary.withValues( context,
alpha: Alpha.highlight, Platform.isIOS ? CupertinoIcons.paperplane : Icons.keyboard_return,
color: theme.buttonPrimary,
), ),
borderRadius: BorderRadius.circular( title: 'Send on Enter',
AppBorderRadius.small, subtitle:
'Enter sends (soft keyboard). Cmd/Ctrl+Enter also available',
trailing: Switch.adaptive(
value: settings.sendOnEnter,
onChanged: (value) =>
ref.read(appSettingsProvider.notifier).setSendOnEnter(value),
), ),
), showChevron: false,
child: Icon( onTap: () => ref
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
.read(appSettingsProvider.notifier) .read(appSettingsProvider.notifier)
.setSocketTransportMode(v); .setSendOnEnter(!settings.sendOnEnter),
},
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,
),
),
),
],
),
), ),
], ],
);
}
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( return ErrorBoundary(
child: user.when( child: user.when(
data: (userData) => Scaffold( data: (userData) => _buildScaffold(
backgroundColor: context.conduitTheme.surfaceBackground, context,
appBar: AppBar( body: _buildProfileBody(context, ref, userData, api),
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, loading: () => _buildScaffold(
), context,
onPressed: () => Navigator.of(context).maybePop(), body: _buildCenteredState(
tooltip: AppLocalizations.of(context)!.back, context,
), ImprovedLoadingState(
// 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(
message: AppLocalizations.of(context)!.loadingProfile, message: AppLocalizations.of(context)!.loadingProfile,
), ),
), ),
), ),
error: (error, stack) => Scaffold( error: (error, stack) => _buildScaffold(
backgroundColor: context.conduitTheme.surfaceBackground, context,
appBar: AppBar( body: _buildCenteredState(
backgroundColor: context.conduitTheme.surfaceBackground, context,
elevation: Elevation.none, ImprovedEmptyState(
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(
title: AppLocalizations.of(context)!.unableToLoadProfile, title: AppLocalizations.of(context)!.unableToLoadProfile,
subtitle: AppLocalizations.of(context)!.pleaseCheckConnection, subtitle: AppLocalizations.of(context)!.pleaseCheckConnection,
icon: UiUtils.platformIcon( 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( Widget _buildProfileHeader(
BuildContext context, BuildContext context,
dynamic user, dynamic user,
@@ -212,23 +198,68 @@ class ProfilePage extends ConsumerWidget {
} }
final email = extractEmail(user) ?? 'No email'; final email = extractEmail(user) ?? 'No email';
final theme = context.conduitTheme;
final accent = theme.buttonPrimary;
return ConduitCard( return Container(
padding: const EdgeInsets.all(Spacing.cardPadding), 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: [ children: [
Container( Container(
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(AppBorderRadius.avatar), borderRadius: BorderRadius.circular(AppBorderRadius.avatar),
boxShadow: ConduitShadows.card, boxShadow: ConduitShadows.high,
), ),
child: UserAvatar( child: UserAvatar(
size: IconSize.avatar, size: IconSize.huge,
imageUrl: avatarUrl, imageUrl: avatarUrl,
fallbackText: initial, fallbackText: initial,
), ),
), ),
const SizedBox(width: Spacing.md), const SizedBox(width: Spacing.lg),
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@@ -236,33 +267,97 @@ class ProfilePage extends ConsumerWidget {
Text( Text(
displayName, displayName,
style: style:
context.conduitTheme.headingMedium?.copyWith( theme.headingMedium?.copyWith(
color: context.conduitTheme.textPrimary, color: theme.textPrimary,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w700,
) ?? ) ??
TextStyle( TextStyle(
color: context.conduitTheme.textPrimary, color: theme.textPrimary,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w700,
fontSize: 22,
), ),
), ),
const SizedBox(height: Spacing.sm), 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, email,
style: style:
context.conduitTheme.bodyMedium?.copyWith( theme.bodySmall?.copyWith(
color: context.conduitTheme.textSecondary, color: theme.textSecondary,
) ?? ) ??
TextStyle(color: context.conduitTheme.textSecondary), TextStyle(color: theme.textSecondary),
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
), ),
], ],
), ),
), ),
], ],
), ),
),
],
),
],
),
); );
} }
Widget _buildAccountSection(BuildContext context, WidgetRef ref) { 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( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@@ -272,106 +367,42 @@ class ProfilePage extends ConsumerWidget {
color: context.conduitTheme.textPrimary, color: context.conduitTheme.textPrimary,
), ),
), ),
const SizedBox(height: Spacing.md), const SizedBox(height: Spacing.sm),
ConduitCard( for (var i = 0; i < items.length; i++) ...[
padding: EdgeInsets.zero, items[i],
child: Column( if (i != items.length - 1) const SizedBox(height: Spacing.md),
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,
),
], ],
),
),
], ],
); );
} }
Widget _buildAccountOption({ Widget _buildAccountOption(
BuildContext context, {
required IconData icon, required IconData icon,
required String title, required String title,
required String subtitle, required String subtitle,
required VoidCallback onTap, required VoidCallback onTap,
bool isDestructive = false, bool isDestructive = false,
bool showChevron = true,
}) { }) {
return Builder( final theme = context.conduitTheme;
builder: (context) => ListTile( final color = isDestructive ? theme.error : theme.buttonPrimary;
contentPadding: const EdgeInsets.symmetric( return _ProfileSettingTile(
horizontal: Spacing.listItemPadding, onTap: onTap,
vertical: Spacing.sm, isDestructive: isDestructive,
), leading: _buildIconBadge(context, icon, color: color),
leading: Container( title: title,
padding: const EdgeInsets.all(Spacing.sm), subtitle: subtitle,
decoration: BoxDecoration( trailing: showChevron
color: isDestructive ? Icon(
? 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(
UiUtils.platformIcon( UiUtils.platformIcon(
ios: CupertinoIcons.chevron_right, ios: CupertinoIcons.chevron_right,
android: Icons.chevron_right, android: Icons.chevron_right,
), ),
color: context.conduitTheme.iconSecondary, color: theme.iconSecondary,
size: IconSize.small, size: IconSize.small,
), )
onTap: onTap, : null,
),
); );
} }
@@ -400,142 +431,125 @@ class ProfilePage extends ConsumerWidget {
? currentModel.name ? currentModel.name
: AppLocalizations.of(context)!.autoSelect; : AppLocalizations.of(context)!.autoSelect;
final theme = context.conduitTheme;
Widget leading; Widget leading;
if (selectedModelExplicit) { 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, size: 32,
imageUrl: modelIconUrl, imageUrl: modelIconUrl,
label: currentModel.name, label: currentModel.name,
),
); );
} else { } else {
leading = Container( leading = _buildIconBadge(
padding: const EdgeInsets.all(Spacing.sm), context,
decoration: BoxDecoration(
color: context.conduitTheme.buttonPrimary.withValues(
alpha: Alpha.highlight,
),
borderRadius: BorderRadius.circular(AppBorderRadius.small),
),
child: Icon(
UiUtils.platformIcon( UiUtils.platformIcon(
ios: CupertinoIcons.wand_stars, ios: CupertinoIcons.wand_stars,
android: Icons.auto_awesome, android: Icons.auto_awesome,
), ),
color: context.conduitTheme.buttonPrimary, color: theme.buttonPrimary,
size: IconSize.medium,
),
); );
} }
return ListTile( return _ProfileSettingTile(
contentPadding: const EdgeInsets.symmetric(
horizontal: Spacing.listItemPadding,
vertical: Spacing.sm,
),
leading: leading, leading: leading,
title: Text( title: AppLocalizations.of(context)!.defaultModel,
AppLocalizations.of(context)!.defaultModel, subtitle: modelLabel,
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,
),
onTap: () => _showModelSelector(context, ref, models), onTap: () => _showModelSelector(context, ref, models),
); );
}, },
loading: () => ListTile( loading: () => _ProfileSettingTile(
contentPadding: const EdgeInsets.symmetric( leading: _buildIconBadge(
horizontal: Spacing.listItemPadding, context,
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( UiUtils.platformIcon(
ios: CupertinoIcons.cube_box, ios: CupertinoIcons.cube_box,
android: Icons.psychology, android: Icons.psychology,
), ),
color: context.conduitTheme.buttonPrimary, color: context.conduitTheme.buttonPrimary,
size: IconSize.medium,
), ),
), title: AppLocalizations.of(context)!.defaultModel,
title: Text( subtitle: AppLocalizations.of(context)!.loadingModels,
AppLocalizations.of(context)!.defaultModel, showChevron: false,
style: context.conduitTheme.bodyLarge?.copyWith( trailing: SizedBox(
color: context.conduitTheme.textPrimary, width: 20,
fontWeight: FontWeight.w500, height: 20,
), child: CircularProgressIndicator(
), strokeWidth: 2,
subtitle: Text( valueColor: AlwaysStoppedAnimation<Color>(
AppLocalizations.of(context)!.loadingModels, context.conduitTheme.buttonPrimary,
style: context.conduitTheme.bodySmall?.copyWith(
color: context.conduitTheme.textSecondary,
), ),
), ),
), ),
error: (error, stack) => ListTile(
contentPadding: const EdgeInsets.symmetric(
horizontal: Spacing.listItemPadding,
vertical: Spacing.sm,
), ),
leading: Container( error: (error, stack) => _ProfileSettingTile(
padding: const EdgeInsets.all(Spacing.sm), leading: _buildIconBadge(
decoration: BoxDecoration( context,
color: context.conduitTheme.error.withValues(
alpha: Alpha.highlight,
),
borderRadius: BorderRadius.circular(AppBorderRadius.small),
),
child: Icon(
UiUtils.platformIcon( UiUtils.platformIcon(
ios: CupertinoIcons.exclamationmark_triangle, ios: CupertinoIcons.exclamationmark_triangle,
android: Icons.error_outline, android: Icons.error_outline,
), ),
color: context.conduitTheme.error, 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, 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. // Theme and language controls moved to AppCustomizationPage.
Widget _buildAboutTile(BuildContext context) { Widget _buildAboutTile(BuildContext context) {
return _buildAccountOption( return _buildAccountOption(
context,
icon: UiUtils.platformIcon( icon: UiUtils.platformIcon(
ios: CupertinoIcons.info, ios: CupertinoIcons.info,
android: Icons.info_outline, 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 { class _DefaultModelBottomSheet extends ConsumerStatefulWidget {
final List<Model> models; final List<Model> models;
final String? currentDefaultModelId; final String? currentDefaultModelId;

View File

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

View File

@@ -532,6 +532,8 @@
"@appCustomization": {"description": "Title of the customization settings page."}, "@appCustomization": {"description": "Title of the customization settings page."},
"appCustomizationSubtitle": "Personalize how names and UI display", "appCustomizationSubtitle": "Personalize how names and UI display",
"@appCustomizationSubtitle": {"description": "Subtitle shown under App Customization tile and page header."}, "@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": "Display",
"@display": {"description": "Section header for visual and layout related settings."}, "@display": {"description": "Section header for visual and layout related settings."},
"realtime": "Realtime", "realtime": "Realtime",

View File

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

View File

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

View File

@@ -1506,6 +1506,12 @@ abstract class AppLocalizations {
/// **'Personalize how names and UI display'** /// **'Personalize how names and UI display'**
String get appCustomizationSubtitle; 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. /// Section header for visual and layout related settings.
/// ///
/// In en, this message translates to: /// In en, this message translates to:

View File

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

View File

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

View File

@@ -792,6 +792,10 @@ class AppLocalizationsFr extends AppLocalizations {
String get appCustomizationSubtitle => String get appCustomizationSubtitle =>
'Personnalisez l\'affichage des noms et de l\'UI'; '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 @override
String get display => 'Affichage'; String get display => 'Affichage';

View File

@@ -781,6 +781,10 @@ class AppLocalizationsIt extends AppLocalizations {
String get appCustomizationSubtitle => String get appCustomizationSubtitle =>
'Personalizza la visualizzazione dei nomi e dell\'UI'; '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 @override
String get display => 'Schermo'; String get display => 'Schermo';