refactor: settings pages
This commit is contained in:
@@ -34,24 +34,63 @@ class AppCustomizationPage extends ConsumerWidget {
|
||||
return AppLocalizations.of(context)!.currentlyUsingLightTheme;
|
||||
}();
|
||||
final locale = ref.watch(localeProvider);
|
||||
final currentLanguageCode = locale?.languageCode ?? 'system';
|
||||
final languageLabel = _resolveLanguageLabel(context, currentLanguageCode);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: context.conduitTheme.surfaceBackground,
|
||||
appBar: AppBar(
|
||||
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,
|
||||
surfaceTintColor: Colors.transparent,
|
||||
elevation: Elevation.none,
|
||||
toolbarHeight: kToolbarHeight - 8,
|
||||
leading: IconButton(
|
||||
toolbarHeight: kToolbarHeight,
|
||||
automaticallyImplyLeading: false,
|
||||
leading: canPop
|
||||
? IconButton(
|
||||
icon: Icon(
|
||||
UiUtils.platformIcon(
|
||||
ios: CupertinoIcons.back,
|
||||
android: Icons.arrow_back,
|
||||
),
|
||||
color: context.conduitTheme.textPrimary,
|
||||
color: context.conduitTheme.iconPrimary,
|
||||
),
|
||||
onPressed: () => Navigator.of(context).maybePop(),
|
||||
tooltip: AppLocalizations.of(context)!.back,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
titleSpacing: 0,
|
||||
title: Text(
|
||||
AppLocalizations.of(context)!.appCustomization,
|
||||
style: AppTypography.headlineSmallStyle.copyWith(
|
||||
@@ -60,537 +99,402 @@ class AppCustomizationPage extends ConsumerWidget {
|
||||
),
|
||||
),
|
||||
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,
|
||||
children: [
|
||||
Text(
|
||||
AppLocalizations.of(context)!.display,
|
||||
style: context.conduitTheme.headingSmall?.copyWith(
|
||||
color: context.conduitTheme.textPrimary,
|
||||
),
|
||||
style:
|
||||
theme.headingSmall?.copyWith(color: theme.textPrimary) ??
|
||||
TextStyle(color: theme.textPrimary, fontSize: 18),
|
||||
),
|
||||
const SizedBox(height: Spacing.sm),
|
||||
_buildThemeSelector(context, ref, themeMode, themeDescription),
|
||||
const SizedBox(height: Spacing.md),
|
||||
ConduitCard(
|
||||
padding: EdgeInsets.zero,
|
||||
child: Column(
|
||||
children: [
|
||||
// Theme mode dropdown
|
||||
ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: Spacing.listItemPadding,
|
||||
vertical: Spacing.sm,
|
||||
),
|
||||
leading: Container(
|
||||
padding: const EdgeInsets.all(Spacing.sm),
|
||||
decoration: BoxDecoration(
|
||||
color: context.conduitTheme.buttonPrimary.withValues(
|
||||
alpha: Alpha.highlight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(
|
||||
AppBorderRadius.small,
|
||||
),
|
||||
),
|
||||
child: Icon(
|
||||
UiUtils.platformIcon(
|
||||
ios: CupertinoIcons.moon_stars,
|
||||
android: Icons.dark_mode,
|
||||
),
|
||||
color: context.conduitTheme.buttonPrimary,
|
||||
size: IconSize.medium,
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
AppLocalizations.of(context)!.darkMode,
|
||||
style: context.conduitTheme.bodyLarge?.copyWith(
|
||||
color: context.conduitTheme.textPrimary,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
themeDescription,
|
||||
style: context.conduitTheme.bodySmall?.copyWith(
|
||||
color: context.conduitTheme.textSecondary,
|
||||
),
|
||||
),
|
||||
trailing: DropdownButtonHideUnderline(
|
||||
child: DropdownButton<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(
|
||||
_CustomizationTile(
|
||||
leading: _buildIconBadge(
|
||||
context,
|
||||
UiUtils.platformIcon(
|
||||
ios: CupertinoIcons.globe,
|
||||
android: Icons.language,
|
||||
),
|
||||
color: context.conduitTheme.buttonPrimary,
|
||||
size: IconSize.medium,
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
AppLocalizations.of(context)!.appLanguage,
|
||||
style: context.conduitTheme.bodyLarge?.copyWith(
|
||||
color: context.conduitTheme.textPrimary,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
label,
|
||||
style: context.conduitTheme.bodySmall?.copyWith(
|
||||
color: context.conduitTheme.textSecondary,
|
||||
),
|
||||
),
|
||||
trailing: Icon(
|
||||
UiUtils.platformIcon(
|
||||
ios: CupertinoIcons.chevron_right,
|
||||
android: Icons.chevron_right,
|
||||
),
|
||||
color: context.conduitTheme.iconSecondary,
|
||||
size: IconSize.small,
|
||||
color: theme.buttonPrimary,
|
||||
),
|
||||
title: AppLocalizations.of(context)!.appLanguage,
|
||||
subtitle: languageLabel,
|
||||
onTap: () async {
|
||||
final selected = await _showLanguageSelector(
|
||||
context,
|
||||
currentCode,
|
||||
currentLanguageCode,
|
||||
);
|
||||
if (selected != null) {
|
||||
if (selected == null) return;
|
||||
if (selected == 'system') {
|
||||
await ref
|
||||
.read(localeProvider.notifier)
|
||||
.setLocale(null);
|
||||
await ref.read(localeProvider.notifier).setLocale(null);
|
||||
} else {
|
||||
await ref
|
||||
.read(localeProvider.notifier)
|
||||
.setLocale(Locale(selected));
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
Divider(color: context.conduitTheme.dividerColor, height: 1),
|
||||
|
||||
SwitchListTile.adaptive(
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: Spacing.listItemPadding,
|
||||
vertical: Spacing.sm,
|
||||
const SizedBox(height: Spacing.md),
|
||||
_CustomizationTile(
|
||||
leading: _buildIconBadge(
|
||||
context,
|
||||
Platform.isIOS ? CupertinoIcons.textformat : Icons.text_fields,
|
||||
color: theme.buttonPrimary,
|
||||
),
|
||||
// Use platform defaults for switch colors to match theme
|
||||
value: settings.omitProviderInModelName,
|
||||
title: Text(
|
||||
AppLocalizations.of(context)!.hideProviderInModelNames,
|
||||
style: context.conduitTheme.bodyLarge?.copyWith(
|
||||
color: context.conduitTheme.textPrimary,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
AppLocalizations.of(
|
||||
title: AppLocalizations.of(context)!.hideProviderInModelNames,
|
||||
subtitle: AppLocalizations.of(
|
||||
context,
|
||||
)!.hideProviderInModelNamesDescription,
|
||||
style: context.conduitTheme.bodySmall?.copyWith(
|
||||
color: context.conduitTheme.textSecondary,
|
||||
),
|
||||
),
|
||||
onChanged: (v) {
|
||||
ref
|
||||
trailing: Switch.adaptive(
|
||||
value: settings.omitProviderInModelName,
|
||||
onChanged: (v) => ref
|
||||
.read(appSettingsProvider.notifier)
|
||||
.setOmitProviderInModelName(v);
|
||||
},
|
||||
secondary: Container(
|
||||
padding: const EdgeInsets.all(Spacing.sm),
|
||||
decoration: BoxDecoration(
|
||||
color: context.conduitTheme.buttonPrimary.withValues(
|
||||
alpha: Alpha.highlight,
|
||||
.setOmitProviderInModelName(v),
|
||||
),
|
||||
borderRadius: BorderRadius.circular(
|
||||
AppBorderRadius.small,
|
||||
),
|
||||
),
|
||||
child: Icon(
|
||||
Platform.isIOS
|
||||
? CupertinoIcons.textformat
|
||||
: Icons.text_fields,
|
||||
color: context.conduitTheme.buttonPrimary,
|
||||
size: IconSize.medium,
|
||||
showChevron: false,
|
||||
onTap: () => ref
|
||||
.read(appSettingsProvider.notifier)
|
||||
.setOmitProviderInModelName(!settings.omitProviderInModelName),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildThemeSelector(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
ThemeMode themeMode,
|
||||
String themeDescription,
|
||||
) {
|
||||
final theme = context.conduitTheme;
|
||||
|
||||
return ConduitCard(
|
||||
padding: const EdgeInsets.all(Spacing.cardPadding),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildIconBadge(
|
||||
context,
|
||||
UiUtils.platformIcon(
|
||||
ios: CupertinoIcons.moon_stars,
|
||||
android: Icons.dark_mode,
|
||||
),
|
||||
color: theme.buttonPrimary,
|
||||
),
|
||||
const SizedBox(width: Spacing.md),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
AppLocalizations.of(context)!.darkMode,
|
||||
style:
|
||||
theme.bodyLarge?.copyWith(
|
||||
color: theme.textPrimary,
|
||||
fontWeight: FontWeight.w600,
|
||||
) ??
|
||||
TextStyle(
|
||||
color: theme.textPrimary,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: Spacing.textSpacing),
|
||||
Text(
|
||||
themeDescription,
|
||||
style:
|
||||
theme.bodySmall?.copyWith(
|
||||
color: theme.textSecondary,
|
||||
) ??
|
||||
TextStyle(color: theme.textSecondary),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: Spacing.lg),
|
||||
// Quick pills (Web / Image Gen)
|
||||
Text(
|
||||
AppLocalizations.of(context)!.onboardQuickTitle,
|
||||
style: context.conduitTheme.headingSmall?.copyWith(
|
||||
color: context.conduitTheme.textPrimary,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: Spacing.md),
|
||||
Consumer(
|
||||
builder: (context, ref, _) {
|
||||
Wrap(
|
||||
spacing: Spacing.sm,
|
||||
runSpacing: Spacing.sm,
|
||||
children: [
|
||||
_buildThemeChip(
|
||||
context,
|
||||
ref,
|
||||
mode: ThemeMode.system,
|
||||
isSelected: themeMode == ThemeMode.system,
|
||||
label: AppLocalizations.of(context)!.system,
|
||||
icon: UiUtils.platformIcon(
|
||||
ios: CupertinoIcons.sparkles,
|
||||
android: Icons.auto_mode,
|
||||
),
|
||||
),
|
||||
_buildThemeChip(
|
||||
context,
|
||||
ref,
|
||||
mode: ThemeMode.light,
|
||||
isSelected: themeMode == ThemeMode.light,
|
||||
label: AppLocalizations.of(context)!.themeLight,
|
||||
icon: UiUtils.platformIcon(
|
||||
ios: CupertinoIcons.sun_max,
|
||||
android: Icons.light_mode,
|
||||
),
|
||||
),
|
||||
_buildThemeChip(
|
||||
context,
|
||||
ref,
|
||||
mode: ThemeMode.dark,
|
||||
isSelected: themeMode == ThemeMode.dark,
|
||||
label: AppLocalizations.of(context)!.themeDark,
|
||||
icon: UiUtils.platformIcon(
|
||||
ios: CupertinoIcons.moon_fill,
|
||||
android: Icons.dark_mode,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildThemeChip(
|
||||
BuildContext context,
|
||||
WidgetRef ref, {
|
||||
required ThemeMode mode,
|
||||
required bool isSelected,
|
||||
required String label,
|
||||
required IconData icon,
|
||||
}) {
|
||||
return ConduitChip(
|
||||
label: label,
|
||||
icon: icon,
|
||||
isSelected: isSelected,
|
||||
onTap: () => ref.read(themeModeProvider.notifier).setTheme(mode),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildQuickPillsSection(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
AppSettings settings,
|
||||
) {
|
||||
final theme = context.conduitTheme;
|
||||
final selectedRaw = ref.watch(
|
||||
appSettingsProvider.select((s) => s.quickPills),
|
||||
);
|
||||
final toolsAsync = ref.watch(toolsListProvider);
|
||||
final tools = toolsAsync.maybeWhen(
|
||||
data: (t) => t,
|
||||
data: (value) => value,
|
||||
orElse: () => const <Tool>[],
|
||||
);
|
||||
final allowed = <String>{
|
||||
'web',
|
||||
'image',
|
||||
...tools.map((t) => t.id),
|
||||
};
|
||||
// Sanitize persisted selection
|
||||
final allowed = <String>{'web', 'image', ...tools.map((t) => t.id)};
|
||||
|
||||
final selected = selectedRaw
|
||||
.where((id) => allowed.contains(id))
|
||||
.take(2)
|
||||
.toList();
|
||||
if (selected.length != selectedRaw.length) {
|
||||
// Persist sanitized list asynchronously
|
||||
Future.microtask(
|
||||
() => ref
|
||||
.read(appSettingsProvider.notifier)
|
||||
.setQuickPills(selected),
|
||||
() => ref.read(appSettingsProvider.notifier).setQuickPills(selected),
|
||||
);
|
||||
}
|
||||
final int selectedCount = selected.length;
|
||||
|
||||
void toggle(String id) async {
|
||||
final current = List<String>.from(selected);
|
||||
if (current.contains(id)) {
|
||||
current.remove(id);
|
||||
final selectedCount = selected.length;
|
||||
|
||||
Future<void> toggle(String id) async {
|
||||
final next = List<String>.from(selected);
|
||||
if (next.contains(id)) {
|
||||
next.remove(id);
|
||||
} else {
|
||||
if (current.length >= 2) return; // enforce max 2
|
||||
current.add(id);
|
||||
if (next.length >= 2) return;
|
||||
next.add(id);
|
||||
}
|
||||
await ref
|
||||
.read(appSettingsProvider.notifier)
|
||||
.setQuickPills(current);
|
||||
await ref.read(appSettingsProvider.notifier).setQuickPills(next);
|
||||
}
|
||||
|
||||
// Build dynamic tool chips list once
|
||||
final List<Widget> dynamicToolChips = ref
|
||||
.watch(toolsListProvider)
|
||||
.maybeWhen<List<Widget>>(
|
||||
data: (tools) => tools.map((Tool t) {
|
||||
final isSel = selected.contains(t.id);
|
||||
final canSelect = selectedCount < 2 || isSel;
|
||||
List<Widget> buildToolChips() {
|
||||
return tools.map((tool) {
|
||||
final isSelected = selected.contains(tool.id);
|
||||
final canSelect = selectedCount < 2 || isSelected;
|
||||
return ConduitChip(
|
||||
label: t.name,
|
||||
label: tool.name,
|
||||
icon: Icons.extension,
|
||||
isSelected: isSel,
|
||||
onTap: canSelect ? () => toggle(t.id) : null,
|
||||
);
|
||||
}).toList(),
|
||||
orElse: () => const <Widget>[],
|
||||
isSelected: isSelected,
|
||||
onTap: canSelect ? () => toggle(tool.id) : null,
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
return ConduitCard(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: Spacing.listItemPadding,
|
||||
vertical: Spacing.sm,
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
AppLocalizations.of(context)!.onboardQuickTitle,
|
||||
style:
|
||||
theme.headingSmall?.copyWith(color: theme.textPrimary) ??
|
||||
TextStyle(color: theme.textPrimary, fontSize: 18),
|
||||
),
|
||||
const SizedBox(height: Spacing.sm),
|
||||
ConduitCard(
|
||||
padding: const EdgeInsets.all(Spacing.cardPadding),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildIconBadge(
|
||||
context,
|
||||
UiUtils.platformIcon(
|
||||
ios: CupertinoIcons.bolt,
|
||||
android: Icons.flash_on,
|
||||
),
|
||||
color: theme.buttonPrimary,
|
||||
),
|
||||
const SizedBox(width: Spacing.md),
|
||||
Expanded(
|
||||
child: Text(
|
||||
AppLocalizations.of(
|
||||
context,
|
||||
)!.appCustomizationSubtitle,
|
||||
style: context.conduitTheme.bodySmall?.copyWith(
|
||||
color: context.conduitTheme.textSecondary,
|
||||
),
|
||||
AppLocalizations.of(context)!.quickActionsDescription,
|
||||
style:
|
||||
theme.bodySmall?.copyWith(
|
||||
color: theme.textSecondary,
|
||||
) ??
|
||||
TextStyle(color: theme.textSecondary),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: selected.isEmpty
|
||||
? null
|
||||
: () async {
|
||||
await ref
|
||||
: () => ref
|
||||
.read(appSettingsProvider.notifier)
|
||||
.setQuickPills(const []);
|
||||
},
|
||||
.setQuickPills(const []),
|
||||
child: Text(AppLocalizations.of(context)!.clear),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: Spacing.sm),
|
||||
const SizedBox(height: Spacing.md),
|
||||
Wrap(
|
||||
spacing: Spacing.sm,
|
||||
runSpacing: Spacing.sm,
|
||||
children: [
|
||||
ConduitChip(
|
||||
label: AppLocalizations.of(context)!.web,
|
||||
icon: Platform.isIOS
|
||||
? CupertinoIcons.search
|
||||
: Icons.search,
|
||||
icon: Platform.isIOS ? CupertinoIcons.search : Icons.search,
|
||||
isSelected: selected.contains('web'),
|
||||
onTap:
|
||||
(selectedCount < 2 || selected.contains('web'))
|
||||
onTap: (selectedCount < 2 || selected.contains('web'))
|
||||
? () => toggle('web')
|
||||
: null,
|
||||
),
|
||||
ConduitChip(
|
||||
label: AppLocalizations.of(context)!.imageGen,
|
||||
icon: Platform.isIOS
|
||||
? CupertinoIcons.photo
|
||||
: Icons.image,
|
||||
icon: Platform.isIOS ? CupertinoIcons.photo : Icons.image,
|
||||
isSelected: selected.contains('image'),
|
||||
onTap:
|
||||
(selectedCount < 2 ||
|
||||
selected.contains('image'))
|
||||
onTap: (selectedCount < 2 || selected.contains('image'))
|
||||
? () => toggle('image')
|
||||
: null,
|
||||
),
|
||||
// Dynamic tools from server
|
||||
...dynamicToolChips,
|
||||
...buildToolChips(),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
}
|
||||
|
||||
const SizedBox(height: Spacing.lg),
|
||||
// Chat input behavior
|
||||
Text(
|
||||
'Chat',
|
||||
style: context.conduitTheme.headingSmall?.copyWith(
|
||||
color: context.conduitTheme.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: Spacing.md),
|
||||
ConduitCard(
|
||||
padding: EdgeInsets.zero,
|
||||
child: Column(
|
||||
children: [
|
||||
ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: Spacing.listItemPadding,
|
||||
vertical: Spacing.sm,
|
||||
),
|
||||
leading: Container(
|
||||
padding: const EdgeInsets.all(Spacing.sm),
|
||||
decoration: BoxDecoration(
|
||||
color: context.conduitTheme.buttonPrimary.withValues(
|
||||
alpha: Alpha.highlight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(
|
||||
AppBorderRadius.small,
|
||||
),
|
||||
),
|
||||
child: Icon(
|
||||
Platform.isIOS
|
||||
? CupertinoIcons.paperplane
|
||||
: Icons.keyboard_return,
|
||||
color: context.conduitTheme.buttonPrimary,
|
||||
size: IconSize.medium,
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
'Send on Enter',
|
||||
style: context.conduitTheme.bodyLarge?.copyWith(
|
||||
color: context.conduitTheme.textPrimary,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
'Enter sends (soft keyboard). Cmd/Ctrl+Enter also available',
|
||||
style: context.conduitTheme.bodySmall?.copyWith(
|
||||
color: context.conduitTheme.textSecondary,
|
||||
),
|
||||
),
|
||||
trailing: Switch.adaptive(
|
||||
value: settings.sendOnEnter,
|
||||
onChanged: (v) => ref
|
||||
.read(appSettingsProvider.notifier)
|
||||
.setSendOnEnter(v),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: Spacing.lg),
|
||||
Text(
|
||||
AppLocalizations.of(context)!.realtime,
|
||||
style: context.conduitTheme.headingSmall?.copyWith(
|
||||
color: context.conduitTheme.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: Spacing.md),
|
||||
ConduitCard(
|
||||
padding: EdgeInsets.zero,
|
||||
child: Column(
|
||||
Widget _buildChatSection(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
AppSettings settings,
|
||||
) {
|
||||
final theme = context.conduitTheme;
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: Spacing.listItemPadding,
|
||||
vertical: Spacing.sm,
|
||||
Text(
|
||||
'Chat',
|
||||
style:
|
||||
theme.headingSmall?.copyWith(color: theme.textPrimary) ??
|
||||
TextStyle(color: theme.textPrimary, fontSize: 18),
|
||||
),
|
||||
leading: Container(
|
||||
padding: const EdgeInsets.all(Spacing.sm),
|
||||
decoration: BoxDecoration(
|
||||
color: context.conduitTheme.buttonPrimary.withValues(
|
||||
alpha: Alpha.highlight,
|
||||
const SizedBox(height: Spacing.sm),
|
||||
_CustomizationTile(
|
||||
leading: _buildIconBadge(
|
||||
context,
|
||||
Platform.isIOS ? CupertinoIcons.paperplane : Icons.keyboard_return,
|
||||
color: theme.buttonPrimary,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(
|
||||
AppBorderRadius.small,
|
||||
title: 'Send on Enter',
|
||||
subtitle:
|
||||
'Enter sends (soft keyboard). Cmd/Ctrl+Enter also available',
|
||||
trailing: Switch.adaptive(
|
||||
value: settings.sendOnEnter,
|
||||
onChanged: (value) =>
|
||||
ref.read(appSettingsProvider.notifier).setSendOnEnter(value),
|
||||
),
|
||||
),
|
||||
child: Icon(
|
||||
Platform.isIOS
|
||||
? CupertinoIcons.waveform
|
||||
: Icons.sync_alt,
|
||||
color: context.conduitTheme.buttonPrimary,
|
||||
size: IconSize.medium,
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
AppLocalizations.of(context)!.transportMode,
|
||||
style: context.conduitTheme.bodyLarge?.copyWith(
|
||||
color: context.conduitTheme.textPrimary,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
AppLocalizations.of(context)!.transportModeDescription,
|
||||
style: context.conduitTheme.bodySmall?.copyWith(
|
||||
color: context.conduitTheme.textSecondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
Spacing.listItemPadding,
|
||||
0,
|
||||
Spacing.listItemPadding,
|
||||
Spacing.md,
|
||||
),
|
||||
child: DropdownButtonFormField<String>(
|
||||
initialValue: settings.socketTransportMode,
|
||||
onChanged: (v) async {
|
||||
if (v == null) return;
|
||||
await ref
|
||||
showChevron: false,
|
||||
onTap: () => ref
|
||||
.read(appSettingsProvider.notifier)
|
||||
.setSocketTransportMode(v);
|
||||
},
|
||||
items: [
|
||||
DropdownMenuItem(
|
||||
value: 'auto',
|
||||
child: Text(
|
||||
AppLocalizations.of(context)!.transportModeAuto,
|
||||
),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: 'ws',
|
||||
child: Text(
|
||||
AppLocalizations.of(context)!.transportModeWs,
|
||||
),
|
||||
),
|
||||
],
|
||||
decoration: InputDecoration(
|
||||
labelText: AppLocalizations.of(context)!.mode,
|
||||
border: const OutlineInputBorder(),
|
||||
isDense: true,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 10,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
Spacing.listItemPadding,
|
||||
0,
|
||||
Spacing.listItemPadding,
|
||||
Spacing.md,
|
||||
),
|
||||
child: Text(
|
||||
settings.socketTransportMode == 'auto'
|
||||
? AppLocalizations.of(context)!.transportModeAutoInfo
|
||||
: AppLocalizations.of(context)!.transportModeWsInfo,
|
||||
style: context.conduitTheme.caption?.copyWith(
|
||||
color: context.conduitTheme.textSecondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
.setSendOnEnter(!settings.sendOnEnter),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
String _resolveLanguageLabel(BuildContext context, String code) {
|
||||
switch (code) {
|
||||
case 'en':
|
||||
return AppLocalizations.of(context)!.english;
|
||||
case 'de':
|
||||
return AppLocalizations.of(context)!.deutsch;
|
||||
case 'fr':
|
||||
return AppLocalizations.of(context)!.francais;
|
||||
case 'it':
|
||||
return AppLocalizations.of(context)!.italiano;
|
||||
default:
|
||||
return AppLocalizations.of(context)!.system;
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildIconBadge(
|
||||
BuildContext context,
|
||||
IconData icon, {
|
||||
required Color color,
|
||||
}) {
|
||||
return Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: color.withValues(alpha: Alpha.highlight),
|
||||
borderRadius: BorderRadius.circular(AppBorderRadius.medium),
|
||||
border: Border.all(
|
||||
color: color.withValues(alpha: 0.2),
|
||||
width: BorderWidth.thin,
|
||||
),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Icon(icon, color: color, size: IconSize.large),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
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,
|
||||
data: (userData) => _buildScaffold(
|
||||
context,
|
||||
body: _buildProfileBody(context, ref, userData, api),
|
||||
),
|
||||
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(
|
||||
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,23 +198,68 @@ 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(
|
||||
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: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(AppBorderRadius.avatar),
|
||||
boxShadow: ConduitShadows.card,
|
||||
boxShadow: ConduitShadows.high,
|
||||
),
|
||||
child: UserAvatar(
|
||||
size: IconSize.avatar,
|
||||
size: IconSize.huge,
|
||||
imageUrl: avatarUrl,
|
||||
fallbackText: initial,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: Spacing.md),
|
||||
const SizedBox(width: Spacing.lg),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -236,33 +267,97 @@ class ProfilePage extends ConsumerWidget {
|
||||
Text(
|
||||
displayName,
|
||||
style:
|
||||
context.conduitTheme.headingMedium?.copyWith(
|
||||
color: context.conduitTheme.textPrimary,
|
||||
fontWeight: FontWeight.w600,
|
||||
theme.headingMedium?.copyWith(
|
||||
color: theme.textPrimary,
|
||||
fontWeight: FontWeight.w700,
|
||||
) ??
|
||||
TextStyle(
|
||||
color: context.conduitTheme.textPrimary,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: theme.textPrimary,
|
||||
fontWeight: FontWeight.w700,
|
||||
fontSize: 22,
|
||||
),
|
||||
),
|
||||
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,
|
||||
style:
|
||||
context.conduitTheme.bodyMedium?.copyWith(
|
||||
color: context.conduitTheme.textSecondary,
|
||||
theme.bodySmall?.copyWith(
|
||||
color: theme.textSecondary,
|
||||
) ??
|
||||
TextStyle(color: context.conduitTheme.textSecondary),
|
||||
TextStyle(color: theme.textSecondary),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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(
|
||||
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: context.conduitTheme.iconSecondary,
|
||||
color: theme.iconSecondary,
|
||||
size: IconSize.small,
|
||||
),
|
||||
onTap: onTap,
|
||||
),
|
||||
)
|
||||
: 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(
|
||||
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(
|
||||
leading = _buildIconBadge(
|
||||
context,
|
||||
UiUtils.platformIcon(
|
||||
ios: CupertinoIcons.wand_stars,
|
||||
android: Icons.auto_awesome,
|
||||
),
|
||||
color: context.conduitTheme.buttonPrimary,
|
||||
size: IconSize.medium,
|
||||
),
|
||||
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,
|
||||
),
|
||||
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(
|
||||
loading: () => _ProfileSettingTile(
|
||||
leading: _buildIconBadge(
|
||||
context,
|
||||
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,
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
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),
|
||||
),
|
||||
child: Icon(
|
||||
error: (error, stack) => _ProfileSettingTile(
|
||||
leading: _buildIconBadge(
|
||||
context,
|
||||
UiUtils.platformIcon(
|
||||
ios: CupertinoIcons.exclamationmark_triangle,
|
||||
android: Icons.error_outline,
|
||||
),
|
||||
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,
|
||||
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