diff --git a/lib/features/profile/views/app_customization_page.dart b/lib/features/profile/views/app_customization_page.dart index bea2151..b3b6fe9 100644 --- a/lib/features/profile/views/app_customization_page.dart +++ b/lib/features/profile/views/app_customization_page.dart @@ -34,566 +34,470 @@ 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( - backgroundColor: context.conduitTheme.surfaceBackground, - elevation: Elevation.none, - toolbarHeight: kToolbarHeight - 8, - leading: IconButton( - icon: Icon( - UiUtils.platformIcon( - ios: CupertinoIcons.back, - android: Icons.arrow_back, - ), - color: context.conduitTheme.textPrimary, + appBar: _buildAppBar(context), + body: SafeArea( + child: ListView( + physics: const BouncingScrollPhysics( + parent: AlwaysScrollableScrollPhysics(), ), - onPressed: () => Navigator.of(context).maybePop(), - tooltip: AppLocalizations.of(context)!.back, - ), - title: Text( - AppLocalizations.of(context)!.appCustomization, - style: AppTypography.headlineSmallStyle.copyWith( - color: context.conduitTheme.textPrimary, - fontWeight: FontWeight.w600, + padding: const EdgeInsets.symmetric( + horizontal: Spacing.pagePadding, + vertical: Spacing.pagePadding, ), - ), - centerTitle: true, - ), - body: SingleChildScrollView( - padding: const EdgeInsets.all(Spacing.pagePadding), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - AppLocalizations.of(context)!.display, - style: context.conduitTheme.headingSmall?.copyWith( - color: context.conduitTheme.textPrimary, - ), - ), - 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( - 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( - 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, - ), - onTap: () async { - final selected = await _showLanguageSelector( - context, - currentCode, - ); - if (selected != null) { - if (selected == 'system') { - 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, - ), - // 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( - context, - )!.hideProviderInModelNamesDescription, - style: context.conduitTheme.bodySmall?.copyWith( - color: context.conduitTheme.textSecondary, - ), - ), - 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, - ), - borderRadius: BorderRadius.circular( - AppBorderRadius.small, - ), - ), - child: Icon( - Platform.isIOS - ? CupertinoIcons.textformat - : Icons.text_fields, - color: context.conduitTheme.buttonPrimary, - size: IconSize.medium, - ), - ), - ), - ], - ), - ), - - 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, _) { - final selectedRaw = ref.watch( - appSettingsProvider.select((s) => s.quickPills), - ); - final toolsAsync = ref.watch(toolsListProvider); - final tools = toolsAsync.maybeWhen( - data: (t) => t, - orElse: () => const [], - ); - final allowed = { - 'web', - 'image', - ...tools.map((t) => t.id), - }; - // Sanitize persisted selection - 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), - ); - } - final int selectedCount = selected.length; - - void toggle(String id) async { - final current = List.from(selected); - if (current.contains(id)) { - current.remove(id); - } else { - if (current.length >= 2) return; // enforce max 2 - current.add(id); - } - await ref - .read(appSettingsProvider.notifier) - .setQuickPills(current); - } - - // Build dynamic tool chips list once - final List dynamicToolChips = ref - .watch(toolsListProvider) - .maybeWhen>( - data: (tools) => tools.map((Tool t) { - final isSel = selected.contains(t.id); - final canSelect = selectedCount < 2 || isSel; - return ConduitChip( - label: t.name, - icon: Icons.extension, - isSelected: isSel, - onTap: canSelect ? () => toggle(t.id) : null, - ); - }).toList(), - orElse: () => const [], - ); - - return ConduitCard( - padding: const EdgeInsets.symmetric( - horizontal: Spacing.listItemPadding, - vertical: Spacing.sm, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Expanded( - child: Text( - AppLocalizations.of( - context, - )!.appCustomizationSubtitle, - style: context.conduitTheme.bodySmall?.copyWith( - color: context.conduitTheme.textSecondary, - ), - ), - ), - TextButton( - onPressed: selected.isEmpty - ? null - : () async { - await ref - .read(appSettingsProvider.notifier) - .setQuickPills(const []); - }, - child: Text(AppLocalizations.of(context)!.clear), - ), - ], - ), - const SizedBox(height: Spacing.sm), - Wrap( - spacing: Spacing.sm, - runSpacing: Spacing.sm, - children: [ - ConduitChip( - label: AppLocalizations.of(context)!.web, - icon: Platform.isIOS - ? CupertinoIcons.search - : Icons.search, - isSelected: 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, - isSelected: selected.contains('image'), - onTap: - (selectedCount < 2 || - selected.contains('image')) - ? () => toggle('image') - : null, - ), - // Dynamic tools from server - ...dynamicToolChips, - ], - ), - ], - ), - ); - }, - ), - - 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( - crossAxisAlignment: CrossAxisAlignment.start, - 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.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( - initialValue: settings.socketTransportMode, - onChanged: (v) async { - if (v == null) return; - await 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, - ), - ), - ), - ], - ), + _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, + 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)!.appCustomization, + style: AppTypography.headlineSmallStyle.copyWith( + color: context.conduitTheme.textPrimary, + fontWeight: FontWeight.w600, + ), + ), + centerTitle: true, + ); + } + + 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: + 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), + _CustomizationTile( + leading: _buildIconBadge( + context, + UiUtils.platformIcon( + ios: CupertinoIcons.globe, + android: Icons.language, + ), + color: theme.buttonPrimary, + ), + title: AppLocalizations.of(context)!.appLanguage, + subtitle: languageLabel, + onTap: () async { + final selected = await _showLanguageSelector( + context, + currentLanguageCode, + ); + if (selected == null) return; + if (selected == 'system') { + await ref.read(localeProvider.notifier).setLocale(null); + } else { + await ref + .read(localeProvider.notifier) + .setLocale(Locale(selected)); + } + }, + ), + const SizedBox(height: Spacing.md), + _CustomizationTile( + leading: _buildIconBadge( + context, + Platform.isIOS ? CupertinoIcons.textformat : Icons.text_fields, + color: theme.buttonPrimary, + ), + title: AppLocalizations.of(context)!.hideProviderInModelNames, + subtitle: AppLocalizations.of( + context, + )!.hideProviderInModelNamesDescription, + trailing: Switch.adaptive( + value: settings.omitProviderInModelName, + onChanged: (v) => ref + .read(appSettingsProvider.notifier) + .setOmitProviderInModelName(v), + ), + 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.md), + 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: (value) => value, + orElse: () => const [], + ); + final allowed = {'web', 'image', ...tools.map((t) => t.id)}; + + final selected = selectedRaw + .where((id) => allowed.contains(id)) + .take(2) + .toList(); + if (selected.length != selectedRaw.length) { + Future.microtask( + () => ref.read(appSettingsProvider.notifier).setQuickPills(selected), + ); + } + + final selectedCount = selected.length; + + Future toggle(String id) async { + final next = List.from(selected); + if (next.contains(id)) { + next.remove(id); + } else { + if (next.length >= 2) return; + next.add(id); + } + await ref.read(appSettingsProvider.notifier).setQuickPills(next); + } + + List buildToolChips() { + return tools.map((tool) { + final isSelected = selected.contains(tool.id); + final canSelect = selectedCount < 2 || isSelected; + return ConduitChip( + label: tool.name, + icon: Icons.extension, + isSelected: isSelected, + onTap: canSelect ? () => toggle(tool.id) : null, + ); + }).toList(); + } + + 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)!.quickActionsDescription, + style: + theme.bodySmall?.copyWith( + color: theme.textSecondary, + ) ?? + TextStyle(color: theme.textSecondary), + ), + ), + TextButton( + onPressed: selected.isEmpty + ? null + : () => ref + .read(appSettingsProvider.notifier) + .setQuickPills(const []), + child: Text(AppLocalizations.of(context)!.clear), + ), + ], + ), + 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, + isSelected: 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, + isSelected: selected.contains('image'), + onTap: (selectedCount < 2 || selected.contains('image')) + ? () => toggle('image') + : null, + ), + ...buildToolChips(), + ], + ), + ], + ), + ), + ], + ); + } + + Widget _buildChatSection( + BuildContext context, + WidgetRef ref, + AppSettings settings, + ) { + final theme = context.conduitTheme; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Chat', + style: + theme.headingSmall?.copyWith(color: theme.textPrimary) ?? + TextStyle(color: theme.textPrimary, fontSize: 18), + ), + const SizedBox(height: Spacing.sm), + _CustomizationTile( + leading: _buildIconBadge( + context, + Platform.isIOS ? CupertinoIcons.paperplane : Icons.keyboard_return, + color: theme.buttonPrimary, + ), + 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), + ), + showChevron: false, + onTap: () => ref + .read(appSettingsProvider.notifier) + .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), + ); + } + Future _showLanguageSelector(BuildContext context, String current) { return showModalBottomSheet( context: context, @@ -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, + ), + ], + ], + ), + ); + } +} diff --git a/lib/features/profile/views/profile_page.dart b/lib/features/profile/views/profile_page.dart index 9bae9d2..d559178 100644 --- a/lib/features/profile/views/profile_page.dart +++ b/lib/features/profile/views/profile_page.dart @@ -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, - ), - 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, - ), - ], - ), - ), + data: (userData) => _buildScaffold( + context, + body: _buildProfileBody(context, ref, userData, api), ), - 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,50 +198,128 @@ 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( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(AppBorderRadius.avatar), - boxShadow: ConduitShadows.card, + padding: const EdgeInsets.symmetric( + horizontal: Spacing.sm, + vertical: Spacing.xs, ), - child: UserAvatar( - size: IconSize.avatar, - imageUrl: avatarUrl, - fallbackText: initial, + 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(width: Spacing.md), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - displayName, - style: - context.conduitTheme.headingMedium?.copyWith( - color: context.conduitTheme.textPrimary, - fontWeight: FontWeight.w600, - ) ?? - TextStyle( - color: context.conduitTheme.textPrimary, - fontWeight: FontWeight.w600, + const SizedBox(height: Spacing.lg), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(AppBorderRadius.avatar), + boxShadow: ConduitShadows.high, + ), + child: UserAvatar( + size: IconSize.huge, + imageUrl: avatarUrl, + fallbackText: initial, + ), + ), + const SizedBox(width: Spacing.lg), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + displayName, + style: + theme.headingMedium?.copyWith( + color: theme.textPrimary, + fontWeight: FontWeight.w700, + ) ?? + TextStyle( + color: theme.textPrimary, + fontWeight: FontWeight.w700, + fontSize: 22, + ), + ), + const SizedBox(height: Spacing.sm), + 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: + theme.bodySmall?.copyWith( + color: theme.textSecondary, + ) ?? + TextStyle(color: theme.textSecondary), + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + ), + ], + ), + ), + ], ), - const SizedBox(height: Spacing.sm), - Text( - email, - style: - context.conduitTheme.bodyMedium?.copyWith( - color: context.conduitTheme.textSecondary, - ) ?? - TextStyle(color: context.conduitTheme.textSecondary), - ), - ], - ), + ), + ], ), ], ), @@ -263,6 +327,37 @@ class ProfilePage extends ConsumerWidget { } 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( - UiUtils.platformIcon( - ios: CupertinoIcons.chevron_right, - android: Icons.chevron_right, - ), - color: context.conduitTheme.iconSecondary, - size: IconSize.small, - ), - onTap: onTap, - ), + 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: theme.iconSecondary, + size: IconSize.small, + ) + : 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( - size: 32, - imageUrl: modelIconUrl, - label: currentModel.name, + 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( - UiUtils.platformIcon( - ios: CupertinoIcons.wand_stars, - android: Icons.auto_awesome, - ), - color: context.conduitTheme.buttonPrimary, - size: IconSize.medium, + leading = _buildIconBadge( + context, + UiUtils.platformIcon( + ios: CupertinoIcons.wand_stars, + android: Icons.auto_awesome, ), + 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, + loading: () => _ProfileSettingTile( + leading: _buildIconBadge( + context, + UiUtils.platformIcon( + ios: CupertinoIcons.cube_box, + android: Icons.psychology, + ), + color: context.conduitTheme.buttonPrimary, ), - leading: Container( - padding: const EdgeInsets.all(Spacing.sm), - decoration: BoxDecoration( - color: context.conduitTheme.buttonPrimary.withValues( - alpha: Alpha.highlight, + title: AppLocalizations.of(context)!.defaultModel, + subtitle: AppLocalizations.of(context)!.loadingModels, + showChevron: false, + trailing: SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation( + context.conduitTheme.buttonPrimary, ), - borderRadius: BorderRadius.circular(AppBorderRadius.small), - ), - child: Icon( - 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, ), ), ), - 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), + error: (error, stack) => _ProfileSettingTile( + leading: _buildIconBadge( + context, + UiUtils.platformIcon( + ios: CupertinoIcons.exclamationmark_triangle, + android: Icons.error_outline, ), - child: Icon( + color: context.conduitTheme.error, + ), + 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.exclamationmark_triangle, - android: Icons.error_outline, + ios: CupertinoIcons.refresh, + android: Icons.refresh, ), color: context.conduitTheme.error, - 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)!.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 models; final String? currentDefaultModelId; diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 336c0c9..b1304c4 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -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", diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index b6dc61f..b739f7a 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -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", diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 51da75a..a938ff2 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -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", diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index 5858287..e9973b9 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -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", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 5094099..cbaaf33 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -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: diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index b3c8359..1bbf1f2 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -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'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 10677d4..1b5d4b6 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -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'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 6be1774..e46a011 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -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'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index 3abe97b..3b8b8bd 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -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';