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(
|
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,
|
|
||||||
),
|
|
||||||
onPressed: () => Navigator.of(context).maybePop(),
|
|
||||||
tooltip: AppLocalizations.of(context)!.back,
|
|
||||||
),
|
|
||||||
// keep reduced height only once
|
|
||||||
titleSpacing: 0.0,
|
|
||||||
title: Text(
|
|
||||||
AppLocalizations.of(context)!.you,
|
|
||||||
style: AppTypography.headlineSmallStyle.copyWith(
|
|
||||||
color: context.conduitTheme.textPrimary,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
centerTitle: true,
|
|
||||||
),
|
|
||||||
body: SingleChildScrollView(
|
|
||||||
padding: const EdgeInsets.all(Spacing.pagePadding),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
// Profile Header - Enhanced with better spacing and animations
|
|
||||||
_buildProfileHeader(context, userData, api)
|
|
||||||
.animate()
|
|
||||||
.fadeIn(duration: AnimationDuration.pageTransition)
|
|
||||||
.slideY(
|
|
||||||
begin: 0.1,
|
|
||||||
end: 0,
|
|
||||||
curve: AnimationCurves.pageTransition,
|
|
||||||
),
|
|
||||||
const SizedBox(height: Spacing.sectionGap),
|
|
||||||
|
|
||||||
// Account Section - Enhanced with improved spacing
|
|
||||||
_buildAccountSection(context, ref)
|
|
||||||
.animate()
|
|
||||||
.fadeIn(
|
|
||||||
delay: AnimationDelay.short,
|
|
||||||
duration: AnimationDuration.pageTransition,
|
|
||||||
)
|
|
||||||
.slideY(
|
|
||||||
begin: 0.1,
|
|
||||||
end: 0,
|
|
||||||
curve: AnimationCurves.pageTransition,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
loading: () => Scaffold(
|
loading: () => _buildScaffold(
|
||||||
backgroundColor: context.conduitTheme.surfaceBackground,
|
context,
|
||||||
appBar: AppBar(
|
body: _buildCenteredState(
|
||||||
backgroundColor: context.conduitTheme.surfaceBackground,
|
context,
|
||||||
elevation: Elevation.none,
|
ImprovedLoadingState(
|
||||||
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,50 +198,128 @@ 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: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
decoration: BoxDecoration(
|
padding: const EdgeInsets.symmetric(
|
||||||
borderRadius: BorderRadius.circular(AppBorderRadius.avatar),
|
horizontal: Spacing.sm,
|
||||||
boxShadow: ConduitShadows.card,
|
vertical: Spacing.xs,
|
||||||
),
|
),
|
||||||
child: UserAvatar(
|
decoration: BoxDecoration(
|
||||||
size: IconSize.avatar,
|
color: theme.surfaceBackground.withValues(alpha: 0.7),
|
||||||
imageUrl: avatarUrl,
|
borderRadius: BorderRadius.circular(AppBorderRadius.pill),
|
||||||
fallbackText: initial,
|
),
|
||||||
|
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),
|
const SizedBox(height: Spacing.lg),
|
||||||
Expanded(
|
Row(
|
||||||
child: Column(
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
children: [
|
||||||
children: [
|
Container(
|
||||||
Text(
|
decoration: BoxDecoration(
|
||||||
displayName,
|
borderRadius: BorderRadius.circular(AppBorderRadius.avatar),
|
||||||
style:
|
boxShadow: ConduitShadows.high,
|
||||||
context.conduitTheme.headingMedium?.copyWith(
|
),
|
||||||
color: context.conduitTheme.textPrimary,
|
child: UserAvatar(
|
||||||
fontWeight: FontWeight.w600,
|
size: IconSize.huge,
|
||||||
) ??
|
imageUrl: avatarUrl,
|
||||||
TextStyle(
|
fallbackText: initial,
|
||||||
color: context.conduitTheme.textPrimary,
|
),
|
||||||
fontWeight: FontWeight.w600,
|
),
|
||||||
|
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) {
|
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)
|
UiUtils.platformIcon(
|
||||||
: context.conduitTheme.buttonPrimary.withValues(
|
ios: CupertinoIcons.chevron_right,
|
||||||
alpha: Alpha.highlight,
|
android: Icons.chevron_right,
|
||||||
),
|
),
|
||||||
borderRadius: BorderRadius.circular(AppBorderRadius.small),
|
color: theme.iconSecondary,
|
||||||
),
|
size: IconSize.small,
|
||||||
child: Icon(
|
)
|
||||||
icon,
|
: null,
|
||||||
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,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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(
|
||||||
size: 32,
|
width: 48,
|
||||||
imageUrl: modelIconUrl,
|
height: 48,
|
||||||
label: currentModel.name,
|
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 {
|
} else {
|
||||||
leading = Container(
|
leading = _buildIconBadge(
|
||||||
padding: const EdgeInsets.all(Spacing.sm),
|
context,
|
||||||
decoration: BoxDecoration(
|
UiUtils.platformIcon(
|
||||||
color: context.conduitTheme.buttonPrimary.withValues(
|
ios: CupertinoIcons.wand_stars,
|
||||||
alpha: Alpha.highlight,
|
android: Icons.auto_awesome,
|
||||||
),
|
|
||||||
borderRadius: BorderRadius.circular(AppBorderRadius.small),
|
|
||||||
),
|
|
||||||
child: Icon(
|
|
||||||
UiUtils.platformIcon(
|
|
||||||
ios: CupertinoIcons.wand_stars,
|
|
||||||
android: Icons.auto_awesome,
|
|
||||||
),
|
|
||||||
color: context.conduitTheme.buttonPrimary,
|
|
||||||
size: IconSize.medium,
|
|
||||||
),
|
),
|
||||||
|
color: theme.buttonPrimary,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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,
|
UiUtils.platformIcon(
|
||||||
|
ios: CupertinoIcons.cube_box,
|
||||||
|
android: Icons.psychology,
|
||||||
|
),
|
||||||
|
color: context.conduitTheme.buttonPrimary,
|
||||||
),
|
),
|
||||||
leading: Container(
|
title: AppLocalizations.of(context)!.defaultModel,
|
||||||
padding: const EdgeInsets.all(Spacing.sm),
|
subtitle: AppLocalizations.of(context)!.loadingModels,
|
||||||
decoration: BoxDecoration(
|
showChevron: false,
|
||||||
color: context.conduitTheme.buttonPrimary.withValues(
|
trailing: SizedBox(
|
||||||
alpha: Alpha.highlight,
|
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(
|
error: (error, stack) => _ProfileSettingTile(
|
||||||
contentPadding: const EdgeInsets.symmetric(
|
leading: _buildIconBadge(
|
||||||
horizontal: Spacing.listItemPadding,
|
context,
|
||||||
vertical: Spacing.sm,
|
UiUtils.platformIcon(
|
||||||
),
|
ios: CupertinoIcons.exclamationmark_triangle,
|
||||||
leading: Container(
|
android: Icons.error_outline,
|
||||||
padding: const EdgeInsets.all(Spacing.sm),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: context.conduitTheme.error.withValues(
|
|
||||||
alpha: Alpha.highlight,
|
|
||||||
),
|
|
||||||
borderRadius: BorderRadius.circular(AppBorderRadius.small),
|
|
||||||
),
|
),
|
||||||
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(
|
UiUtils.platformIcon(
|
||||||
ios: CupertinoIcons.exclamationmark_triangle,
|
ios: CupertinoIcons.refresh,
|
||||||
android: Icons.error_outline,
|
android: Icons.refresh,
|
||||||
),
|
),
|
||||||
color: context.conduitTheme.error,
|
color: context.conduitTheme.error,
|
||||||
size: IconSize.medium,
|
size: IconSize.small,
|
||||||
),
|
|
||||||
),
|
|
||||||
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,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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