diff --git a/assets/translations/en-US.json b/assets/translations/en-US.json index c4757c3..d47cbab 100644 --- a/assets/translations/en-US.json +++ b/assets/translations/en-US.json @@ -241,6 +241,8 @@ "settingsMisc": "Misc", "settingsMiscAbout": "About", "settingsMiscAboutDescription": "View the version information of Solian.", + "settingsAccountLanguage": "Account Language", + "settingsAccountLanguageDescription": "Set the language for email, notification, and other account-related content.", "sensitiveContent": "Sensitive Content", "sensitiveContentCollapsed": "Sensitive content has been collapsed.", "sensitiveContentDescription": "This content has been marked as sensitive, and may not be suitable for all viewers.", @@ -606,5 +608,6 @@ "one": "{} Source Point", "other": "{} Source Points" }, - "aiThinkingProcess": "AI Thinking Process" + "aiThinkingProcess": "AI Thinking Process", + "accountSettingsApplied": "Account settings have been applied." } diff --git a/assets/translations/zh-CN.json b/assets/translations/zh-CN.json index f7f5340..e07913b 100644 --- a/assets/translations/zh-CN.json +++ b/assets/translations/zh-CN.json @@ -239,6 +239,8 @@ "settingsMisc": "杂项", "settingsMiscAbout": "关于", "settingsMiscAboutDescription": "查看 Solian 的版本信息。", + "settingsAccountLanguage": "帐号偏好语言", + "settingsAccountLanguageDescription": "设置邮件、通知和其他帐号相关内容的语言。", "sensitiveContent": "敏感内容", "sensitiveContentCollapsed": "敏感内容已折叠。", "sensitiveContentDescription": "此内容已被标记,可能不适合所有人查看。", @@ -604,5 +606,6 @@ "one": "{} 源点", "other": "{} 源点" }, - "aiThinkingProcess": "AI 思考过程" + "aiThinkingProcess": "AI 思考过程", + "accountSettingsApplied": "帐号设置已应用。" } diff --git a/assets/translations/zh-HK.json b/assets/translations/zh-HK.json index fec4541..35d4b2b 100644 --- a/assets/translations/zh-HK.json +++ b/assets/translations/zh-HK.json @@ -239,6 +239,8 @@ "settingsMisc": "雜項", "settingsMiscAbout": "關於", "settingsMiscAboutDescription": "查看 Solian 的版本信息。", + "settingsAccountLanguage": "帳號偏好語言", + "settingsAccountLanguageDescription": "設置郵件、通知和其他帳號相關內容的語言。", "sensitiveContent": "敏感內容", "sensitiveContentCollapsed": "敏感內容已摺疊。", "sensitiveContentDescription": "此內容已被標記,可能不適合所有人查看。", @@ -604,5 +606,6 @@ "one": "{} 源點", "other": "{} 源點" }, - "aiThinkingProcess": "AI 思考過程" + "aiThinkingProcess": "AI 思考過程", + "accountSettingsApplied": "帳號設置已應用。" } diff --git a/assets/translations/zh-TW.json b/assets/translations/zh-TW.json index 6a09248..e19a0e6 100644 --- a/assets/translations/zh-TW.json +++ b/assets/translations/zh-TW.json @@ -239,6 +239,8 @@ "settingsMisc": "雜項", "settingsMiscAbout": "關於", "settingsMiscAboutDescription": "查看 Solian 的版本信息。", + "settingsAccountLanguage": "帳號偏好語言", + "settingsAccountLanguageDescription": "設置郵件、通知和其他帳號相關內容的語言。", "sensitiveContent": "敏感內容", "sensitiveContentCollapsed": "敏感內容已摺疊。", "sensitiveContentDescription": "此內容已被標記,可能不適合所有人查看。", @@ -604,5 +606,6 @@ "one": "{} 源點", "other": "{} 源點" }, - "aiThinkingProcess": "AI 思考過程" + "aiThinkingProcess": "AI 思考過程", + "accountSettingsApplied": "帳號設置已應用。" } diff --git a/lib/providers/userinfo.dart b/lib/providers/userinfo.dart index 03ae8b5..081bed0 100644 --- a/lib/providers/userinfo.dart +++ b/lib/providers/userinfo.dart @@ -53,4 +53,11 @@ class UserProvider extends ChangeNotifier { user = null; notifyListeners(); } + + void setLanguage(String? value) { + if (value == null) return; + if (user == null) return; + user = user!.copyWith(language: value); + notifyListeners(); + } } diff --git a/lib/screens/account/account_settings.dart b/lib/screens/account/account_settings.dart index fb9e680..4c6fee4 100644 --- a/lib/screens/account/account_settings.dart +++ b/lib/screens/account/account_settings.dart @@ -1,17 +1,41 @@ +import 'package:collection/collection.dart'; +import 'package:dropdown_button2/dropdown_button2.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:provider/provider.dart'; +import 'package:styled_widget/styled_widget.dart'; import 'package:surface/providers/sn_network.dart'; +import 'package:surface/providers/userinfo.dart'; import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/navigation/app_scaffold.dart'; +import 'package:intl/locale.dart'; class AccountSettingsScreen extends StatelessWidget { const AccountSettingsScreen({super.key}); + Future _setAccountLanguage(BuildContext context, Locale? value) async { + if (value == null) return; + try { + final sn = context.read(); + final ua = context.read(); + await sn.client.put('/cgi/id/users/me/language', data: { + 'language': value.toString(), + }); + if (!context.mounted) return; + context.showSnackbar('accountSettingsApplied'.tr()); + await ua.refreshUser(); + } catch (err) { + if (!context.mounted) return; + context.showErrorDialog(err); + } + } + @override Widget build(BuildContext context) { + final ua = context.watch(); + return AppScaffold( appBar: AppBar( leading: PageBackButton(), @@ -21,6 +45,42 @@ class AccountSettingsScreen extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + ListTile( + title: Text('settingsAccountLanguage').tr(), + subtitle: Text('settingsAccountLanguageDescription').tr(), + contentPadding: const EdgeInsets.only(left: 24, right: 17), + leading: const Icon(Symbols.translate), + trailing: DropdownButtonHideUnderline( + child: DropdownButton2( + isExpanded: true, + items: [ + ...EasyLocalization.of(context)!.supportedLocales.mapIndexed((idx, ele) { + return DropdownMenuItem( + value: Locale.parse(ele.toString()), + child: Text('${ele.languageCode}-${ele.countryCode}').fontSize(14), + ); + }), + ], + value: ua.user?.language != null ? Locale.parse(ua.user!.language) : Locale.parse('en-US'), + onChanged: (Locale? value) { + if (value == null) return; + _setAccountLanguage(context, value); + ua.setLanguage(value.toString()); + }, + buttonStyleData: const ButtonStyleData( + padding: EdgeInsets.symmetric( + horizontal: 16, + vertical: 5, + ), + height: 40, + width: 160, + ), + menuItemStyleData: const MenuItemStyleData( + height: 40, + ), + ), + ), + ), ListTile( title: Text('accountProfileEdit').tr(), subtitle: Text('accountProfileEditSubtitle').tr(), diff --git a/lib/screens/auth/register.dart b/lib/screens/auth/register.dart index bd33053..15fe156 100644 --- a/lib/screens/auth/register.dart +++ b/lib/screens/auth/register.dart @@ -44,6 +44,7 @@ class _RegisterScreenState extends State { 'nick': nickname, 'email': email, 'password': password, + 'language': EasyLocalization.of(context)!.currentLocale.toString(), }); if (!context.mounted) return; diff --git a/lib/types/account.dart b/lib/types/account.dart index b6c52bf..9995a03 100644 --- a/lib/types/account.dart +++ b/lib/types/account.dart @@ -21,6 +21,7 @@ class SnAccount with _$SnAccount { required String name, required String nick, required Map permNodes, + required String language, required SnAccountProfile? profile, @Default([]) List badges, required DateTime? suspendedAt, diff --git a/lib/types/account.freezed.dart b/lib/types/account.freezed.dart index e4de56a..3b29f73 100644 --- a/lib/types/account.freezed.dart +++ b/lib/types/account.freezed.dart @@ -33,6 +33,7 @@ mixin _$SnAccount { String get name => throw _privateConstructorUsedError; String get nick => throw _privateConstructorUsedError; Map get permNodes => throw _privateConstructorUsedError; + String get language => throw _privateConstructorUsedError; SnAccountProfile? get profile => throw _privateConstructorUsedError; List get badges => throw _privateConstructorUsedError; DateTime? get suspendedAt => throw _privateConstructorUsedError; @@ -69,6 +70,7 @@ abstract class $SnAccountCopyWith<$Res> { String name, String nick, Map permNodes, + String language, SnAccountProfile? profile, List badges, DateTime? suspendedAt, @@ -107,6 +109,7 @@ class _$SnAccountCopyWithImpl<$Res, $Val extends SnAccount> Object? name = null, Object? nick = null, Object? permNodes = null, + Object? language = null, Object? profile = freezed, Object? badges = null, Object? suspendedAt = freezed, @@ -164,6 +167,10 @@ class _$SnAccountCopyWithImpl<$Res, $Val extends SnAccount> ? _value.permNodes : permNodes // ignore: cast_nullable_to_non_nullable as Map, + language: null == language + ? _value.language + : language // ignore: cast_nullable_to_non_nullable + as String, profile: freezed == profile ? _value.profile : profile // ignore: cast_nullable_to_non_nullable @@ -231,6 +238,7 @@ abstract class _$$SnAccountImplCopyWith<$Res> String name, String nick, Map permNodes, + String language, SnAccountProfile? profile, List badges, DateTime? suspendedAt, @@ -268,6 +276,7 @@ class __$$SnAccountImplCopyWithImpl<$Res> Object? name = null, Object? nick = null, Object? permNodes = null, + Object? language = null, Object? profile = freezed, Object? badges = null, Object? suspendedAt = freezed, @@ -325,6 +334,10 @@ class __$$SnAccountImplCopyWithImpl<$Res> ? _value._permNodes : permNodes // ignore: cast_nullable_to_non_nullable as Map, + language: null == language + ? _value.language + : language // ignore: cast_nullable_to_non_nullable + as String, profile: freezed == profile ? _value.profile : profile // ignore: cast_nullable_to_non_nullable @@ -373,6 +386,7 @@ class _$SnAccountImpl extends _SnAccount { required this.name, required this.nick, required final Map permNodes, + required this.language, required this.profile, final List badges = const [], required this.suspendedAt, @@ -429,6 +443,8 @@ class _$SnAccountImpl extends _SnAccount { return EqualUnmodifiableMapView(_permNodes); } + @override + final String language; @override final SnAccountProfile? profile; final List _badges; @@ -453,7 +469,7 @@ class _$SnAccountImpl extends _SnAccount { @override String toString() { - return 'SnAccount(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, confirmedAt: $confirmedAt, contacts: $contacts, avatar: $avatar, banner: $banner, description: $description, name: $name, nick: $nick, permNodes: $permNodes, profile: $profile, badges: $badges, suspendedAt: $suspendedAt, affiliatedId: $affiliatedId, affiliatedTo: $affiliatedTo, automatedBy: $automatedBy, automatedId: $automatedId)'; + return 'SnAccount(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, confirmedAt: $confirmedAt, contacts: $contacts, avatar: $avatar, banner: $banner, description: $description, name: $name, nick: $nick, permNodes: $permNodes, language: $language, profile: $profile, badges: $badges, suspendedAt: $suspendedAt, affiliatedId: $affiliatedId, affiliatedTo: $affiliatedTo, automatedBy: $automatedBy, automatedId: $automatedId)'; } @override @@ -479,6 +495,8 @@ class _$SnAccountImpl extends _SnAccount { (identical(other.nick, nick) || other.nick == nick) && const DeepCollectionEquality() .equals(other._permNodes, _permNodes) && + (identical(other.language, language) || + other.language == language) && (identical(other.profile, profile) || other.profile == profile) && const DeepCollectionEquality().equals(other._badges, _badges) && (identical(other.suspendedAt, suspendedAt) || @@ -509,6 +527,7 @@ class _$SnAccountImpl extends _SnAccount { name, nick, const DeepCollectionEquality().hash(_permNodes), + language, profile, const DeepCollectionEquality().hash(_badges), suspendedAt, @@ -548,6 +567,7 @@ abstract class _SnAccount extends SnAccount { required final String name, required final String nick, required final Map permNodes, + required final String language, required final SnAccountProfile? profile, final List badges, required final DateTime? suspendedAt, @@ -586,6 +606,8 @@ abstract class _SnAccount extends SnAccount { @override Map get permNodes; @override + String get language; + @override SnAccountProfile? get profile; @override List get badges; diff --git a/lib/types/account.g.dart b/lib/types/account.g.dart index 87ea7fb..def3adb 100644 --- a/lib/types/account.g.dart +++ b/lib/types/account.g.dart @@ -26,6 +26,7 @@ _$SnAccountImpl _$$SnAccountImplFromJson(Map json) => name: json['name'] as String, nick: json['nick'] as String, permNodes: json['perm_nodes'] as Map, + language: json['language'] as String, profile: json['profile'] == null ? null : SnAccountProfile.fromJson(json['profile'] as Map), @@ -56,6 +57,7 @@ Map _$$SnAccountImplToJson(_$SnAccountImpl instance) => 'name': instance.name, 'nick': instance.nick, 'perm_nodes': instance.permNodes, + 'language': instance.language, 'profile': instance.profile?.toJson(), 'badges': instance.badges.map((e) => e.toJson()).toList(), 'suspended_at': instance.suspendedAt?.toIso8601String(), diff --git a/lib/widgets/attachment/attachment_zoom.dart b/lib/widgets/attachment/attachment_zoom.dart index 35397e7..8be522d 100644 --- a/lib/widgets/attachment/attachment_zoom.dart +++ b/lib/widgets/attachment/attachment_zoom.dart @@ -152,6 +152,7 @@ class _AttachmentZoomViewState extends State { child: GestureDetector( behavior: HitTestBehavior.translucent, child: Scaffold( + backgroundColor: Colors.transparent, body: Stack( children: [ Builder(builder: (context) {