diff --git a/iosApp/iosApp.xcodeproj/project.pbxproj b/iosApp/iosApp.xcodeproj/project.pbxproj index cccf53f85..64655bc48 100644 --- a/iosApp/iosApp.xcodeproj/project.pbxproj +++ b/iosApp/iosApp.xcodeproj/project.pbxproj @@ -609,7 +609,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2025082901; + CURRENT_PROJECT_VERSION = 2025091001; DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\""; DEVELOPMENT_TEAM = 7LFDZ96332; ENABLE_PREVIEWS = YES; @@ -627,7 +627,7 @@ "@executable_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 15.0; - MARKETING_VERSION = 0.4.9; + MARKETING_VERSION = 0.5.0; OTHER_LDFLAGS = ( "$(inherited)", "-lsqlite3", @@ -657,7 +657,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2025082901; + CURRENT_PROJECT_VERSION = 2025091001; DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\""; DEVELOPMENT_TEAM = 7LFDZ96332; ENABLE_PREVIEWS = YES; @@ -675,7 +675,7 @@ "@executable_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 15.0; - MARKETING_VERSION = 0.4.9; + MARKETING_VERSION = 0.5.0; OTHER_LDFLAGS = ( "$(inherited)", "-lsqlite3", diff --git a/iosApp/iosApp/Data/ReleaseLogManager.swift b/iosApp/iosApp/Data/ReleaseLogManager.swift index 00a7e6566..c2d0ff603 100644 --- a/iosApp/iosApp/Data/ReleaseLogManager.swift +++ b/iosApp/iosApp/Data/ReleaseLogManager.swift @@ -7,11 +7,12 @@ struct ReleaseLogEntry { } @MainActor -class ReleaseLogManager: ObservableObject { +@Observable +class ReleaseLogManager { static let shared = ReleaseLogManager() - @Published var releaseLogEntries: [ReleaseLogEntry] = [] - @Published var isLoading = false + var releaseLogEntries: [ReleaseLogEntry] = [] + var isLoading = false private init() { loadReleaseLog() diff --git a/iosApp/iosApp/FlareApp.swift b/iosApp/iosApp/FlareApp.swift index 95ede8252..34539822b 100644 --- a/iosApp/iosApp/FlareApp.swift +++ b/iosApp/iosApp/FlareApp.swift @@ -9,8 +9,8 @@ struct FlareApp: SwiftUI.App { #else @UIApplicationDelegateAdaptor(AppDelegate.self) var delegate #endif - @StateObject private var router = FlareRouter.shared - @StateObject private var podcastManager = IOSPodcastManager.shared + @State private var router = FlareRouter.shared + @State private var podcastManager = IOSPodcastManager.shared @State var theme = FlareTheme.shared @State private var shouldShowVersionBanner: Bool = false diff --git a/iosApp/iosApp/ReleaseLog.md b/iosApp/iosApp/ReleaseLog.md index 554cba8f1..b2669e78e 100644 --- a/iosApp/iosApp/ReleaseLog.md +++ b/iosApp/iosApp/ReleaseLog.md @@ -1,3 +1,19 @@ +# Flare 0.5.0 Release Notes + +## 🎉 新功能 +- **Optimize code**: 将Swift UI低版本特性升级成iOS18+,性能更好 +- **Timeline List**: 优化部分代码 +- **Following/followers list**: 增加 关注/ 粉丝 列表 + +## 🎉 修复/调整 +- **App Tabbar**: 调整App Tabbar 隐藏/显示逻辑 +- **Release Notes**: 修复Release Notes 页面翻译显示重复问题 +- **闪退**: 修复点赞等 调用KMP方法 闪退问题 +- **多语言显示**: 调整X 的 部分显示内容 + +--- + + # Flare 0.4.9 Release Notes ## 🎉 新功能 diff --git a/iosApp/iosApp/UI/Navigation/ComposeManager.swift b/iosApp/iosApp/UI/Navigation/ComposeManager.swift index 2d5c5d683..3833ff580 100644 --- a/iosApp/iosApp/UI/Navigation/ComposeManager.swift +++ b/iosApp/iosApp/UI/Navigation/ComposeManager.swift @@ -4,12 +4,13 @@ import shared import SwiftUI import UIKit -class ComposeManager: ObservableObject { +@Observable +class ComposeManager { static let shared = ComposeManager() - @Published var showCompose = false - @Published var composeAccountType: AccountType? - @Published var composeStatus: FlareComposeStatus? + var showCompose = false + var composeAccountType: AccountType? + var composeStatus: FlareComposeStatus? private init() {} diff --git a/iosApp/iosApp/UI/Navigation/FlareRootView.swift b/iosApp/iosApp/UI/Navigation/FlareRootView.swift index 9cb9022a9..fe346ff0f 100644 --- a/iosApp/iosApp/UI/Navigation/FlareRootView.swift +++ b/iosApp/iosApp/UI/Navigation/FlareRootView.swift @@ -4,9 +4,9 @@ import SwiftUI struct FlareRootView: View { @State var menuState = FlareMenuState() - @StateObject private var router = FlareRouter.shared - @StateObject private var composeManager = ComposeManager.shared - @StateObject private var timelineState = TimelineExtState() + @State private var router = FlareRouter.shared + @State private var composeManager = ComposeManager.shared + @State private var timelineState = TimelineExtState() @State private var presenter = ActiveAccountPresenter() @Environment(\.appSettings) private var appSettings @Environment(FlareTheme.self) private var theme @@ -36,7 +36,7 @@ struct FlareRootView: View { .applyTheme(theme) .environment(menuState) .environment(router) - .environmentObject(timelineState) + .environment(timelineState) .sheet(isPresented: $router.isSheetPresented) { if let destination = router.activeDestination { FlareDestinationView( @@ -67,7 +67,10 @@ struct FlareRootView: View { secondaryButton: .cancel() ) } - .sheet(isPresented: $composeManager.showCompose) { + .sheet(isPresented: Binding( + get: { composeManager.showCompose }, + set: { composeManager.showCompose = $0 } + )) { if let composeAccountType = composeManager.composeAccountType { NavigationView { ComposeScreen( diff --git a/iosApp/iosApp/UI/Navigation/FlareRouter.swift b/iosApp/iosApp/UI/Navigation/FlareRouter.swift index 821782826..b7080415f 100644 --- a/iosApp/iosApp/UI/Navigation/FlareRouter.swift +++ b/iosApp/iosApp/UI/Navigation/FlareRouter.swift @@ -1,4 +1,3 @@ -import Combine import os.log import SafariServices import shared @@ -6,13 +5,11 @@ import SwiftUI import UIKit @Observable -class FlareRouter: ObservableObject { +class FlareRouter { public static let shared = FlareRouter() public var menuState: FlareMenuState - private var cancellables = Set() - var activeDestination: FlareDestination? var presentationType: FlarePresentationType = .push diff --git a/iosApp/iosApp/UI/Page/Compose/ErrorToast.swift b/iosApp/iosApp/UI/Page/Compose/ErrorToast.swift index 33554358e..095e2ec11 100644 --- a/iosApp/iosApp/UI/Page/Compose/ErrorToast.swift +++ b/iosApp/iosApp/UI/Page/Compose/ErrorToast.swift @@ -1,10 +1,11 @@ import SwiftUI -class ErrorToastManager: ObservableObject { +@Observable +class ErrorToastManager { static let shared = ErrorToastManager() - @Published var isShowing = false - @Published var message = "" + var isShowing = false + var message = "" private init() {} @@ -70,17 +71,19 @@ extension View { overlay( ErrorToastOverlay() ) - .environmentObject(ErrorToastManager.shared) } } struct ErrorToastOverlay: View { - @ObservedObject private var toastManager = ErrorToastManager.shared + private var toastManager = ErrorToastManager.shared var body: some View { ErrorToast( message: toastManager.message, - isShowing: $toastManager.isShowing + isShowing: Binding( + get: { toastManager.isShowing }, + set: { toastManager.isShowing = $0 } + ) ) } } diff --git a/iosApp/iosApp/UI/Page/Compose/Timeline/TimelineStatus/TimelineStatusViewV2.swift b/iosApp/iosApp/UI/Page/Compose/Timeline/TimelineStatus/TimelineStatusViewV2.swift index 476da9e9a..41da522f3 100644 --- a/iosApp/iosApp/UI/Page/Compose/Timeline/TimelineStatus/TimelineStatusViewV2.swift +++ b/iosApp/iosApp/UI/Page/Compose/Timeline/TimelineStatus/TimelineStatusViewV2.swift @@ -39,10 +39,7 @@ struct TimelineStatusViewV2: View, Equatable { static func == (lhs: TimelineStatusViewV2, rhs: TimelineStatusViewV2) -> Bool { guard lhs.item.id == rhs.item.id else { return false } - return lhs.item.content.raw == rhs.item.content.raw && - lhs.item.user?.key == rhs.item.user?.key && - lhs.item.timestamp == rhs.item.timestamp && - + return lhs.item.id == rhs.item.id && lhs.item.likeCount == rhs.item.likeCount && lhs.item.isLiked == rhs.item.isLiked && lhs.item.retweetCount == rhs.item.retweetCount && @@ -50,9 +47,7 @@ struct TimelineStatusViewV2: View, Equatable { lhs.item.replyCount == rhs.item.replyCount && lhs.item.bookmarkCount == rhs.item.bookmarkCount && lhs.item.isBookmarked == rhs.item.isBookmarked && - lhs.item.sensitive == rhs.item.sensitive && - lhs.item.images.count == rhs.item.images.count && - lhs.isDetail == rhs.isDetail + lhs.item.sensitive == rhs.item.sensitive } @State private var state = TimelineItemState() diff --git a/iosApp/iosApp/UI/Page/Compose/Translate/TranslatableText.swift b/iosApp/iosApp/UI/Page/Compose/Translate/TranslatableText.swift index 0616d808f..f9bae61c7 100644 --- a/iosApp/iosApp/UI/Page/Compose/Translate/TranslatableText.swift +++ b/iosApp/iosApp/UI/Page/Compose/Translate/TranslatableText.swift @@ -7,7 +7,7 @@ import SwiftUI struct TranslatableText: View { let originalText: String let forceTranslate: Bool - @StateObject private var transViewModel = TranslationViewModel() + @State private var transViewModel = TranslationViewModel() private let languageDetector = LanguageDetector() @Environment(\.appSettings) private var appSettings @Environment(\.isInCaptureMode) private var isInCaptureMode diff --git a/iosApp/iosApp/UI/Page/Compose/Translate/TranslationViewModel.swift b/iosApp/iosApp/UI/Page/Compose/Translate/TranslationViewModel.swift index 6ac037c79..ac2f71d67 100644 --- a/iosApp/iosApp/UI/Page/Compose/Translate/TranslationViewModel.swift +++ b/iosApp/iosApp/UI/Page/Compose/Translate/TranslationViewModel.swift @@ -2,10 +2,11 @@ import Foundation import SwiftUI @MainActor -class TranslationViewModel: ObservableObject { - @Published var translatedText: String? - @Published var isTranslating = false - @Published var error: Error? +@Observable +class TranslationViewModel { + var translatedText: String? + var isTranslating = false + var error: Error? private let translationService: TranslationService diff --git a/iosApp/iosApp/UI/Page/Home/Data/AppBarTabSettingStore.swift b/iosApp/iosApp/UI/Page/Home/Data/AppBarTabSettingStore.swift index 2241223e3..09493d4d4 100644 --- a/iosApp/iosApp/UI/Page/Home/Data/AppBarTabSettingStore.swift +++ b/iosApp/iosApp/UI/Page/Home/Data/AppBarTabSettingStore.swift @@ -9,17 +9,18 @@ extension Notification.Name { static let listTitleDidUpdate = Notification.Name("listTitleDidUpdate") } -class AppBarTabSettingStore: ObservableObject, TabStateProvider { +@Observable +class AppBarTabSettingStore: TabStateProvider { static let shared = AppBarTabSettingStore(accountType: AccountTypeGuest()) - @Published var primaryHomeItems: [FLTabItem] = [] // 主要标签(不可更改状态)Appbar 第一个Home 标签 - @Published var secondaryItems: [FLTabItem] = [] // 所有次要标签 - @Published var availableAppBarTabsItems: [FLTabItem] = [] // UserDefaults 存储的已启用标签 + var primaryHomeItems: [FLTabItem] = [] // 主要标签(不可更改状态)Appbar 第一个Home 标签 + var secondaryItems: [FLTabItem] = [] // 所有次要标签 + var availableAppBarTabsItems: [FLTabItem] = [] // UserDefaults 存储的已启用标签 // 简化列表状态管理,只存储已pin的列表ID - @Published var pinnedListIds: [String] = [] // 收藏的列表ID - @Published var listTitles: [String: String] = [:] // 列表ID到标题的映射 - @Published var listIconUrls: [String: String] = [:] // 列表ID到头像URL的映射 + var pinnedListIds: [String] = [] // 收藏的列表ID + var listTitles: [String: String] = [:] // 列表ID到标题的映射 + var listIconUrls: [String: String] = [:] // 列表ID到头像URL的映射 // 添加统一配置存储 private var appBarItems: [AppBarItemConfig] = [] @@ -27,9 +28,9 @@ class AppBarTabSettingStore: ObservableObject, TabStateProvider { // 添加同步锁,确保线程安全 private let storageLock = NSLock() - @Published var selectedAppBarTabKey: String = "" // 选中的 tab key - @Published var currentPresenter: TimelinePresenter? - @Published var currentUser: UiUserV2? + var selectedAppBarTabKey: String = "" // 选中的 tab key + var currentPresenter: TimelinePresenter? + var currentUser: UiUserV2? private var presenter = ActiveAccountPresenter() private let settingsManager = FLTabSettingsManager() @@ -147,7 +148,6 @@ class AppBarTabSettingStore: ObservableObject, TabStateProvider { listIconUrls = [:] appBarItems = [] // 清空统一配置 - objectWillChange.send() FlareLog.debug("状态清除完成") } @@ -332,9 +332,6 @@ class AppBarTabSettingStore: ObservableObject, TabStateProvider { } else if !availableAppBarTabsItems.isEmpty { selectedAppBarTabKey = availableAppBarTabsItems[0].key } - - // 通知UI更新 - objectWillChange.send() } // 从配置更新UI状态 @@ -563,7 +560,6 @@ class AppBarTabSettingStore: ObservableObject, TabStateProvider { saveAppBarConfig() // 发送变更通知 - objectWillChange.send() notificationService.postTabsDidUpdateNotification() // 恢复选中状态 @@ -661,9 +657,6 @@ class AppBarTabSettingStore: ObservableObject, TabStateProvider { newTitle: newTitle ) - // 通知UI更新 - self.objectWillChange.send() - break } } @@ -843,9 +836,6 @@ class AppBarTabSettingStore: ObservableObject, TabStateProvider { } } - // 通知UI更新 - objectWillChange.send() - // 添加发送TabsDidUpdate通知,确保HomeTabController更新UI notificationService.postTabsDidUpdateNotification() FlareLog.debug("发送TabsDidUpdate通知,标签状态已更改: \(id)") diff --git a/iosApp/iosApp/UI/Page/Home/Model/TimelineExtState.swift b/iosApp/iosApp/UI/Page/Home/Model/TimelineExtState.swift index 35120f282..7d48c90c3 100644 --- a/iosApp/iosApp/UI/Page/Home/Model/TimelineExtState.swift +++ b/iosApp/iosApp/UI/Page/Home/Model/TimelineExtState.swift @@ -1,10 +1,11 @@ import SwiftUI -class TimelineExtState: ObservableObject { - @Published var scrollToTopTrigger = false - @Published var showFloatingButton = false +@Observable +class TimelineExtState { + var scrollToTopTrigger = false + var showFloatingButton = false - @Published var tabBarOffset: CGFloat = 0 // TabBar偏移量,0=显示,100=隐藏 + var tabBarOffset: CGFloat = 0 // TabBar偏移量,0=显示,100=隐藏 private var lastScrollOffset: CGFloat = 0 func updateTabBarOffset(currentOffset: CGFloat, isHomeTab: Bool) { @@ -15,24 +16,41 @@ class TimelineExtState: ObservableObject { return } + // 滚动到顶部时显示TabBar if currentOffset <= 0 { if tabBarOffset != 0 { tabBarOffset = 0 + lastScrollOffset = currentOffset } - lastScrollOffset = currentOffset return } let scrollDelta = currentOffset - lastScrollOffset - if scrollDelta > 30, tabBarOffset == 0 { - tabBarOffset = 100 - } + // 向下滚动 隐藏TabBar + if scrollDelta > 0 { + guard tabBarOffset != 100 else { + lastScrollOffset = currentOffset + return + } - else if scrollDelta < -30, tabBarOffset != 0 { - tabBarOffset = 0 + if scrollDelta > 30 { + tabBarOffset = 100 + lastScrollOffset = currentOffset + } } - lastScrollOffset = currentOffset + // 向上滚动 显示TabBar + else if scrollDelta < 0 { + guard tabBarOffset != 0 else { + lastScrollOffset = currentOffset + return + } + + if scrollDelta < -10 { + tabBarOffset = 0 + lastScrollOffset = currentOffset + } + } } } diff --git a/iosApp/iosApp/UI/Page/Home/Model/TimelineViewModel.swift b/iosApp/iosApp/UI/Page/Home/Model/TimelineViewModel.swift index 5d25282c7..3599ab3bf 100644 --- a/iosApp/iosApp/UI/Page/Home/Model/TimelineViewModel.swift +++ b/iosApp/iosApp/UI/Page/Home/Model/TimelineViewModel.swift @@ -13,19 +13,19 @@ class TimelineViewModel { private(set) var presenter: PresenterBase? private let stateConverter = PagingStateConverter() - private var refreshDebounceTimer: Timer? - private var cancellables = Set() private var dataSourceTask: Task? private(set) var isLoadingMore: Bool = false -// private var isLoadMoreInProgress: Bool = false var scrollToId: String = "" -// @ObservationIgnored -// private var visibleItems: [TimelineItem] = [] -// + // private var isLoadMoreInProgress: Bool = false + // private var refreshDebounceTimer: Timer? + // private var cancellables = Set() + // @ObservationIgnored + // private var visibleItems: [TimelineItem] = [] + // // private let visibilityQueue = DispatchQueue(label: "timeline.visibility", qos: .userInitiated) var hasMore: Bool { diff --git a/iosApp/iosApp/UI/Page/Home/View/AppBar/AppBarSettingTabItemRowView.swift b/iosApp/iosApp/UI/Page/Home/View/AppBar/AppBarSettingTabItemRowView.swift index f719c07e5..67ebfa5d0 100644 --- a/iosApp/iosApp/UI/Page/Home/View/AppBar/AppBarSettingTabItemRowView.swift +++ b/iosApp/iosApp/UI/Page/Home/View/AppBar/AppBarSettingTabItemRowView.swift @@ -190,7 +190,7 @@ struct ListTabItemRowRow: View { } .padding(.leading, 8) } - .onReceive(store.$listTitles) { _ in + .onChange(of: store.listTitles) { _, _ in if let updatedTitle = store.listTitles[listId] { currentTitle = updatedTitle } diff --git a/iosApp/iosApp/UI/Page/Home/View/AppBar/HomeAppBarSettingsView.swift b/iosApp/iosApp/UI/Page/Home/View/AppBar/HomeAppBarSettingsView.swift index 8586668cf..5bab665fd 100644 --- a/iosApp/iosApp/UI/Page/Home/View/AppBar/HomeAppBarSettingsView.swift +++ b/iosApp/iosApp/UI/Page/Home/View/AppBar/HomeAppBarSettingsView.swift @@ -5,7 +5,7 @@ import SwiftUI struct HomeAppBarSettingsView: View { @Environment(\.dismiss) var dismiss - @ObservedObject var store: AppBarTabSettingStore = .shared + var store: AppBarTabSettingStore = .shared // 列表 presenter 状态 @State private var listPresenter: AllListPresenter @@ -744,7 +744,6 @@ struct HomeAppBarSettingsView: View { DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { // 确保store的状态更新 store.saveTabs() - store.objectWillChange.send() // 重置状态 isActionInProgress = false @@ -775,7 +774,6 @@ struct HomeAppBarSettingsView: View { DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { // 确保store的状态更新 store.saveTabs() - store.objectWillChange.send() // 重置状态 isActionInProgress = false diff --git a/iosApp/iosApp/UI/Page/Home/View/Floating/FloatingScrollToTopButton.swift b/iosApp/iosApp/UI/Page/Home/View/Floating/FloatingScrollToTopButton.swift index abbb27162..061ef9088 100644 --- a/iosApp/iosApp/UI/Page/Home/View/Floating/FloatingScrollToTopButton.swift +++ b/iosApp/iosApp/UI/Page/Home/View/Floating/FloatingScrollToTopButton.swift @@ -1,7 +1,7 @@ import SwiftUI struct FloatingScrollToTopButton: View { - @EnvironmentObject private var timelineState: TimelineExtState + @Environment(TimelineExtState.self) private var timelineState @Environment(FlareTheme.self) private var theme var body: some View { diff --git a/iosApp/iosApp/UI/Page/Home/View/Home/FlareTabBarV2.swift b/iosApp/iosApp/UI/Page/Home/View/Home/FlareTabBarV2.swift index 2f2b98dfb..68f6dd316 100644 --- a/iosApp/iosApp/UI/Page/Home/View/Home/FlareTabBarV2.swift +++ b/iosApp/iosApp/UI/Page/Home/View/Home/FlareTabBarV2.swift @@ -9,7 +9,7 @@ struct FlareTabBarV2: View { @Environment(FlareMenuState.self) private var menuState @Environment(FlareTheme.self) private var theme @Environment(\.appSettings) private var appSettings - @EnvironmentObject private var timelineState: TimelineExtState + @Environment(TimelineExtState.self) private var timelineState let accountType: AccountType @Namespace private var tabBarNamespace diff --git a/iosApp/iosApp/UI/Page/Home/View/Home/HomeScreenSwiftUI.swift b/iosApp/iosApp/UI/Page/Home/View/Home/HomeScreenSwiftUI.swift index 440598c25..bfde60829 100644 --- a/iosApp/iosApp/UI/Page/Home/View/Home/HomeScreenSwiftUI.swift +++ b/iosApp/iosApp/UI/Page/Home/View/Home/HomeScreenSwiftUI.swift @@ -6,8 +6,8 @@ struct HomeScreenSwiftUI: View { var onSwitchToMenuTab: (() -> Void)? - @StateObject private var tabStore = AppBarTabSettingStore.shared - @EnvironmentObject private var timelineState: TimelineExtState + @State private var tabStore = AppBarTabSettingStore.shared + @Environment(TimelineExtState.self) private var timelineState @State private var selectedHomeAppBarTabKey: String = "" @State private var showAppbarSettings = false @State private var showLogin = false diff --git a/iosApp/iosApp/UI/Page/Home/View/Home/HomeTabViewContentV2.swift b/iosApp/iosApp/UI/Page/Home/View/Home/HomeTabViewContentV2.swift index 7f35e9d1f..49aa52fdf 100644 --- a/iosApp/iosApp/UI/Page/Home/View/Home/HomeTabViewContentV2.swift +++ b/iosApp/iosApp/UI/Page/Home/View/Home/HomeTabViewContentV2.swift @@ -10,7 +10,7 @@ struct HomeTabViewContentV2: View { @Environment(FlareMenuState.self) private var menuState @Environment(FlareTheme.self) private var theme @Environment(\.appSettings) private var appSettings - @EnvironmentObject private var timelineState: TimelineExtState + @Environment(TimelineExtState.self) private var timelineState let accountType: AccountType @@ -108,7 +108,7 @@ struct HomeTabViewContentV2: View { accountType: accountType ) .offset(y: timelineState.tabBarOffset) - .animation(.easeInOut(duration: 0.3), value: timelineState.tabBarOffset) + .animation(.easeInOut(duration: 0.1), value: timelineState.tabBarOffset) Rectangle() .fill(.clear) diff --git a/iosApp/iosApp/UI/Page/Home/View/Home/HomeTabViewContentViewSwiftUI.swift b/iosApp/iosApp/UI/Page/Home/View/Home/HomeTabViewContentViewSwiftUI.swift index 1494c019c..40dd5794d 100644 --- a/iosApp/iosApp/UI/Page/Home/View/Home/HomeTabViewContentViewSwiftUI.swift +++ b/iosApp/iosApp/UI/Page/Home/View/Home/HomeTabViewContentViewSwiftUI.swift @@ -3,7 +3,7 @@ import SwiftUI import SwiftUIIntrospect struct HomeTabViewContentViewSwiftUI: View { - @ObservedObject var tabStore: AppBarTabSettingStore + var tabStore: AppBarTabSettingStore @Binding var selectedTab: String @Environment(\.appSettings) private var appSettings diff --git a/iosApp/iosApp/UI/Page/Home/View/TimelineViewSwiftUI/TimelineItemsView.swift b/iosApp/iosApp/UI/Page/Home/View/TimelineViewSwiftUI/TimelineItemsView.swift index 25c414d4f..dc6509c69 100644 --- a/iosApp/iosApp/UI/Page/Home/View/TimelineViewSwiftUI/TimelineItemsView.swift +++ b/iosApp/iosApp/UI/Page/Home/View/TimelineViewSwiftUI/TimelineItemsView.swift @@ -13,12 +13,13 @@ struct TimelineItemsView: View { TimelineStatusViewV2( item: item, timelineViewModel: viewModel - ).id(item.id) - .padding(.horizontal, 16) - .padding(.vertical, 4) - .onAppear { - // viewModel.itemOnAppear(item: item) - FlareLog.debug("🔍 [TimelineItemsView] onAppear for id: '\(item.id)', content: '\(item.content.raw)'") + ) +// .id(item.id) + .padding(.horizontal, 16) + .padding(.vertical, 4) +// .onAppear { + // viewModel.itemOnAppear(item: item) + // FlareLog.debug("🔍 [TimelineItemsView] onAppear for id: '\(item.id)', content: '\(item.content.raw)'") // Task { // if hasMore, !viewModel.isLoadingMore, @@ -36,11 +37,11 @@ struct TimelineItemsView: View { // } // } // } - } - .onDisappear { - FlareLog.debug("🔍 [TimelineItemsView] onDisappear for id: '\(item.id)'") - // viewModel.itemDidDisappear(item: item) - } +// } +// .onDisappear { + // FlareLog.debug("🔍 [TimelineItemsView] onDisappear for id: '\(item.id)'") + // viewModel.itemDidDisappear(item: item) +// } } if hasMore { diff --git a/iosApp/iosApp/UI/Page/Home/View/TimelineViewSwiftUI/TimelineViewSwiftUIV4.swift b/iosApp/iosApp/UI/Page/Home/View/TimelineViewSwiftUI/TimelineViewSwiftUIV4.swift index ae8c17299..09191c901 100644 --- a/iosApp/iosApp/UI/Page/Home/View/TimelineViewSwiftUI/TimelineViewSwiftUIV4.swift +++ b/iosApp/iosApp/UI/Page/Home/View/TimelineViewSwiftUI/TimelineViewSwiftUIV4.swift @@ -4,11 +4,11 @@ import SwiftUI struct TimelineViewSwiftUIV4: View { let tab: FLTabItem - @ObservedObject var store: AppBarTabSettingStore + var store: AppBarTabSettingStore let isCurrentAppBarTabSelected: Bool @Environment(FlareTheme.self) private var theme @Environment(\.shouldShowVersionBanner) private var shouldShowVersionBanner - @EnvironmentObject private var timelineState: TimelineExtState + @Environment(TimelineExtState.self) private var timelineState @State private var timeLineViewModel = TimelineViewModel() @State private var isInitialized: Bool = false @@ -34,6 +34,8 @@ struct TimelineViewSwiftUIV4: View { } var body: some View { + @Bindable var bindableTimelineState = timelineState + ScrollViewReader { proxy in VStack { List { @@ -100,7 +102,7 @@ struct TimelineViewSwiftUIV4: View { } action: { _, newValue in timeLineViewModel.handleScrollOffsetChange( newValue.contentOffset.y, - showFloatingButton: $timelineState.showFloatingButton, + showFloatingButton: $bindableTimelineState.showFloatingButton, timelineState: timelineState, isHomeTab: isCurrentAppBarTabSelected ) diff --git a/iosApp/iosApp/UI/Page/Home/View/TimelineWaterfall/WaterfallItemsView.swift b/iosApp/iosApp/UI/Page/Home/View/TimelineWaterfall/WaterfallItemsView.swift index 566cec950..b492cf8cf 100644 --- a/iosApp/iosApp/UI/Page/Home/View/TimelineWaterfall/WaterfallItemsView.swift +++ b/iosApp/iosApp/UI/Page/Home/View/TimelineWaterfall/WaterfallItemsView.swift @@ -10,7 +10,7 @@ struct WaterfallItemsView: View { @Binding var scrolledID: String? let isCurrentAppBarTabSelected: Bool let viewModel: TimelineViewModel - @EnvironmentObject private var timelineState: TimelineExtState + @Environment(TimelineExtState.self) private var timelineState @Environment(FlareRouter.self) private var router @State private var scrollThreshold: CGFloat = 500 @@ -39,6 +39,8 @@ struct WaterfallItemsView: View { } var body: some View { + @Bindable var bindableTimelineState = timelineState + ScrollView { LazyVStack(spacing: 0) { WaterfallGrid(waterfallItems, id: \.id) { item in @@ -97,7 +99,7 @@ struct WaterfallItemsView: View { .onScrollGeometryChange(for: ScrollGeometry.self) { geometry in geometry } action: { _, newValue in - viewModel.handleScrollOffsetChange(newValue.contentOffset.y, showFloatingButton: $timelineState.showFloatingButton) + viewModel.handleScrollOffsetChange(newValue.contentOffset.y, showFloatingButton: $bindableTimelineState.showFloatingButton) let currentOffset = newValue.contentOffset.y diff --git a/iosApp/iosApp/UI/Page/Home/View/TimelineWaterfall/WaterfallView.swift b/iosApp/iosApp/UI/Page/Home/View/TimelineWaterfall/WaterfallView.swift index c640cba94..7ef4a237f 100644 --- a/iosApp/iosApp/UI/Page/Home/View/TimelineWaterfall/WaterfallView.swift +++ b/iosApp/iosApp/UI/Page/Home/View/TimelineWaterfall/WaterfallView.swift @@ -4,7 +4,7 @@ import WaterfallGrid struct WaterfallView: View { let tab: FLTabItem - @ObservedObject var store: AppBarTabSettingStore + var store: AppBarTabSettingStore let isCurrentAppBarTabSelected: Bool let displayType: TimelineDisplayType diff --git a/iosApp/iosApp/UI/Page/List/SwiftUIView/AllFeedsView.swift b/iosApp/iosApp/UI/Page/List/SwiftUIView/AllFeedsView.swift index 4e6386537..642a97692 100644 --- a/iosApp/iosApp/UI/Page/List/SwiftUIView/AllFeedsView.swift +++ b/iosApp/iosApp/UI/Page/List/SwiftUIView/AllFeedsView.swift @@ -13,7 +13,7 @@ struct AllFeedsView: View { private let accountType: AccountType @Environment(FlareTheme.self) private var theme - @StateObject private var tabSettingStore: AppBarTabSettingStore + @State private var tabSettingStore: AppBarTabSettingStore init(accountType: AccountType) { presenter = .init(accountType: accountType) @@ -32,7 +32,7 @@ struct AllFeedsView: View { } } _isMissingFeedData = State(initialValue: missingFeedData) - _tabSettingStore = StateObject(wrappedValue: AppBarTabSettingStore.shared) + _tabSettingStore = State(wrappedValue: AppBarTabSettingStore.shared) } var body: some View { diff --git a/iosApp/iosApp/UI/Page/List/SwiftUIView/AllListsView.swift b/iosApp/iosApp/UI/Page/List/SwiftUIView/AllListsView.swift index 04828107e..3d62aad4b 100644 --- a/iosApp/iosApp/UI/Page/List/SwiftUIView/AllListsView.swift +++ b/iosApp/iosApp/UI/Page/List/SwiftUIView/AllListsView.swift @@ -14,7 +14,7 @@ struct AllListsView: View { private let accountType: AccountType @Environment(FlareTheme.self) private var theme - @StateObject private var tabSettingStore: AppBarTabSettingStore + @State private var tabSettingStore: AppBarTabSettingStore init(accountType: AccountType) { presenter = .init(accountType: accountType) @@ -40,7 +40,7 @@ struct AllListsView: View { } } _isMastodonUser = State(initialValue: isMastodon) - _tabSettingStore = StateObject(wrappedValue: AppBarTabSettingStore.shared) + _tabSettingStore = State(wrappedValue: AppBarTabSettingStore.shared) } var body: some View { diff --git a/iosApp/iosApp/UI/Page/Message/chat/DMAudioMessageView.swift b/iosApp/iosApp/UI/Page/Message/chat/DMAudioMessageView.swift index e6a14eb6c..ccbc3e1c1 100644 --- a/iosApp/iosApp/UI/Page/Message/chat/DMAudioMessageView.swift +++ b/iosApp/iosApp/UI/Page/Message/chat/DMAudioMessageView.swift @@ -10,11 +10,12 @@ import SwiftUI // var waveformCache: [String: [CGFloat]] = [:] // Consider if cache is needed with pre-calculated data /// 音频播放器状态 -private class AudioPlayerState: ObservableObject { - @Published var isPlaying = false - @Published var progress: Double = 0 - @Published var duration: Double = 0 - @Published var currentTime: Double = 0 // Added current time +@Observable +private class AudioPlayerState { + var isPlaying = false + var progress: Double = 0 + var duration: Double = 0 + var currentTime: Double = 0 // Added current time private var player: AVPlayer? private var timeObserver: Any? @@ -104,7 +105,7 @@ struct DMAudioMessageView: View { let url: URL let media: UiMedia let isCurrentUser: Bool - @StateObject private var playerState = AudioPlayerState() + @State private var playerState = AudioPlayerState() @State private var waveformSamples: [CGFloat] = Array(repeating: 0.05, count: 50) var body: some View { diff --git a/iosApp/iosApp/UI/Page/Profile/Data/ProfilePresenterWrapper.swift b/iosApp/iosApp/UI/Page/Profile/Data/ProfilePresenterWrapper.swift index cb8aa3f5a..9f419f8f3 100644 --- a/iosApp/iosApp/UI/Page/Profile/Data/ProfilePresenterWrapper.swift +++ b/iosApp/iosApp/UI/Page/Profile/Data/ProfilePresenterWrapper.swift @@ -1,4 +1,3 @@ -import Combine import Foundation import shared import SwiftUI @@ -13,8 +12,9 @@ struct ProfileTabViewModel { var isMediaTab: Bool { mediaPresenter != nil } } -class ProfilePresenterWrapper: ObservableObject { - @Published var selectedTabKey: String? { +@Observable +class ProfilePresenterWrapper { + var selectedTabKey: String? { didSet { if let tabKey = selectedTabKey { Task { @MainActor in @@ -24,9 +24,9 @@ class ProfilePresenterWrapper: ObservableObject { } } - @Published var availableTabs: [FLTabItem] = [] - @Published private(set) var currentTabViewModel: ProfileTabViewModel? - @Published private(set) var isInitialized: Bool = false + var availableTabs: [FLTabItem] = [] + private(set) var currentTabViewModel: ProfileTabViewModel? + private(set) var isInitialized: Bool = false var isCurrentTabActive: Bool { guard let selectedTabKey, diff --git a/iosApp/iosApp/UI/Page/Profile/SwiftUIView/ProfileSwiftUIViewV2.swift b/iosApp/iosApp/UI/Page/Profile/SwiftUIView/ProfileSwiftUIViewV2.swift index e3b088cc0..8ef96cd45 100644 --- a/iosApp/iosApp/UI/Page/Profile/SwiftUIView/ProfileSwiftUIViewV2.swift +++ b/iosApp/iosApp/UI/Page/Profile/SwiftUIView/ProfileSwiftUIViewV2.swift @@ -7,9 +7,9 @@ struct ProfileSwiftUIViewV2: View { let userKey: MicroBlogKey? let showBackButton: Bool - @StateObject private var presenterWrapper: ProfilePresenterWrapper + @State private var presenterWrapper: ProfilePresenterWrapper @Environment(FlareTheme.self) private var theme - @EnvironmentObject private var timelineState: TimelineExtState + @Environment(TimelineExtState.self) private var timelineState @Environment(\.appSettings) private var appSettings @State private var showUserNameInNavBar = false @@ -20,10 +20,13 @@ struct ProfileSwiftUIViewV2: View { self.showBackButton = showBackButton let presenterWrapper = ProfilePresenterWrapper(accountType: accountType, userKey: userKey) - _presenterWrapper = StateObject(wrappedValue: presenterWrapper) + _presenterWrapper = State(wrappedValue: presenterWrapper) } var body: some View { + @Bindable var bindableTimelineState = timelineState + @Bindable var bindablePresenterWrapper = presenterWrapper + ZStack(alignment: .bottomTrailing) { if presenterWrapper.isInitialized { ObservePresenter(presenter: presenterWrapper.profilePresenter) { state in @@ -45,7 +48,7 @@ struct ProfileSwiftUIViewV2: View { // Tab Bar if !presenterWrapper.availableTabs.isEmpty { ProfileTabBarViewV2( - selectedTabKey: $presenterWrapper.selectedTabKey, + selectedTabKey: $bindablePresenterWrapper.selectedTabKey, availableTabs: presenterWrapper.availableTabs ) .listRowInsets(EdgeInsets()) @@ -82,7 +85,7 @@ struct ProfileSwiftUIViewV2: View { .onScrollGeometryChange(for: ScrollGeometry.self) { geometry in geometry } action: { _, newValue in - handleProfileScrollChange(newValue.contentOffset.y) + handleProfileScrollChange(newValue.contentOffset.y, bindableTimelineState: $bindableTimelineState.showFloatingButton) } .onChange(of: timelineState.scrollToTopTrigger) { _, _ in let isCurrentTab = presenterWrapper.isCurrentTabActive @@ -158,13 +161,13 @@ struct ProfileSwiftUIViewV2: View { } extension ProfileSwiftUIViewV2 { - private func handleProfileScrollChange(_ offsetY: CGFloat) { + private func handleProfileScrollChange(_ offsetY: CGFloat, bindableTimelineState: Binding) { // FlareLog.debug("📜 [ProfileSwiftUIViewV2] Profile滚动检测 - offsetY: \(offsetY), tabBarOffset: \(timelineState.tabBarOffset)") if let currentTabViewModel = presenterWrapper.currentTabViewModel { currentTabViewModel.timelineViewModel.handleScrollOffsetChange( offsetY, - showFloatingButton: $timelineState.showFloatingButton, + showFloatingButton: bindableTimelineState, timelineState: timelineState, isHomeTab: true ) diff --git a/iosApp/iosApp/UI/Page/Profile/SwiftUIView/ProfileTimelineContentView.swift b/iosApp/iosApp/UI/Page/Profile/SwiftUIView/ProfileTimelineContentView.swift index e1642479f..a9e8133ed 100644 --- a/iosApp/iosApp/UI/Page/Profile/SwiftUIView/ProfileTimelineContentView.swift +++ b/iosApp/iosApp/UI/Page/Profile/SwiftUIView/ProfileTimelineContentView.swift @@ -6,10 +6,12 @@ struct ProfileTimelineContentView: View { let timelineViewModel: TimelineViewModel let isCurrentTab: Bool - @EnvironmentObject private var timelineState: TimelineExtState + @Environment(TimelineExtState.self) private var timelineState @Environment(FlareTheme.self) private var theme var body: some View { + @Bindable var bindableTimelineState = timelineState + Group { switch timelineViewModel.timelineState { case .loading: @@ -28,7 +30,7 @@ struct ProfileTimelineContentView: View { FlareLog.debug("📜 [ProfileTimelineContentView] Timeline滚动检测 - offsetY: \(newValue.contentOffset.y)") timelineViewModel.handleScrollOffsetChange( newValue.contentOffset.y, - showFloatingButton: $timelineState.showFloatingButton, + showFloatingButton: $bindableTimelineState.showFloatingButton, timelineState: timelineState, isHomeTab: isCurrentTab ) diff --git a/iosApp/iosApp/UI/Page/Profile/SwiftUIView/ProfileWaterfallContentView.swift b/iosApp/iosApp/UI/Page/Profile/SwiftUIView/ProfileWaterfallContentView.swift index d0f0575b7..5d454d227 100644 --- a/iosApp/iosApp/UI/Page/Profile/SwiftUIView/ProfileWaterfallContentView.swift +++ b/iosApp/iosApp/UI/Page/Profile/SwiftUIView/ProfileWaterfallContentView.swift @@ -7,10 +7,12 @@ struct ProfileWaterfallContentView: View { let selectedTabKey: String? let isCurrentTab: Bool - @EnvironmentObject private var timelineState: TimelineExtState + @Environment(TimelineExtState.self) private var timelineState @Environment(FlareTheme.self) private var theme var body: some View { + @Bindable var bindableTimelineState = timelineState + Group { switch timelineViewModel.timelineState { case .loading: @@ -34,7 +36,7 @@ struct ProfileWaterfallContentView: View { FlareLog.debug("📜 [ProfileWaterfallContentView] Media滚动检测 - offsetY: \(newValue.contentOffset.y)") timelineViewModel.handleScrollOffsetChange( newValue.contentOffset.y, - showFloatingButton: $timelineState.showFloatingButton, + showFloatingButton: $bindableTimelineState.showFloatingButton, timelineState: timelineState, isHomeTab: isCurrentTab ) diff --git a/iosApp/iosApp/UI/Page/Settings/View/ReleaseLogBannerView.swift b/iosApp/iosApp/UI/Page/Settings/View/ReleaseLogBannerView.swift index ddb416b8b..1f10e65c4 100644 --- a/iosApp/iosApp/UI/Page/Settings/View/ReleaseLogBannerView.swift +++ b/iosApp/iosApp/UI/Page/Settings/View/ReleaseLogBannerView.swift @@ -1,12 +1,16 @@ import SwiftUI struct ReleaseLogBannerView: View { - @ObservedObject private var releaseLogManager = ReleaseLogManager.shared + private var releaseLogManager = ReleaseLogManager.shared @Environment(FlareTheme.self) private var theme @State private var showReleaseLogSheet = false let onDismiss: () -> Void + init(onDismiss: @escaping () -> Void) { + self.onDismiss = onDismiss + } + var body: some View { Button(action: { showReleaseLogSheet = true diff --git a/iosApp/iosApp/UI/Page/Settings/View/ReleaseLogScreen.swift b/iosApp/iosApp/UI/Page/Settings/View/ReleaseLogScreen.swift index 7da83ea92..84097f1b3 100644 --- a/iosApp/iosApp/UI/Page/Settings/View/ReleaseLogScreen.swift +++ b/iosApp/iosApp/UI/Page/Settings/View/ReleaseLogScreen.swift @@ -2,10 +2,10 @@ import MarkdownUI import SwiftUI struct ReleaseLogScreen: View { - @StateObject private var releaseLogManager = ReleaseLogManager.shared - @StateObject private var translationViewModel = TranslationViewModel() + @State private var releaseLogManager = ReleaseLogManager.shared + @State private var translationViewModel = TranslationViewModel() @State private var isTranslating = false - @State private var translatedContent: String? + @State private var translatedEntries: [String: String] = [:] // 按版本存储翻译结果 @Environment(FlareTheme.self) private var theme var body: some View { @@ -17,8 +17,8 @@ struct ReleaseLogScreen: View { } else { ForEach(releaseLogManager.releaseLogEntries, id: \.version) { entry in VStack(alignment: .leading, spacing: 12) { - if let translated = translatedContent { - Markdown(translated) + if let translatedContent = translatedEntries[entry.version] { + Markdown(translatedContent) .markdownTheme(.flareMarkdownStyle(using: theme.flareTextBodyTextStyle, fontScale: theme.fontSizeScale)) .padding() } else { @@ -59,26 +59,74 @@ struct ReleaseLogScreen: View { guard !releaseLogManager.releaseLogEntries.isEmpty else { return } isTranslating = true - let fullContent = releaseLogManager.releaseLogEntries - .map(\.content) - .joined(separator: "\n\n----------\n\n") Task { - do { - let locale = Locale.current - let targetLanguage = locale.language.languageCode?.identifier ?? "en" - let translationService = GoogleTranslationService(targetLanguage: targetLanguage) - let result = try await translationService.translate(text: fullContent) - await MainActor.run { - translatedContent = result.translatedText - isTranslating = false + let locale = Locale.current + let targetLanguage = locale.language.languageCode?.identifier ?? "en" + let translationService = GoogleTranslationService(targetLanguage: targetLanguage) + + await translateWithMergedContent(translationService: translationService) + } + } + + private func translateWithMergedContent(translationService: GoogleTranslationService) async { + let entries = releaseLogManager.releaseLogEntries + let separator = "###FLARE_RELEASE_LOG_SEPARATOR###" + + let fullContent = entries.map(\.content).joined(separator: separator) + + do { + let result = try await translationService.translate(text: fullContent) + + let translatedParts = result.translatedText.components(separatedBy: separator) + + guard translatedParts.count == entries.count else { + print("拆分结果数量不匹配,降级到逐个翻译") + await fallbackToIndividualTranslation(translationService: translationService) + return + } + + var newTranslatedEntries: [String: String] = [:] + for (index, entry) in entries.enumerated() { + let translatedContent = translatedParts[index].trimmingCharacters(in: .whitespacesAndNewlines) + if !translatedContent.isEmpty { + newTranslatedEntries[entry.version] = translatedContent } + } + + await MainActor.run { + translatedEntries = newTranslatedEntries + isTranslating = false + } + + } catch { + print("合并翻译失败,降级到逐个翻译: \(error)") + await fallbackToIndividualTranslation(translationService: translationService) + } + } + + private func fallbackToIndividualTranslation(translationService: GoogleTranslationService) async { + let entries = releaseLogManager.releaseLogEntries + var newTranslatedEntries: [String: String] = [:] + + newTranslatedEntries = translatedEntries + + for entry in entries { + if translatedEntries[entry.version] != nil { + continue + } + + do { + let result = try await translationService.translate(text: entry.content) + newTranslatedEntries[entry.version] = result.translatedText } catch { - await MainActor.run { - isTranslating = false - print("翻译失败: \(error)") - } + print("翻译条目失败 \(entry.version): \(error)") } } + + await MainActor.run { + translatedEntries = newTranslatedEntries + isTranslating = false + } } } diff --git a/iosApp/iosApp/UI/Page/Space/DraggablePlayerOverlay.swift b/iosApp/iosApp/UI/Page/Space/DraggablePlayerOverlay.swift index 68b6aa1f7..44fcd769a 100644 --- a/iosApp/iosApp/UI/Page/Space/DraggablePlayerOverlay.swift +++ b/iosApp/iosApp/UI/Page/Space/DraggablePlayerOverlay.swift @@ -2,7 +2,7 @@ import shared import SwiftUI struct DraggablePlayerOverlay: View { - @StateObject private var manager = IOSPodcastManager.shared + @State private var manager = IOSPodcastManager.shared @State private var accumulatedOffset: CGSize = .zero @State private var dragOffset: CGSize = .zero diff --git a/iosApp/iosApp/UI/Page/Space/LiveFloatingPlayerView.swift b/iosApp/iosApp/UI/Page/Space/LiveFloatingPlayerView.swift index e197fa9dd..5a782b7b9 100644 --- a/iosApp/iosApp/UI/Page/Space/LiveFloatingPlayerView.swift +++ b/iosApp/iosApp/UI/Page/Space/LiveFloatingPlayerView.swift @@ -3,7 +3,7 @@ import shared import SwiftUI struct LiveFloatingPlayerView: View { - @StateObject private var manager = IOSPodcastManager.shared + @State private var manager = IOSPodcastManager.shared @Environment(FlareRouter.self) private var router @State private var showPodcastSheet: Bool = false diff --git a/iosApp/iosApp/UI/Page/Space/ReplayFloatingPlayerView.swift b/iosApp/iosApp/UI/Page/Space/ReplayFloatingPlayerView.swift index 2a3f90f26..31b6b7a38 100644 --- a/iosApp/iosApp/UI/Page/Space/ReplayFloatingPlayerView.swift +++ b/iosApp/iosApp/UI/Page/Space/ReplayFloatingPlayerView.swift @@ -1,10 +1,9 @@ import AVFoundation -import Combine import shared import SwiftUI struct ReplayFloatingPlayerView: View { - @ObservedObject var manager: IOSPodcastManager + @Bindable var manager: IOSPodcastManager @State private var displayTime: Double = 0.0 @State private var isEditingSlider: Bool = false @@ -26,15 +25,15 @@ struct ReplayFloatingPlayerView: View { .padding(.horizontal) .padding(.bottom, 5) .transition(.move(edge: .bottom).combined(with: .opacity)) - .onReceive(manager.currentTimePublisher) { receivedTime in + .onChange(of: manager.currentTime) { _, newTime in if !isEditingSlider, !manager.isSeeking { - displayTime = receivedTime + displayTime = newTime } } .onAppear { displayTime = manager.currentPlaybackTime } - .onChange(of: manager.currentPodcast?.id) { _ in + .onChange(of: manager.currentPodcast?.id) { _, _ in displayTime = manager.currentPlaybackTime } } diff --git a/iosApp/iosApp/UI/Page/Space/iOSPodcastManager.swift b/iosApp/iosApp/UI/Page/Space/iOSPodcastManager.swift index 2fa56a70b..688c13227 100644 --- a/iosApp/iosApp/UI/Page/Space/iOSPodcastManager.swift +++ b/iosApp/iosApp/UI/Page/Space/iOSPodcastManager.swift @@ -1,8 +1,8 @@ import AVFoundation -import Combine import Foundation import MediaPlayer import shared +import SwiftUI import UIKit enum PodcastPlaybackState: Equatable { @@ -24,24 +24,20 @@ enum PodcastPlaybackState: Equatable { } } -class IOSPodcastManager: ObservableObject { +@Observable +class IOSPodcastManager { static let shared = IOSPodcastManager() - @Published private(set) var currentPodcast: UiPodcast? = nil - @Published private(set) var isPlaying: Bool = false - @Published private(set) var playbackState: PodcastPlaybackState = .stopped - @Published private(set) var duration: Double? = nil - @Published private(set) var canSeek: Bool = false - @Published private(set) var isSeeking: Bool = false - + private(set) var currentPodcast: UiPodcast? + private(set) var isPlaying: Bool = false + private(set) var playbackState: PodcastPlaybackState = .stopped + private(set) var duration: Double? + private(set) var canSeek: Bool = false + private(set) var isSeeking: Bool = false private(set) var currentTime: Double = 0.0 - private let currentTimeSubject = CurrentValueSubject(0.0) - var currentTimePublisher: AnyPublisher { - currentTimeSubject.eraseToAnyPublisher() - } var currentPlaybackTime: Double { - currentTimeSubject.value + currentTime } private var player: AVPlayer? @@ -105,7 +101,6 @@ class IOSPodcastManager: ObservableObject { self.isPlaying = false self.duration = nil self.currentTime = 0.0 - self.currentTimeSubject.send(0.0) self.canSeek = false self.isSeeking = false self.updateNowPlayingInfo() @@ -133,7 +128,6 @@ class IOSPodcastManager: ObservableObject { self.isPlaying = false self.duration = nil self.currentTime = 0.0 - self.currentTimeSubject.send(0.0) MPNowPlayingInfoCenter.default().nowPlayingInfo = nil } } @@ -196,7 +190,6 @@ class IOSPodcastManager: ObservableObject { FlareLog.debug("iOSPodcastManager Seek completed successfully to \(time).") DispatchQueue.main.async { self.currentTime = time - self.currentTimeSubject.send(time) self.updateNowPlayingInfo() } } else { @@ -346,8 +339,6 @@ class IOSPodcastManager: ObservableObject { // Only update if time has actually changed significantly and not during seeking if abs(currentTimeSeconds - currentTime) > 0.01, !isSeeking { currentTime = currentTimeSeconds - currentTimeSubject.send(currentTimeSeconds) - updateNowPlayingInfo() } } @@ -524,10 +515,14 @@ class IOSPodcastManager: ObservableObject { } catch let error as NSError where error.domain == NSURLErrorDomain && error.code == NSURLErrorCancelled { FlareLog.debug("iOSPodcastManager Artwork fetch task explicitly cancelled for ID: \(podcast.id).") - await MainActor.run { self.artworkLoadingTasks.removeValue(forKey: podcast.id) } + await MainActor.run { + _ = self.artworkLoadingTasks.removeValue(forKey: podcast.id) + } } catch { FlareLog.error("iOSPodcastManager Error fetching artwork: \(error.localizedDescription)") - await MainActor.run { self.artworkLoadingTasks.removeValue(forKey: podcast.id) } + await MainActor.run { + _ = self.artworkLoadingTasks.removeValue(forKey: podcast.id) + } } } diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/BlueskyDataSource.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/BlueskyDataSource.kt index 425ca2de8..1bae76e9b 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/BlueskyDataSource.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/BlueskyDataSource.kt @@ -94,6 +94,7 @@ import dev.dimension.flare.data.network.bluesky.BlueskyService import dev.dimension.flare.data.network.bluesky.model.DidDoc import dev.dimension.flare.data.repository.AccountRepository import dev.dimension.flare.data.repository.LocalFilterRepository +import dev.dimension.flare.data.repository.LoginExpiredException import dev.dimension.flare.data.repository.tryRun import dev.dimension.flare.model.AccountType import dev.dimension.flare.model.MicroBlogKey @@ -138,6 +139,7 @@ import sh.christian.ozone.api.Nsid import sh.christian.ozone.api.RKey import sh.christian.ozone.api.model.JsonContent import sh.christian.ozone.api.model.JsonContent.Companion.encodeAsJsonContent +import sh.christian.ozone.api.response.AtpException import kotlin.time.Clock import kotlin.uuid.Uuid @@ -734,8 +736,14 @@ internal class BlueskyDataSource( } StatusActionResult.success() - } catch (e: Exception) { - StatusActionResult.failure(e) + } catch (e: Throwable) { + val errorMessage = + when (e) { + is AtpException -> e.error?.message ?: e.error?.error ?: e.message ?: "Unknown Bluesky error" + is LoginExpiredException -> "Login expired, please re-login" + else -> e.message ?: e::class.simpleName ?: "Unknown error" + } + StatusActionResult.failure(errorMessage) } } @@ -781,8 +789,14 @@ internal class BlueskyDataSource( } StatusActionResult.success() - } catch (e: Exception) { - StatusActionResult.failure(e) + } catch (e: Throwable) { + val errorMessage = + when (e) { + is AtpException -> e.error?.message ?: e.error?.error ?: e.message ?: "Unknown Bluesky error" + is LoginExpiredException -> "Login expired, please re-login" + else -> e.message ?: e::class.simpleName ?: "Unknown error" + } + StatusActionResult.failure(errorMessage) } } diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/MastodonDataSource.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/MastodonDataSource.kt index 356da686e..ccab058f0 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/MastodonDataSource.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/MastodonDataSource.kt @@ -33,6 +33,7 @@ import dev.dimension.flare.data.datasource.microblog.StatusEvent import dev.dimension.flare.data.datasource.microblog.memoryPager import dev.dimension.flare.data.datasource.microblog.relationKeyWithUserKey import dev.dimension.flare.data.datasource.microblog.timelinePager +import dev.dimension.flare.data.network.mastodon.MastodonException import dev.dimension.flare.data.network.mastodon.MastodonService import dev.dimension.flare.data.network.mastodon.api.model.PostAccounts import dev.dimension.flare.data.network.mastodon.api.model.PostList @@ -43,6 +44,7 @@ import dev.dimension.flare.data.network.mastodon.api.model.PostVote import dev.dimension.flare.data.network.mastodon.api.model.Visibility import dev.dimension.flare.data.repository.AccountRepository import dev.dimension.flare.data.repository.LocalFilterRepository +import dev.dimension.flare.data.repository.LoginExpiredException import dev.dimension.flare.data.repository.tryRun import dev.dimension.flare.model.AccountType import dev.dimension.flare.model.MicroBlogKey @@ -612,8 +614,14 @@ internal open class MastodonDataSource( updateLikeStatus(statusKey, shouldLike, result) StatusActionResult.success() - } catch (e: Exception) { - StatusActionResult.failure(e) + } catch (e: Throwable) { + val errorMessage = + when (e) { + is MastodonException -> e.error ?: e.message ?: "Unknown Mastodon error" + is LoginExpiredException -> "Login expired, please re-login" + else -> e.message ?: e::class.simpleName ?: "Unknown error" + } + StatusActionResult.failure(errorMessage) } } @@ -633,8 +641,14 @@ internal open class MastodonDataSource( updateReblogStatus(statusKey, shouldReblog, result) StatusActionResult.success() - } catch (e: Exception) { - StatusActionResult.failure(e) + } catch (e: Throwable) { + val errorMessage = + when (e) { + is MastodonException -> e.error ?: e.message ?: "Unknown Mastodon error" + is LoginExpiredException -> "Login expired, please re-login" + else -> e.message ?: e::class.simpleName ?: "Unknown error" + } + StatusActionResult.failure(errorMessage) } } @@ -654,8 +668,14 @@ internal open class MastodonDataSource( updateBookmarkStatus(statusKey, shouldBookmark, result) StatusActionResult.success() - } catch (e: Exception) { - StatusActionResult.failure(e) + } catch (e: Throwable) { + val errorMessage = + when (e) { + is MastodonException -> e.error ?: e.message ?: "Unknown Mastodon error" + is LoginExpiredException -> "Login expired, please re-login" + else -> e.message ?: e::class.simpleName ?: "Unknown error" + } + StatusActionResult.failure(errorMessage) } } diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/MisskeyDataSource.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/MisskeyDataSource.kt index 45c7c0559..b9968a7cf 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/MisskeyDataSource.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/misskey/MisskeyDataSource.kt @@ -51,6 +51,7 @@ import dev.dimension.flare.data.network.misskey.api.model.UsersListsUpdateReques import dev.dimension.flare.data.network.misskey.api.model.UsersShowRequest import dev.dimension.flare.data.repository.AccountRepository import dev.dimension.flare.data.repository.LocalFilterRepository +import dev.dimension.flare.data.repository.LoginExpiredException import dev.dimension.flare.data.repository.tryRun import dev.dimension.flare.model.AccountType import dev.dimension.flare.model.MicroBlogKey @@ -936,8 +937,13 @@ internal class MisskeyDataSource( ) StatusActionResult.success() - } catch (e: Exception) { - StatusActionResult.failure(e) + } catch (e: Throwable) { + val errorMessage = + when (e) { + is LoginExpiredException -> "Login expired, please re-login" + else -> e.message ?: e::class.simpleName ?: "Unknown error" + } + StatusActionResult.failure(errorMessage) } } @@ -962,8 +968,13 @@ internal class MisskeyDataSource( } StatusActionResult.success() - } catch (e: Exception) { - StatusActionResult.failure(e) + } catch (e: Throwable) { + val errorMessage = + when (e) { + is LoginExpiredException -> "Login expired, please re-login" + else -> e.message ?: e::class.simpleName ?: "Unknown error" + } + StatusActionResult.failure(errorMessage) } } diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/vvo/VVODataSource.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/vvo/VVODataSource.kt index 59a9cc3e2..6337d2888 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/vvo/VVODataSource.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/vvo/VVODataSource.kt @@ -778,8 +778,13 @@ internal class VVODataSource( ) StatusActionResult.success() - } catch (e: Exception) { - StatusActionResult.failure(e) + } catch (e: Throwable) { + val errorMessage = + when (e) { + is LoginExpiredException -> "Login expired, please re-login" + else -> e.message ?: e::class.simpleName ?: "Unknown error" + } + StatusActionResult.failure(errorMessage) } } } @@ -825,8 +830,13 @@ internal class VVODataSource( ) StatusActionResult.success() - } catch (e: Exception) { - StatusActionResult.failure(e) + } catch (e: Throwable) { + val errorMessage = + when (e) { + is LoginExpiredException -> "Login expired, please re-login" + else -> e.message ?: e::class.simpleName ?: "Unknown error" + } + StatusActionResult.failure(errorMessage) } } } diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/XQTDataSource.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/XQTDataSource.kt index bce66ba64..d2dc79f1d 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/XQTDataSource.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/xqt/XQTDataSource.kt @@ -75,6 +75,7 @@ import dev.dimension.flare.data.network.xqt.model.User import dev.dimension.flare.data.network.xqt.model.UserUnavailable import dev.dimension.flare.data.repository.AccountRepository import dev.dimension.flare.data.repository.LocalFilterRepository +import dev.dimension.flare.data.repository.LoginExpiredException import dev.dimension.flare.data.repository.tryRun import dev.dimension.flare.model.AccountType import dev.dimension.flare.model.MicroBlogKey @@ -792,8 +793,13 @@ internal class XQTDataSource( updateLikeStatus(statusKey, shouldLike) StatusActionResult.success() - } catch (e: Exception) { - StatusActionResult.failure(e) + } catch (e: Throwable) { + val errorMessage = + when (e) { + is LoginExpiredException -> "Login expired, please re-login" + else -> e.message ?: e::class.simpleName ?: "Unknown error" + } + StatusActionResult.failure(errorMessage) } } @@ -996,8 +1002,13 @@ internal class XQTDataSource( updateBookmarkStatus(statusKey, shouldBookmark) StatusActionResult.success() - } catch (e: Exception) { - StatusActionResult.failure(e) + } catch (e: Throwable) { + val errorMessage = + when (e) { + is LoginExpiredException -> "Login expired, please re-login" + else -> e.message ?: e::class.simpleName ?: "Unknown error" + } + StatusActionResult.failure(errorMessage) } } @@ -1032,8 +1043,13 @@ internal class XQTDataSource( updateRetweetStatus(statusKey, shouldRetweet) StatusActionResult.success() - } catch (e: Exception) { - StatusActionResult.failure(e) + } catch (e: Throwable) { + val errorMessage = + when (e) { + is LoginExpiredException -> "Login expired, please re-login" + else -> e.message ?: e::class.simpleName ?: "Unknown error" + } + StatusActionResult.failure(errorMessage) } }