refactor: settings pages
This commit is contained in:
@@ -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,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user