feat(frontend): add multi-language support (en, zh-Hans, zh-Hant, ja)
This commit is contained in:
@@ -2,6 +2,7 @@ import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../l10n/app_localizations.dart';
|
||||
|
||||
import '../providers/auth_provider.dart';
|
||||
import '../providers/settings_provider.dart';
|
||||
@@ -88,21 +89,21 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _confirmLogout(AuthProvider auth) async {
|
||||
Future<void> _confirmLogout(AuthProvider auth, AppLocalizations l10n) async {
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return AlertDialog(
|
||||
title: const Text("Confirm Logout"),
|
||||
content: const Text("Are you sure you want to log out now?"),
|
||||
title: Text(l10n.confirmLogout),
|
||||
content: Text(l10n.confirmLogoutDesc),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
child: const Text("Cancel"),
|
||||
child: Text(l10n.cancel),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
child: const Text("Logout"),
|
||||
child: Text(l10n.logout),
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -119,10 +120,11 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
final auth = context.watch<AuthProvider>();
|
||||
final settings = context.watch<SettingsProvider>();
|
||||
final isAuthenticated = auth.isAuthenticated;
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text("Settings", style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
title: Text(l10n.settings, style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
centerTitle: true,
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
@@ -134,12 +136,58 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
_buildProfileSection(auth),
|
||||
const SizedBox(height: 32),
|
||||
],
|
||||
_buildSectionTitle("Network Configuration"),
|
||||
|
||||
_buildSectionTitle(l10n.language),
|
||||
const SizedBox(height: 16),
|
||||
DropdownButtonFormField<Locale?>(
|
||||
initialValue: settings.locale == null
|
||||
? null
|
||||
: AppLocalizations.supportedLocales.cast<Locale?>().firstWhere(
|
||||
(l) => l?.languageCode == settings.locale?.languageCode &&
|
||||
l?.scriptCode == settings.locale?.scriptCode,
|
||||
orElse: () => null,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
labelText: l10n.selectLanguage,
|
||||
prefixIcon: const Icon(Icons.language),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
items: [
|
||||
DropdownMenuItem(
|
||||
value: null,
|
||||
child: Text(l10n.system),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: const Locale('en'),
|
||||
child: Text(l10n.english),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: const Locale('zh'),
|
||||
child: Text(l10n.simplifiedChinese),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: const Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hant'),
|
||||
child: Text(l10n.traditionalChinese),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: const Locale('ja'),
|
||||
child: Text(l10n.japanese),
|
||||
),
|
||||
],
|
||||
onChanged: (Locale? newLocale) {
|
||||
settings.setLocale(newLocale);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
_buildSectionTitle(l10n.networkConfiguration),
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
controller: _urlController,
|
||||
decoration: InputDecoration(
|
||||
labelText: "Backend Server URL",
|
||||
labelText: l10n.backendServerUrl,
|
||||
hintText: "http://127.0.0.1:8080",
|
||||
prefixIcon: Icon(Icons.lan),
|
||||
border: OutlineInputBorder(
|
||||
@@ -157,13 +205,13 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text("Server URL Updated"),
|
||||
content: Text(l10n.serverUrlUpdated),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
},
|
||||
icon: Icon(Icons.save),
|
||||
label: Text("Save Network Settings"),
|
||||
label: Text(l10n.saveNetworkSettings),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Theme.of(
|
||||
context,
|
||||
@@ -179,28 +227,28 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
_buildSectionTitle("Theme Customization"),
|
||||
_buildSectionTitle(l10n.themeCustomization),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
"Appearance Mode",
|
||||
l10n.appearanceMode,
|
||||
style: Theme.of(context).textTheme.labelLarge,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
SegmentedButton<ThemeMode>(
|
||||
segments: const [
|
||||
segments: [
|
||||
ButtonSegment<ThemeMode>(
|
||||
value: ThemeMode.system,
|
||||
label: Text("System"),
|
||||
label: Text(l10n.system),
|
||||
icon: Icon(Icons.brightness_auto),
|
||||
),
|
||||
ButtonSegment<ThemeMode>(
|
||||
value: ThemeMode.light,
|
||||
label: Text("Light"),
|
||||
label: Text(l10n.light),
|
||||
icon: Icon(Icons.light_mode),
|
||||
),
|
||||
ButtonSegment<ThemeMode>(
|
||||
value: ThemeMode.dark,
|
||||
label: Text("Dark"),
|
||||
label: Text(l10n.dark),
|
||||
icon: Icon(Icons.dark_mode),
|
||||
),
|
||||
],
|
||||
@@ -210,7 +258,7 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Text("Accent Color", style: Theme.of(context).textTheme.labelLarge),
|
||||
Text(l10n.accentColor, style: Theme.of(context).textTheme.labelLarge),
|
||||
const SizedBox(height: 12),
|
||||
Wrap(
|
||||
spacing: 12,
|
||||
@@ -248,26 +296,26 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
}).toList(),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
_buildSectionTitle("Explore"),
|
||||
_buildSectionTitle(l10n.explore),
|
||||
const SizedBox(height: 8),
|
||||
SwitchListTile.adaptive(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: const Text("Live Preview Thumbnails"),
|
||||
subtitle: const Text(
|
||||
"Show cached snapshot covers for live rooms when available.",
|
||||
title: Text(l10n.livePreviewThumbnails),
|
||||
subtitle: Text(
|
||||
l10n.livePreviewThumbnailsDesc,
|
||||
),
|
||||
value: settings.livePreviewThumbnailsEnabled,
|
||||
onChanged: settings.setLivePreviewThumbnailsEnabled,
|
||||
),
|
||||
if (isAuthenticated) ...[
|
||||
const SizedBox(height: 32),
|
||||
_buildSectionTitle("Security"),
|
||||
_buildSectionTitle(l10n.security),
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
controller: _oldPasswordController,
|
||||
obscureText: true,
|
||||
decoration: InputDecoration(
|
||||
labelText: "Old Password",
|
||||
labelText: l10n.oldPassword,
|
||||
prefixIcon: const Icon(Icons.lock_outline),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
@@ -279,7 +327,7 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
controller: _newPasswordController,
|
||||
obscureText: true,
|
||||
decoration: InputDecoration(
|
||||
labelText: "New Password",
|
||||
labelText: l10n.newPassword,
|
||||
prefixIcon: const Icon(Icons.lock_reset),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
@@ -292,7 +340,7 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: _handleChangePassword,
|
||||
icon: const Icon(Icons.update),
|
||||
label: const Text("Change Password"),
|
||||
label: Text(l10n.changePassword),
|
||||
style: OutlinedButton.styleFrom(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
@@ -304,9 +352,9 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: FilledButton.tonalIcon(
|
||||
onPressed: () => _confirmLogout(auth),
|
||||
onPressed: () => _confirmLogout(auth, l10n),
|
||||
icon: const Icon(Icons.logout),
|
||||
label: const Text("Logout"),
|
||||
label: Text(l10n.logout),
|
||||
style: FilledButton.styleFrom(
|
||||
foregroundColor: Colors.redAccent,
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
|
||||
Reference in New Issue
Block a user