refactor: settings pages
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -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<Color>(
|
||||
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<Model> models;
|
||||
final String? currentDefaultModelId;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user