mirror of
https://github.com/spotiflacapp/SpotiFLAC-Mobile.git
synced 2026-05-31 19:05:05 +07:00
Strip doc comments, section dividers, HTML comments, and Flutter template boilerplate that add no informational value. No logic or behavior changes.
836 lines
28 KiB
Dart
836 lines
28 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:spotiflac_android/l10n/l10n.dart';
|
|
import 'package:spotiflac_android/l10n/supported_locales.dart';
|
|
import 'package:spotiflac_android/providers/settings_provider.dart';
|
|
import 'package:spotiflac_android/providers/theme_provider.dart';
|
|
import 'package:spotiflac_android/utils/app_bar_layout.dart';
|
|
import 'package:spotiflac_android/widgets/settings_group.dart';
|
|
|
|
class AppearanceSettingsPage extends ConsumerWidget {
|
|
const AppearanceSettingsPage({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final themeSettings = ref.watch(themeProvider);
|
|
final settings = ref.watch(settingsProvider);
|
|
final colorScheme = Theme.of(context).colorScheme;
|
|
final topPadding = normalizedHeaderTopPadding(context);
|
|
|
|
return PopScope(
|
|
canPop: true,
|
|
child: Scaffold(
|
|
body: CustomScrollView(
|
|
slivers: [
|
|
SliverAppBar(
|
|
expandedHeight: 120 + topPadding,
|
|
collapsedHeight: kToolbarHeight,
|
|
floating: false,
|
|
pinned: true,
|
|
backgroundColor: colorScheme.surface,
|
|
surfaceTintColor: Colors.transparent,
|
|
leading: IconButton(
|
|
tooltip: MaterialLocalizations.of(context).backButtonTooltip,
|
|
icon: const Icon(Icons.arrow_back),
|
|
onPressed: () => Navigator.pop(context),
|
|
),
|
|
flexibleSpace: _AppBarTitle(
|
|
title: context.l10n.appearanceTitle,
|
|
topPadding: topPadding,
|
|
),
|
|
),
|
|
|
|
SliverToBoxAdapter(
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 16,
|
|
vertical: 8,
|
|
),
|
|
child: _ThemePreviewCard(),
|
|
),
|
|
),
|
|
|
|
SliverToBoxAdapter(
|
|
child: SettingsSectionHeader(title: context.l10n.sectionColor),
|
|
),
|
|
|
|
SliverToBoxAdapter(
|
|
child: SettingsGroup(
|
|
children: [
|
|
SettingsSwitchItem(
|
|
icon: Icons.wallpaper,
|
|
title: context.l10n.appearanceDynamicColor,
|
|
subtitle: context.l10n.appearanceDynamicColorSubtitle,
|
|
value: themeSettings.useDynamicColor,
|
|
onChanged: (value) => ref
|
|
.read(themeProvider.notifier)
|
|
.setUseDynamicColor(value),
|
|
showDivider: false,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
if (!themeSettings.useDynamicColor)
|
|
SliverToBoxAdapter(
|
|
child: Padding(
|
|
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
|
|
child: _ColorPalettePicker(
|
|
currentColor: themeSettings.seedColorValue,
|
|
onColorSelected: (color) =>
|
|
ref.read(themeProvider.notifier).setSeedColor(color),
|
|
),
|
|
),
|
|
),
|
|
|
|
SliverToBoxAdapter(
|
|
child: SettingsSectionHeader(title: context.l10n.sectionTheme),
|
|
),
|
|
SliverToBoxAdapter(
|
|
child: SettingsGroup(
|
|
children: [
|
|
_ThemeModeSelector(
|
|
currentMode: themeSettings.themeMode,
|
|
onChanged: (mode) =>
|
|
ref.read(themeProvider.notifier).setThemeMode(mode),
|
|
),
|
|
if (Theme.of(context).brightness == Brightness.dark)
|
|
SettingsSwitchItem(
|
|
icon: Icons.brightness_2,
|
|
title: context.l10n.appearanceAmoledDark,
|
|
subtitle: context.l10n.appearanceAmoledDarkSubtitle,
|
|
value: themeSettings.useAmoled,
|
|
onChanged: (value) =>
|
|
ref.read(themeProvider.notifier).setUseAmoled(value),
|
|
showDivider: false,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
SliverToBoxAdapter(
|
|
child: SettingsSectionHeader(title: context.l10n.sectionLanguage),
|
|
),
|
|
SliverToBoxAdapter(
|
|
child: SettingsGroup(
|
|
children: [
|
|
_LanguageSelector(
|
|
currentLocale: settings.locale,
|
|
onChanged: (locale) =>
|
|
ref.read(settingsProvider.notifier).setLocale(locale),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
SliverToBoxAdapter(
|
|
child: SettingsSectionHeader(title: context.l10n.sectionLayout),
|
|
),
|
|
SliverToBoxAdapter(
|
|
child: SettingsGroup(
|
|
children: [
|
|
_HistoryViewSelector(
|
|
currentMode: settings.historyViewMode,
|
|
onChanged: (mode) => ref
|
|
.read(settingsProvider.notifier)
|
|
.setHistoryViewMode(mode),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
const SliverFillRemaining(
|
|
hasScrollBody: false,
|
|
child: SizedBox(height: 32),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _ThemePreviewCard extends StatelessWidget {
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final colorScheme = Theme.of(context).colorScheme;
|
|
final isDark = Theme.of(context).brightness == Brightness.dark;
|
|
|
|
return RepaintBoundary(
|
|
child: LayoutBuilder(
|
|
builder: (context, constraints) {
|
|
final cardWidth = constraints.maxWidth;
|
|
final previewHeight = (cardWidth * 0.56).clamp(170.0, 220.0);
|
|
final innerWidth = (cardWidth - 48).clamp(220.0, 320.0);
|
|
final innerHeight = (previewHeight * 0.70).clamp(120.0, 160.0);
|
|
final innerPadding = (innerHeight * 0.11).clamp(12.0, 18.0);
|
|
final artworkSize = (innerHeight - (innerPadding * 2)).clamp(
|
|
80.0,
|
|
120.0,
|
|
);
|
|
|
|
return Container(
|
|
constraints: BoxConstraints(minHeight: previewHeight),
|
|
width: double.infinity,
|
|
decoration: BoxDecoration(
|
|
color: colorScheme.surfaceContainerHighest,
|
|
borderRadius: BorderRadius.circular(28),
|
|
),
|
|
clipBehavior: Clip.antiAlias,
|
|
child: Stack(
|
|
children: [
|
|
Positioned(
|
|
top: -(previewHeight * 0.25),
|
|
right: -(previewHeight * 0.25),
|
|
child: Container(
|
|
width: previewHeight,
|
|
height: previewHeight,
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
color: colorScheme.primaryContainer.withValues(
|
|
alpha: 0.5,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
Positioned(
|
|
bottom: -(previewHeight * 0.15),
|
|
left: -(previewHeight * 0.15),
|
|
child: Container(
|
|
width: previewHeight * 0.75,
|
|
height: previewHeight * 0.75,
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
color: colorScheme.tertiaryContainer.withValues(
|
|
alpha: 0.5,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
Center(
|
|
child: Container(
|
|
width: innerWidth,
|
|
height: innerHeight,
|
|
padding: EdgeInsets.all(innerPadding),
|
|
decoration: BoxDecoration(
|
|
color: colorScheme.surface,
|
|
borderRadius: BorderRadius.circular(20),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withValues(alpha: 0.1),
|
|
blurRadius: 12,
|
|
offset: const Offset(0, 8),
|
|
),
|
|
],
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Container(
|
|
width: artworkSize,
|
|
height: artworkSize,
|
|
decoration: BoxDecoration(
|
|
color: colorScheme.primary,
|
|
borderRadius: BorderRadius.circular(16),
|
|
),
|
|
child: Icon(
|
|
Icons.music_note,
|
|
color: colorScheme.onPrimary,
|
|
size: artworkSize * 0.44,
|
|
),
|
|
),
|
|
const SizedBox(width: 16),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Container(
|
|
width: double.infinity,
|
|
height: 14,
|
|
decoration: BoxDecoration(
|
|
color: colorScheme.onSurface,
|
|
borderRadius: BorderRadius.circular(4),
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Container(
|
|
width: 80,
|
|
height: 10,
|
|
decoration: BoxDecoration(
|
|
color: colorScheme.primary,
|
|
borderRadius: BorderRadius.circular(4),
|
|
),
|
|
),
|
|
const SizedBox(height: 24),
|
|
Row(
|
|
children: [
|
|
Icon(
|
|
Icons.skip_previous,
|
|
size: 24,
|
|
color: colorScheme.onSurfaceVariant,
|
|
),
|
|
const SizedBox(width: 12),
|
|
Icon(
|
|
Icons.play_circle_fill,
|
|
size: 32,
|
|
color: colorScheme.primary,
|
|
),
|
|
const SizedBox(width: 12),
|
|
Icon(
|
|
Icons.skip_next,
|
|
size: 24,
|
|
color: colorScheme.onSurfaceVariant,
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
Positioned(
|
|
bottom: 12,
|
|
right: 12,
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 10,
|
|
vertical: 4,
|
|
),
|
|
decoration: BoxDecoration(
|
|
color: Colors.black.withValues(alpha: 0.6),
|
|
borderRadius: BorderRadius.circular(20),
|
|
),
|
|
child: Text(
|
|
isDark
|
|
? context.l10n.appearanceThemeDark
|
|
: context.l10n.appearanceThemeLight,
|
|
style: const TextStyle(
|
|
color: Colors.white,
|
|
fontSize: 10,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _ColorPalettePicker extends StatelessWidget {
|
|
final int currentColor;
|
|
final ValueChanged<Color> onColorSelected;
|
|
const _ColorPalettePicker({
|
|
required this.currentColor,
|
|
required this.onColorSelected,
|
|
});
|
|
|
|
static const _colors = [
|
|
Color(0xFF1DB954),
|
|
Color(0xFF6750A4),
|
|
Color(0xFF0061A4),
|
|
Color(0xFF006E1C),
|
|
Color(0xFFBA1A1A),
|
|
Color(0xFF984061),
|
|
Color(0xFF7D5260),
|
|
Color(0xFF006874),
|
|
];
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return SingleChildScrollView(
|
|
scrollDirection: Axis.horizontal,
|
|
child: Row(
|
|
children: _colors.map((color) {
|
|
final isSelected = color.toARGB32() == currentColor;
|
|
final colorHex = color
|
|
.toARGB32()
|
|
.toRadixString(16)
|
|
.padLeft(8, '0')
|
|
.toUpperCase();
|
|
return Padding(
|
|
padding: const EdgeInsets.only(right: 12),
|
|
child: Semantics(
|
|
button: true,
|
|
selected: isSelected,
|
|
label: context.l10n.appearanceSelectAccentColor(colorHex),
|
|
child: GestureDetector(
|
|
onTap: () => onColorSelected(color),
|
|
child: _ColorPaletteItem(color: color, isSelected: isSelected),
|
|
),
|
|
),
|
|
);
|
|
}).toList(),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _ColorPaletteItem extends StatelessWidget {
|
|
final Color color;
|
|
final bool isSelected;
|
|
|
|
const _ColorPaletteItem({required this.color, required this.isSelected});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final scheme = ColorScheme.fromSeed(
|
|
seedColor: color,
|
|
brightness: Theme.of(context).brightness,
|
|
);
|
|
final size = 64.0;
|
|
|
|
return Stack(
|
|
children: [
|
|
Container(
|
|
width: size,
|
|
height: size,
|
|
decoration: BoxDecoration(borderRadius: BorderRadius.circular(20)),
|
|
clipBehavior: Clip.antiAlias,
|
|
child: Column(
|
|
children: [
|
|
Expanded(
|
|
child: Row(
|
|
children: [
|
|
Expanded(child: Container(color: scheme.primaryContainer)),
|
|
Expanded(child: Container(color: scheme.tertiaryContainer)),
|
|
],
|
|
),
|
|
),
|
|
Expanded(
|
|
child: Row(
|
|
children: [
|
|
Expanded(
|
|
child: Container(color: scheme.secondaryContainer),
|
|
),
|
|
Expanded(child: Container(color: scheme.surfaceContainer)),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
if (isSelected)
|
|
Positioned.fill(
|
|
child: Center(
|
|
child: Container(
|
|
padding: const EdgeInsets.all(4),
|
|
decoration: const BoxDecoration(
|
|
color: Colors.white,
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: Icon(Icons.check, size: 16, color: scheme.primary),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
class _AppBarTitle extends StatelessWidget {
|
|
final String title;
|
|
final double topPadding;
|
|
|
|
const _AppBarTitle({required this.title, required this.topPadding});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final colorScheme = Theme.of(context).colorScheme;
|
|
return LayoutBuilder(
|
|
builder: (context, constraints) {
|
|
final maxHeight = 120 + topPadding;
|
|
final minHeight = kToolbarHeight + topPadding;
|
|
final expandRatio =
|
|
((constraints.maxHeight - minHeight) / (maxHeight - minHeight))
|
|
.clamp(0.0, 1.0);
|
|
final leftPadding = 56 - (32 * expandRatio);
|
|
return FlexibleSpaceBar(
|
|
expandedTitleScale: 1.0,
|
|
titlePadding: EdgeInsets.only(left: leftPadding, bottom: 16),
|
|
title: Text(
|
|
title,
|
|
style: TextStyle(
|
|
fontSize: 20 + (8 * expandRatio),
|
|
fontWeight: FontWeight.bold,
|
|
color: colorScheme.onSurface,
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
class _ThemeModeSelector extends StatelessWidget {
|
|
final ThemeMode currentMode;
|
|
final ValueChanged<ThemeMode> onChanged;
|
|
const _ThemeModeSelector({
|
|
required this.currentMode,
|
|
required this.onChanged,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Padding(
|
|
padding: const EdgeInsets.all(12),
|
|
child: Row(
|
|
children: [
|
|
_ThemeModeChip(
|
|
icon: Icons.brightness_auto,
|
|
label: context.l10n.appearanceThemeSystem,
|
|
isSelected: currentMode == ThemeMode.system,
|
|
onTap: () => onChanged(ThemeMode.system),
|
|
),
|
|
const SizedBox(width: 8),
|
|
_ThemeModeChip(
|
|
icon: Icons.light_mode,
|
|
label: context.l10n.appearanceThemeLight,
|
|
isSelected: currentMode == ThemeMode.light,
|
|
onTap: () => onChanged(ThemeMode.light),
|
|
),
|
|
const SizedBox(width: 8),
|
|
_ThemeModeChip(
|
|
icon: Icons.dark_mode,
|
|
label: context.l10n.appearanceThemeDark,
|
|
isSelected: currentMode == ThemeMode.dark,
|
|
onTap: () => onChanged(ThemeMode.dark),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _ThemeModeChip extends StatelessWidget {
|
|
final IconData icon;
|
|
final String label;
|
|
final bool isSelected;
|
|
final VoidCallback onTap;
|
|
const _ThemeModeChip({
|
|
required this.icon,
|
|
required this.label,
|
|
required this.isSelected,
|
|
required this.onTap,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final colorScheme = Theme.of(context).colorScheme;
|
|
final isDark = Theme.of(context).brightness == Brightness.dark;
|
|
|
|
final unselectedColor = isDark
|
|
? Color.alphaBlend(
|
|
Colors.white.withValues(alpha: 0.05),
|
|
colorScheme.surface,
|
|
)
|
|
: Color.alphaBlend(
|
|
Colors.black.withValues(alpha: 0.05),
|
|
colorScheme.surfaceContainerHighest,
|
|
);
|
|
|
|
return Expanded(
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
color: isSelected ? colorScheme.primaryContainer : unselectedColor,
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: !isDark && !isSelected
|
|
? Border.all(
|
|
color: colorScheme.outlineVariant.withValues(alpha: 0.5),
|
|
width: 1,
|
|
)
|
|
: null,
|
|
),
|
|
child: Material(
|
|
color: Colors.transparent,
|
|
borderRadius: BorderRadius.circular(12),
|
|
child: InkWell(
|
|
onTap: onTap,
|
|
borderRadius: BorderRadius.circular(12),
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 14),
|
|
child: Column(
|
|
children: [
|
|
Icon(
|
|
icon,
|
|
color: isSelected
|
|
? colorScheme.onPrimaryContainer
|
|
: colorScheme.onSurfaceVariant,
|
|
),
|
|
const SizedBox(height: 6),
|
|
Text(
|
|
label,
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
fontWeight: isSelected
|
|
? FontWeight.w600
|
|
: FontWeight.normal,
|
|
color: isSelected
|
|
? colorScheme.onPrimaryContainer
|
|
: colorScheme.onSurfaceVariant,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _HistoryViewSelector extends StatelessWidget {
|
|
final String currentMode;
|
|
final ValueChanged<String> onChanged;
|
|
const _HistoryViewSelector({
|
|
required this.currentMode,
|
|
required this.onChanged,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final colorScheme = Theme.of(context).colorScheme;
|
|
return Padding(
|
|
padding: const EdgeInsets.all(12),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Padding(
|
|
padding: const EdgeInsets.only(left: 8, bottom: 8),
|
|
child: Text(
|
|
context.l10n.appearanceHistoryView,
|
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
|
color: colorScheme.onSurfaceVariant,
|
|
),
|
|
),
|
|
),
|
|
Row(
|
|
children: [
|
|
_ViewModeChip(
|
|
icon: Icons.view_list,
|
|
label: context.l10n.appearanceHistoryViewList,
|
|
isSelected: currentMode == 'list',
|
|
onTap: () => onChanged('list'),
|
|
),
|
|
const SizedBox(width: 8),
|
|
_ViewModeChip(
|
|
icon: Icons.grid_view,
|
|
label: context.l10n.appearanceHistoryViewGrid,
|
|
isSelected: currentMode == 'grid',
|
|
onTap: () => onChanged('grid'),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _ViewModeChip extends StatelessWidget {
|
|
final IconData icon;
|
|
final String label;
|
|
final bool isSelected;
|
|
final VoidCallback onTap;
|
|
const _ViewModeChip({
|
|
required this.icon,
|
|
required this.label,
|
|
required this.isSelected,
|
|
required this.onTap,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final colorScheme = Theme.of(context).colorScheme;
|
|
final isDark = Theme.of(context).brightness == Brightness.dark;
|
|
|
|
final unselectedColor = isDark
|
|
? Color.alphaBlend(
|
|
Colors.white.withValues(alpha: 0.05),
|
|
colorScheme.surface,
|
|
)
|
|
: Color.alphaBlend(
|
|
Colors.black.withValues(alpha: 0.05),
|
|
colorScheme.surfaceContainerHighest,
|
|
);
|
|
|
|
return Expanded(
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
color: isSelected ? colorScheme.primaryContainer : unselectedColor,
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: !isDark && !isSelected
|
|
? Border.all(
|
|
color: colorScheme.outlineVariant.withValues(alpha: 0.5),
|
|
width: 1,
|
|
)
|
|
: null,
|
|
),
|
|
child: Material(
|
|
color: Colors.transparent,
|
|
borderRadius: BorderRadius.circular(12),
|
|
child: InkWell(
|
|
onTap: onTap,
|
|
borderRadius: BorderRadius.circular(12),
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 14),
|
|
child: Column(
|
|
children: [
|
|
Icon(
|
|
icon,
|
|
color: isSelected
|
|
? colorScheme.onPrimaryContainer
|
|
: colorScheme.onSurfaceVariant,
|
|
),
|
|
const SizedBox(height: 6),
|
|
Text(
|
|
label,
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
fontWeight: isSelected
|
|
? FontWeight.w600
|
|
: FontWeight.normal,
|
|
color: isSelected
|
|
? colorScheme.onPrimaryContainer
|
|
: colorScheme.onSurfaceVariant,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _LanguageSelector extends StatelessWidget {
|
|
final String currentLocale;
|
|
final ValueChanged<String> onChanged;
|
|
const _LanguageSelector({
|
|
required this.currentLocale,
|
|
required this.onChanged,
|
|
});
|
|
|
|
static const _allLanguages = [
|
|
('system', 'System Default', Icons.phone_android),
|
|
('en', 'English', Icons.language),
|
|
('id', 'Bahasa Indonesia', Icons.language),
|
|
('de', 'Deutsch', Icons.language),
|
|
('es', 'Español', Icons.language),
|
|
('es_ES', 'Español (España)', Icons.language),
|
|
('fr', 'Français', Icons.language),
|
|
('hi', 'हिन्दी', Icons.language),
|
|
('ja', '日本語', Icons.language),
|
|
('ko', '한국어', Icons.language),
|
|
('nl', 'Nederlands', Icons.language),
|
|
('pt', 'Português', Icons.language),
|
|
('pt_PT', 'Português (Brasil)', Icons.language),
|
|
('ru', 'Русский', Icons.language),
|
|
('tr', 'Türkçe', Icons.language),
|
|
('uk', 'Українська', Icons.language),
|
|
('zh', '简体中文', Icons.language),
|
|
('zh_CN', '简体中文 (中国)', Icons.language),
|
|
('zh_TW', '繁體中文', Icons.language),
|
|
];
|
|
|
|
/// Uses filteredLocaleCodes from supported_locales.dart (generated file).
|
|
List<(String, String, IconData)> get _languages {
|
|
return _allLanguages.where((lang) {
|
|
if (lang.$1 == 'system') return true;
|
|
return filteredLocaleCodes.contains(lang.$1);
|
|
}).toList();
|
|
}
|
|
|
|
String _getLanguageName(String code) {
|
|
for (final lang in _allLanguages) {
|
|
if (lang.$1 == code) return lang.$2;
|
|
}
|
|
return code;
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final colorScheme = Theme.of(context).colorScheme;
|
|
return ListTile(
|
|
leading: Icon(Icons.language, color: colorScheme.onSurfaceVariant),
|
|
title: Text(context.l10n.appearanceLanguage),
|
|
subtitle: Text(_getLanguageName(currentLocale)),
|
|
trailing: Icon(Icons.chevron_right, color: colorScheme.onSurfaceVariant),
|
|
onTap: () => _showLanguagePicker(context),
|
|
);
|
|
}
|
|
|
|
void _showLanguagePicker(BuildContext context) {
|
|
final colorScheme = Theme.of(context).colorScheme;
|
|
showModalBottomSheet<void>(
|
|
context: context,
|
|
useRootNavigator: true,
|
|
backgroundColor: colorScheme.surface,
|
|
shape: const RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
|
),
|
|
builder: (context) => SafeArea(
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Padding(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Text(
|
|
context.l10n.appearanceLanguage,
|
|
style: Theme.of(
|
|
context,
|
|
).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
|
|
),
|
|
),
|
|
const Divider(height: 1),
|
|
Flexible(
|
|
child: ListView.builder(
|
|
shrinkWrap: true,
|
|
itemCount: _languages.length,
|
|
itemBuilder: (context, index) {
|
|
final lang = _languages[index];
|
|
final isSelected = currentLocale == lang.$1;
|
|
return ListTile(
|
|
leading: Icon(
|
|
lang.$3,
|
|
color: isSelected
|
|
? colorScheme.primary
|
|
: colorScheme.onSurfaceVariant,
|
|
),
|
|
title: Text(
|
|
lang.$2,
|
|
style: TextStyle(
|
|
color: isSelected
|
|
? colorScheme.primary
|
|
: colorScheme.onSurface,
|
|
fontWeight: isSelected
|
|
? FontWeight.w600
|
|
: FontWeight.normal,
|
|
),
|
|
),
|
|
trailing: isSelected
|
|
? Icon(Icons.check, color: colorScheme.primary)
|
|
: null,
|
|
onTap: () {
|
|
onChanged(lang.$1);
|
|
Navigator.pop(context);
|
|
},
|
|
);
|
|
},
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|