12 changed files with 332 additions and 237 deletions

No files matched your search

+27 -4
View File
@@ -2,14 +2,37 @@
import '../../l10n/app_localizations.dart'; import '../../l10n/app_localizations.dart';
const int _serverErrorCode = 500; /// 按后端 `ErrorCode` 取当前语言文案。未收录的业务码回退服务端 message。
/// 用户端不展示内部故障文案。500 与空提示改成可理解的说明,业务提示原样保留。
String userFacingApiMessage(AppLocalizations l10n, int code, String message) { String userFacingApiMessage(AppLocalizations l10n, int code, String message) {
if (code >= _serverErrorCode || message.isEmpty) return l10n.authServerError; final localized = _apiMessage(l10n, code);
if (localized != null) return localized;
if (message.isEmpty) return l10n.authServerError;
return message; return message;
} }
/// 已定义错误码。500 用服务繁忙,不展示「系统异常」。
String? _apiMessage(AppLocalizations l10n, int code) {
return switch (code) {
400 => l10n.apiErrorBadRequest,
401 => l10n.apiErrorUnauthorized,
403 => l10n.apiErrorForbidden,
404 => l10n.apiErrorNotFound,
429 => l10n.apiErrorTooManyRequests,
500 => l10n.authServerError,
100000 => l10n.apiErrorLoginFailed,
100001 => l10n.apiErrorAccountDisabled,
100002 => l10n.apiErrorCoinNotEnough,
100003 => l10n.apiErrorEpisodeUnavailable,
100004 => l10n.apiErrorEpisodeUnlocked,
100005 => l10n.apiErrorOrderInvalid,
100006 => l10n.apiErrorVipExpired,
200001 => l10n.apiErrorDataExists,
200002 => l10n.apiErrorDataNotFound,
200003 => l10n.apiErrorNotAllowed,
_ => null,
};
}
/// logic 里的错误在两种情况下是"哨兵值":本地网络异常(读不到后端提示语)与 /// logic 里的错误在两种情况下是"哨兵值":本地网络异常(读不到后端提示语)与
/// 我们自己包装的失败。这里统一翻译成当前语言的文案;服务端给的中文提示原样展示。 /// 我们自己包装的失败。这里统一翻译成当前语言的文案;服务端给的中文提示原样展示。
String errorText(AppLocalizations l10n, String? code, String fallback) { String errorText(AppLocalizations l10n, String? code, String fallback) {
+15 -6
View File
@@ -438,6 +438,21 @@
"authPasswordWeak": "Password needs both letters and numbers", "authPasswordWeak": "Password needs both letters and numbers",
"authNetworkError": "Network error, please try again", "authNetworkError": "Network error, please try again",
"authServerError": "The service is busy. Please try again shortly.", "authServerError": "The service is busy. Please try again shortly.",
"apiErrorBadRequest": "Invalid request",
"apiErrorUnauthorized": "Sign in to continue",
"apiErrorForbidden": "You don't have permission",
"apiErrorNotFound": "Not found",
"apiErrorTooManyRequests": "Too many requests. Please try again later.",
"apiErrorLoginFailed": "Incorrect account or password",
"apiErrorAccountDisabled": "This account has been disabled",
"apiErrorCoinNotEnough": "Not enough coins",
"apiErrorEpisodeUnavailable": "This episode is unavailable",
"apiErrorEpisodeUnlocked": "This episode is already unlocked",
"apiErrorOrderInvalid": "This order is already paid or closed",
"apiErrorVipExpired": "Membership has expired",
"apiErrorDataExists": "This record already exists",
"apiErrorDataNotFound": "Not found",
"apiErrorNotAllowed": "This action isn't available right now",
"authSwitchToSignUp": "New here? Create an account", "authSwitchToSignUp": "New here? Create an account",
"authSwitchToSignIn": "Already have an account? Sign in", "authSwitchToSignIn": "Already have an account? Sign in",
"authGateTitle": "Sign in to continue", "authGateTitle": "Sign in to continue",
@@ -595,10 +610,6 @@
"accountDeactivateWarning": "Requests enter a 15-day cooling-off period. All devices are signed out immediately.", "accountDeactivateWarning": "Requests enter a 15-day cooling-off period. All devices are signed out immediately.",
"accountDeactivateReason": "Reason (optional)", "accountDeactivateReason": "Reason (optional)",
"accountDeactivateReasonHint": "Add more detail here", "accountDeactivateReasonHint": "Add more detail here",
"accountDeactivateReasonUnused": "Not using it anymore",
"accountDeactivateReasonContent": "Not interested in the content",
"accountDeactivateReasonPrivacy": "Privacy concerns",
"accountDeactivateReasonOther": "Other",
"accountRequestDeactivate": "Request account deletion", "accountRequestDeactivate": "Request account deletion",
"accountConfirmDeactivateTitle": "Request account deletion?", "accountConfirmDeactivateTitle": "Request account deletion?",
"accountConfirmDeactivateMessage": "All devices will be signed out immediately. You can cancel during the 15-day cooling-off period after signing in again.", "accountConfirmDeactivateMessage": "All devices will be signed out immediately. You can cancel during the 15-day cooling-off period after signing in again.",
@@ -722,8 +733,6 @@
"homeBadgeHot": "Hot", "homeBadgeHot": "Hot",
"homeBadgeNew": "New", "homeBadgeNew": "New",
"homeBadgeAnime": "Anime", "homeBadgeAnime": "Anime",
"homeGridFilterEmpty": "No dramas match these filters",
"homeGridClearFilter": "Clear filters",
"emptyContent": "No content yet", "emptyContent": "No content yet",
"recommendTopTab": "Recommend", "recommendTopTab": "Recommend",
"playlistTopTab": "Playlists", "playlistTopTab": "Playlists",
+15 -6
View File
@@ -412,6 +412,21 @@
"authPasswordWeak": "Kata sandi perlu huruf dan angka", "authPasswordWeak": "Kata sandi perlu huruf dan angka",
"authNetworkError": "Jaringan bermasalah, coba lagi", "authNetworkError": "Jaringan bermasalah, coba lagi",
"authServerError": "Layanan sedang sibuk. Coba lagi sebentar.", "authServerError": "Layanan sedang sibuk. Coba lagi sebentar.",
"apiErrorBadRequest": "Permintaan tidak valid",
"apiErrorUnauthorized": "Masuk untuk melanjutkan",
"apiErrorForbidden": "Anda tidak memiliki izin",
"apiErrorNotFound": "Tidak ditemukan",
"apiErrorTooManyRequests": "Terlalu banyak permintaan. Coba lagi nanti.",
"apiErrorLoginFailed": "Akun atau kata sandi salah",
"apiErrorAccountDisabled": "Akun ini telah dinonaktifkan",
"apiErrorCoinNotEnough": "Koin tidak cukup",
"apiErrorEpisodeUnavailable": "Episode ini tidak tersedia",
"apiErrorEpisodeUnlocked": "Episode ini sudah terbuka",
"apiErrorOrderInvalid": "Pesanan ini sudah dibayar atau ditutup",
"apiErrorVipExpired": "Keanggotaan telah berakhir",
"apiErrorDataExists": "Data sudah ada",
"apiErrorDataNotFound": "Data tidak ditemukan",
"apiErrorNotAllowed": "Tindakan ini tidak tersedia saat ini",
"authSwitchToSignUp": "Baru di sini? Buat akun", "authSwitchToSignUp": "Baru di sini? Buat akun",
"authSwitchToSignIn": "Sudah punya akun? Masuk", "authSwitchToSignIn": "Sudah punya akun? Masuk",
"authGateTitle": "Masuk untuk melanjutkan", "authGateTitle": "Masuk untuk melanjutkan",
@@ -559,10 +574,6 @@
"accountDeactivateWarning": "Permintaan masuk masa tenang 15 hari. Semua perangkat langsung keluar.", "accountDeactivateWarning": "Permintaan masuk masa tenang 15 hari. Semua perangkat langsung keluar.",
"accountDeactivateReason": "Alasan (opsional)", "accountDeactivateReason": "Alasan (opsional)",
"accountDeactivateReasonHint": "Tambahkan penjelasan di sini", "accountDeactivateReasonHint": "Tambahkan penjelasan di sini",
"accountDeactivateReasonUnused": "Tidak lagi digunakan",
"accountDeactivateReasonContent": "Konten tidak menarik",
"accountDeactivateReasonPrivacy": "Masalah privasi",
"accountDeactivateReasonOther": "Lainnya",
"accountRequestDeactivate": "Ajukan penghapusan akun", "accountRequestDeactivate": "Ajukan penghapusan akun",
"accountConfirmDeactivateTitle": "Ajukan penghapusan akun?", "accountConfirmDeactivateTitle": "Ajukan penghapusan akun?",
"accountConfirmDeactivateMessage": "Semua perangkat akan langsung keluar. Setelah masuk lagi, kamu dapat membatalkannya selama masa tenang 15 hari.", "accountConfirmDeactivateMessage": "Semua perangkat akan langsung keluar. Setelah masuk lagi, kamu dapat membatalkannya selama masa tenang 15 hari.",
@@ -679,8 +690,6 @@
"homeBadgeHot": "Populer", "homeBadgeHot": "Populer",
"homeBadgeNew": "Baru", "homeBadgeNew": "Baru",
"homeBadgeAnime": "Anime", "homeBadgeAnime": "Anime",
"homeGridFilterEmpty": "Tidak ada drama yang cocok dengan filter",
"homeGridClearFilter": "Hapus filter",
"emptyContent": "Belum ada konten", "emptyContent": "Belum ada konten",
"recommendTopTab": "Rekomendasi", "recommendTopTab": "Rekomendasi",
"playlistTopTab": "Daftar Putar", "playlistTopTab": "Daftar Putar",
+90 -36
View File
@@ -1906,6 +1906,96 @@ abstract class AppLocalizations {
/// **'The service is busy. Please try again shortly.'** /// **'The service is busy. Please try again shortly.'**
String get authServerError; String get authServerError;
/// No description provided for @apiErrorBadRequest.
///
/// In en, this message translates to:
/// **'Invalid request'**
String get apiErrorBadRequest;
/// No description provided for @apiErrorUnauthorized.
///
/// In en, this message translates to:
/// **'Sign in to continue'**
String get apiErrorUnauthorized;
/// No description provided for @apiErrorForbidden.
///
/// In en, this message translates to:
/// **'You don\'t have permission'**
String get apiErrorForbidden;
/// No description provided for @apiErrorNotFound.
///
/// In en, this message translates to:
/// **'Not found'**
String get apiErrorNotFound;
/// No description provided for @apiErrorTooManyRequests.
///
/// In en, this message translates to:
/// **'Too many requests. Please try again later.'**
String get apiErrorTooManyRequests;
/// No description provided for @apiErrorLoginFailed.
///
/// In en, this message translates to:
/// **'Incorrect account or password'**
String get apiErrorLoginFailed;
/// No description provided for @apiErrorAccountDisabled.
///
/// In en, this message translates to:
/// **'This account has been disabled'**
String get apiErrorAccountDisabled;
/// No description provided for @apiErrorCoinNotEnough.
///
/// In en, this message translates to:
/// **'Not enough coins'**
String get apiErrorCoinNotEnough;
/// No description provided for @apiErrorEpisodeUnavailable.
///
/// In en, this message translates to:
/// **'This episode is unavailable'**
String get apiErrorEpisodeUnavailable;
/// No description provided for @apiErrorEpisodeUnlocked.
///
/// In en, this message translates to:
/// **'This episode is already unlocked'**
String get apiErrorEpisodeUnlocked;
/// No description provided for @apiErrorOrderInvalid.
///
/// In en, this message translates to:
/// **'This order is already paid or closed'**
String get apiErrorOrderInvalid;
/// No description provided for @apiErrorVipExpired.
///
/// In en, this message translates to:
/// **'Membership has expired'**
String get apiErrorVipExpired;
/// No description provided for @apiErrorDataExists.
///
/// In en, this message translates to:
/// **'This record already exists'**
String get apiErrorDataExists;
/// No description provided for @apiErrorDataNotFound.
///
/// In en, this message translates to:
/// **'Not found'**
String get apiErrorDataNotFound;
/// No description provided for @apiErrorNotAllowed.
///
/// In en, this message translates to:
/// **'This action isn\'t available right now'**
String get apiErrorNotAllowed;
/// No description provided for @authSwitchToSignUp. /// No description provided for @authSwitchToSignUp.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -2488,30 +2578,6 @@ abstract class AppLocalizations {
/// **'Add more detail here'** /// **'Add more detail here'**
String get accountDeactivateReasonHint; String get accountDeactivateReasonHint;
/// No description provided for @accountDeactivateReasonUnused.
///
/// In en, this message translates to:
/// **'Not using it anymore'**
String get accountDeactivateReasonUnused;
/// No description provided for @accountDeactivateReasonContent.
///
/// In en, this message translates to:
/// **'Not interested in the content'**
String get accountDeactivateReasonContent;
/// No description provided for @accountDeactivateReasonPrivacy.
///
/// In en, this message translates to:
/// **'Privacy concerns'**
String get accountDeactivateReasonPrivacy;
/// No description provided for @accountDeactivateReasonOther.
///
/// In en, this message translates to:
/// **'Other'**
String get accountDeactivateReasonOther;
/// No description provided for @accountRequestDeactivate. /// No description provided for @accountRequestDeactivate.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -3136,18 +3202,6 @@ abstract class AppLocalizations {
/// **'Anime'** /// **'Anime'**
String get homeBadgeAnime; String get homeBadgeAnime;
/// No description provided for @homeGridFilterEmpty.
///
/// In en, this message translates to:
/// **'No dramas match these filters'**
String get homeGridFilterEmpty;
/// No description provided for @homeGridClearFilter.
///
/// In en, this message translates to:
/// **'Clear filters'**
String get homeGridClearFilter;
/// No description provided for @emptyContent. /// No description provided for @emptyContent.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
+46 -18
View File
@@ -968,6 +968,52 @@ class AppLocalizationsEn extends AppLocalizations {
String get authServerError => String get authServerError =>
'The service is busy. Please try again shortly.'; 'The service is busy. Please try again shortly.';
@override
String get apiErrorBadRequest => 'Invalid request';
@override
String get apiErrorUnauthorized => 'Sign in to continue';
@override
String get apiErrorForbidden => 'You don\'t have permission';
@override
String get apiErrorNotFound => 'Not found';
@override
String get apiErrorTooManyRequests =>
'Too many requests. Please try again later.';
@override
String get apiErrorLoginFailed => 'Incorrect account or password';
@override
String get apiErrorAccountDisabled => 'This account has been disabled';
@override
String get apiErrorCoinNotEnough => 'Not enough coins';
@override
String get apiErrorEpisodeUnavailable => 'This episode is unavailable';
@override
String get apiErrorEpisodeUnlocked => 'This episode is already unlocked';
@override
String get apiErrorOrderInvalid => 'This order is already paid or closed';
@override
String get apiErrorVipExpired => 'Membership has expired';
@override
String get apiErrorDataExists => 'This record already exists';
@override
String get apiErrorDataNotFound => 'Not found';
@override
String get apiErrorNotAllowed => 'This action isn\'t available right now';
@override @override
String get authSwitchToSignUp => 'New here? Create an account'; String get authSwitchToSignUp => 'New here? Create an account';
@@ -1289,18 +1335,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get accountDeactivateReasonHint => 'Add more detail here'; String get accountDeactivateReasonHint => 'Add more detail here';
@override
String get accountDeactivateReasonUnused => 'Not using it anymore';
@override
String get accountDeactivateReasonContent => 'Not interested in the content';
@override
String get accountDeactivateReasonPrivacy => 'Privacy concerns';
@override
String get accountDeactivateReasonOther => 'Other';
@override @override
String get accountRequestDeactivate => 'Request account deletion'; String get accountRequestDeactivate => 'Request account deletion';
@@ -1638,12 +1672,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get homeBadgeAnime => 'Anime'; String get homeBadgeAnime => 'Anime';
@override
String get homeGridFilterEmpty => 'No dramas match these filters';
@override
String get homeGridClearFilter => 'Clear filters';
@override @override
String get emptyContent => 'No content yet'; String get emptyContent => 'No content yet';
+46 -18
View File
@@ -971,6 +971,52 @@ class AppLocalizationsId extends AppLocalizations {
@override @override
String get authServerError => 'Layanan sedang sibuk. Coba lagi sebentar.'; String get authServerError => 'Layanan sedang sibuk. Coba lagi sebentar.';
@override
String get apiErrorBadRequest => 'Permintaan tidak valid';
@override
String get apiErrorUnauthorized => 'Masuk untuk melanjutkan';
@override
String get apiErrorForbidden => 'Anda tidak memiliki izin';
@override
String get apiErrorNotFound => 'Tidak ditemukan';
@override
String get apiErrorTooManyRequests =>
'Terlalu banyak permintaan. Coba lagi nanti.';
@override
String get apiErrorLoginFailed => 'Akun atau kata sandi salah';
@override
String get apiErrorAccountDisabled => 'Akun ini telah dinonaktifkan';
@override
String get apiErrorCoinNotEnough => 'Koin tidak cukup';
@override
String get apiErrorEpisodeUnavailable => 'Episode ini tidak tersedia';
@override
String get apiErrorEpisodeUnlocked => 'Episode ini sudah terbuka';
@override
String get apiErrorOrderInvalid => 'Pesanan ini sudah dibayar atau ditutup';
@override
String get apiErrorVipExpired => 'Keanggotaan telah berakhir';
@override
String get apiErrorDataExists => 'Data sudah ada';
@override
String get apiErrorDataNotFound => 'Data tidak ditemukan';
@override
String get apiErrorNotAllowed => 'Tindakan ini tidak tersedia saat ini';
@override @override
String get authSwitchToSignUp => 'Baru di sini? Buat akun'; String get authSwitchToSignUp => 'Baru di sini? Buat akun';
@@ -1288,18 +1334,6 @@ class AppLocalizationsId extends AppLocalizations {
@override @override
String get accountDeactivateReasonHint => 'Tambahkan penjelasan di sini'; String get accountDeactivateReasonHint => 'Tambahkan penjelasan di sini';
@override
String get accountDeactivateReasonUnused => 'Tidak lagi digunakan';
@override
String get accountDeactivateReasonContent => 'Konten tidak menarik';
@override
String get accountDeactivateReasonPrivacy => 'Masalah privasi';
@override
String get accountDeactivateReasonOther => 'Lainnya';
@override @override
String get accountRequestDeactivate => 'Ajukan penghapusan akun'; String get accountRequestDeactivate => 'Ajukan penghapusan akun';
@@ -1638,12 +1672,6 @@ class AppLocalizationsId extends AppLocalizations {
@override @override
String get homeBadgeAnime => 'Anime'; String get homeBadgeAnime => 'Anime';
@override
String get homeGridFilterEmpty => 'Tidak ada drama yang cocok dengan filter';
@override
String get homeGridClearFilter => 'Hapus filter';
@override @override
String get emptyContent => 'Belum ada konten'; String get emptyContent => 'Belum ada konten';
+45 -18
View File
@@ -955,6 +955,51 @@ class AppLocalizationsZh extends AppLocalizations {
@override @override
String get authServerError => '服务繁忙,请稍后再试'; String get authServerError => '服务繁忙,请稍后再试';
@override
String get apiErrorBadRequest => '参数校验失败';
@override
String get apiErrorUnauthorized => '未登录或登录已过期';
@override
String get apiErrorForbidden => '无权限';
@override
String get apiErrorNotFound => '资源不存在';
@override
String get apiErrorTooManyRequests => '请求过于频繁,请稍后再试';
@override
String get apiErrorLoginFailed => '账号或密码错误';
@override
String get apiErrorAccountDisabled => '账号已停用';
@override
String get apiErrorCoinNotEnough => '金币余额不足';
@override
String get apiErrorEpisodeUnavailable => '剧集未发布或不存在';
@override
String get apiErrorEpisodeUnlocked => '剧集已解锁';
@override
String get apiErrorOrderInvalid => '订单已支付或已关闭';
@override
String get apiErrorVipExpired => '会员已过期';
@override
String get apiErrorDataExists => '数据已存在';
@override
String get apiErrorDataNotFound => '数据不存在';
@override
String get apiErrorNotAllowed => '当前状态不允许该操作';
@override @override
String get authSwitchToSignUp => '还没有账号?去注册'; String get authSwitchToSignUp => '还没有账号?去注册';
@@ -1266,18 +1311,6 @@ class AppLocalizationsZh extends AppLocalizations {
@override @override
String get accountDeactivateReasonHint => '也可以在这里补充说明'; String get accountDeactivateReasonHint => '也可以在这里补充说明';
@override
String get accountDeactivateReasonUnused => '不再使用';
@override
String get accountDeactivateReasonContent => '内容不感兴趣';
@override
String get accountDeactivateReasonPrivacy => '隐私顾虑';
@override
String get accountDeactivateReasonOther => '其他';
@override @override
String get accountRequestDeactivate => '申请注销账号'; String get accountRequestDeactivate => '申请注销账号';
@@ -1613,12 +1646,6 @@ class AppLocalizationsZh extends AppLocalizations {
@override @override
String get homeBadgeAnime => '漫剧'; String get homeBadgeAnime => '漫剧';
@override
String get homeGridFilterEmpty => '没有符合筛选条件的短剧';
@override
String get homeGridClearFilter => '清除筛选';
@override @override
String get emptyContent => '暂无内容'; String get emptyContent => '暂无内容';
+15 -6
View File
@@ -374,6 +374,21 @@
"authPasswordWeak": "密码需同时包含字母和数字", "authPasswordWeak": "密码需同时包含字母和数字",
"authNetworkError": "网络异常,请稍后重试", "authNetworkError": "网络异常,请稍后重试",
"authServerError": "服务繁忙,请稍后再试", "authServerError": "服务繁忙,请稍后再试",
"apiErrorBadRequest": "参数校验失败",
"apiErrorUnauthorized": "未登录或登录已过期",
"apiErrorForbidden": "无权限",
"apiErrorNotFound": "资源不存在",
"apiErrorTooManyRequests": "请求过于频繁,请稍后再试",
"apiErrorLoginFailed": "账号或密码错误",
"apiErrorAccountDisabled": "账号已停用",
"apiErrorCoinNotEnough": "金币余额不足",
"apiErrorEpisodeUnavailable": "剧集未发布或不存在",
"apiErrorEpisodeUnlocked": "剧集已解锁",
"apiErrorOrderInvalid": "订单已支付或已关闭",
"apiErrorVipExpired": "会员已过期",
"apiErrorDataExists": "数据已存在",
"apiErrorDataNotFound": "数据不存在",
"apiErrorNotAllowed": "当前状态不允许该操作",
"authSwitchToSignUp": "还没有账号?去注册", "authSwitchToSignUp": "还没有账号?去注册",
"authSwitchToSignIn": "已有账号?去登录", "authSwitchToSignIn": "已有账号?去登录",
"authGateTitle": "登录后可继续", "authGateTitle": "登录后可继续",
@@ -521,10 +536,6 @@
"accountDeactivateWarning": "提交后进入 15 天冷静期,所有设备会立即退出登录。", "accountDeactivateWarning": "提交后进入 15 天冷静期,所有设备会立即退出登录。",
"accountDeactivateReason": "注销原因(选填)", "accountDeactivateReason": "注销原因(选填)",
"accountDeactivateReasonHint": "也可以在这里补充说明", "accountDeactivateReasonHint": "也可以在这里补充说明",
"accountDeactivateReasonUnused": "不再使用",
"accountDeactivateReasonContent": "内容不感兴趣",
"accountDeactivateReasonPrivacy": "隐私顾虑",
"accountDeactivateReasonOther": "其他",
"accountRequestDeactivate": "申请注销账号", "accountRequestDeactivate": "申请注销账号",
"accountConfirmDeactivateTitle": "确认申请注销?", "accountConfirmDeactivateTitle": "确认申请注销?",
"accountConfirmDeactivateMessage": "所有设备会立即退出登录。重新登录后,可在 15 天冷静期内撤销申请。", "accountConfirmDeactivateMessage": "所有设备会立即退出登录。重新登录后,可在 15 天冷静期内撤销申请。",
@@ -641,8 +652,6 @@
"homeBadgeHot": "热剧", "homeBadgeHot": "热剧",
"homeBadgeNew": "新剧", "homeBadgeNew": "新剧",
"homeBadgeAnime": "漫剧", "homeBadgeAnime": "漫剧",
"homeGridFilterEmpty": "没有符合筛选条件的短剧",
"homeGridClearFilter": "清除筛选",
"emptyContent": "暂无内容", "emptyContent": "暂无内容",
"recommendTopTab": "推荐", "recommendTopTab": "推荐",
"playlistTopTab": "剧单", "playlistTopTab": "剧单",
@@ -1,106 +0,0 @@
// Author: fruit.com.cn
import 'package:flutter/material.dart';
import '../../core/theme/app_colors.dart';
import '../../core/theme/app_dimens.dart';
import '../../l10n/app_localizations.dart';
import 'account_deactivate_logic.dart';
/// 注销原因:固定选项在标题下换行排列,补充说明留在输入框内。
class DeactivateReasonField extends StatelessWidget {
const DeactivateReasonField({
super.key,
required this.controller,
required this.onChanged,
});
final TextEditingController controller;
final VoidCallback onChanged;
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final options = _options(l10n);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.accountDeactivateReason,
style: const TextStyle(
color: AppColors.textPrimary,
fontSize: AppDimens.listTitle,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: AppDimens.spaceSm),
Wrap(
spacing: AppDimens.spaceSm,
runSpacing: AppDimens.spaceSm,
children: [
for (var index = 0; index < options.length; index++)
_chip(options[index], index, options),
],
),
const SizedBox(height: AppDimens.spaceMd),
TextField(
key: const Key('account-deactivate-reason'),
controller: controller,
maxLength: AccountDeactivateLogic.maxReasonLength,
minLines: 3,
maxLines: 4,
textAlignVertical: TextAlignVertical.top,
onChanged: (_) => onChanged(),
decoration: InputDecoration(
hintText: l10n.accountDeactivateReasonHint,
floatingLabelBehavior: FloatingLabelBehavior.never,
alignLabelWithHint: true,
contentPadding: const EdgeInsets.all(AppDimens.cardPadding),
),
),
],
);
}
Widget _chip(_ReasonOption option, int index, List<_ReasonOption> options) {
final selected = option.custom
? _customSelected(options)
: controller.text == option.label;
return ChoiceChip(
key: Key('account-deactivate-reason-$index'),
label: Text(option.label),
selected: selected,
onSelected: (_) => _select(option),
);
}
bool _customSelected(List<_ReasonOption> options) {
final text = controller.text.trim();
if (text.isEmpty) return false;
return options
.where((item) => !item.custom)
.every((item) => item.label != text);
}
void _select(_ReasonOption option) {
controller.text = option.custom ? '' : option.label;
controller.selection = TextSelection.collapsed(
offset: controller.text.length,
);
onChanged();
}
List<_ReasonOption> _options(AppLocalizations l10n) => [
_ReasonOption(l10n.accountDeactivateReasonUnused),
_ReasonOption(l10n.accountDeactivateReasonContent),
_ReasonOption(l10n.accountDeactivateReasonPrivacy),
_ReasonOption(l10n.accountDeactivateReasonOther, custom: true),
];
}
class _ReasonOption {
const _ReasonOption(this.label, {this.custom = false});
final String label;
final bool custom;
}
+3 -2
View File
@@ -8,6 +8,7 @@ import 'package:provider/provider.dart';
import '../../core/data/remote/api_client.dart'; import '../../core/data/remote/api_client.dart';
import '../../core/data/services/auth_controller.dart'; import '../../core/data/services/auth_controller.dart';
import '../../core/formatting/error_text.dart';
import '../../core/platform/haptics.dart'; import '../../core/platform/haptics.dart';
import '../../core/platform/keyboard_inset.dart'; import '../../core/platform/keyboard_inset.dart';
import '../../core/theme/app_colors.dart'; import '../../core/theme/app_colors.dart';
@@ -180,7 +181,7 @@ class _PhoneAuthSheetState extends State<PhoneAuthSheet> {
).showSnackBar(SnackBar(content: Text(l10n.authCodeSent))); ).showSnackBar(SnackBar(content: Text(l10n.authCodeSent)));
_codeFocus.requestFocus(); _codeFocus.requestFocus();
} on ApiException catch (error) { } on ApiException catch (error) {
failureMessage = error.message; failureMessage = userFacingApiMessage(l10n, error.code, error.message);
} catch (_) { } catch (_) {
failureMessage = l10n.authNetworkError; failureMessage = l10n.authNetworkError;
} finally { } finally {
@@ -228,7 +229,7 @@ class _PhoneAuthSheetState extends State<PhoneAuthSheet> {
} }
if (mounted) Navigator.of(context).pop(true); if (mounted) Navigator.of(context).pop(true);
} on ApiException catch (error) { } on ApiException catch (error) {
failureMessage = error.message; failureMessage = userFacingApiMessage(l10n, error.code, error.message);
} catch (_) { } catch (_) {
failureMessage = l10n.authNetworkError; failureMessage = l10n.authNetworkError;
} finally { } finally {
+1 -17
View File
@@ -71,23 +71,7 @@ void main() {
); );
final l10n = await AppLocalizations.delegate.load(const Locale('en')); final l10n = await AppLocalizations.delegate.load(const Locale('en'));
final titleTop = tester expect(find.text(l10n.accountDeactivateReason), findsOneWidget);
.getTopLeft(find.text(l10n.accountDeactivateReason))
.dy;
final fieldTop = tester
.getTopLeft(find.byKey(const Key('account-deactivate-reason')))
.dy;
expect(fieldTop, greaterThan(titleTop));
expect(find.text(l10n.accountDeactivateReasonUnused), findsOneWidget);
await tester.tap(find.byKey(const Key('account-deactivate-reason-0')));
await tester.pumpAndSettle();
expect(
tester
.widget<TextField>(find.byKey(const Key('account-deactivate-reason')))
.controller
?.text,
l10n.accountDeactivateReasonUnused,
);
await tester.enterText( await tester.enterText(
find.byKey(const Key('account-deactivate-reason')), find.byKey(const Key('account-deactivate-reason')),
'不再使用', '不再使用',
+29
View File
@@ -0,0 +1,29 @@
// Author: fruit.com.cn
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:fruitshorts/core/formatting/error_text.dart';
import 'package:fruitshorts/l10n/app_localizations.dart';
void main() {
test('should_keep_login_failure_message_when_code_is_business', () async {
final l10n = await AppLocalizations.delegate.load(const Locale('zh'));
expect(
userFacingApiMessage(l10n, 100000, '账号或密码错误'),
'账号或密码错误',
);
expect(userFacingApiMessage(l10n, 500, '系统异常'), l10n.authServerError);
final english = await AppLocalizations.delegate.load(const Locale('en'));
expect(
userFacingApiMessage(english, 100000, '账号或密码错误'),
'Incorrect account or password',
);
final indonesian = await AppLocalizations.delegate.load(const Locale('id'));
expect(
userFacingApiMessage(indonesian, 100000, '账号或密码错误'),
'Akun atau kata sandi salah',
);
});
}