feat: 更新AppSettingFeature以增强用户体验和本地化支持

- 在AppSettingFeature中新增登出确认和关于我们弹窗的状态和Action。
- 更新AppSettingView以支持登出确认和关于我们弹窗的逻辑。
- 替换多个视图中的NSLocalizedString为LocalizedString,提升本地化一致性。
- 在Localizable.strings中新增相关本地化文本,确保多语言支持。
This commit is contained in:
edwinQQQ
2025-07-31 18:29:03 +08:00
parent 17ad000e4b
commit 01779a95c8
12 changed files with 244 additions and 91 deletions

View File

@@ -47,6 +47,10 @@ struct AppSettingFeature {
var showImageSourceActionSheet: Bool = false
//
var selectedImageSource: AppImageSource? = nil
//
var showLogoutConfirmation: Bool = false
var showAboutUs: Bool = false
}
enum Action: Equatable {
@@ -87,6 +91,11 @@ struct AppSettingFeature {
case setShowImageSourceActionSheet(Bool)
//
case selectImageSource(AppImageSource)
//
case showLogoutConfirmation(Bool)
case showAboutUs(Bool)
case logoutConfirmed
}
@Dependency(\.apiService) var apiService
@@ -101,6 +110,11 @@ struct AppSettingFeature {
return .none
case .logoutTapped:
//
state.showLogoutConfirmation = true
return .none
case .logoutConfirmed:
//
return .run { send in
await UserInfoManager.clearAllAuthenticationData()
@@ -162,7 +176,7 @@ struct AppSettingFeature {
return .none
case .aboutUsTapped:
//
state.showAboutUs = true
return .none
case .deactivateAccountTapped:
@@ -277,6 +291,14 @@ struct AppSettingFeature {
state.selectedImageSource = source
// ImagePickerWithPreviewView
return .none
case .showLogoutConfirmation(let show):
state.showLogoutConfirmation = show
return .none
case .showAboutUs(let show):
state.showAboutUs = show
return .none
}
}
}

View File

@@ -55,12 +55,12 @@ struct EMailLoginFeature {
case .getVerificationCodeTapped:
guard !state.email.isEmpty else {
state.errorMessage = NSLocalizedString("email_login.email_required", comment: "")
state.errorMessage = LocalizedStringSync("email_login.email_required", comment: "")
return .none
}
guard ValidationHelper.isValidEmail(state.email) else {
state.errorMessage = NSLocalizedString("email_login.invalid_email", comment: "")
state.errorMessage = LocalizedStringSync("email_login.invalid_email", comment: "")
return .none
}
@@ -105,12 +105,12 @@ struct EMailLoginFeature {
case .loginButtonTapped(let email, let verificationCode):
guard !email.isEmpty && !verificationCode.isEmpty else {
state.errorMessage = NSLocalizedString("email_login.fields_required", comment: "")
state.errorMessage = LocalizedStringSync("email_login.fields_required", comment: "")
return .none
}
guard ValidationHelper.isValidEmail(email) else {
state.errorMessage = NSLocalizedString("email_login.invalid_email", comment: "")
state.errorMessage = LocalizedStringSync("email_login.invalid_email", comment: "")
return .none
}

View File

@@ -57,12 +57,12 @@ struct RecoverPasswordFeature {
case .getVerificationCodeTapped:
guard !state.email.isEmpty else {
state.errorMessage = NSLocalizedString("recover_password.email_required", comment: "")
state.errorMessage = LocalizedStringSync("recover_password.email_required", comment: "")
return .none
}
guard ValidationHelper.isValidEmail(state.email) else {
state.errorMessage = NSLocalizedString("recover_password.invalid_email", comment: "")
state.errorMessage = LocalizedStringSync("recover_password.invalid_email", comment: "")
return .none
}
@@ -101,23 +101,23 @@ struct RecoverPasswordFeature {
if let apiError = error as? APIError {
state.errorMessage = apiError.localizedDescription
} else {
state.errorMessage = NSLocalizedString("recover_password.code_send_failed", comment: "")
state.errorMessage = LocalizedStringSync("recover_password.code_send_failed", comment: "")
}
return .none
case .resetPasswordTapped:
guard !state.email.isEmpty && !state.verificationCode.isEmpty && !state.newPassword.isEmpty else {
state.errorMessage = NSLocalizedString("recover_password.fields_required", comment: "")
state.errorMessage = LocalizedStringSync("recover_password.fields_required", comment: "")
return .none
}
guard ValidationHelper.isValidEmail(state.email) else {
state.errorMessage = NSLocalizedString("recover_password.invalid_email", comment: "")
state.errorMessage = LocalizedStringSync("recover_password.invalid_email", comment: "")
return .none
}
guard ValidationHelper.isValidPassword(state.newPassword) else {
state.errorMessage = NSLocalizedString("recover_password.invalid_password", comment: "")
state.errorMessage = LocalizedStringSync("recover_password.invalid_password", comment: "")
return .none
}
@@ -160,7 +160,7 @@ struct RecoverPasswordFeature {
if let apiError = error as? APIError {
state.errorMessage = apiError.localizedDescription
} else {
state.errorMessage = NSLocalizedString("recover_password.reset_failed", comment: "")
state.errorMessage = LocalizedStringSync("recover_password.reset_failed", comment: "")
}
return .none
@@ -199,7 +199,7 @@ struct ResetPasswordResponse: Codable, Equatable {
///
var errorMessage: String {
return message ?? NSLocalizedString("recover_password.reset_failed", comment: "")
return message ?? LocalizedStringSync("recover_password.reset_failed", comment: "")
}
}

View File

@@ -37,6 +37,9 @@
"id_login.forgot_password" = "Forgot Password?";
"id_login.login_button" = "Login";
"id_login.logging_in" = "Logging in...";
"id_login.password" = "Password";
"id_login.login" = "Login";
"id_login.user_id" = "User ID";
// MARK: - Email Login Page
"email_login.title" = "Email Login";
@@ -48,6 +51,9 @@
"email_login.code_sent" = "Verification code sent";
"email_login.login_button" = "Login";
"email_login.logging_in" = "Logging in...";
"email_login.email" = "Email";
"email_login.verification_code" = "Verification Code";
"email_login.login" = "Login";
"placeholder.enter_email" = "Please enter email";
"placeholder.enter_verification_code" = "Please enter verification code";
@@ -129,6 +135,10 @@
"appSetting.checkUpdates" = "Check for Updates";
"appSetting.logout" = "Log Out";
"appSetting.aboutUs" = "About Us";
"appSetting.aboutUs.title" = "About Us";
"appSetting.logoutConfirmation.title" = "Confirm Logout";
"appSetting.logoutConfirmation.confirm" = "Confirm Logout";
"appSetting.logoutConfirmation.message" = "Are you sure you want to logout from your current account?";
"appSetting.deactivateAccount" = "Deactivate Account";
"appSetting.logoutAccount" = "Log out of account";
@@ -206,4 +216,8 @@
"config.last_updated" = "Last Updated: %@";
"config.click_to_load" = "Click the button below to load configuration";
"config.use_new_tca" = "Use new TCA API component";
"config.clear_error" = "Clear Error";
"config.clear_error" = "Clear Error";
"config.version" = "Version";
"config.debug_mode" = "Debug Mode";
"config.api_timeout" = "API Timeout";
"config.max_retries" = "Max Retries";

View File

@@ -38,6 +38,9 @@
"id_login.forgot_password" = "忘记密码?";
"id_login.login_button" = "登录";
"id_login.logging_in" = "登录中...";
"id_login.password" = "密码";
"id_login.login" = "登录";
"id_login.user_id" = "用户ID";
// MARK: - 邮箱登录页面
"email_login.title" = "邮箱登录";
@@ -49,6 +52,9 @@
"email_login.code_sent" = "验证码已发送";
"email_login.login_button" = "登录";
"email_login.logging_in" = "登录中...";
"email_login.email" = "邮箱";
"email_login.verification_code" = "验证码";
"email_login.login" = "登录";
"placeholder.enter_email" = "请输入邮箱";
"placeholder.enter_verification_code" = "请输入验证码";
@@ -125,6 +131,10 @@
"appSetting.checkUpdates" = "检查更新";
"appSetting.logout" = "退出登录";
"appSetting.aboutUs" = "关于我们";
"appSetting.aboutUs.title" = "关于我们";
"appSetting.logoutConfirmation.title" = "确认退出";
"appSetting.logoutConfirmation.confirm" = "确认退出";
"appSetting.logoutConfirmation.message" = "确定要退出当前账户吗?";
"appSetting.deactivateAccount" = "注销帐号";
"appSetting.logoutAccount" = "退出账户";
@@ -203,3 +213,7 @@
"config.click_to_load" = "点击下方按钮加载配置";
"config.use_new_tca" = "使用新的 TCA API 组件";
"config.clear_error" = "清除错误";
"config.version" = "版本";
"config.debug_mode" = "调试模式";
"config.api_timeout" = "API 超时";
"config.max_retries" = "最大重试次数";

View File

@@ -46,6 +46,8 @@ class LocalizationManager: ObservableObject {
} catch {
debugErrorSync("❌ 保存语言设置失败: \(error)")
}
// UserDefaults 使
UserDefaults.standard.set(currentLanguage.rawValue, forKey: "AppLanguage")
//
objectWillChange.send()
}
@@ -67,6 +69,9 @@ class LocalizationManager: ObservableObject {
// 使
self.currentLanguage = Self.getSystemPreferredLanguage()
}
// UserDefaults
UserDefaults.standard.set(self.currentLanguage.rawValue, forKey: "AppLanguage")
}
// MARK: -
@@ -150,6 +155,26 @@ func LocalizedString(_ key: String, comment: String = "") -> String {
return LocalizationManager.shared.localizedString(key)
}
///
/// TCA reducer
/// - Parameters:
/// - key: key
/// - comment: NSLocalizedString
/// - Returns:
func LocalizedStringSync(_ key: String, comment: String = "") -> String {
// UserDefaults @MainActor
let currentLanguage = UserDefaults.standard.string(forKey: "AppLanguage") ?? "en"
//
guard let path = Bundle.main.path(forResource: currentLanguage, ofType: "lproj"),
let bundle = Bundle(path: path) else {
// key
return NSLocalizedString(key, comment: comment)
}
return NSLocalizedString(key, bundle: bundle, comment: comment)
}
// MARK: - LocalizedTextModifier
///
struct LocalizedTextModifier: ViewModifier {

View File

@@ -29,7 +29,7 @@ struct AppSettingView: View {
@ViewBuilder
private func mainView() -> some View {
WithPerceptionTracking {
GeometryReader { geometry in
let baseView = GeometryReader { geometry in
ZStack {
//
Image("bg")
@@ -93,68 +93,143 @@ struct AppSettingView: View {
}
}
.navigationBarHidden(true)
// ActionSheet
.confirmationDialog(
"请选择图片来源",
isPresented: Binding(
get: { store.showImageSourceActionSheet },
set: { store.send(.setShowImageSourceActionSheet($0)) }
),
titleVisibility: .visible
) {
Button(LocalizedString("app_settings.take_photo", comment: "拍照")) {
store.send(.selectImageSource(AppImageSource.camera))
//
pickerStore.send(.inner(.selectSource(.camera)))
let viewWithActionSheet = baseView
.confirmationDialog(
"请选择图片来源",
isPresented: Binding(
get: { store.showImageSourceActionSheet },
set: { store.send(.setShowImageSourceActionSheet($0)) }
),
titleVisibility: .visible
) {
Button(LocalizedString("app_settings.take_photo", comment: "拍照")) {
store.send(.selectImageSource(AppImageSource.camera))
//
pickerStore.send(.inner(.selectSource(.camera)))
}
Button(LocalizedString("app_settings.select_from_album", comment: "从相册选择")) {
store.send(.selectImageSource(AppImageSource.photoLibrary))
//
pickerStore.send(.inner(.selectSource(.photoLibrary)))
}
Button(LocalizedString("common.cancel", comment: "取消"), role: .cancel) { }
}
Button(LocalizedString("app_settings.select_from_album", comment: "从相册选择")) {
store.send(.selectImageSource(AppImageSource.photoLibrary))
//
pickerStore.send(.inner(.selectSource(.photoLibrary)))
}
Button(LocalizedString("common.cancel", comment: "取消"), role: .cancel) { }
}
//
.sheet(isPresented: Binding(
get: { store.showImagePicker },
set: { store.send(.setShowImagePicker($0)) }
)) {
ImagePickerWithPreviewView(
store: pickerStore,
onUpload: { images in
if let firstImage = images.first,
let imageData = firstImage.jpegData(compressionQuality: 0.8) {
store.send(.avatarSelected(imageData))
let viewWithImagePicker = viewWithActionSheet
.sheet(isPresented: Binding(
get: { store.showImagePicker },
set: { store.send(.setShowImagePicker($0)) }
)) {
ImagePickerWithPreviewView(
store: pickerStore,
onUpload: { images in
if let firstImage = images.first,
let imageData = firstImage.jpegData(compressionQuality: 0.8) {
store.send(.avatarSelected(imageData))
}
store.send(.setShowImagePicker(false))
},
onCancel: {
store.send(.setShowImagePicker(false))
}
store.send(.setShowImagePicker(false))
},
onCancel: {
store.send(.setShowImagePicker(false))
)
}
let viewWithAlert = viewWithImagePicker
.alert(LocalizedString("appSetting.nickname", comment: "编辑昵称"), isPresented: Binding(
get: { store.isEditingNickname },
set: { store.send(.nicknameEditAlert($0)) }
)) {
TextField(LocalizedString("appSetting.nickname", comment: "请输入昵称"), text: Binding(
get: { store.nicknameInput },
set: { store.send(.nicknameInputChanged($0)) }
))
Button(LocalizedString("common.cancel", comment: "取消")) {
store.send(.nicknameEditAlert(false))
}
Button(LocalizedString("common.confirm", comment: "确认")) {
let trimmed = store.nicknameInput.trimmingCharacters(in: .whitespacesAndNewlines)
if !trimmed.isEmpty {
store.send(.nicknameEditConfirmed(trimmed))
}
store.send(.nicknameEditAlert(false))
}
} message: {
Text(LocalizedString("appSetting.nickname", comment: "请输入新的昵称"))
}
let viewWithPrivacyPolicy = viewWithAlert
.webView(
isPresented: Binding(
get: { store.showPrivacyPolicy },
set: { isPresented in
if !isPresented {
store.send(.privacyPolicyDismissed)
}
}
),
url: APIConfiguration.webURL(for: .privacyPolicy)
)
}
//
.alert(LocalizedString("appSetting.nickname", comment: "编辑昵称"), isPresented: Binding(
get: { store.isEditingNickname },
set: { store.send(.nicknameEditAlert($0)) }
)) {
TextField(LocalizedString("appSetting.nickname", comment: "请输入昵称"), text: Binding(
get: { store.nicknameInput },
set: { store.send(.nicknameInputChanged($0)) }
))
Button(LocalizedString("common.cancel", comment: "取消")) {
store.send(.nicknameEditAlert(false))
}
Button(LocalizedString("common.confirm", comment: "确认")) {
let trimmed = store.nicknameInput.trimmingCharacters(in: .whitespacesAndNewlines)
if !trimmed.isEmpty {
store.send(.nicknameEditConfirmed(trimmed))
let viewWithUserAgreement = viewWithPrivacyPolicy
.webView(
isPresented: Binding(
get: { store.showUserAgreement },
set: { isPresented in
if !isPresented {
store.send(.userAgreementDismissed)
}
}
),
url: APIConfiguration.webURL(for: .userAgreement)
)
let viewWithDeactivateAccount = viewWithUserAgreement
.webView(
isPresented: Binding(
get: { store.showDeactivateAccount },
set: { isPresented in
if !isPresented {
store.send(.deactivateAccountDismissed)
}
}
),
url: APIConfiguration.webURL(for: .deactivateAccount)
)
viewWithDeactivateAccount
//
.alert(LocalizedString("appSetting.logoutConfirmation.title", comment: "确认退出"), isPresented: Binding(
get: { store.showLogoutConfirmation },
set: { store.send(.showLogoutConfirmation($0)) }
)) {
Button(LocalizedString("common.cancel", comment: "取消"), role: .cancel) {
store.send(.showLogoutConfirmation(false))
}
Button(LocalizedString("appSetting.logoutConfirmation.confirm", comment: "确认退出"), role: .destructive) {
store.send(.logoutConfirmed)
store.send(.showLogoutConfirmation(false))
}
} message: {
Text(LocalizedString("appSetting.logoutConfirmation.message", comment: "确定要退出当前账户吗?"))
}
//
.alert(LocalizedString("appSetting.aboutUs.title", comment: "关于我们"), isPresented: Binding(
get: { store.showAboutUs },
set: { store.send(.showAboutUs($0)) }
)) {
Button(LocalizedString("common.ok", comment: "确定")) {
store.send(.showAboutUs(false))
}
} message: {
VStack(alignment: .leading, spacing: 8) {
Text(LocalizedString("feedList.title", comment: "享受您的生活时光"))
.font(.headline)
Text(LocalizedString("feedList.slogan", comment: "疾病如同残酷的统治者,\n而时间是我们最宝贵的财富。\n我们活着的每一刻,都是对不可避免命运的胜利。"))
.font(.body)
}
store.send(.nicknameEditAlert(false))
}
} message: {
Text(LocalizedString("appSetting.nickname", comment: "请输入新的昵称"))
}
}
}
@@ -330,10 +405,13 @@ struct SettingRow: View {
.foregroundColor(.white)
.frame(width: 24)
VStack(alignment: .leading, spacing: 4) {
HStack {
Text(title)
.font(.system(size: 16))
.foregroundColor(.white)
.multilineTextAlignment(.leading)
Spacer()
if !subtitle.isEmpty {
Text(subtitle)

View File

@@ -37,7 +37,7 @@ struct CreateFeedView: View {
isTextEditorFocused = false //
}
}
.navigationTitle(NSLocalizedString("createFeed.title", comment: "Image & Text Publish"))
.navigationTitle(LocalizedString("createFeed.title", comment: "Image & Text Publish"))
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.hidden, for: .navigationBar)
.navigationBarBackButtonHidden(true)
@@ -53,7 +53,7 @@ struct CreateFeedView: View {
}
ToolbarItem(placement: .principal) {
Text(NSLocalizedString("createFeed.title", comment: "Image & Text Publish"))
Text(LocalizedString("createFeed.title", comment: "Image & Text Publish"))
.font(.system(size: 18, weight: .medium))
.foregroundColor(.white)
}
@@ -120,9 +120,9 @@ struct CreateFeedView: View {
if store.isUploadingImages {
return "上传中..."
} else if store.isLoading {
return NSLocalizedString("createFeed.publishing", comment: "Publishing...")
return LocalizedString("createFeed.publishing", comment: "Publishing...")
} else {
return NSLocalizedString("createFeed.publish", comment: "Publish")
return LocalizedString("createFeed.publish", comment: "Publish")
}
}
@@ -143,7 +143,7 @@ struct ContentInputSection: View {
RoundedRectangle(cornerRadius: 8)
.fill(Color.init(hex: 0x1C143A))
if store.content.isEmpty {
Text(NSLocalizedString("createFeed.enterContent", comment: "Enter Content"))
Text(LocalizedString("createFeed.enterContent", comment: "Enter Content"))
.foregroundColor(.white.opacity(0.5))
.padding(.horizontal, 16)
.padding(.vertical, 12)
@@ -322,9 +322,9 @@ struct PublishButtonSection: View {
if store.isUploadingImages {
return "上传图片中..."
} else if store.isLoading {
return NSLocalizedString("createFeed.publishing", comment: "Publishing...")
return LocalizedString("createFeed.publishing", comment: "Publishing...")
} else {
return NSLocalizedString("createFeed.publish", comment: "Publish")
return LocalizedString("createFeed.publish", comment: "Publish")
}
}
}

View File

@@ -22,7 +22,7 @@ struct DetailView: View {
//
WithPerceptionTracking {
CustomNavigationBar(
title: NSLocalizedString("detail.title", comment: "Detail page title"),
title: LocalizedString("detail.title", comment: "Detail page title"),
showDeleteButton: isCurrentUserDynamic,
isDeleteLoading: store.isDeleteLoading,
onBack: {

View File

@@ -162,7 +162,7 @@ struct LoginView: View {
Button(action: {
showLanguageSettings = true
}) {
Text(LocalizedString("login.language", comment: ""))
Text(LocalizedString("setting.language", comment: ""))
.font(.system(size: 14))
.foregroundColor(.white.opacity(0.8))
}
@@ -170,7 +170,7 @@ struct LoginView: View {
Button(action: {
showUserAgreement = true
}) {
Text(LocalizedString("login.user_agreement", comment: ""))
Text(LocalizedString("login.agreement", comment: ""))
.font(.system(size: 14))
.foregroundColor(.white.opacity(0.8))
}
@@ -178,7 +178,7 @@ struct LoginView: View {
Button(action: {
showPrivacyPolicy = true
}) {
Text(LocalizedString("login.privacy_policy", comment: ""))
Text(LocalizedString("login.policy", comment: ""))
.font(.system(size: 14))
.foregroundColor(.white.opacity(0.8))
}

View File

@@ -58,7 +58,7 @@ struct RecoverPasswordView: View {
} else if countdown > 0 {
return "\(countdown)s"
} else {
return NSLocalizedString("recover_password.get_code", comment: "")
return LocalizedString("recover_password.get_code", comment: "")
}
}
@@ -92,7 +92,7 @@ struct RecoverPasswordView: View {
.frame(height: 60)
//
Text(NSLocalizedString("recover_password.title", comment: ""))
Text(LocalizedString("recover_password.title", comment: ""))
.font(.system(size: 28, weight: .medium))
.foregroundColor(.white)
.padding(.bottom, 80)
@@ -165,7 +165,7 @@ struct RecoverPasswordView: View {
TextField("", text: $email)
.placeholder(when: email.isEmpty) {
Text(NSLocalizedString("recover_password.placeholder_email", comment: ""))
Text(LocalizedString("recover_password.placeholder_email", comment: ""))
.foregroundColor(.white.opacity(0.6))
}
.foregroundColor(.white)
@@ -189,7 +189,7 @@ struct RecoverPasswordView: View {
HStack {
TextField("", text: $verificationCode)
.placeholder(when: verificationCode.isEmpty) {
Text(NSLocalizedString("recover_password.placeholder_verification_code", comment: ""))
Text(LocalizedString("recover_password.placeholder_verification_code", comment: ""))
.foregroundColor(.white.opacity(0.6))
}
.foregroundColor(.white)
@@ -238,7 +238,7 @@ struct RecoverPasswordView: View {
if isNewPasswordVisible {
TextField("", text: $newPassword)
.placeholder(when: newPassword.isEmpty) {
Text(NSLocalizedString("recover_password.placeholder_new_password", comment: ""))
Text(LocalizedString("recover_password.placeholder_new_password", comment: ""))
.foregroundColor(.white.opacity(0.6))
}
.foregroundColor(.white)
@@ -246,7 +246,7 @@ struct RecoverPasswordView: View {
} else {
SecureField("", text: $newPassword)
.placeholder(when: newPassword.isEmpty) {
Text(NSLocalizedString("recover_password.placeholder_new_password", comment: ""))
Text(LocalizedString("recover_password.placeholder_new_password", comment: ""))
.foregroundColor(.white.opacity(0.6))
}
.foregroundColor(.white)
@@ -287,7 +287,7 @@ struct RecoverPasswordView: View {
.progressViewStyle(CircularProgressViewStyle(tint: .white))
.scaleEffect(0.8)
}
Text(store.isResetLoading ? NSLocalizedString("recover_password.resetting", comment: "") : NSLocalizedString("recover_password.confirm_button", comment: ""))
Text(store.isResetLoading ? LocalizedString("recover_password.resetting", comment: "") : LocalizedString("recover_password.confirm_button", comment: ""))
.font(.system(size: 18, weight: .semibold))
.foregroundColor(.white)
}

View File

@@ -70,7 +70,7 @@ struct SplashView: View {
.frame(width: 100, height: 100)
// - 40pt
Text(NSLocalizedString("splash.title", comment: "E-Parti"))
Text(LocalizedString("splash.title", comment: "E-Parti"))
.font(.system(size: 40, weight: .regular))
.foregroundColor(.white)