diff --git a/iosApp/iosApp/UI/Navigation/FlareDestinationView.swift b/iosApp/iosApp/UI/Navigation/FlareDestinationView.swift index a8ca5c3f4..0bbfbbff0 100644 --- a/iosApp/iosApp/UI/Navigation/FlareDestinationView.swift +++ b/iosApp/iosApp/UI/Navigation/FlareDestinationView.swift @@ -8,7 +8,7 @@ struct FlareDestinationView: View { let destination: FlareDestination let router: FlareRouter - @Environment(FlareAppState.self) private var appState + @Environment(FlareMenuState.self) private var menuState @Environment(\.appSettings) private var appSettings @Environment(FlareTheme.self) private var theme @@ -24,8 +24,7 @@ struct FlareDestinationView: View { case let .profile(accountType, userKey): ProfileTabScreenUikit( accountType: accountType, - userKey: userKey, - toProfileMedia: { _ in } + userKey: userKey ) .environment(router) @@ -33,8 +32,7 @@ struct FlareDestinationView: View { ProfileWithUserNameScreen( accountType: accountType, userName: userName, - host: host, - toProfileMedia: { _ in } + host: host ) .environment(router) @@ -125,23 +123,23 @@ struct FlareDestinationView: View { case let .download(accountType): DownloadManagerScreen(accountType: accountType) .environment(router) - .environment(appState) + .environment(menuState) case let .instanceScreen(host, _): InstanceScreen(host: host) .environment(router) - .environment(appState) + .environment(menuState) case let .podcastSheet(accountType, podcastId): PodcastSheetView(accountType: accountType, podcastId: podcastId) .environment(router) - .environment(appState) + .environment(menuState) .environment(\.appSettings, appSettings) case let .spaces(accountType): SpaceScreen(accountType: accountType) .environment(router) - .environment(appState) + .environment(menuState) default: Text("page not found for destination: \(String(describing: destination))") @@ -149,7 +147,7 @@ struct FlareDestinationView: View { } } .environment(router) - .environment(appState) + .environment(menuState) .background(theme.primaryBackgroundColor) .foregroundColor(theme.labelColor) } diff --git a/iosApp/iosApp/UI/Navigation/FlareRootView.swift b/iosApp/iosApp/UI/Navigation/FlareRootView.swift index 27ded9e01..9cb9022a9 100644 --- a/iosApp/iosApp/UI/Navigation/FlareRootView.swift +++ b/iosApp/iosApp/UI/Navigation/FlareRootView.swift @@ -3,7 +3,7 @@ import shared import SwiftUI struct FlareRootView: View { - @State var appState = FlareAppState() + @State var menuState = FlareMenuState() @StateObject private var router = FlareRouter.shared @StateObject private var composeManager = ComposeManager.shared @StateObject private var timelineState = TimelineExtState() @@ -34,7 +34,7 @@ struct FlareRootView: View { HomeTabViewContentV2(accountType: accountType) .environment(theme) .applyTheme(theme) - .environment(appState) + .environment(menuState) .environment(router) .environmentObject(timelineState) .sheet(isPresented: $router.isSheetPresented) { @@ -44,7 +44,7 @@ struct FlareRootView: View { router: router ).environment(theme) .applyTheme(theme) - .environment(appState) + .environment(menuState) } } .fullScreenCover(isPresented: $router.isFullScreenPresented) { @@ -56,7 +56,7 @@ struct FlareRootView: View { .environment(theme) .applyTheme(theme) .environment(\.appSettings, appSettings) - .environment(appState) + .environment(menuState) } } .alert(isPresented: $router.isDialogPresented) { @@ -78,7 +78,7 @@ struct FlareRootView: View { .environment(theme) .applyTheme(theme) .environment(router) - .environment(appState) + .environment(menuState) .environment(\.appSettings, appSettings) } } @@ -92,7 +92,7 @@ struct FlareRootView: View { .navigationViewStyle(StackNavigationViewStyle()) .onAppear { setupInitialState() - router.appState = appState + router.menuState = menuState } .frame(maxWidth: .infinity, maxHeight: .infinity) ) diff --git a/iosApp/iosApp/UI/Navigation/FlareRouter.swift b/iosApp/iosApp/UI/Navigation/FlareRouter.swift index 089570494..821782826 100644 --- a/iosApp/iosApp/UI/Navigation/FlareRouter.swift +++ b/iosApp/iosApp/UI/Navigation/FlareRouter.swift @@ -9,7 +9,7 @@ import UIKit class FlareRouter: ObservableObject { public static let shared = FlareRouter() - public var appState: FlareAppState + public var menuState: FlareMenuState private var cancellables = Set() @@ -110,8 +110,8 @@ class FlareRouter: ObservableObject { log: .default, type: .debug, String(describing: tab)) } - init(appState: FlareAppState = FlareAppState()) { - self.appState = appState + init(menuState: FlareMenuState = FlareMenuState()) { + self.menuState = menuState os_log("[FlareRouter] Initialized router: %{public}@", log: .default, type: .debug, String(describing: ObjectIdentifier(self))) } diff --git a/iosApp/iosApp/UI/Page/Compose/Text/FlareMarkdownStyle.swift b/iosApp/iosApp/UI/Page/Compose/Text/FlareMarkdownStyle.swift index 6395bfcbd..8d63dbd4d 100644 --- a/iosApp/iosApp/UI/Page/Compose/Text/FlareMarkdownStyle.swift +++ b/iosApp/iosApp/UI/Page/Compose/Text/FlareMarkdownStyle.swift @@ -1,12 +1,10 @@ import MarkdownUI import SwiftUI -// 扩展的是 MarkdownUI.Theme extension MarkdownUI.Theme { static func flareMarkdownStyle(using style: FlareTextStyle.Style, fontScale: Double) -> MarkdownUI.Theme { - // 获取基础字体大小 let baseFontSize = style.font.pointSize - // 应用用户的字体缩放设置 + let scaledFontSize = baseFontSize * fontScale return MarkdownUI.Theme() @@ -23,21 +21,17 @@ extension MarkdownUI.Theme { // UnderlineStyle(.single) } .strong { - // 粗体文本 FontWeight(.semibold) ForegroundColor(Color(style.textColor)) } .emphasis { - // 斜体文本 FontStyle(.italic) ForegroundColor(Color(style.textColor)) } .code { - // 行内代码 FontFamilyVariant(.monospaced) FontSize(scaledFontSize * 0.9) ForegroundColor(Color(style.textColor)) - // BackgroundColor(Color.gray.opacity(0.1)) } } } diff --git a/iosApp/iosApp/UI/Page/Compose/Text/FlareText.swift b/iosApp/iosApp/UI/Page/Compose/Text/FlareText.swift index fe8d91bb2..bcbb00961 100644 --- a/iosApp/iosApp/UI/Page/Compose/Text/FlareText.swift +++ b/iosApp/iosApp/UI/Page/Compose/Text/FlareText.swift @@ -4,8 +4,8 @@ import SwiftUI import TwitterText public enum FlareTextType { - case body - case caption + case flareTextTypeBody + case flareTextTypeCaption } public struct FlareText: View, Equatable { @@ -20,10 +20,7 @@ public struct FlareText: View, Equatable { public static func == (lhs: FlareText, rhs: FlareText) -> Bool { lhs.text == rhs.text && lhs.markdownText == rhs.markdownText && - lhs.textType == rhs.textType && - lhs.isRTL == rhs.isRTL - // 注意:linkHandler是函数类型,无法比较,但通常不影响渲染 - // Environment变量由SwiftUI自动处理 + lhs.textType == rhs.textType } public init( @@ -46,10 +43,10 @@ public struct FlareText: View, Equatable { public var body: some View { let currentStyle: FlareTextStyle.Style = switch textType { - case .body: - theme.bodyTextStyle - case .caption: - theme.captionTextStyle + case .flareTextTypeBody: + theme.flareTextBodyTextStyle + case .flareTextTypeCaption: + theme.flareTextCaptionTextStyle } switch appSettings.appearanceSettings.renderEngine { diff --git a/iosApp/iosApp/UI/Page/Compose/Text/FlareTextStyle.swift b/iosApp/iosApp/UI/Page/Compose/Text/FlareTextStyle.swift index 6e1c76252..3db67c907 100644 --- a/iosApp/iosApp/UI/Page/Compose/Text/FlareTextStyle.swift +++ b/iosApp/iosApp/UI/Page/Compose/Text/FlareTextStyle.swift @@ -37,33 +37,6 @@ public enum FlareTextStyle { lhs.cashtagColor == rhs.cashtagColor } - // public static let `default` = Style( - // font: .systemFont(ofSize: 16), - // textColor: UIColor.black, // .Text.primary, - // linkColor: UIColor.black, - // mentionColor: UIColor.black, - // hashtagColor: UIColor.black, - // cashtagColor: UIColor.black - // ) - - // public static let timeline = Style( - // font: .systemFont(ofSize: 16), - // textColor: UIColor.black, - // linkColor: UIColor.black, - // mentionColor: UIColor.black, - // hashtagColor: UIColor.black, - // cashtagColor: UIColor.black - // ) - - // public static let quote = Style( - // font: .systemFont(ofSize: 15), - // textColor: UIColor.black, - // linkColor: UIColor.black.withAlphaComponent(0.8), - // mentionColor: UIColor.black.withAlphaComponent(0.8), - // hashtagColor: UIColor.black.withAlphaComponent(0.8), - // cashtagColor: UIColor.black.withAlphaComponent(0.8) - // ) - public init( font: UIFont = .systemFont(ofSize: 16), textColor: UIColor = UIColor.black, diff --git a/iosApp/iosApp/UI/Page/Compose/Timeline/CardPreview/LinkPreview.swift b/iosApp/iosApp/UI/Page/Compose/Timeline/CardPreview/LinkPreview.swift deleted file mode 100644 index 90853052d..000000000 --- a/iosApp/iosApp/UI/Page/Compose/Timeline/CardPreview/LinkPreview.swift +++ /dev/null @@ -1,39 +0,0 @@ -import shared -import SwiftUI - -struct LinkPreview: View { - let card: UiCard - var body: some View { - Link(destination: URL(string: card.url)!) { - HStack { - if let media = card.media { - MediaItemComponent(media: media) - .frame(width: 64, height: 64) - } - VStack(alignment: .leading) { - Text(card.title) - .lineLimit(1) - if let desc = card.description_ { - Text(desc) - .font(.caption) - .foregroundStyle(.gray) - .lineLimit(2) - } - } - .foregroundStyle(.foreground) - .if(card.media == nil) { view in - view.padding() - } - Spacer() - } - } - .frame(maxWidth: 600) - .buttonStyle(.plain) - #if os(iOS) - .background(Color(UIColor.secondarySystemBackground)) - #else - .background(Color(NSColor.windowBackgroundColor)) - #endif - .cornerRadius(8) - } -} diff --git a/iosApp/iosApp/UI/Page/Compose/TimelineV2/LinkPreviewV2.swift b/iosApp/iosApp/UI/Page/Compose/Timeline/CardPreview/LinkPreviewV2.swift similarity index 85% rename from iosApp/iosApp/UI/Page/Compose/TimelineV2/LinkPreviewV2.swift rename to iosApp/iosApp/UI/Page/Compose/Timeline/CardPreview/LinkPreviewV2.swift index c7bf8221e..0902c2f97 100644 --- a/iosApp/iosApp/UI/Page/Compose/TimelineV2/LinkPreviewV2.swift +++ b/iosApp/iosApp/UI/Page/Compose/Timeline/CardPreview/LinkPreviewV2.swift @@ -1,19 +1,18 @@ import SwiftUI struct LinkPreviewV2: View { - let card: Card // 使用Swift Card类型 + let card: Card var body: some View { Link(destination: URL(string: card.url)!) { HStack { if let media = card.media { - // 使用V2版本的MediaItemComponent MediaItemComponentV2(media: media) .frame(width: 64, height: 64) } VStack(alignment: .leading) { - Text(card.title ?? "") // 处理可选值 + Text(card.title ?? "") .lineLimit(1) - if let desc = card.description, !desc.isEmpty { // 确保description不为空 + if let desc = card.description, !desc.isEmpty { Text(desc) .font(.caption) .foregroundStyle(.gray) diff --git a/iosApp/iosApp/UI/Page/Compose/Timeline/CardPreview/PodcastPreview.swift b/iosApp/iosApp/UI/Page/Compose/Timeline/CardPreview/PodcastPreview.swift deleted file mode 100644 index b93fb8623..000000000 --- a/iosApp/iosApp/UI/Page/Compose/Timeline/CardPreview/PodcastPreview.swift +++ /dev/null @@ -1,37 +0,0 @@ -import shared -import SwiftUI - -struct PodcastPreview: View { - let card: UiCard - - private var podcastId: String { - URL(string: card.url)?.lastPathComponent ?? "" - } - - var body: some View { - HStack(spacing: 12) { - Image(systemName: "headphones.circle.fill") - .imageScale(.large) - .foregroundColor(.pink) - - VStack(alignment: .leading, spacing: 2) { - Text("X Audio Space") - .font(.subheadline.weight(.medium)) - .foregroundColor(.primary) - - if !podcastId.isEmpty { - Text(podcastId) - .font(.caption) - .foregroundColor(.secondary) - .lineLimit(1) - } - } - - Spacer() - } - .padding(12) - .background(.thinMaterial) - .cornerRadius(10) - .contentShape(Rectangle()) - } -} diff --git a/iosApp/iosApp/UI/Page/Compose/TimelineV2/PodcastPreviewV2.swift b/iosApp/iosApp/UI/Page/Compose/Timeline/CardPreview/PodcastPreviewV2.swift similarity index 93% rename from iosApp/iosApp/UI/Page/Compose/TimelineV2/PodcastPreviewV2.swift rename to iosApp/iosApp/UI/Page/Compose/Timeline/CardPreview/PodcastPreviewV2.swift index b88f52e5c..1d1cf736a 100644 --- a/iosApp/iosApp/UI/Page/Compose/TimelineV2/PodcastPreviewV2.swift +++ b/iosApp/iosApp/UI/Page/Compose/Timeline/CardPreview/PodcastPreviewV2.swift @@ -1,8 +1,7 @@ -// import Foundation import SwiftUI struct PodcastPreviewV2: View { - let card: Card // 使用Swift Card类型 + let card: Card private var podcastId: String { URL(string: card.url)?.lastPathComponent ?? "" diff --git a/iosApp/iosApp/UI/Page/Compose/Timeline/ShareButton/ShareButton.swift b/iosApp/iosApp/UI/Page/Compose/Timeline/ShareButton/ShareButton.swift deleted file mode 100644 index cc1e28c03..000000000 --- a/iosApp/iosApp/UI/Page/Compose/Timeline/ShareButton/ShareButton.swift +++ /dev/null @@ -1,336 +0,0 @@ -import Awesome -import Generated -import JXPhotoBrowser -import Kingfisher -import MarkdownUI -import os.log -import shared -import SwiftDate -import SwiftUI -import UIKit - -#if canImport(_Translation_SwiftUI) - import Translation -#endif - -#if canImport(_Translation_SwiftUI) - extension View { - func addTranslateView(isPresented: Binding, text: String) -> some View { - #if targetEnvironment(macCatalyst) || os(visionOS) - FlareLog.warning("addTranslateView: Translation not supported on macCatalyst/visionOS") - return self - #else - if #available(iOS 17.4, *) { - return self.translationPresentation(isPresented: isPresented, text: text) - } else { - FlareLog.warning("addTranslateView: iOS version < 17.4, translation not available") - return self - } - #endif - } - } -#endif - -struct ShareButton: View { - @Environment(\.colorScheme) var colorScheme - @Environment(\.appSettings) var appSettings - @Environment(\.openURL) private var openURL - @Environment(FlareRouter.self) var router - @Environment(FlareTheme.self) private var theme - - @State private var isShareAsImageSheetPresented: Bool = false - @State private var showTextForSelection: Bool = false - @State private var renderer: ImageRenderer? - @State private var capturedImage: UIImage? - @State private var isPreparingShare: Bool = false - @State private var showTranslation: Bool = false - // @State private var showReportAlert = false - @State private var showSelectUrlSheet: Bool = false - - let content: UiTimelineItemContentStatus - let view: TimelineStatusView - - private var statusUrl: URL? { - guard let urlString = content.url as String? else { return nil } - return URL(string: urlString) - } - - private func prepareScreenshot(completion: @escaping (UIImage?) -> Void) { - let captureView = StatusCaptureWrapper(content: view) - .environment(\.appSettings, appSettings) - .environment(\.colorScheme, colorScheme) - .environment(\.isInCaptureMode, true) - .environment(router) - .environment(theme).applyTheme(theme) - - let controller = UIHostingController(rootView: captureView) - - let targetSize = controller.sizeThatFits(in: CGSize( - width: UIScreen.main.bounds.width - 24, - height: UIView.layoutFittingExpandedSize.height - )) - - controller.view.frame = CGRect(origin: .zero, size: targetSize) - controller.view.backgroundColor = UIColor.clear - - controller.view.layoutIfNeeded() - - DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { - let image = ScreenshotRenderer.render(captureView) - completion(image) - } - } - - private func getShareTitle(allContent: Bool) -> String { - if allContent { - return content.content.raw - } - let maxLength = 100 - if content.content.raw.count > maxLength { - let index = content.content.raw.index(content.content.raw.startIndex, offsetBy: maxLength) - return String(content.content.raw[.., text: String) -> some View { + #if targetEnvironment(macCatalyst) || os(visionOS) + FlareLog.warning("addTranslateView: Translation not supported on macCatalyst/visionOS") + return self + #else + if #available(iOS 17.4, *) { + return self.translationPresentation(isPresented: isPresented, text: text) + } else { + FlareLog.warning("addTranslateView: iOS version < 17.4, translation not available") + return self + } + #endif + } + } +#endif + +private struct CaptureMode: EnvironmentKey { + static let defaultValue: Bool = false +} + +extension EnvironmentValues { + var isInCaptureMode: Bool { + get { self[CaptureMode.self] } + set { self[CaptureMode.self] = newValue } + } +} + enum MoreActionType { case sharePost case shareAsImage diff --git a/iosApp/iosApp/UI/Page/Compose/Timeline/ShareButton/StatusShareAsImageView.swift b/iosApp/iosApp/UI/Page/Compose/Timeline/ShareButton/StatusShareAsImageView.swift deleted file mode 100644 index ec24ada26..000000000 --- a/iosApp/iosApp/UI/Page/Compose/Timeline/ShareButton/StatusShareAsImageView.swift +++ /dev/null @@ -1,147 +0,0 @@ -import Generated -import SwiftUI -import UIKit -import UniformTypeIdentifiers - -@available(iOS 16.0, *) -struct TransferableImage: Transferable { - let image: UIImage - - static var transferRepresentation: some TransferRepresentation { - DataRepresentation(exportedContentType: .png) { transferableImage in - guard let data = transferableImage.image.pngData() else { - throw URLError(.badServerResponse) - } - return data - } - } -} - -private struct CaptureMode: EnvironmentKey { - static let defaultValue: Bool = false -} - -extension EnvironmentValues { - var isInCaptureMode: Bool { - get { self[CaptureMode.self] } - set { self[CaptureMode.self] = newValue } - } -} - -struct StatusCaptureWrapper: View { - let content: TimelineStatusView - - var body: some View { - content - .padding(.horizontal, 12) - .padding(.vertical, 8) - // .background(Color(.systemBackground)) - } -} - -class ScreenshotRenderer { - static func render(_ view: some View) -> UIImage? { - let controller = UIHostingController(rootView: view) - let targetSize = controller.view.sizeThatFits(CGSize( - width: UIScreen.main.bounds.width - 24, - height: UIView.layoutFittingExpandedSize.height - )) - - controller.view.bounds = CGRect(origin: .zero, size: targetSize) - controller.view.backgroundColor = .clear - - let renderer = UIGraphicsImageRenderer(size: targetSize) - return renderer.image { _ in - controller.view.drawHierarchy(in: controller.view.bounds, afterScreenUpdates: true) - } - } -} - -struct StatusShareAsImageView: View { - @Environment(\.dismiss) private var dismiss - @Environment(\.colorScheme) private var colorScheme - @Environment(\.appSettings) private var appSettings - @Environment(FlareRouter.self) private var router - @Environment(FlareTheme.self) private var theme - - let content: TimelineStatusView - let renderer: ImageRenderer - @State private var capturedImage: UIImage? - @State private var isImageReady: Bool = false - let shareText: String - - var rendererImage: Image { - if let image = capturedImage { - return Image(uiImage: image) - } - return Image(uiImage: renderer.uiImage ?? UIImage()) - } - - var body: some View { - NavigationStack { - Form { - Section { - if isImageReady { - rendererImage - .resizable() - .scaledToFit() - } else { - ProgressView() - .frame(maxWidth: .infinity, minHeight: 200) - } - } - .listRowBackground(colorScheme == .dark ? Color.black : Color.white) - - Section { - if let image = capturedImage { - ShareLink( - item: TransferableImage(image: image), - subject: Text(shareText), - message: Text(shareText), - preview: SharePreview( - shareText, - image: rendererImage - ) - ) { - Label("Share", systemImage: "square.and.arrow.up") - .foregroundColor(theme.tintColor) - } - .disabled(!isImageReady) - } - } - } - .scrollContentBackground(.hidden) - .toolbar { - ToolbarItem(placement: .navigationBarTrailing) { - Button { - dismiss() - } label: { - Text("Done") - .bold() - } - } - } - .navigationTitle("Share post as image") - .navigationBarTitleDisplayMode(.inline) - } - .presentationBackground(.ultraThinMaterial) - .presentationCornerRadius(16) - .onAppear { - let view = StatusCaptureWrapper(content: content) - .environment(\.appSettings, appSettings) - .environment(\.colorScheme, colorScheme) - .environment(\.isInCaptureMode, true) - .environment(router) - .environment(theme).applyTheme(theme) - - // 增加延迟时间,确保敏感内容和媒体完全加载后再截图 - DispatchQueue.main.asyncAfter(deadline: .now() + 1.2) { - if let image = ScreenshotRenderer.render(view) { - capturedImage = image - isImageReady = true - } - } - } - .environment(theme).applyTheme(theme) - } -} diff --git a/iosApp/iosApp/UI/Page/Compose/TimelineV2/StatusShareAsImageViewV2.swift b/iosApp/iosApp/UI/Page/Compose/Timeline/ShareButton/StatusShareAsImageViewV2.swift similarity index 100% rename from iosApp/iosApp/UI/Page/Compose/TimelineV2/StatusShareAsImageViewV2.swift rename to iosApp/iosApp/UI/Page/Compose/Timeline/ShareButton/StatusShareAsImageViewV2.swift diff --git a/iosApp/iosApp/UI/Page/Compose/Timeline/StatusItemView.swift b/iosApp/iosApp/UI/Page/Compose/Timeline/StatusItemView.swift index a43a15b0c..684f2d8ed 100644 --- a/iosApp/iosApp/UI/Page/Compose/Timeline/StatusItemView.swift +++ b/iosApp/iosApp/UI/Page/Compose/Timeline/StatusItemView.swift @@ -5,6 +5,7 @@ struct StatusItemView: View { @Environment(\.openURL) private var openURL @Environment(\.appSettings) private var appSettings @Environment(FlareRouter.self) private var router + @Environment(FlareTheme.self) private var theme let data: UiTimeline let detailKey: MicroBlogKey? @@ -69,30 +70,40 @@ struct StatusItemView: View { if let content = data.content { switch onEnum(of: content) { - case let .status(data): Button(action: { - if detailKey != data.statusKey { - // data.onClicked(.init(launcher: AppleUriLauncher(openURL: openURL))) - router.navigate(to: .statusDetailV2( - accountType: UserManager.shared.getCurrentAccountType() ?? AccountTypeGuest(), - statusKey: data.statusKey - )) - } - }, label: { - TimelineStatusView( - data: data, - onMediaClick: { index, _ in - // data.onMediaClicked(.init(launcher: AppleUriLauncher(openURL: openURL)), media, KotlinInt(integerLiteral: index)) - router.navigate(to: .statusMedia( - accountType: UserManager.shared.getCurrentAccountType() ?? AccountTypeGuest(), - statusKey: data.statusKey, - index: index - )) - }, - isDetail: detailKey == data.statusKey, - enableTranslation: enableTranslation - ).id("CommonTimelineStatusComponent_\(data.statusKey)") - }) - .buttonStyle(.plain) + case let .status(data): + // Button(action: { + // if detailKey != data.statusKey { + // // data.onClicked(.init(launcher: AppleUriLauncher(openURL: openURL))) + // router.navigate(to: .statusDetailV2( + // accountType: UserManager.shared.getCurrentAccountType() ?? AccountTypeGuest(), + // statusKey: data.statusKey + // )) + // } + // }, label: { + TimelineStatusViewV2( + item: TimelineItem.from(self.data), + timelineViewModel: nil +// isDetail: detailKey == data.statusKey + ) + .listStyle(.plain) + .listRowBackground(theme.primaryBackgroundColor) + .listRowInsets(EdgeInsets()) + .listRowSeparator(.hidden) + // TimelineStatusView( + // data: data, + // onMediaClick: { index, _ in + // // data.onMediaClicked(.init(launcher: AppleUriLauncher(openURL: openURL)), media, KotlinInt(integerLiteral: index)) + // router.navigate(to: .statusMedia( + // accountType: UserManager.shared.getCurrentAccountType() ?? AccountTypeGuest(), + // statusKey: data.statusKey, + // index: index + // )) + // }, + // isDetail: detailKey == data.statusKey, + // enableTranslation: enableTranslation + // ).id("CommonTimelineStatusComponent_\(data.statusKey)") + // }) + // .buttonStyle(.plain) case let .user(data): HStack { UserComponent( diff --git a/iosApp/iosApp/UI/Page/Compose/Timeline/StatusTimelineBuilder.swift b/iosApp/iosApp/UI/Page/Compose/Timeline/StatusTimelineBuilder.swift index 8c41d2f9f..afa3a5477 100644 --- a/iosApp/iosApp/UI/Page/Compose/Timeline/StatusTimelineBuilder.swift +++ b/iosApp/iosApp/UI/Page/Compose/Timeline/StatusTimelineBuilder.swift @@ -5,6 +5,7 @@ struct StatusTimelineComponent: View { @Environment(\.horizontalSizeClass) private var horizontalSizeClass let data: PagingState let detailKey: MicroBlogKey? + @Environment(\.appSettings) private var appSettings var body: some View { switch onEnum(of: data) { @@ -41,7 +42,7 @@ struct StatusTimelineComponent: View { } } .onAppear { - // success.get(index: index) + success.get(index: index) } .if(horizontalSizeClass != .compact) { view in view.padding([.horizontal]) diff --git a/iosApp/iosApp/UI/Page/Compose/Timeline/TimelineStatus/StatusViewModel.swift b/iosApp/iosApp/UI/Page/Compose/Timeline/TimelineStatus/ActionProcessor.swift similarity index 61% rename from iosApp/iosApp/UI/Page/Compose/Timeline/TimelineStatus/StatusViewModel.swift rename to iosApp/iosApp/UI/Page/Compose/Timeline/TimelineStatus/ActionProcessor.swift index aaa4e766f..85c2ca739 100644 --- a/iosApp/iosApp/UI/Page/Compose/Timeline/TimelineStatus/StatusViewModel.swift +++ b/iosApp/iosApp/UI/Page/Compose/Timeline/TimelineStatus/ActionProcessor.swift @@ -9,67 +9,6 @@ import SwiftDate import SwiftUI import UIKit -@Observable -class StatusContentViewModel { - let content: UiRichText - let isRTL: Bool - - init(content: UiRichText) { - self.content = content - isRTL = content.isRTL - } - - var hasContent: Bool { !content.raw.isEmpty } - var rawText: String { content.raw } - var markdownText: String { content.markdown } -} - -struct StatusViewModel { - let data: UiTimelineItemContentStatus - let isDetail: Bool - let enableTranslation: Bool - - init(data: UiTimelineItemContentStatus, isDetail: Bool, enableTranslation: Bool = true) { - self.data = data - self.isDetail = isDetail - self.enableTranslation = enableTranslation - } - - var statusData: UiTimelineItemContentStatus { data } - var shouldShowTranslation: Bool { enableTranslation } - var isDetailView: Bool { isDetail } - - var hasUser: Bool { data.user != nil } - var hasAboveTextContent: Bool { data.aboveTextContent != nil } - var hasContentWarning: Bool { data.contentWarning != nil && !data.contentWarning!.raw.isEmpty } - var hasContent: Bool { !data.content.raw.isEmpty } - var hasImages: Bool { !data.images.isEmpty } - var hasCard: Bool { data.card != nil } - var hasQuote: Bool { !data.quote.isEmpty } - var hasBottomContent: Bool { data.bottomContent != nil } - var hasActions: Bool { !data.actions.isEmpty } - - var isPodcastCard: Bool { - guard let card = data.card, - let url = URL(string: card.url) else { return false } - return url.scheme == "flare" && url.host?.lowercased() == "podcast" - } - - var shouldShowLinkPreview: Bool { - guard let card = data.card else { return false } - return !isPodcastCard && card.media != nil - } - - func getProcessedActions() -> (mainActions: [StatusAction], moreActions: [StatusActionItem]) { - ActionProcessor.processActions(data.actions) - } - - func getFormattedDate() -> String { - let dateInRegion = DateInRegion(data.createdAt, region: .current) - return dateInRegion.toRelative(since: DateInRegion(Date(), region: .current)) - } -} - enum ActionProcessor { /*** 🔍 [TimelineActionsView] Total actions count: 4 diff --git a/iosApp/iosApp/UI/Page/Compose/Timeline/TimelineStatus/StatusActionsView.swift b/iosApp/iosApp/UI/Page/Compose/Timeline/TimelineStatus/StatusActionsView.swift deleted file mode 100644 index b1fc6d236..000000000 --- a/iosApp/iosApp/UI/Page/Compose/Timeline/TimelineStatus/StatusActionsView.swift +++ /dev/null @@ -1,302 +0,0 @@ -import Awesome -import Generated -import JXPhotoBrowser -import Kingfisher -import MarkdownUI -import os.log -import shared -import SwiftDate -import SwiftUI -import UIKit - -struct StatusActionsView: View { - let viewModel: StatusViewModel - let appSettings: AppSettings - let openURL: OpenURLAction - let parentView: TimelineStatusView - - var body: some View { - Spacer().frame(height: 10) - - if appSettings.appearanceSettings.showActions || viewModel.isDetailView, viewModel.hasActions { - let processedActions = viewModel.getProcessedActions() - - HStack(spacing: 0) { - ForEach(0 ..< processedActions.mainActions.count, id: \.self) { actionIndex in - let action = processedActions.mainActions[actionIndex] - - StatusActionButton( - action: action, - isDetail: viewModel.isDetailView, - openURL: openURL - ) - .frame(maxWidth: .infinity) - .padding(.horizontal, 10) - } - - ShareButton(content: viewModel.statusData, view: parentView) - .frame(maxWidth: .infinity) - .padding(.horizontal, 0) - } - .padding(.vertical, 6) -// .labelStyle(CenteredLabelStyle()) - .buttonStyle(.borderless) - .opacity(0.6) - .if(!viewModel.isDetailView) { view in - view.font(.caption) - } - .allowsHitTesting(true) - .contentShape(Rectangle()) - .onTapGesture {} - } - } -} - -struct StatusActionButton: View { - let action: StatusAction - let isDetail: Bool - let openURL: OpenURLAction - - var body: some View { - switch onEnum(of: action) { - case let .item(item): - Button(action: { - handleItemAction(item) - }, - label: { - StatusActionLabel(item: item) - }) - case .asyncActionItem: - EmptyView() - case let .group(group): - EmptyView() - // menu给删了,代码留着吧 -// Menu { -// // ForEach(0 ..< group.actions.count, id: \.self) { subActionIndex in -// // let subAction = group.actions[subActionIndex] -// // if case let .item(item) = onEnum(of: subAction) { -// // StatusActionMenuItem(item: item, openURL: openURL) -// // } -// // } -// } label: { -// StatusActionLabel(item: group.displayItem) -// } - } - } - - private func handleItemAction(_ item: StatusActionItem) { - if let clickable = item as? StatusActionItemClickable { - os_log("[URL点击] 状态操作点击: %{public}@", log: .default, type: .debug, String(describing: type(of: item))) - clickable.onClicked(.init(launcher: AppleUriLauncher(openURL: openURL))) - - if case .report = onEnum(of: item) { - ToastView( - icon: UIImage(systemName: "flag.fill"), - message: " report success" - ).show() - } - } - } -} - -// bottom action -struct StatusActionLabel: View { - let item: StatusActionItem - @Environment(\.colorScheme) var colorScheme - @Environment(FlareTheme.self) private var theme - - var body: some View { - Label { - let textContent = - switch onEnum(of: item) { - case let .like(data): - formatCount(data.humanizedCount.isEmpty ? 0 : Int64(data.humanizedCount) ?? 0) - case let .retweet(data): - formatCount(data.humanizedCount.isEmpty ? 0 : Int64(data.humanizedCount) ?? 0) - case let .quote(data): - formatCount(data.humanizedCount.isEmpty ? 0 : Int64(data.humanizedCount) ?? 0) - case let .reply(data): - formatCount(data.humanizedCount.isEmpty ? 0 : Int64(data.humanizedCount) ?? 0) - case let .bookmark(data): - formatCount(data.humanizedCount.isEmpty ? 0 : Int64(data.humanizedCount) ?? 0) - default: "" - } - Text(textContent).font(.system(size: 12)) - } icon: { - switch onEnum(of: item) { - case let .bookmark(data): - if data.bookmarked { - Image(asset: Asset.Image.Status.Toolbar.bookmarkFilled) - .renderingMode(.template) - } else { - Image(asset: Asset.Image.Status.Toolbar.bookmark) - .renderingMode(.template) - } - case .delete: - Image(asset: Asset.Image.Status.Toolbar.delete) - .renderingMode(.template) - case let .like(data): - if data.liked { - Image(asset: Asset.Image.Status.Toolbar.favorite) - .renderingMode(.template) - } else { - Image(asset: Asset.Image.Status.Toolbar.favoriteBorder) - .renderingMode(.template) - } - case .more: - Image(asset: Asset.Image.Status.more) - .renderingMode(.template) - .rotationEffect(.degrees(90)) - case .quote: - Image(asset: Asset.Image.Status.Toolbar.quote) - .renderingMode(.template) - case let .reaction(data): - if data.reacted { - Awesome.Classic.Solid.minus.image - } else { - Awesome.Classic.Solid.plus.image - } - case .reply: - Image(asset: Asset.Image.Status.Toolbar.chatBubbleOutline) - .renderingMode(.template) - case .report: - Image(asset: Asset.Image.Status.Toolbar.flag) - .renderingMode(.template) - case let .retweet(data): - if data.retweeted { - Image(asset: Asset.Image.Status.Toolbar.repeat) - .renderingMode(.template) - } else { - Image(asset: Asset.Image.Status.Toolbar.repeat) - .renderingMode(.template) - } - } - } - .foregroundStyle(theme.labelColor, theme.labelColor) - } -} - -// struct CenteredLabelStyle: LabelStyle { -// @Environment(FlareTheme.self) private var theme - -// func makeBody(configuration: Configuration) -> some View { -// HStack(spacing: 4) { -// configuration.icon.foregroundColor(theme.labelColor) -// configuration.title -// .font(.system(size: 12)) -// } -// .frame(maxWidth: .infinity, alignment: .center) -// } -// } - -// struct StatusActionMenuItem: View { -// let item: StatusActionItem -// let openURL: OpenURLAction -// @Environment(\.colorScheme) var colorScheme // Add if not present, for icon logic - -// var body: some View { -// let role: ButtonRole? = -// if let colorData = item as? StatusActionItemColorized { -// switch colorData.color { -// case .red: .destructive -// case .primaryColor: nil -// case .contentColor: nil -// case .error: .destructive -// } -// } else { -// nil -// } - -// Button( -// role: role, -// action: { -// if let clickable = item as? StatusActionItemClickable { -// clickable.onClicked(.init(launcher: AppleUriLauncher(openURL: openURL))) -// } -// }, -// label: { -// let text: LocalizedStringKey = -// switch onEnum(of: item) { -// case let .bookmark(data): -// data.bookmarked -// ? LocalizedStringKey("status_action_unbookmark") -// : LocalizedStringKey("status_action_bookmark") -// case .delete: -// LocalizedStringKey("status_action_delete") -// case let .like(data): -// data.liked -// ? LocalizedStringKey( -// "status_action_unlike") -// : LocalizedStringKey( -// "status_action_like") -// case .quote: LocalizedStringKey("quote") -// case .reaction: -// LocalizedStringKey( -// "status_action_add_reaction") -// case .reply: -// LocalizedStringKey("status_action_reply") -// case .report: LocalizedStringKey("report") -// case let .retweet(data): -// data.retweeted -// ? LocalizedStringKey("retweet_remove") -// : LocalizedStringKey("retweet") -// case .more: -// LocalizedStringKey("status_action_more") -// } -// Label { -// Text(text) -// } icon: { -// // Inlined icon logic for StatusActionMenuItem -// switch onEnum(of: item) { -// case let .bookmark(data): -// if data.bookmarked { -// Image(asset: Asset.Image.Status.Toolbar.bookmarkFilled) -// .renderingMode(.template) -// } else { -// Image(asset: Asset.Image.Status.Toolbar.bookmark) -// .renderingMode(.template) -// } -// case .delete: -// Image(asset: Asset.Image.Status.Toolbar.delete) -// .renderingMode(.template) -// case let .like(data): -// if data.liked { -// Image(asset: Asset.Image.Status.Toolbar.favorite) -// .renderingMode(.template) -// } else { -// Image(asset: Asset.Image.Status.Toolbar.favoriteBorder) -// .renderingMode(.template) -// } -// case .more: -// Image(asset: Asset.Image.Status.more) -// .renderingMode(.template) -// .rotationEffect(.degrees(90)) -// case .quote: -// Image(asset: Asset.Image.Status.Toolbar.quote) -// .renderingMode(.template) -// case let .reaction(data): -// if data.reacted { -// Awesome.Classic.Solid.minus.image -// } else { -// Awesome.Classic.Solid.plus.image -// } -// case .reply: -// Image(asset: Asset.Image.Status.Toolbar.chatBubbleOutline) -// .renderingMode(.template) -// case .report: -// Image(asset: Asset.Image.Status.Toolbar.flag) -// .renderingMode(.template) -// case let .retweet(data): -// if data.retweeted { -// Image(asset: Asset.Image.Status.Toolbar.repeat) -// .renderingMode(.template) -// } else { -// Image(asset: Asset.Image.Status.Toolbar.repeat) -// .renderingMode(.template) -// } -// } -// } -// } -// ) -// } -// } diff --git a/iosApp/iosApp/UI/Page/Compose/Timeline/TimelineStatus/StatusContentView.swift b/iosApp/iosApp/UI/Page/Compose/Timeline/TimelineStatus/StatusContentView.swift deleted file mode 100644 index d140ad5ac..000000000 --- a/iosApp/iosApp/UI/Page/Compose/Timeline/TimelineStatus/StatusContentView.swift +++ /dev/null @@ -1,256 +0,0 @@ -import Awesome -import Generated -import JXPhotoBrowser -import Kingfisher -import MarkdownUI -import os.log -import shared -import SwiftDate -import SwiftUI -import UIKit - -struct StatusContentView: View { - let viewModel: StatusViewModel - let appSettings: AppSettings - let theme: FlareTheme - let openURL: OpenURLAction - let onMediaClick: (Int, UiMedia) -> Void - let onPodcastCardTap: (UiCard) -> Void - - var body: some View { - VStack(alignment: .leading) { - // Reply content - if viewModel.hasAboveTextContent, let aboveTextContent = viewModel.statusData.aboveTextContent { - StatusReplyView(aboveTextContent: aboveTextContent) - } - - // Content warning - if viewModel.hasContentWarning, let cwText = viewModel.statusData.contentWarning { - StatusContentWarningView(contentWarning: cwText, theme: theme, openURL: openURL) - } - - Spacer().frame(height: 10) - - // Main content - StatusMainContentView( - viewModel: viewModel, - appSettings: appSettings, - theme: theme, - openURL: openURL - ) - - // Media - if viewModel.hasImages { - StatusMediaView( - viewModel: viewModel, - appSettings: appSettings, - onMediaClick: onMediaClick - ) - } - - // Card (Podcast or Link Preview) - if viewModel.hasCard, let card = viewModel.statusData.card { - StatusCardView( - card: card, - viewModel: viewModel, - appSettings: appSettings, - onPodcastCardTap: onPodcastCardTap - ) - } - - // Quote - if viewModel.hasQuote { - StatusQuoteView(quotes: viewModel.statusData.quote, onMediaClick: onMediaClick) - } - - // misskey 的+ 的emojis - if viewModel.hasBottomContent, let bottomContent = viewModel.statusData.bottomContent { - StatusBottomContentView(bottomContent: bottomContent) - } - - // Detail date - if viewModel.isDetailView { - StatusDetailDateView(createdAt: viewModel.statusData.createdAt) - } - } - } -} - -struct StatusReplyView: View { - let aboveTextContent: UiTimelineItemContentStatusAboveTextContent - - var body: some View { - switch onEnum(of: aboveTextContent) { - case let .replyTo(data): - Text(String(localized: "Reply to \(data.handle.removingHandleFirstPrefix("@"))")) - .font(.caption) - .opacity(0.6) - } - Spacer().frame(height: 4) - } -} - -struct StatusMediaView: View { - let viewModel: StatusViewModel - let appSettings: AppSettings - let onMediaClick: (Int, UiMedia) -> Void - - var body: some View { - Spacer().frame(height: 8) - - MediaComponent( - hideSensitive: viewModel.statusData.sensitive && !appSettings.appearanceSettings.showSensitiveContent, - medias: viewModel.statusData.images, - onMediaClick: { index, media in - PhotoBrowserManager.shared.showPhotoBrowser( - media: media, - images: viewModel.statusData.images, - initialIndex: index - ) - }, - sensitive: viewModel.statusData.sensitive - ) - } -} - -struct StatusCardView: View { - let card: UiCard - let viewModel: StatusViewModel - let appSettings: AppSettings - let onPodcastCardTap: (UiCard) -> Void - - var body: some View { - if viewModel.isPodcastCard { - PodcastPreview(card: card) - .onTapGesture { - onPodcastCardTap(card) - } - } else if appSettings.appearanceSettings.showLinkPreview, viewModel.shouldShowLinkPreview { - LinkPreview(card: card) - } - } -} - -struct StatusBottomContentView: View { - let bottomContent: UiTimelineItemContentStatusBottomContent - - var body: some View { - switch onEnum(of: bottomContent) { - case let .reaction(data): - ScrollView(.horizontal) { - LazyHStack { - if !data.emojiReactions.isEmpty { - ForEach(0 ..< data.emojiReactions.count, id: \.self) { index in - let reaction = data.emojiReactions[index] - Button(action: { - reaction.onClicked() - }) { - HStack { - if !reaction.url.isEmpty { - KFImage(URL(string: reaction.url)) - .resizable() - .scaledToFit() - } else { - Text(reaction.name) - } - Text(reaction.humanizedCount) - } - } - .buttonStyle(.borderless) - } - } - } - } - } - } -} - -struct StatusDetailDateView: View { - let createdAt: Date - - var body: some View { - Spacer().frame(height: 4) - HStack { - Text(createdAt, style: .date) - Text(createdAt, style: .time) - } - .opacity(0.6) - } -} - -struct StatusContentWarningView: View { - let contentWarning: UiRichText - let theme: FlareTheme - let openURL: OpenURLAction - - var body: some View { - Button(action: { - // withAnimation { - // // expanded = !expanded - // } - }) { - Image(systemName: "exclamationmark.triangle") - .foregroundColor(theme.labelColor) - - FlareText( - contentWarning.raw, - contentWarning.markdown, - textType: .caption, - isRTL: contentWarning.isRTL - ) - .onLinkTap { url in - openURL(url) - } - .lineSpacing(CGFloat(theme.lineSpacing)) - .foregroundColor(theme.labelColor) - // Markdown() - // .font(.caption2) - // .markdownInlineImageProvider(.emoji) - // Spacer() - // if expanded { - // Image(systemName: "arrowtriangle.down.circle.fill") - // } else { - // Image(systemName: "arrowtriangle.left.circle.fill") - // } - } - .opacity(0.6) - .buttonStyle(.plain) - // if expanded { - // Spacer() - // .frame(height: 8) - // } - } -} - -struct StatusMainContentView: View { - let viewModel: StatusViewModel - let appSettings: AppSettings - let theme: FlareTheme - let openURL: OpenURLAction - - var body: some View { - if viewModel.hasContent { - let content = viewModel.statusData.content - FlareText( - content.raw, - content.markdown, - textType: .body, - isRTL: content.isRTL - ) - .onLinkTap { url in - openURL(url) - } - .lineSpacing(CGFloat(theme.lineSpacing)) - .foregroundColor(theme.labelColor) - - // appSettings.appearanceSettings.autoTranslate, - if viewModel.shouldShowTranslation { - TranslatableText(originalText: content.raw) - } - } else { - Text("") - .font(.system(size: 16)) - .foregroundColor(theme.labelColor) - } - } -} diff --git a/iosApp/iosApp/UI/Page/Compose/TimelineV2/StatusContentViewV2.swift b/iosApp/iosApp/UI/Page/Compose/Timeline/TimelineStatus/StatusContentViewV2.swift similarity index 98% rename from iosApp/iosApp/UI/Page/Compose/TimelineV2/StatusContentViewV2.swift rename to iosApp/iosApp/UI/Page/Compose/Timeline/TimelineStatus/StatusContentViewV2.swift index db7e7e08e..9319073fd 100644 --- a/iosApp/iosApp/UI/Page/Compose/TimelineV2/StatusContentViewV2.swift +++ b/iosApp/iosApp/UI/Page/Compose/Timeline/TimelineStatus/StatusContentViewV2.swift @@ -179,7 +179,7 @@ struct StatusContentWarningViewV2: View { FlareText( contentWarning.raw, contentWarning.markdown, - textType: .caption, + textType: .flareTextTypeCaption, isRTL: contentWarning.isRTL ) .onLinkTap { url in @@ -220,7 +220,7 @@ struct StatusMainContentViewV2: View { FlareText( content.raw, content.markdown, - textType: .body, + textType: .flareTextTypeBody, isRTL: content.isRTL ) .onLinkTap { url in diff --git a/iosApp/iosApp/UI/Page/Compose/Timeline/TimelineStatus/StatusHeaderView.swift b/iosApp/iosApp/UI/Page/Compose/Timeline/TimelineStatus/StatusHeaderView.swift deleted file mode 100644 index 64b60df84..000000000 --- a/iosApp/iosApp/UI/Page/Compose/Timeline/TimelineStatus/StatusHeaderView.swift +++ /dev/null @@ -1,117 +0,0 @@ -import Awesome -import Generated -import JXPhotoBrowser -import Kingfisher -import MarkdownUI -import os.log -import shared -import SwiftDate -import SwiftUI -import UIKit - -struct StatusHeaderView: View { - let viewModel: StatusViewModel - @Environment(FlareRouter.self) private var router - @Environment(FlareTheme.self) private var theme - - var body: some View { - HStack(alignment: .top) { - HStack(alignment: .center, spacing: 1) { - if viewModel.hasUser, let user = viewModel.statusData.user { - UserComponent( - user: user, - topEndContent: viewModel.statusData.topEndContent as? UiTimelineItemContentStatusTopEndContent - ) - .id("UserComponent_\(user.key)") - .environment(router) - } - - Spacer() - // icon + time - - // 更多按钮 - // if !processActions().moreActions.isEmpty { - // Menu { - // ForEach(0 ..< processActions().moreActions.count, id: \.self) { index in - // let item = processActions().moreActions[index] - // let role: ButtonRole? = - // if let colorData = item as? StatusActionItemColorized { - // switch colorData.color { - // case .red: .destructive - // case .primaryColor: nil - // case .contentColor: nil - // case .error: .destructive - // } - // } else { - // nil - // } - - // Button( - // role: role, - // action: { - // if let clickable = item as? StatusActionItemClickable { - // clickable.onClicked( - // .init(launcher: AppleUriLauncher(openURL: openURL))) - // // 如果是举报操作,显示 Toast - // if case .report = onEnum(of: item) { - // showReportToast() - // } - // } - // }, - // label: { - // let text: LocalizedStringKey = - // switch onEnum(of: item) { - // case let .bookmark(data): - // data.bookmarked - // ? LocalizedStringKey("status_action_unbookmark") - // : LocalizedStringKey("status_action_bookmark") - // case .delete: LocalizedStringKey("status_action_delete") - // case let .like(data): - // data.liked - // ? LocalizedStringKey("status_action_unlike") - // : LocalizedStringKey("status_action_like") - // case .quote: LocalizedStringKey("quote") - // case .reaction: - // LocalizedStringKey("status_action_add_reaction") - // case .reply: LocalizedStringKey("status_action_reply") - // case .report: LocalizedStringKey("report") - // case let .retweet(data): - // data.retweeted - // ? LocalizedStringKey("retweet_remove") - // : LocalizedStringKey("retweet") - // case .more: LocalizedStringKey("status_action_more") - // } - // Label { - // Text(text) - // } icon: { - // StatusActionItemIcon(item: item) - // } - // } - // ) - // } - // } label: { - // Image(asset: Asset.Image.Status.more) - // .renderingMode(.template) - // .rotationEffect(.degrees(0)) - // .foregroundColor(theme.labelColor) - // .modifier(SmallIconModifier()) - // } - // .padding(.top, 0) - // } - - if !viewModel.isDetailView { - Text(viewModel.getFormattedDate()) - .foregroundColor(.gray) - .font(.caption) - .frame(minWidth: 80, alignment: .trailing) - } - } - .padding(.bottom, 1) - } - .allowsHitTesting(true) - .contentShape(Rectangle()) - .onTapGesture { - // 空的手势处理 - } - } -} diff --git a/iosApp/iosApp/UI/Page/Compose/TimelineV2/StatusHeaderViewV2.swift b/iosApp/iosApp/UI/Page/Compose/Timeline/TimelineStatus/StatusHeaderViewV2.swift similarity index 98% rename from iosApp/iosApp/UI/Page/Compose/TimelineV2/StatusHeaderViewV2.swift rename to iosApp/iosApp/UI/Page/Compose/Timeline/TimelineStatus/StatusHeaderViewV2.swift index 9a44ac5d8..d16cdba77 100644 --- a/iosApp/iosApp/UI/Page/Compose/TimelineV2/StatusHeaderViewV2.swift +++ b/iosApp/iosApp/UI/Page/Compose/Timeline/TimelineStatus/StatusHeaderViewV2.swift @@ -82,7 +82,7 @@ struct UserComponentV2: View, Equatable { Markdown(user.name.markdown) .lineLimit(1) .font(.headline) - .markdownTheme(.flareMarkdownStyle(using: theme.bodyTextStyle, fontScale: theme.fontSizeScale)) + .markdownTheme(.flareMarkdownStyle(using: theme.flareTextBodyTextStyle, fontScale: theme.fontSizeScale)) .markdownInlineImageProvider(.emoji) } HStack { diff --git a/iosApp/iosApp/UI/Page/Compose/Timeline/TimelineStatus/StatusQuoteView/QuotedStatus.swift b/iosApp/iosApp/UI/Page/Compose/Timeline/TimelineStatus/StatusQuoteView/QuotedStatus.swift index 29a9921c3..5875d8693 100644 --- a/iosApp/iosApp/UI/Page/Compose/Timeline/TimelineStatus/StatusQuoteView/QuotedStatus.swift +++ b/iosApp/iosApp/UI/Page/Compose/Timeline/TimelineStatus/StatusQuoteView/QuotedStatus.swift @@ -46,7 +46,7 @@ struct QuotedStatus: View { FlareText( data.content.raw, data.content.markdown, - textType: .body, + textType: .flareTextTypeBody, isRTL: data.content.isRTL ) .onLinkTap { url in diff --git a/iosApp/iosApp/UI/Page/Compose/TimelineV2/StatusQuoteViewV2.swift b/iosApp/iosApp/UI/Page/Compose/Timeline/TimelineStatus/StatusQuoteView/StatusQuoteViewV2.swift similarity index 97% rename from iosApp/iosApp/UI/Page/Compose/TimelineV2/StatusQuoteViewV2.swift rename to iosApp/iosApp/UI/Page/Compose/Timeline/TimelineStatus/StatusQuoteView/StatusQuoteViewV2.swift index f85d3ef3b..54aa64894 100644 --- a/iosApp/iosApp/UI/Page/Compose/TimelineV2/StatusQuoteViewV2.swift +++ b/iosApp/iosApp/UI/Page/Compose/Timeline/TimelineStatus/StatusQuoteView/StatusQuoteViewV2.swift @@ -82,7 +82,7 @@ struct QuotedStatusV2: View { Markdown(user.name.markdown) .lineLimit(1) .font(.subheadline) - .markdownTheme(.flareMarkdownStyle(using: theme.bodyTextStyle, fontScale: theme.fontSizeScale)) + .markdownTheme(.flareMarkdownStyle(using: theme.flareTextBodyTextStyle, fontScale: theme.fontSizeScale)) .markdownInlineImageProvider(.emoji) Text(user.handle) .lineLimit(1) @@ -99,7 +99,7 @@ struct QuotedStatusV2: View { FlareText( item.content.raw, item.content.markdown, - textType: .body, + textType: .flareTextTypeBody, isRTL: item.content.isRTL ) .onLinkTap { url in diff --git a/iosApp/iosApp/UI/Page/Compose/TimelineV2/StatusRetweetHeaderComponentV2.swift b/iosApp/iosApp/UI/Page/Compose/Timeline/TimelineStatus/StatusRetweetHeaderComponentV2.swift similarity index 99% rename from iosApp/iosApp/UI/Page/Compose/TimelineV2/StatusRetweetHeaderComponentV2.swift rename to iosApp/iosApp/UI/Page/Compose/Timeline/TimelineStatus/StatusRetweetHeaderComponentV2.swift index c0a705213..aa2150849 100644 --- a/iosApp/iosApp/UI/Page/Compose/TimelineV2/StatusRetweetHeaderComponentV2.swift +++ b/iosApp/iosApp/UI/Page/Compose/Timeline/TimelineStatus/StatusRetweetHeaderComponentV2.swift @@ -28,7 +28,7 @@ struct StatusRetweetHeaderComponentV2: View, Equatable { } .frame(alignment: .center) .lineLimit(1) - .markdownTheme(.flareMarkdownStyle(using: theme.captionTextStyle, fontScale: theme.fontSizeScale)) + .markdownTheme(.flareMarkdownStyle(using: theme.flareTextCaptionTextStyle, fontScale: theme.fontSizeScale)) .markdownTextStyle(\.text) { FontSize(12) } diff --git a/iosApp/iosApp/UI/Page/Compose/TimelineV2/TimelineActionsViewV2.swift b/iosApp/iosApp/UI/Page/Compose/Timeline/TimelineStatus/TimelineActionsViewV2.swift similarity index 100% rename from iosApp/iosApp/UI/Page/Compose/TimelineV2/TimelineActionsViewV2.swift rename to iosApp/iosApp/UI/Page/Compose/Timeline/TimelineStatus/TimelineActionsViewV2.swift diff --git a/iosApp/iosApp/UI/Page/Compose/Timeline/TimelineStatus/TimelineStatusView.swift b/iosApp/iosApp/UI/Page/Compose/Timeline/TimelineStatus/TimelineStatusView.swift deleted file mode 100644 index ada05e8ff..000000000 --- a/iosApp/iosApp/UI/Page/Compose/Timeline/TimelineStatus/TimelineStatusView.swift +++ /dev/null @@ -1,96 +0,0 @@ -import Awesome -import Generated -import JXPhotoBrowser -import Kingfisher -import MarkdownUI -import os.log -import shared -import SwiftDate -import SwiftUI -import UIKit - -struct TimelineStatusView: View { - let data: UiTimelineItemContentStatus - let isDetail: Bool - let enableTranslation: Bool - @State private var showMedia: Bool = false - @State private var showShareMenu: Bool = false - - @Environment(\.openURL) private var openURL - @Environment(\.appSettings) private var appSettings - @Environment(FlareRouter.self) private var router - @Environment(FlareTheme.self) private var theme - - let onMediaClick: (Int, UiMedia) -> Void - - init(data: UiTimelineItemContentStatus, onMediaClick: @escaping (Int, UiMedia) -> Void, isDetail: Bool, enableTranslation: Bool = true) { - self.data = data - self.isDetail = isDetail - self.enableTranslation = enableTranslation - self.onMediaClick = onMediaClick - } - - // 每次都要算,性能堪忧,无解,后期想办法 - private var viewModel: StatusViewModel { - StatusViewModel(data: data, isDetail: isDetail, enableTranslation: enableTranslation) - } - - var body: some View { - VStack(alignment: .leading) { - Spacer().frame(height: 2) - - StatusHeaderView(viewModel: viewModel) - - StatusContentView( - viewModel: viewModel, - appSettings: appSettings, - theme: theme, - openURL: openURL, - onMediaClick: onMediaClick, - onPodcastCardTap: handlePodcastCardTap - ) - - StatusActionsView( - viewModel: viewModel, - appSettings: appSettings, - openURL: openURL, - parentView: self - ) - - // Spacer().frame(height: 3) - } - .frame(alignment: .leading) - .contentShape(Rectangle()) - .onTapGesture { - handleStatusTap() - } - } - - private func handleStatusTap() { - // if let tapLocation = UIApplication.shared.windows.first?.hitTest( - // UIApplication.shared.windows.first?.convert(CGPoint(x: 0, y: 0), to: nil) ?? .zero, - // with: nil - // ) { - // let bottomActionBarFrame = CGRect( - // x: 16, y: tapLocation.frame.height - 44, - // width: tapLocation.frame.width - 32, height: 44 - // ) - // if !bottomActionBarFrame.contains(tapLocation.frame.origin) { - router.navigate(to: .statusDetailV2( - accountType: UserManager.shared.getCurrentAccountType() ?? AccountTypeGuest(), - statusKey: viewModel.statusData.statusKey - )) - // } - // } - } - - private func handlePodcastCardTap(card: UiCard) { - if let route = AppDeepLinkHelper().parse(url: card.url) as? AppleRoute.Podcast { - FlareLog.debug("TimelineStatusView Podcast Card Tapped, navigating via router to: podcastSheet(accountType: \(route.accountType), podcastId: \(route.id))") - router.navigate(to: .podcastSheet(accountType: route.accountType, podcastId: route.id)) - } else { - let parsedRoute = AppDeepLinkHelper().parse(url: card.url) - FlareLog.error("TimelineStatusView Error: Could not parse Podcast URL from card: \(card.url). Parsed type: \(type(of: parsedRoute)) Optional value: \(String(describing: parsedRoute))") - } - } -} diff --git a/iosApp/iosApp/UI/Page/Compose/TimelineV2/TimelineStatusViewV2.swift b/iosApp/iosApp/UI/Page/Compose/Timeline/TimelineStatus/TimelineStatusViewV2.swift similarity index 99% rename from iosApp/iosApp/UI/Page/Compose/TimelineV2/TimelineStatusViewV2.swift rename to iosApp/iosApp/UI/Page/Compose/Timeline/TimelineStatus/TimelineStatusViewV2.swift index 524dd7683..793e55e92 100644 --- a/iosApp/iosApp/UI/Page/Compose/TimelineV2/TimelineStatusViewV2.swift +++ b/iosApp/iosApp/UI/Page/Compose/Timeline/TimelineStatus/TimelineStatusViewV2.swift @@ -114,7 +114,6 @@ struct TimelineStatusViewV2: View, Equatable { Spacer().frame(height: 16) } } - .padding(.horizontal, 16) .frame(alignment: .leading) #if canImport(_Translation_SwiftUI) diff --git a/iosApp/iosApp/UI/Page/Compose/TimelineV2/ShareButtonV2.swift b/iosApp/iosApp/UI/Page/Compose/TimelineV2/ShareButtonV2.swift deleted file mode 100644 index 25b6f91cf..000000000 --- a/iosApp/iosApp/UI/Page/Compose/TimelineV2/ShareButtonV2.swift +++ /dev/null @@ -1,160 +0,0 @@ -import Awesome -import Generated -import JXPhotoBrowser -import Kingfisher -import MarkdownUI -import os.log -import SwiftDate -import SwiftUI -import UIKit - -#if canImport(_Translation_SwiftUI) - import Translation -#endif - -struct ShareButtonV2: View { - @Environment(\.colorScheme) var colorScheme - @Environment(\.appSettings) var appSettings - @Environment(\.isInCaptureMode) private var isInCaptureMode: Bool - - @Environment(FlareRouter.self) var router - @Environment(FlareTheme.self) private var theme - - @State private var showTextForSelection: Bool = false - @State private var showTranslation: Bool = false - @State private var showSelectUrlSheet: Bool = false - - let item: TimelineItem - let onShare: (MoreActionType) -> Void - - private var statusUrl: URL? { - guard !item.url.isEmpty else { return nil } - return URL(string: item.url) - } - - var body: some View { - if !isInCaptureMode { - Menu { - Button(action: { - ToastView(icon: UIImage(systemName: "checkmark.circle"), message: NSLocalizedString("Report Success", comment: "")).show() - }) { - Label("Report", systemImage: "exclamationmark.triangle") - } - Divider() - Button(action: { - UIPasteboard.general.string = item.content.raw - ToastView(icon: UIImage(systemName: "checkmark.circle"), message: NSLocalizedString("Copy Success", comment: "")).show() - }) { - Label("Copy Text ", systemImage: "doc.on.doc") - } - - Button(action: { - UIPasteboard.general.string = item.content.markdown - ToastView(icon: UIImage(systemName: "checkmark.circle"), message: NSLocalizedString("Copy Success", comment: "")).show() - }) { - Label("Copy Text (MarkDown)", systemImage: "doc.on.doc") - } - - if !item.images.isEmpty { - Button(action: { - showSelectUrlSheet = true - }) { - Label("Copy Media Link", systemImage: "photo.on.rectangle") - } - } - - Button(action: { - showTextForSelection = true - }) { - Label("Copy Any", systemImage: "text.cursor") - } - .buttonStyle(PlainButtonStyle()) - - if let url = statusUrl { - Button(action: { - UIPasteboard.general.string = url.absoluteString - ToastView(icon: UIImage(systemName: "checkmark.circle"), message: NSLocalizedString("Copy Success", comment: "")).show() - }) { - Label("Copy Tweet Link", systemImage: "link") - } - Divider() - Button(action: { - router.handleDeepLink(url) - }) { - Label("Open in Browser", systemImage: "safari") - } - } - - Divider() - -// Menu { - Button(action: { - onShare(.sharePost) - }) { - Label("Share Post", systemImage: "square.and.arrow.up") - } - - Button(action: { - onShare(.shareAsImage) - }) { - Label("Share as Image", systemImage: "camera") - } -// } label: { -// Label("Share", systemImage: "square.and.arrow.up") -// } - Divider() - #if canImport(_Translation_SwiftUI) - Button(action: { - showTranslation = true - }) { - Label("System Translate", systemImage: "character.bubble") - } - #endif - - Divider() - - Button(action: { - FlareLog.debug("ShareButtonV2 Save Media tapped") - ToastView( - icon: UIImage(systemName: "arrow.down.to.line"), - message: String(localized: "download to App \n Download Manager") - ).show() - }) { - Label("Save Media", systemImage: "arrow.down.to.line") - } - - if !item.images.isEmpty { - Button(action: { - showSelectUrlSheet = true - }) { - Label("Copy Media URLs", systemImage: "link") - } - } - } label: { - HStack { - Spacer() - Image(systemName: "square.and.arrow.up") - .renderingMode(.template) - .font(.system(size: 16)) - Spacer() - } - .contentShape(Rectangle()) - } - #if canImport(_Translation_SwiftUI) - .addTranslateView(isPresented: $showTranslation, text: item.content.raw) - #endif - .sheet(isPresented: $showTextForSelection) { - let imageURLsString = item.images.map(\.url).joined(separator: "\n") - let selectableContent = AttributedString(item.content.markdown + "\n" + imageURLsString) - - StatusRowSelectableTextView(content: selectableContent) - .tint(.accentColor) - } - .sheet(isPresented: $showSelectUrlSheet) { - let urlsString = item.images.map(\.url).joined(separator: "\n") - StatusRowSelectableTextView(content: AttributedString(urlsString)) - .tint(.accentColor) - } - } - } -} diff --git a/iosApp/iosApp/UI/Page/Compose/TimelineV2/MediaComponentV2.swift b/iosApp/iosApp/UI/Page/Compose/media/MediaComponentV2.swift similarity index 88% rename from iosApp/iosApp/UI/Page/Compose/TimelineV2/MediaComponentV2.swift rename to iosApp/iosApp/UI/Page/Compose/media/MediaComponentV2.swift index 2d0c04b41..92f3c272b 100644 --- a/iosApp/iosApp/UI/Page/Compose/TimelineV2/MediaComponentV2.swift +++ b/iosApp/iosApp/UI/Page/Compose/media/MediaComponentV2.swift @@ -3,20 +3,19 @@ import Kingfisher import shared import SwiftUI -// medias struct MediaComponentV2: View { @State var hideSensitive: Bool @State private var aiDetectedSensitive: Bool = false @Environment(\.appSettings) private var appSettings @Environment(\.isInCaptureMode) private var isInCaptureMode - let medias: [Media] // 使用Swift Media类型 - let onMediaClick: (Int, Media) -> Void // 使用Swift Media类型 + let medias: [Media] + let onMediaClick: (Int, Media) -> Void let sensitive: Bool var body: some View { let showSensitiveButton = medias.allSatisfy { media in - media.type == .image || media.type == .video // 使用Swift Media类型判断 + media.type == .image || media.type == .video } && (sensitive || aiDetectedSensitive) // - 媒体遮罩逻辑 @@ -85,14 +84,8 @@ struct MediaComponentV2: View { } } -// - FeedMediaViewModel扩展,添加从Swift Media转换的方法 - extension FeedMediaViewModel { - /// 从Swift Media类型创建FeedMediaViewModel - /// - Parameter media: Swift Media对象 - /// - Returns: FeedMediaViewModel实例 static func from(_ media: Media) -> FeedMediaViewModel { - // 直接创建FeedMediaViewModel,不依赖UiMedia let id = media.url let url = URL(string: media.url) ?? URL(string: "about:blank")! let previewUrl = URL(string: media.previewUrl ?? media.url) @@ -114,7 +107,6 @@ extension FeedMediaViewModel { videoMedia = nil } - // 创建一个自定义的FeedMediaViewModel实例 return FeedMediaViewModel( id: id, url: url, @@ -129,10 +121,7 @@ extension FeedMediaViewModel { } } -// - FeedMediaViewModel自定义初始化器 - extension FeedMediaViewModel { - /// 自定义初始化器,用于从Swift Media创建FeedMediaViewModel init( id: String, url: URL, diff --git a/iosApp/iosApp/UI/Page/Compose/media/MediaItemComponent.swift b/iosApp/iosApp/UI/Page/Compose/media/MediaItemComponent.swift index 35f16b224..eb9de1ab3 100644 --- a/iosApp/iosApp/UI/Page/Compose/media/MediaItemComponent.swift +++ b/iosApp/iosApp/UI/Page/Compose/media/MediaItemComponent.swift @@ -3,16 +3,17 @@ import Kingfisher import shared import SwiftUI -struct MediaItemComponent: View { - let media: UiMedia - - var body: some View { - let viewModel = FeedMediaViewModel(media: media) - SingleMediaView( - viewModel: viewModel, - isSingleVideo: true, - fixedAspectRatio: nil, - action: {} - ) - } -} +// +// struct MediaItemComponent: View { +// let media: UiMedia +// +// var body: some View { +// let viewModel = FeedMediaViewModel(media: media) +// SingleMediaView( +// viewModel: viewModel, +// isSingleVideo: true, +// fixedAspectRatio: nil, +// action: {} +// ) +// } +// } diff --git a/iosApp/iosApp/UI/Page/Compose/TimelineV2/MediaItemComponentV2.swift b/iosApp/iosApp/UI/Page/Compose/media/MediaItemComponentV2.swift similarity index 65% rename from iosApp/iosApp/UI/Page/Compose/TimelineV2/MediaItemComponentV2.swift rename to iosApp/iosApp/UI/Page/Compose/media/MediaItemComponentV2.swift index 346e61a50..7e603074e 100644 --- a/iosApp/iosApp/UI/Page/Compose/TimelineV2/MediaItemComponentV2.swift +++ b/iosApp/iosApp/UI/Page/Compose/media/MediaItemComponentV2.swift @@ -4,10 +4,10 @@ import shared import SwiftUI struct MediaItemComponentV2: View { - let media: Media // 使用Swift Media类型 + let media: Media var body: some View { - let viewModel = FeedMediaViewModel.from(media) // 使用MediaComponentV2中定义的转换方法 + let viewModel = FeedMediaViewModel.from(media) SingleMediaView( viewModel: viewModel, isSingleVideo: true, diff --git a/iosApp/iosApp/UI/Page/Compose/TimelineV2/PhotoBrowserManagerV2.swift b/iosApp/iosApp/UI/Page/Compose/media/PhotoBrowserManagerV2.swift similarity index 98% rename from iosApp/iosApp/UI/Page/Compose/TimelineV2/PhotoBrowserManagerV2.swift rename to iosApp/iosApp/UI/Page/Compose/media/PhotoBrowserManagerV2.swift index 2a68b2ce8..4a811b288 100644 --- a/iosApp/iosApp/UI/Page/Compose/TimelineV2/PhotoBrowserManagerV2.swift +++ b/iosApp/iosApp/UI/Page/Compose/media/PhotoBrowserManagerV2.swift @@ -13,8 +13,8 @@ class PhotoBrowserManagerV2 { @MainActor func showPhotoBrowser( - media _: Media, // 使用Swift Media类型 - images: [Media], // 使用Swift Media类型 + media _: Media, + images: [Media], initialIndex: Int, headers: [String: String] = [:], onDismiss: (() -> Void)? = nil @@ -31,7 +31,7 @@ class PhotoBrowserManagerV2 { browser.cellClassAtIndex = { index in let media = images[index] - switch media.type { // 使用Swift Media类型判断 + switch media.type { case .video: return MediaBrowserVideoCell.self case .gif: @@ -157,7 +157,7 @@ class PhotoBrowserManagerV2 { browser.cellWillAppear = { [weak self] cell, index in let media = images[index] - switch media.type { // 使用Swift Media类型判断 + switch media.type { case .video: if let videoCell = cell as? MediaBrowserVideoCell { self?.currentVideoCell = videoCell diff --git a/iosApp/iosApp/UI/DataLayer/FlareTimelineState.swift b/iosApp/iosApp/UI/Page/DataLayer/FlareTimelineState.swift similarity index 100% rename from iosApp/iosApp/UI/DataLayer/FlareTimelineState.swift rename to iosApp/iosApp/UI/Page/DataLayer/FlareTimelineState.swift diff --git a/iosApp/iosApp/UI/DataLayer/PagingStateConverter.swift b/iosApp/iosApp/UI/Page/DataLayer/PagingStateConverter.swift similarity index 100% rename from iosApp/iosApp/UI/DataLayer/PagingStateConverter.swift rename to iosApp/iosApp/UI/Page/DataLayer/PagingStateConverter.swift diff --git a/iosApp/iosApp/UI/DataLayer/TimelineImagePrefetcher.swift b/iosApp/iosApp/UI/Page/DataLayer/TimelineImagePrefetcher.swift similarity index 100% rename from iosApp/iosApp/UI/DataLayer/TimelineImagePrefetcher.swift rename to iosApp/iosApp/UI/Page/DataLayer/TimelineImagePrefetcher.swift diff --git a/iosApp/iosApp/UI/Page/Download/Storage/DownloadHelper.swift b/iosApp/iosApp/UI/Page/Download/Storage/DownloadHelper.swift index 1a626aa4f..7af624de3 100644 --- a/iosApp/iosApp/UI/Page/Download/Storage/DownloadHelper.swift +++ b/iosApp/iosApp/UI/Page/Download/Storage/DownloadHelper.swift @@ -14,7 +14,7 @@ class DownloadHelper { DownloadManager.shared.download(url: url, fileName: downloadFileName) } - func startMediaDownload(url: String, mediaType: MediaType, previewImageUrl: String? = nil) { + func startMediaDownload(url: String, mediaType: DownMediaType, previewImageUrl: String? = nil) { let fileName = getFileNameWithExtension(url: url, mediaType: mediaType) let task = DownloadManager.shared.download(url: url, fileName: fileName) @@ -23,7 +23,7 @@ class DownloadHelper { } } - private func getFileNameWithExtension(url: String, mediaType: MediaType) -> String { + private func getFileNameWithExtension(url: String, mediaType: DownMediaType) -> String { if let urlObj = URL(string: url), !urlObj.lastPathComponent.isEmpty { let fileName = urlObj.lastPathComponent @@ -67,7 +67,7 @@ class DownloadHelper { } } -enum MediaType { +enum DownMediaType { case image case gif case video diff --git a/iosApp/iosApp/UI/Page/Download/View/DownloadManagerScreen.swift b/iosApp/iosApp/UI/Page/Download/View/DownloadManagerScreen.swift index df6260efe..f05e1c73b 100644 --- a/iosApp/iosApp/UI/Page/Download/View/DownloadManagerScreen.swift +++ b/iosApp/iosApp/UI/Page/Download/View/DownloadManagerScreen.swift @@ -18,7 +18,7 @@ struct ShareableFile: Identifiable { @MainActor struct DownloadManagerScreen: View { @Environment(FlareRouter.self) private var router - @Environment(FlareAppState.self) private var menuState + @Environment(FlareMenuState.self) private var menuState let accountType: AccountType @State private var downloadTasks: [DownloadTask] = [] diff --git a/iosApp/iosApp/UI/Page/Home/View/Home/FlareTabBarV2.swift b/iosApp/iosApp/UI/Page/Home/View/Home/FlareTabBarV2.swift index 262b607f0..be9435a93 100644 --- a/iosApp/iosApp/UI/Page/Home/View/Home/FlareTabBarV2.swift +++ b/iosApp/iosApp/UI/Page/Home/View/Home/FlareTabBarV2.swift @@ -6,7 +6,7 @@ import SwiftUI struct FlareTabBarV2: View { @Environment(FlareRouter.self) private var router - @Environment(FlareAppState.self) private var appState + @Environment(FlareMenuState.self) private var menuState @Environment(FlareTheme.self) private var theme @Environment(\.appSettings) private var appSettings @EnvironmentObject private var timelineState: TimelineExtState diff --git a/iosApp/iosApp/UI/Page/Home/View/Home/FlareTabItem.swift b/iosApp/iosApp/UI/Page/Home/View/Home/FlareTabItem.swift index c144ea663..c55f18ea5 100644 --- a/iosApp/iosApp/UI/Page/Home/View/Home/FlareTabItem.swift +++ b/iosApp/iosApp/UI/Page/Home/View/Home/FlareTabItem.swift @@ -9,7 +9,7 @@ struct FlareTabItem: View { let content: () -> Content - @Environment(FlareAppState.self) private var appState + @Environment(FlareMenuState.self) private var menuState @Environment(FlareTheme.self) private var theme init(tabType: FlareHomeTabs, @ViewBuilder content: @escaping () -> Content) { @@ -22,7 +22,7 @@ struct FlareTabItem: View { content() .navigationDestination(for: FlareDestination.self) { destination in FlareDestinationView(destination: destination, router: router) - .environment(appState) + .environment(menuState) } .background(theme.primaryBackgroundColor) .foregroundColor(theme.labelColor) diff --git a/iosApp/iosApp/UI/Page/Home/View/Home/HomeTabViewContentV2.swift b/iosApp/iosApp/UI/Page/Home/View/Home/HomeTabViewContentV2.swift index ff91ea0b9..05e9f1131 100644 --- a/iosApp/iosApp/UI/Page/Home/View/Home/HomeTabViewContentV2.swift +++ b/iosApp/iosApp/UI/Page/Home/View/Home/HomeTabViewContentV2.swift @@ -8,7 +8,7 @@ import SwiftUI struct HomeTabViewContentV2: View { @Environment(FlareRouter.self) private var router - @Environment(FlareAppState.self) private var appState + @Environment(FlareMenuState.self) private var menuState @Environment(FlareTheme.self) private var theme @Environment(\.appSettings) private var appSettings @EnvironmentObject private var timelineState: TimelineExtState @@ -48,7 +48,7 @@ struct HomeTabViewContentV2: View { FlareTabItem(tabType: .menu) { FlareMenuView() } - .environment(appState) + .environment(menuState) } .customizationID("tabview_menu") @@ -61,7 +61,7 @@ struct HomeTabViewContentV2: View { } ) } - .environment(appState) + .environment(menuState) } .customizationID("tabview_timeline") @@ -71,7 +71,7 @@ struct HomeTabViewContentV2: View { FlareTabItem(tabType: .notification) { NotificationTabScreen(accountType: accountType) } - .environment(appState) + .environment(menuState) } .customizationID("tabview_notification") } @@ -81,7 +81,7 @@ struct HomeTabViewContentV2: View { FlareTabItem(tabType: .discover) { DiscoverTabScreen(accountType: accountType) } - .environment(appState) + .environment(menuState) } .customizationID("tabview_discover") @@ -91,11 +91,10 @@ struct HomeTabViewContentV2: View { FlareTabItem(tabType: .profile) { ProfileTabScreenUikit( accountType: accountType, - userKey: nil, - toProfileMedia: { _ in } + userKey: nil ) } - .environment(appState) + .environment(menuState) } .customizationID("tabview_profile") } @@ -104,7 +103,7 @@ struct HomeTabViewContentV2: View { .ignoresSafeArea(.container, edges: .bottom) .padding(.bottom, -120) - if !appState.isCustomTabBarHidden { + if !menuState.isCustomTabBarHidden { VStack(spacing: 0) { FlareTabBarV2( accountType: accountType diff --git a/iosApp/iosApp/UI/Page/Home/View/Tabview/SearchScreen.swift b/iosApp/iosApp/UI/Page/Home/View/Tabview/SearchScreen.swift index 3541218f9..eb8250fb5 100644 --- a/iosApp/iosApp/UI/Page/Home/View/Tabview/SearchScreen.swift +++ b/iosApp/iosApp/UI/Page/Home/View/Tabview/SearchScreen.swift @@ -36,20 +36,21 @@ struct SearchScreen: View { view.padding(.horizontal) } } - }.listRowBackground(theme.primaryBackgroundColor) + } + .listRowInsets(EdgeInsets()) + .listRowSeparator(.hidden) + .listRowBackground(theme.primaryBackgroundColor) default: EmptyView() .listRowSeparator(.hidden) } - Section("search_status_title") { - StatusTimelineComponent( - data: state.status, - detailKey: nil - ) - .listStyle(.plain) - .listRowBackground(theme.primaryBackgroundColor) - }.listStyle(.plain).listRowBackground(theme.primaryBackgroundColor) - } + StatusTimelineComponent( + data: state.status, + detailKey: nil + ).listRowBackground(theme.primaryBackgroundColor) + .listRowInsets(EdgeInsets()) + + }.padding(.horizontal, 16).listStyle(.plain) } } } diff --git a/iosApp/iosApp/UI/Page/Home/View/TimelineViewSwiftUI/TimelineItemsView.swift b/iosApp/iosApp/UI/Page/Home/View/TimelineViewSwiftUI/TimelineItemsView.swift index 538a8ee27..d2a4f2874 100644 --- a/iosApp/iosApp/UI/Page/Home/View/TimelineViewSwiftUI/TimelineItemsView.swift +++ b/iosApp/iosApp/UI/Page/Home/View/TimelineViewSwiftUI/TimelineItemsView.swift @@ -13,30 +13,29 @@ struct TimelineItemsView: View { TimelineStatusViewV2( item: item, timelineViewModel: viewModel - ) - .padding(.vertical, 4) - .onAppear { - // viewModel.itemDidAppear(item: item) + ).padding(.horizontal, 16) + .padding(.vertical, 4) + .onAppear { + // viewModel.itemDidAppear(item: item) - Task { - if hasMore, - !viewModel.isLoadingMore, - items.count >= 7, + Task { + if hasMore, !viewModel.isLoadingMore, + items.count >= 7, - item.id == items[items.count - 5].id || - item.id == items[items.count - 6].id - { - FlareLog.debug("[TimelineItemsView] 🚀 预加载触发 ") + item.id == items[items.count - 5].id || + item.id == items[items.count - 6].id + { + FlareLog.debug("[TimelineItemsView] 🚀 预加载触发 ") - do { - try await viewModel.handleLoadMore() - FlareLog.debug("[TimelineItemsView] ✅ 预加载成功 - 新总数: \(items.count)") - } catch { - FlareLog.error("[TimelineItemsView] ❌ 预加载失败: \(error)") + do { + try await viewModel.handleLoadMore() + FlareLog.debug("[TimelineItemsView] ✅ 预加载成功 - 新总数: \(items.count)") + } catch { + FlareLog.error("[TimelineItemsView] ❌ 预加载失败: \(error)") + } } } } - } // .onDisappear { // viewModel.itemDidDisappear(item: item) // } diff --git a/iosApp/iosApp/UI/Page/Home/View/TimelineViewSwiftUI/TimelineViewSwiftUIV4.swift b/iosApp/iosApp/UI/Page/Home/View/TimelineViewSwiftUI/TimelineViewSwiftUIV4.swift index 38433c8ac..4dbc287ae 100644 --- a/iosApp/iosApp/UI/Page/Home/View/TimelineViewSwiftUI/TimelineViewSwiftUIV4.swift +++ b/iosApp/iosApp/UI/Page/Home/View/TimelineViewSwiftUI/TimelineViewSwiftUIV4.swift @@ -37,11 +37,11 @@ struct TimelineViewSwiftUIV4: View { TimelineStatusViewV2( item: createSampleTimelineItem(), timelineViewModel: timeLineViewModel - ) - .redacted(reason: .placeholder) - .listRowBackground(theme.primaryBackgroundColor) - .listRowInsets(EdgeInsets()) - .listRowSeparator(.hidden) + ).padding(.horizontal, 16) + .redacted(reason: .placeholder) + .listRowBackground(theme.primaryBackgroundColor) + .listRowInsets(EdgeInsets()) + .listRowSeparator(.hidden) } case let .loaded(items, hasMore): diff --git a/iosApp/iosApp/UI/Page/List/SwiftUIView/AllFeedsView.swift b/iosApp/iosApp/UI/Page/List/SwiftUIView/AllFeedsView.swift index 8e43549ff..822b013dc 100644 --- a/iosApp/iosApp/UI/Page/List/SwiftUIView/AllFeedsView.swift +++ b/iosApp/iosApp/UI/Page/List/SwiftUIView/AllFeedsView.swift @@ -10,7 +10,7 @@ struct AllFeedsView: View { @State private var presenter: PinnableTimelineTabPresenter @Environment(FlareRouter.self) private var router @Environment(\.appSettings) private var appSettings - @Environment(FlareAppState.self) private var appState + @Environment(FlareMenuState.self) private var menuState @State private var lastKnownItemCount: Int = 0 @State private var currentUser: UiUserV2? @State private var isMissingFeedData: Bool = false diff --git a/iosApp/iosApp/UI/Page/List/SwiftUIView/AllListsView.swift b/iosApp/iosApp/UI/Page/List/SwiftUIView/AllListsView.swift index 51be50ba2..d434ebcd3 100644 --- a/iosApp/iosApp/UI/Page/List/SwiftUIView/AllListsView.swift +++ b/iosApp/iosApp/UI/Page/List/SwiftUIView/AllListsView.swift @@ -4,13 +4,11 @@ import os import shared import SwiftUI -private let logger = Logger(subsystem: "com.flare.app", category: "AllListsView") - struct AllListsView: View { @State private var presenter: AllListPresenter @Environment(FlareRouter.self) private var router @Environment(\.appSettings) private var appSettings - @Environment(FlareAppState.self) private var appState + @Environment(FlareMenuState.self) private var menuState @State private var lastKnownItemCount: Int = 0 @State private var currentUser: UiUserV2? @State private var isMastodonUser: Bool = false @@ -36,7 +34,10 @@ struct AllListsView: View { if platformTypeString == "mastodon" { isMastodon = true - logger.debug("current user platform: \(platformTypeString), is Mastodon: \(isMastodon)") + FlareLog + .debug( + "current user platform: \(platformTypeString), is Mastodon: \(isMastodon)" + ) } } _isMastodonUser = State(initialValue: isMastodon) diff --git a/iosApp/iosApp/UI/Page/Menu/FlareAppState.swift b/iosApp/iosApp/UI/Page/Menu/FlareMenuState.swift similarity index 64% rename from iosApp/iosApp/UI/Page/Menu/FlareAppState.swift rename to iosApp/iosApp/UI/Page/Menu/FlareMenuState.swift index b0ed68bf4..50f8b7e01 100644 --- a/iosApp/iosApp/UI/Page/Menu/FlareAppState.swift +++ b/iosApp/iosApp/UI/Page/Menu/FlareMenuState.swift @@ -2,8 +2,7 @@ import Combine import SwiftUI @Observable -class FlareAppState { - // hidden home custom tabbar +class FlareMenuState { var isCustomTabBarHidden: Bool = false init() {} diff --git a/iosApp/iosApp/UI/Page/Menu/View/FlareMenuView.swift b/iosApp/iosApp/UI/Page/Menu/View/FlareMenuView.swift index 559155de3..4508c6ccd 100644 --- a/iosApp/iosApp/UI/Page/Menu/View/FlareMenuView.swift +++ b/iosApp/iosApp/UI/Page/Menu/View/FlareMenuView.swift @@ -18,7 +18,7 @@ struct FlareMenuView: View { @State private var accountType: AccountType = AccountTypeGuest() @Environment(FlareRouter.self) private var router - @Environment(FlareAppState.self) private var appState + @Environment(FlareMenuState.self) private var menuState @Environment(FlareTheme.self) private var theme var body: some View { @@ -40,7 +40,7 @@ struct FlareMenuView: View { // only show list button when user login if !(accountType is AccountTypeGuest) { Button(action: { - appState.isCustomTabBarHidden = true + menuState.isCustomTabBarHidden = true router.navigate(to: .lists(accountType: accountType)) }) { HStack { @@ -60,7 +60,7 @@ struct FlareMenuView: View { // only show feeds button when user login and platform is Bluesky if currentUser?.isBluesky == true { Button(action: { - appState.isCustomTabBarHidden = true + menuState.isCustomTabBarHidden = true router.navigate(to: .feeds(accountType: accountType)) }) { HStack { @@ -81,7 +81,7 @@ struct FlareMenuView: View { // Message if currentUser?.isXQt == true || currentUser?.isBluesky == true { Button(action: { - appState.isCustomTabBarHidden = true + menuState.isCustomTabBarHidden = true router.navigate(to: .messages(accountType: accountType)) }) { HStack { @@ -101,7 +101,7 @@ struct FlareMenuView: View { // X Spaces if currentUser?.isXQt == true { Button(action: { - appState.isCustomTabBarHidden = true + menuState.isCustomTabBarHidden = true router.navigate(to: .spaces(accountType: accountType)) }) { HStack { @@ -121,7 +121,7 @@ struct FlareMenuView: View { // download manager Button(action: { - appState.isCustomTabBarHidden = true + menuState.isCustomTabBarHidden = true router.navigate(to: .download(accountType: accountType)) }) { HStack { @@ -141,7 +141,7 @@ struct FlareMenuView: View { if currentUser?.isMastodon == true || currentUser?.isMisskey == true { Spacer() Button(action: { - appState.isCustomTabBarHidden = true + menuState.isCustomTabBarHidden = true let host = UserManager.shared.instanceMetadata?.instance.domain ?? currentUser?.key.host ?? "" let platformType = currentUser?.platformType ?? PlatformType.mastodon router.navigate(to: .instanceScreen(host: host, platformType: platformType)) @@ -192,7 +192,7 @@ struct FlareMenuView: View { } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) .onAppear { - appState.isCustomTabBarHidden = false + menuState.isCustomTabBarHidden = false checkAndUpdateUserState() } .sheet(isPresented: $showLogin) { diff --git a/iosApp/iosApp/UI/Page/Message/view/MessageScreen.swift b/iosApp/iosApp/UI/Page/Message/view/MessageScreen.swift index a2fba46e5..b963abd17 100644 --- a/iosApp/iosApp/UI/Page/Message/view/MessageScreen.swift +++ b/iosApp/iosApp/UI/Page/Message/view/MessageScreen.swift @@ -5,7 +5,7 @@ struct MessageScreen: View { let accountType: AccountType @Environment(FlareRouter.self) private var router - @Environment(FlareAppState.self) private var appState + @Environment(FlareMenuState.self) private var menuState var body: some View { DMListView(accountType: accountType) diff --git a/iosApp/iosApp/UI/Page/ProfileNew/Common/ProfileListViewController.swift b/iosApp/iosApp/UI/Page/Profile/Common/ProfileListViewController.swift similarity index 100% rename from iosApp/iosApp/UI/Page/ProfileNew/Common/ProfileListViewController.swift rename to iosApp/iosApp/UI/Page/Profile/Common/ProfileListViewController.swift diff --git a/iosApp/iosApp/UI/Page/ProfileNew/Components/MediaCollectionViewCell.swift b/iosApp/iosApp/UI/Page/Profile/Components/MediaCollectionViewCell.swift similarity index 90% rename from iosApp/iosApp/UI/Page/ProfileNew/Components/MediaCollectionViewCell.swift rename to iosApp/iosApp/UI/Page/Profile/Components/MediaCollectionViewCell.swift index 5ac4d8c38..51d811bf6 100644 --- a/iosApp/iosApp/UI/Page/ProfileNew/Components/MediaCollectionViewCell.swift +++ b/iosApp/iosApp/UI/Page/Profile/Components/MediaCollectionViewCell.swift @@ -22,22 +22,18 @@ class MediaCollectionViewCell: UICollectionViewCell { } func configure(with media: UiMedia, appSettings: AppSettings, onTap: @escaping () -> Void) { - // 创建 ProfileMediaItemView let mediaView = ProfileMediaItemView( media: media, appSetting: appSettings, onTap: onTap ) - // 如果已经有 hostingController,先移除 hostingController?.view.removeFromSuperview() hostingController = nil - // 创建新的 hostingController let controller = UIHostingController(rootView: mediaView) hostingController = controller - // 添加到 contentView controller.view.backgroundColor = .clear contentView.addSubview(controller.view) controller.view.translatesAutoresizingMaskIntoConstraints = false diff --git a/iosApp/iosApp/UI/Page/ProfileNew/Components/ProfileNewHeaderView.swift b/iosApp/iosApp/UI/Page/Profile/Components/ProfileNewHeaderView.swift similarity index 92% rename from iosApp/iosApp/UI/Page/ProfileNew/Components/ProfileNewHeaderView.swift rename to iosApp/iosApp/UI/Page/Profile/Components/ProfileNewHeaderView.swift index a1d97b269..378ce385d 100644 --- a/iosApp/iosApp/UI/Page/ProfileNew/Components/ProfileNewHeaderView.swift +++ b/iosApp/iosApp/UI/Page/Profile/Components/ProfileNewHeaderView.swift @@ -585,3 +585,60 @@ class ProfileNewHeaderView: UIView { return hostingController.view } } + +struct UserFollowsFansCount: View { + let followCount: String + let fansCount: String + var body: some View { + HStack { + Text(followCount) + .fontWeight(.bold) + Text("following") + .foregroundColor(.secondary) + .font(.footnote) + Divider() + Text(fansCount) + .fontWeight(.bold) + Text("fans") + .foregroundColor(.secondary) + .font(.footnote) + } + .font(.caption) + } +} + +struct UserInfoFieldsView: View { + let fields: [String: UiRichText] + var body: some View { + if fields.count > 0 { + VStack(alignment: .leading) { + let keys = fields.map(\.key).sorted() + ForEach(0 ..< keys.count, id: \.self) { index in + let key = keys[index] + Text(key) + .font(.subheadline) + .foregroundColor(.secondary) + Markdown(fields[key]?.markdown ?? "").markdownTextStyle(textStyle: { + FontFamilyVariant(.normal) + FontSize(.em(0.9)) + ForegroundColor(.primary) + }).markdownInlineImageProvider(.emoji) + .padding(.vertical, 4) + if index != keys.count - 1 { + Divider() + } + } + .padding(.horizontal) + } + .padding(.vertical) + #if os(iOS) + .background(Color(UIColor.secondarySystemBackground)) + #else + .background(Color(NSColor.windowBackgroundColor)) + #endif + .clipShape(RoundedRectangle(cornerRadius: 8)) + } else { + EmptyView() + } + } +} diff --git a/iosApp/iosApp/UI/Page/Compose/Timeline/TimelineDataManager.swift b/iosApp/iosApp/UI/Page/Profile/Components/TimelineDataManager.swift similarity index 100% rename from iosApp/iosApp/UI/Page/Compose/Timeline/TimelineDataManager.swift rename to iosApp/iosApp/UI/Page/Profile/Components/TimelineDataManager.swift diff --git a/iosApp/iosApp/UI/Page/Compose/Timeline/TimelineState.swift b/iosApp/iosApp/UI/Page/Profile/Components/TimelineState.swift similarity index 86% rename from iosApp/iosApp/UI/Page/Compose/Timeline/TimelineState.swift rename to iosApp/iosApp/UI/Page/Profile/Components/TimelineState.swift index 160e56b94..672076a12 100644 --- a/iosApp/iosApp/UI/Page/Compose/Timeline/TimelineState.swift +++ b/iosApp/iosApp/UI/Page/Profile/Components/TimelineState.swift @@ -8,26 +8,11 @@ class TimelineLoadingState { private(set) var loadingRows: Set = [] private let preloadDistance: Int = 10 - // 添加行到加载队列 - func addLoadingRow(_ row: Int) { - loadingRows.insert(row) - } - - // 从加载队列移除行 - func removeLoadingRow(_ row: Int) { - loadingRows.remove(row) - } - // 清空加载队列 func clearLoadingRows() { loadingRows.removeAll() } - // 检查行是否正在加载 - func isRowLoading(_ row: Int) -> Bool { - loadingRows.contains(row) - } - // 检查并触发预加载 func checkAndTriggerPreload(currentRow: Int, data: PagingState) { guard case let .success(successData) = onEnum(of: data) else { return } diff --git a/iosApp/iosApp/UI/Page/Compose/Timeline/TimelineViewController.swift b/iosApp/iosApp/UI/Page/Profile/Components/TimelineViewController.swift similarity index 100% rename from iosApp/iosApp/UI/Page/Compose/Timeline/TimelineViewController.swift rename to iosApp/iosApp/UI/Page/Profile/Components/TimelineViewController.swift diff --git a/iosApp/iosApp/UI/Page/ProfileNew/Data/ProfileMediaPresenterWrapper.swift b/iosApp/iosApp/UI/Page/Profile/Data/ProfileMediaPresenterWrapper.swift similarity index 93% rename from iosApp/iosApp/UI/Page/ProfileNew/Data/ProfileMediaPresenterWrapper.swift rename to iosApp/iosApp/UI/Page/Profile/Data/ProfileMediaPresenterWrapper.swift index c40c4effb..575ff8d02 100644 --- a/iosApp/iosApp/UI/Page/ProfileNew/Data/ProfileMediaPresenterWrapper.swift +++ b/iosApp/iosApp/UI/Page/Profile/Data/ProfileMediaPresenterWrapper.swift @@ -4,10 +4,8 @@ import shared import SwiftUI class ProfileMediaPresenterWrapper: ObservableObject { - // - Properties let presenter: ProfileMediaPresenter - // - Init init(accountType: AccountType, userKey: MicroBlogKey?) { os_log("[📔][ProfileMediaPresenterWrapper - init]初始化: accountType=%{public}@, userKey=%{public}@", log: .default, type: .debug, String(describing: accountType), userKey?.description ?? "nil") presenter = .init(accountType: accountType, userKey: userKey) diff --git a/iosApp/iosApp/UI/Page/ProfileNew/Data/ProfilePresenterService.swift b/iosApp/iosApp/UI/Page/Profile/Data/ProfilePresenterService.swift similarity index 84% rename from iosApp/iosApp/UI/Page/ProfileNew/Data/ProfilePresenterService.swift rename to iosApp/iosApp/UI/Page/Profile/Data/ProfilePresenterService.swift index 39ae4ec51..8bbe7a719 100644 --- a/iosApp/iosApp/UI/Page/ProfileNew/Data/ProfilePresenterService.swift +++ b/iosApp/iosApp/UI/Page/Profile/Data/ProfilePresenterService.swift @@ -72,6 +72,21 @@ class ProfilePresenterService { return store } + // 新增:设置TimelineViewModel的方法,类似TimelineViewSwiftUIV4的setupDataSource + @MainActor + func setupTimelineViewModel(accountType: AccountType, userKey: MicroBlogKey?) async { + let presenterWrapper = getOrCreatePresenter(accountType: accountType, userKey: userKey) + let tabStore = getOrCreateTabStore(userKey: userKey) + + os_log("[📔][ProfilePresenterService] 开始设置TimelineViewModel: accountType=%{public}@, userKey=%{public}@", + log: .default, type: .debug, + String(describing: accountType), userKey?.description ?? "nil") + + await presenterWrapper.setupTimelineViewModel(with: tabStore) + + os_log("[📔][ProfilePresenterService] TimelineViewModel设置完成", log: .default, type: .debug) + } + func clearCache() { cacheLock.lock() defer { cacheLock.unlock() } diff --git a/iosApp/iosApp/UI/Page/Profile/Data/ProfilePresenterWrapper.swift b/iosApp/iosApp/UI/Page/Profile/Data/ProfilePresenterWrapper.swift new file mode 100644 index 000000000..0f30b3221 --- /dev/null +++ b/iosApp/iosApp/UI/Page/Profile/Data/ProfilePresenterWrapper.swift @@ -0,0 +1,71 @@ +import Foundation +import os.log +import shared +import SwiftUI + +class ProfilePresenterWrapper: ObservableObject { + let presenter: ProfileNewPresenter + @Published var isShowAppBar: Bool? = nil // nil: 初始状态, true: 显示, false: 隐藏 + + // 新增:TimelineViewModel集成 + private(set) var timelineViewModel: TimelineViewModel? + private let accountType: AccountType + private let userKey: MicroBlogKey? + + init(accountType: AccountType, userKey: MicroBlogKey?) { + os_log("[📔][ProfilePresenterWrapper - init]初始化: accountType=%{public}@, userKey=%{public}@", log: .default, type: .debug, String(describing: accountType), userKey?.description ?? "nil") + + self.accountType = accountType + self.userKey = userKey + presenter = .init(accountType: accountType, userKey: userKey) + + isShowAppBar = nil + } + + func updateNavigationState(showAppBar: Bool?) { + os_log("[📔][ProfilePresenterWrapper]更新导航栏状态: showAppBar=%{public}@", log: .default, type: .debug, String(describing: showAppBar)) + Task { @MainActor in + isShowAppBar = showAppBar + } + } + + // 新增:TimelineViewModel初始化方法 + @MainActor + func setupTimelineViewModel(with tabStore: ProfileTabSettingStore) async { + guard timelineViewModel == nil else { + os_log("[📔][ProfilePresenterWrapper]TimelineViewModel已存在,跳过初始化", log: .default, type: .debug) + return + } + + os_log("[📔][ProfilePresenterWrapper]开始初始化TimelineViewModel", log: .default, type: .debug) + + // 创建TimelineViewModel实例 + let viewModel = TimelineViewModel() + timelineViewModel = viewModel + + // 从ProfileTabSettingStore获取当前的Timeline Presenter + if let timelinePresenter = tabStore.currentPresenter { + await viewModel.setupDataSource(presenter: timelinePresenter) + os_log("[📔][ProfilePresenterWrapper]TimelineViewModel初始化完成,使用TabStore的presenter", log: .default, type: .debug) + } else { + os_log("[📔][ProfilePresenterWrapper]⚠️ TabStore中没有可用的Timeline Presenter", log: .default, type: .error) + } + } + + // 新增:更新TimelineViewModel的数据源(当tab切换时调用) + @MainActor + func updateTimelineViewModel(with presenter: TimelinePresenter) async { + guard let viewModel = timelineViewModel else { + os_log("[📔][ProfilePresenterWrapper]⚠️ TimelineViewModel未初始化,无法更新", log: .default, type: .error) + return + } + + os_log("[📔][ProfilePresenterWrapper]更新TimelineViewModel数据源", log: .default, type: .debug) + await viewModel.setupDataSource(presenter: presenter) + } + + // 新增:获取TimelineViewModel(如果已初始化) + func getTimelineViewModel() -> TimelineViewModel? { + timelineViewModel + } +} diff --git a/iosApp/iosApp/UI/Page/ProfileNew/Data/ProfileTabSettingStore.swift b/iosApp/iosApp/UI/Page/Profile/Data/ProfileTabSettingStore.swift similarity index 96% rename from iosApp/iosApp/UI/Page/ProfileNew/Data/ProfileTabSettingStore.swift rename to iosApp/iosApp/UI/Page/Profile/Data/ProfileTabSettingStore.swift index 49153ac1d..5495e4a62 100644 --- a/iosApp/iosApp/UI/Page/ProfileNew/Data/ProfileTabSettingStore.swift +++ b/iosApp/iosApp/UI/Page/Profile/Data/ProfileTabSettingStore.swift @@ -4,8 +4,8 @@ import shared import SwiftUI class ProfileTabSettingStore: ObservableObject, TabStateProvider { - @Published var availableTabs: [FLTabItem] = [] // 当前显示的所有标签 - @Published var selectedTabKey: String? // 当前选中的标签 + @Published var availableTabs: [FLTabItem] = [] + @Published var selectedTabKey: String? @Published var currentUser: UiUserV2? @Published var currentPresenter: TimelinePresenter? @Published var currentMediaPresenter: ProfileMediaPresenter? @@ -13,7 +13,7 @@ class ProfileTabSettingStore: ObservableObject, TabStateProvider { private var isInitializing = false private var presenterCache: [String: TimelinePresenter] = [:] - private var mediaPresenterCache: [String: ProfileMediaPresenter] = [:] // 媒体presenter缓存 + private var mediaPresenterCache: [String: ProfileMediaPresenter] = [:] var onTabChange: ((Int) -> Void)? diff --git a/iosApp/iosApp/UI/Page/ProfileNew/Model/ProfileUserInfo.swift b/iosApp/iosApp/UI/Page/Profile/Model/ProfileUserInfo.swift similarity index 84% rename from iosApp/iosApp/UI/Page/ProfileNew/Model/ProfileUserInfo.swift rename to iosApp/iosApp/UI/Page/Profile/Model/ProfileUserInfo.swift index e37781f04..b9877efd0 100644 --- a/iosApp/iosApp/UI/Page/ProfileNew/Model/ProfileUserInfo.swift +++ b/iosApp/iosApp/UI/Page/Profile/Model/ProfileUserInfo.swift @@ -1,7 +1,6 @@ import Foundation import shared -// 整合所有用户资料页需要的信息 struct ProfileUserInfo: Equatable { let profile: UiProfile let relation: UiRelation? @@ -11,16 +10,13 @@ struct ProfileUserInfo: Equatable { let fields: [String: UiRichText] let canSendMessage: Bool - // 从 ProfileState 创建 static func from(state: ProfileNewState) -> ProfileUserInfo? { - // 只有在用户信息加载成功时才创建 guard case let .success(user) = onEnum(of: state.userState), let profile = user.data as? UiProfile else { return nil } - // 获取关系状态 let relation: UiRelation? = { if case let .success(rel) = onEnum(of: state.relationState) { return rel.data @@ -28,7 +24,6 @@ struct ProfileUserInfo: Equatable { return nil }() - // 获取是否是当前用户 let isMe: Bool = { if case let .success(me) = onEnum(of: state.isMe) { return me.data as! Bool @@ -36,7 +31,6 @@ struct ProfileUserInfo: Equatable { return false }() - // 获取是否可以发送消息 let canSendMessage: Bool = { if case let .success(can) = onEnum(of: state.canSendMessage) { return can.data as! Bool @@ -44,11 +38,9 @@ struct ProfileUserInfo: Equatable { return false }() - // 获取关注和粉丝数 let followCount = profile.matrices.followsCountHumanized let fansCount = profile.matrices.fansCountHumanized - // 获取用户字段信息 let fields: [String: UiRichText] = { if let bottomContent = profile.bottomContent, let fieldsContent = bottomContent as? UiProfileBottomContentFields @@ -69,10 +61,7 @@ struct ProfileUserInfo: Equatable { ) } - // - Equatable - static func == (lhs: ProfileUserInfo, rhs: ProfileUserInfo) -> Bool { - // 比较关键字段,避免复杂对象比较 lhs.profile.key.description == rhs.profile.key.description && lhs.isMe == rhs.isMe && lhs.followCount == rhs.followCount && diff --git a/iosApp/iosApp/UI/Page/ProfileNew/SwiftUIView/CommonProfileHeader.swift b/iosApp/iosApp/UI/Page/Profile/SwiftUIView/CommonProfileHeader.swift similarity index 100% rename from iosApp/iosApp/UI/Page/ProfileNew/SwiftUIView/CommonProfileHeader.swift rename to iosApp/iosApp/UI/Page/Profile/SwiftUIView/CommonProfileHeader.swift diff --git a/iosApp/iosApp/UI/Page/ProfileNew/SwiftUIView/ProfileMediaListScreen.swift b/iosApp/iosApp/UI/Page/Profile/SwiftUIView/ProfileMediaListScreen.swift similarity index 100% rename from iosApp/iosApp/UI/Page/ProfileNew/SwiftUIView/ProfileMediaListScreen.swift rename to iosApp/iosApp/UI/Page/Profile/SwiftUIView/ProfileMediaListScreen.swift diff --git a/iosApp/iosApp/UI/Page/ProfileNew/SwiftUIView/ProfileWithUserNameScreen.swift b/iosApp/iosApp/UI/Page/Profile/SwiftUIView/ProfileWithUserNameScreen.swift similarity index 92% rename from iosApp/iosApp/UI/Page/ProfileNew/SwiftUIView/ProfileWithUserNameScreen.swift rename to iosApp/iosApp/UI/Page/Profile/SwiftUIView/ProfileWithUserNameScreen.swift index 0790e94cc..e929f4262 100644 --- a/iosApp/iosApp/UI/Page/ProfileNew/SwiftUIView/ProfileWithUserNameScreen.swift +++ b/iosApp/iosApp/UI/Page/Profile/SwiftUIView/ProfileWithUserNameScreen.swift @@ -8,14 +8,12 @@ import SwiftUI struct ProfileWithUserNameScreen: View { @State private var presenter: ProfileWithUserNameAndHostPresenter private let accountType: AccountType - let toProfileMedia: (MicroBlogKey) -> Void @Environment(FlareRouter.self) var router @Environment(FlareTheme.self) private var theme - init(accountType: AccountType, userName: String, host: String, toProfileMedia: @escaping (MicroBlogKey) -> Void) { + init(accountType: AccountType, userName: String, host: String) { self.accountType = accountType presenter = .init(userName: userName, host: host, accountType: accountType) - self.toProfileMedia = toProfileMedia os_log("[📔][ProfileWithUserNameScreen - init]ProfileWithUserNameScreen: userName=%{public}@, host=%{public}@", log: .default, type: .debug, userName, host) } @@ -61,8 +59,7 @@ struct ProfileWithUserNameScreen: View { ProfileTabScreenUikit( accountType: accountType, - userKey: data.data.key, - toProfileMedia: toProfileMedia + userKey: data.data.key ) .onAppear { os_log("[📔][ProfileWithUserNameScreen]成功加载用户信息: userKey=%{public}@", log: .default, type: .debug, data.data.key.description) diff --git a/iosApp/iosApp/UI/Page/ProfileNew/UIKitView/ProfileMediaViewController.swift b/iosApp/iosApp/UI/Page/Profile/UIKitView/ProfileMediaViewController.swift similarity index 100% rename from iosApp/iosApp/UI/Page/ProfileNew/UIKitView/ProfileMediaViewController.swift rename to iosApp/iosApp/UI/Page/Profile/UIKitView/ProfileMediaViewController.swift diff --git a/iosApp/iosApp/UI/Page/ProfileNew/UIKitView/ProfileRefreshViewController.swift b/iosApp/iosApp/UI/Page/Profile/UIKitView/ProfileRefreshViewController.swift similarity index 95% rename from iosApp/iosApp/UI/Page/ProfileNew/UIKitView/ProfileRefreshViewController.swift rename to iosApp/iosApp/UI/Page/Profile/UIKitView/ProfileRefreshViewController.swift index 73102251d..a48b391f1 100644 --- a/iosApp/iosApp/UI/Page/ProfileNew/UIKitView/ProfileRefreshViewController.swift +++ b/iosApp/iosApp/UI/Page/Profile/UIKitView/ProfileRefreshViewController.swift @@ -17,14 +17,13 @@ class ProfileNewRefreshViewController: UIViewController { private var state: ProfileNewState? private var selectedTab: Binding? private var isShowAppBar: Binding? - private var isShowsegmentedBackButton: Binding? private var horizontalSizeClass: UserInterfaceSizeClass? private var appSettings: AppSettings? - private var toProfileMedia: ((MicroBlogKey) -> Void)? private var accountType: AccountType? private var userKey: MicroBlogKey? private var tabStore: ProfileTabSettingStore? private var mediaPresenterWrapper: ProfileMediaPresenterWrapper? + private var presenterWrapper: ProfilePresenterWrapper? private var listViewControllers: [Int: JXPagingViewListViewDelegate] = [:] private var themeObserver: NSObjectProtocol? @@ -57,11 +56,11 @@ class ProfileNewRefreshViewController: UIViewController { isShowAppBar: Binding, horizontalSizeClass: UserInterfaceSizeClass?, appSettings: AppSettings, - toProfileMedia: @escaping (MicroBlogKey) -> Void, accountType: AccountType, userKey: MicroBlogKey?, tabStore: ProfileTabSettingStore, mediaPresenterWrapper: ProfileMediaPresenterWrapper, + presenterWrapper: ProfilePresenterWrapper, theme: FlareTheme ) { self.userInfo = userInfo @@ -70,11 +69,11 @@ class ProfileNewRefreshViewController: UIViewController { self.isShowAppBar = isShowAppBar self.horizontalSizeClass = horizontalSizeClass self.appSettings = appSettings - self.toProfileMedia = toProfileMedia self.accountType = accountType self.userKey = userKey self.tabStore = tabStore self.mediaPresenterWrapper = mediaPresenterWrapper + self.presenterWrapper = presenterWrapper self.theme = theme setupThemeObserver() @@ -278,16 +277,25 @@ class ProfileNewRefreshViewController: UIViewController { Task { if let currentList = self.pagingView.validListDict[self.segmentedView.selectedIndex] { if let timelineVC = currentList as? TimelineViewController, - let timelineState = timelineVC.presenter?.models.value as? TimelineState + let presenterWrapper = self.presenterWrapper, + let timelineViewModel = presenterWrapper.getTimelineViewModel() { - // 触发时间线刷新 + // 使用TimelineViewModel的现代化刷新方法 + await timelineViewModel.handleRefresh() + os_log("[📔][ProfileRefreshViewController] TimelineViewModel刷新完成", log: .default, type: .debug) + } else if let timelineVC = currentList as? TimelineViewController, + let timelineState = timelineVC.presenter?.models.value as? TimelineState + { + // 降级到传统刷新方法(如果TimelineViewModel未初始化) try? await timelineState.refresh() + os_log("[📔][ProfileRefreshViewController] 使用传统TimelineState刷新", log: .default, type: .debug) } else if let mediaVC = currentList as? ProfileMediaViewController, let mediaPresenterWrapper = self.mediaPresenterWrapper, case let .success(data) = onEnum(of: mediaPresenterWrapper.presenter.models.value.mediaState) { // 触发媒体列表刷新 data.retry() + os_log("[📔][ProfileRefreshViewController] 媒体列表刷新完成", log: .default, type: .debug) } await MainActor.run { @@ -418,7 +426,6 @@ class ProfileNewRefreshViewController: UIViewController { // 离开页面时重置状态,不然 详情页会导致没appbar isShowAppBar?.wrappedValue = true - isShowsegmentedBackButton?.wrappedValue = false // 确保系统导航栏状态正确 navigationController?.setNavigationBarHidden(false, animated: animated) @@ -454,7 +461,6 @@ class ProfileNewRefreshViewController: UIViewController { // 在返回前重置导航状态 // 离开页面时重置状态,不然 详情页会导致没appbar isShowAppBar?.wrappedValue = true - isShowsegmentedBackButton?.wrappedValue = false // 确保导航栏可见 navigationController?.setNavigationBarHidden(false, animated: true) diff --git a/iosApp/iosApp/UI/Page/ProfileNew/UIKitView/ProfileStretchRefreshControl.swift b/iosApp/iosApp/UI/Page/Profile/UIKitView/ProfileStretchRefreshControl.swift similarity index 100% rename from iosApp/iosApp/UI/Page/ProfileNew/UIKitView/ProfileStretchRefreshControl.swift rename to iosApp/iosApp/UI/Page/Profile/UIKitView/ProfileStretchRefreshControl.swift diff --git a/iosApp/iosApp/UI/Page/ProfileNew/UIKitView/ProfileTabScreenUikit.swift b/iosApp/iosApp/UI/Page/Profile/UIKitView/ProfileTabScreenUikit.swift similarity index 84% rename from iosApp/iosApp/UI/Page/ProfileNew/UIKitView/ProfileTabScreenUikit.swift rename to iosApp/iosApp/UI/Page/Profile/UIKitView/ProfileTabScreenUikit.swift index 63ccc98ab..6ad390230 100644 --- a/iosApp/iosApp/UI/Page/ProfileNew/UIKitView/ProfileTabScreenUikit.swift +++ b/iosApp/iosApp/UI/Page/Profile/UIKitView/ProfileTabScreenUikit.swift @@ -7,7 +7,6 @@ import shared import SwiftUI struct ProfileTabScreenUikit: View { - let toProfileMedia: (MicroBlogKey) -> Void let accountType: AccountType let userKey: MicroBlogKey? let showBackButton: Bool @@ -23,13 +22,11 @@ struct ProfileTabScreenUikit: View { @Environment(\.horizontalSizeClass) private var horizontalSizeClass @Environment(\.appSettings) private var appSettings @Environment(FlareRouter.self) private var router - @Environment(FlareAppState.self) private var appState + @Environment(FlareMenuState.self) private var menuState init( - accountType: AccountType, userKey: MicroBlogKey?, - toProfileMedia: @escaping (MicroBlogKey) -> Void, showBackButton: Bool = true + accountType: AccountType, userKey: MicroBlogKey?, showBackButton: Bool = true ) { - self.toProfileMedia = toProfileMedia self.accountType = accountType self.userKey = userKey self.showBackButton = showBackButton @@ -62,8 +59,15 @@ struct ProfileTabScreenUikit: View { String(describing: userKey) ) + // 新增:初始化TimelineViewModel(类似TimelineViewSwiftUIV4的setupDataSource) + let _ = Task { @MainActor in + await ProfilePresenterService.shared.setupTimelineViewModel( + accountType: accountType, + userKey: userKey + ) + } + if userKey == nil { - // ProfileNewRefreshViewControllerWrapper( userInfo: userInfo, state: state as! ProfileNewState, @@ -72,17 +76,14 @@ struct ProfileTabScreenUikit: View { get: { presenterWrapper.isShowAppBar }, set: { presenterWrapper.updateNavigationState(showAppBar: $0) } ), - isShowsegmentedBackButton: Binding( - get: { presenterWrapper.isShowsegmentedBackButton }, - set: { _ in } // 只读绑定,因为这个值由 isShowAppBar 控制 - ), + horizontalSizeClass: horizontalSizeClass, appSettings: appSettings, - toProfileMedia: toProfileMedia, accountType: accountType, userKey: userKey, tabStore: tabStore, mediaPresenterWrapper: mediaPresenterWrapper, + presenterWrapper: presenterWrapper, theme: theme ) .ignoresSafeArea(edges: .top) @@ -96,17 +97,14 @@ struct ProfileTabScreenUikit: View { get: { presenterWrapper.isShowAppBar }, set: { presenterWrapper.updateNavigationState(showAppBar: $0) } ), - isShowsegmentedBackButton: Binding( - get: { presenterWrapper.isShowsegmentedBackButton }, - set: { _ in } // 只读绑定,因为这个值由 isShowAppBar 控制 - ), + horizontalSizeClass: horizontalSizeClass, appSettings: appSettings, - toProfileMedia: toProfileMedia, accountType: accountType, userKey: userKey, tabStore: tabStore, mediaPresenterWrapper: mediaPresenterWrapper, + presenterWrapper: presenterWrapper, theme: theme ) .ignoresSafeArea(edges: .top) @@ -120,14 +118,13 @@ struct ProfileNewRefreshViewControllerWrapper: UIViewControllerRepresentable { let state: ProfileNewState @Binding var selectedTab: Int @Binding var isShowAppBar: Bool? - @Binding var isShowsegmentedBackButton: Bool let horizontalSizeClass: UserInterfaceSizeClass? let appSettings: AppSettings - let toProfileMedia: (MicroBlogKey) -> Void let accountType: AccountType let userKey: MicroBlogKey? let tabStore: ProfileTabSettingStore let mediaPresenterWrapper: ProfileMediaPresenterWrapper + let presenterWrapper: ProfilePresenterWrapper let theme: FlareTheme func makeUIViewController(context _: Context) -> ProfileNewRefreshViewController { @@ -139,11 +136,11 @@ struct ProfileNewRefreshViewControllerWrapper: UIViewControllerRepresentable { isShowAppBar: $isShowAppBar, horizontalSizeClass: horizontalSizeClass, appSettings: appSettings, - toProfileMedia: toProfileMedia, accountType: accountType, userKey: userKey, tabStore: tabStore, mediaPresenterWrapper: mediaPresenterWrapper, + presenterWrapper: presenterWrapper, theme: theme ) return controller @@ -160,11 +157,11 @@ struct ProfileNewRefreshViewControllerWrapper: UIViewControllerRepresentable { isShowAppBar: $isShowAppBar, horizontalSizeClass: horizontalSizeClass, appSettings: appSettings, - toProfileMedia: toProfileMedia, accountType: accountType, userKey: userKey, tabStore: tabStore, mediaPresenterWrapper: mediaPresenterWrapper, + presenterWrapper: presenterWrapper, theme: theme ) } diff --git a/iosApp/iosApp/UI/Page/ProfileNew/Components/ProfileHeaderView.swift b/iosApp/iosApp/UI/Page/ProfileNew/Components/ProfileHeaderView.swift deleted file mode 100644 index 35af85809..000000000 --- a/iosApp/iosApp/UI/Page/ProfileNew/Components/ProfileHeaderView.swift +++ /dev/null @@ -1,75 +0,0 @@ -import MarkdownUI -import shared -import SwiftUI - -// -// struct ProfileHeaderView: View { -// let userInfo: ProfileUserInfo -// let state: ProfileState -// let onFollowClick: (UiRelation) -> Void -// -// var body: some View { -// CommonProfileHeader( -// userInfo: userInfo, -// state: state, -// onFollowClick: onFollowClick -// ) -// } -// } - -struct UserFollowsFansCount: View { - let followCount: String - let fansCount: String - var body: some View { - HStack { - Text(followCount) - .fontWeight(.bold) - Text("following") - .foregroundColor(.secondary) - .font(.footnote) - Divider() - Text(fansCount) - .fontWeight(.bold) - Text("fans") - .foregroundColor(.secondary) - .font(.footnote) - } - .font(.caption) - } -} - -struct UserInfoFieldsView: View { - let fields: [String: UiRichText] - var body: some View { - if fields.count > 0 { - VStack(alignment: .leading) { - let keys = fields.map(\.key).sorted() - ForEach(0 ..< keys.count, id: \.self) { index in - let key = keys[index] - Text(key) - .font(.subheadline) - .foregroundColor(.secondary) - Markdown(fields[key]?.markdown ?? "").markdownTextStyle(textStyle: { - FontFamilyVariant(.normal) - FontSize(.em(0.9)) - ForegroundColor(.primary) - }).markdownInlineImageProvider(.emoji) - .padding(.vertical, 4) - if index != keys.count - 1 { - Divider() - } - } - .padding(.horizontal) - } - .padding(.vertical) - #if os(iOS) - .background(Color(UIColor.secondarySystemBackground)) - #else - .background(Color(NSColor.windowBackgroundColor)) - #endif - .clipShape(RoundedRectangle(cornerRadius: 8)) - } else { - EmptyView() - } - } -} diff --git a/iosApp/iosApp/UI/Page/ProfileNew/Components/ProfileTabBarView.swift b/iosApp/iosApp/UI/Page/ProfileNew/Components/ProfileTabBarView.swift deleted file mode 100644 index 6c4ee92c2..000000000 --- a/iosApp/iosApp/UI/Page/ProfileNew/Components/ProfileTabBarView.swift +++ /dev/null @@ -1,44 +0,0 @@ -import shared -import SwiftUI - -struct ProfileTabBarView: View { - let tabs: [FLTabItem] - @Binding var selectedTab: Int - let onTabSelected: (Int) -> Void - - var body: some View { - ScrollView(.horizontal, showsIndicators: false) { - HStack(spacing: 24) { - ForEach(Array(tabs.enumerated()), id: \.element.key) { index, tab in - Button(action: { - onTabSelected(index) - }) { - VStack(spacing: 4) { - switch tab.metaData.title { - case let .text(title): - Text(title) - .font(.system(size: 16)) - .foregroundColor(selectedTab == index ? .primary : .gray) - .fontWeight(selectedTab == index ? .semibold : .regular) - case let .localized(key): - Text(NSLocalizedString(key, comment: "")) - .font(.system(size: 16)) - .foregroundColor(selectedTab == index ? .primary : .gray) - .fontWeight(selectedTab == index ? .semibold : .regular) - } - - Rectangle() - .fill(selectedTab == index ? Color.accentColor : Color.clear) - .frame(height: 2) - .frame(width: 24) - } - } - } - } - .padding(.horizontal) - } - .frame(height: 44) - .padding(.top, 18) -// .background(FColors.Background.swiftUIPrimary) - } -} diff --git a/iosApp/iosApp/UI/Page/ProfileNew/Data/ProfilePresenterWrapper.swift b/iosApp/iosApp/UI/Page/ProfileNew/Data/ProfilePresenterWrapper.swift deleted file mode 100644 index 25a45e5b6..000000000 --- a/iosApp/iosApp/UI/Page/ProfileNew/Data/ProfilePresenterWrapper.swift +++ /dev/null @@ -1,37 +0,0 @@ -import Foundation -import os.log -import shared -import SwiftUI - -class ProfilePresenterWrapper: ObservableObject { - // - Properties - let presenter: ProfileNewPresenter - @Published var isShowAppBar: Bool? = nil // nil: 初始状态, true: 显示, false: 隐藏 - @Published var isShowsegmentedBackButton: Bool = false - - // - Init - init(accountType: AccountType, userKey: MicroBlogKey?) { - os_log("[📔][ProfilePresenterWrapper - init]初始化: accountType=%{public}@, userKey=%{public}@", log: .default, type: .debug, String(describing: accountType), userKey?.description ?? "nil") - - presenter = .init(accountType: accountType, userKey: userKey) - - // 初始化导航栏状态 - isShowAppBar = nil - isShowsegmentedBackButton = false - } - - // 更新导航栏状态 - func updateNavigationState(showAppBar: Bool?) { - os_log("[📔][ProfilePresenterWrapper]更新导航栏状态: showAppBar=%{public}@", log: .default, type: .debug, String(describing: showAppBar)) - Task { @MainActor in - isShowAppBar = showAppBar - - // 根据 isShowAppBar 状态更新 isShowsegmentedBackButton - if let showAppBar { - isShowsegmentedBackButton = !showAppBar - } else { - isShowsegmentedBackButton = false - } - } - } -} diff --git a/iosApp/iosApp/UI/Page/ProfileNew/SwiftUIView/HeaderPageScrollView.swift b/iosApp/iosApp/UI/Page/ProfileNew/SwiftUIView/HeaderPageScrollView.swift deleted file mode 100644 index 13f9dade1..000000000 --- a/iosApp/iosApp/UI/Page/ProfileNew/SwiftUIView/HeaderPageScrollView.swift +++ /dev/null @@ -1,286 +0,0 @@ -import SwiftUI - -// -// struct PageLabel { -// var title: String -// var symbolImage: String -// } -// -// @resultBuilder -// struct PageLabelBuilder { -// static func buildBlock(_ components: PageLabel...) -> [PageLabel] { -// components.compactMap(\.self) -// } -// } -// -// struct HeaderPageScrollView: View { -// var displaysSymbols: Bool = false -// /// Header View -// var header: Header -// /// Labels (Tab Title or Tab Image) -// var labels: [PageLabel] -// /// Tab Views -// var pages: Pages -// /// For Refreshing -// var onRefresh: () async -> Void -// -// init( -// displaysSymbols: Bool, -// @ViewBuilder header: @escaping () -> Header, -// @PageLabelBuilder labels: @escaping () -> [PageLabel], -// @ViewBuilder pages: @escaping () -> Pages, -// onRefresh: @escaping () async -> Void = {} -// ) { -// self.displaysSymbols = displaysSymbols -// self.header = header() -// self.labels = labels() -// self.pages = pages() -// self.onRefresh = onRefresh -// -// let count = labels().count -// _scrollPositions = .init(initialValue: .init(repeating: .init(), count: count)) -// _scrollGeometries = .init(initialValue: .init(repeating: .init(), count: count)) -// } -// -// /// View Properties -// @State private var activeTab: String? -// @State private var headerHeight: CGFloat = 0 -// @State private var scrollGeometries: [ScrollGeometry] -// @State private var scrollPositions: [ScrollPosition] -// /// Main Scroll Properties -// @State private var mainScrollDisabled: Bool = false -// @State private var mainScrollPhase: ScrollPhase = .idle -// @State private var mainScrollGeometry: ScrollGeometry = .init() -// var body: some View { -// GeometryReader { -// let size = $0.size -// -// ScrollView(.horizontal) { -// /// Using HStack allows us to maintain references to other scrollviews, enabling us to update them when necessary. -// HStack(spacing: 0) { -// Group(subviews: pages) { collection in -// /// Checking both collection and labels match with each other -// if collection.count != labels.count { -// Text("Tabviews and labels does not match!") -// .frame(width: size.width, height: size.height) -// } else { -// ForEach(labels, id: \.title) { label in -// PageScrollView(label: label, size: size, collection: collection) -// } -// } -// } -// } -// .scrollTargetLayout() -// } -// .scrollTargetBehavior(.paging) -// .ignoresSafeArea(edges: .top) -// .scrollPosition(id: $activeTab) -// .scrollIndicators(.hidden) -// .scrollDisabled(mainScrollDisabled) -// /// Disabling Interaction when scroll view is animating to avoid unintentional taps! -// .allowsHitTesting(mainScrollPhase == .idle) -// .onScrollPhaseChange { _, newPhase in -// mainScrollPhase = newPhase -// } -//// .onScrollGeometryChange(for: ScrollGeometry.self, of: { -//// $0 -//// }, action: { _, newValue in -//// mainScrollGeometry = newValue -//// }) -// .mask { -// Rectangle() -// .ignoresSafeArea(.all, edges: .bottom) -// } -// .onAppear { -// /// Setting up Initial Tab Value -// guard activeTab == nil else { return } -// -// activeTab = labels.first?.title -// } -// } -// } -// -// @ViewBuilder -// func PageScrollView(label: PageLabel, size: CGSize, collection: SubviewsCollection) -> some View { -// let index = labels.firstIndex(where: { $0.title == label.title }) ?? 0 -// -// ScrollView(.vertical) { -// /// Using LazyVstack for Optimizing Performance as it LazyLoads Views! -// LazyVStack(spacing: 0, pinnedViews: [.sectionHeaders]) { -// /// Only Showing Header for the active Tab View! -// ZStack { -// if activeTab == label.title { -// header -// /// Making it as a sticky one so that it won't move left or right when interacting! -// .visualEffect { content, proxy in -// content -// .offset(x: -proxy.frame(in: .scrollView(axis: .horizontal)).minX) -// } -// .onGeometryChange(for: CGFloat.self) { -// $0.size.height -// } action: { newValue in -// headerHeight = newValue -// } -// .transition(.identity) -// } else { -// Rectangle() -// .foregroundStyle(.clear) -// .frame(height: headerHeight) -// .transition(.identity) -// } -// } -// .simultaneousGesture(horizontalScrollDisableGesture) -// -// /// Using Pinned Views to actually pin our tab bar at the top! -// Section { -// collection[index] -// /// Let's make it to be scrollable to the top even if the view does not have enough content -// /// 40 - Tab Bar Size, -5 is given so that it will not reset scrollviews when it's bounces! -// .frame(minHeight: size.height - 35, alignment: .top) -// } header: { -// /// Doing the same behaviour as the header view! -// ZStack { -// if activeTab == label.title { -// CustomTabBar() -// .visualEffect { content, proxy in -// content -// .offset(x: -proxy.frame(in: .scrollView(axis: .horizontal)).minX) -// } -// .transition(.identity).padding(.top, 40) -// } else { -// Rectangle() -// .foregroundStyle(.clear) -// .frame(height: 40) -// .transition(.identity) -// } -// } -// .simultaneousGesture(horizontalScrollDisableGesture) -// } -// } -// } -// .onScrollGeometryChange(for: ScrollGeometry.self, of: { -// $0 -// }, action: { _, newValue in -// scrollGeometries[index] = newValue -// -// if newValue.offsetY < 0 { -// resetScrollViews(label) -// } -// }) -// .scrollPosition($scrollPositions[index]) -// .onScrollPhaseChange { _, newPhase in -// let geometry = scrollGeometries[index] -// let maxOffset = min(geometry.offsetY, headerHeight) -// -// if newPhase == .idle, maxOffset <= headerHeight { -// updateOtherScrollViews(label, to: maxOffset) -// } -// -// /// Fail-Safe -// if newPhase == .idle, mainScrollDisabled { -// mainScrollDisabled = false -// } -// } -// .frame(width: size.width) -// .scrollClipDisabled() -// .refreshable { -// await onRefresh() -// } -// .zIndex(activeTab == label.title ? 1000 : 0) -// } -// -// /// Custom Tab Bar -// @ViewBuilder -// func CustomTabBar() -> some View { -// let progress = max(min(mainScrollGeometry.offsetX / mainScrollGeometry.containerSize.width, CGFloat(labels.count - 1)), 0) -// -// VStack(alignment: .leading, spacing: 5) { -// HStack(spacing: 0) { -// ForEach(labels, id: \.title) { label in -// Group { -// if displaysSymbols { -// Image(systemName: label.symbolImage) -// } else { -// Text(label.title) -// } -// } -// .frame(maxWidth: .infinity) -// .foregroundStyle(activeTab == label.title ? Color.primary : .gray) -// .onTapGesture { -// withAnimation(.easeInOut(duration: 0.25)) { -// activeTab = label.title -// } -// } -// } -// } -// .frame(maxHeight: .infinity) -// -// ZStack(alignment: .leading) { -// Rectangle() -// .fill(.gray.opacity(0.5)) -// .frame(height: 1) -// -// /// Let's Create a sliding indicator for the tab bar! -// Capsule() -// .frame(width: 50, height: 4) -// .containerRelativeFrame(.horizontal) { value, _ in -// value / CGFloat(labels.count) -// } -// .visualEffect { content, proxy in -// content -// .offset(x: proxy.size.width * progress, y: -1) -// } -// } -// } -// .frame(height: 40) -// .background(.background) -// } -// -// var horizontalScrollDisableGesture: some Gesture { -// DragGesture(minimumDistance: 0) -// .onChanged { _ in -// mainScrollDisabled = true -// }.onEnded { _ in -// mainScrollDisabled = false -// } -// } -// -// /// Reset's Page ScrollView to it's Initial Position -// func resetScrollViews(_ from: PageLabel) { -// for index in labels.indices { -// let label = labels[index] -// -// if label.title != from.title { -// scrollPositions[index].scrollTo(y: 0) -// } -// } -// } -// -// /// Update Other scrollviews to match up with the current scroll view till reaching it's header height -// func updateOtherScrollViews(_ from: PageLabel, to: CGFloat) { -// for index in labels.indices { -// let label = labels[index] -// let offset = scrollGeometries[index].offsetY -// -// let wantsUpdate = offset < headerHeight || to < headerHeight -// -// if wantsUpdate, label.title != from.title { -// scrollPositions[index].scrollTo(y: to) -// } -// } -// } -// } -// -// private extension ScrollGeometry { -// init() { -// self.init(contentOffset: .zero, contentSize: .zero, contentInsets: .init(.zero), containerSize: .zero) -// } -// -// var offsetY: CGFloat { -// contentOffset.y + contentInsets.top -// } -// -// var offsetX: CGFloat { -// contentOffset.x + contentInsets.leading -// } -// } diff --git a/iosApp/iosApp/UI/Page/ProfileNew/SwiftUIView/ProfileContentView.swift b/iosApp/iosApp/UI/Page/ProfileNew/SwiftUIView/ProfileContentView.swift deleted file mode 100644 index 2cce0af25..000000000 --- a/iosApp/iosApp/UI/Page/ProfileNew/SwiftUIView/ProfileContentView.swift +++ /dev/null @@ -1,80 +0,0 @@ -import shared -import SwiftUI - -// struct ProfileContentView: View { -// let tabs: [FLTabItem] -// @Binding var selectedTab: Int -// let refresh: () async -> Void -// let accountType: AccountType -// let userKey: MicroBlogKey? -// @ObservedObject var tabStore: ProfileTabSettingStore -// -// var body: some View { -// if let selectedTab = tabStore.availableTabs.first(where: { $0.key == tabStore.selectedTabKey }) { -// if selectedTab is FLProfileMediaTabItem { -// ProfileMediaListScreen(accountType: accountType, userKey: userKey, tabStore: tabStore) -// } else if let presenter = tabStore.currentPresenter { -// ProfileTimelineView( -// presenter: presenter, -// refresh: refresh -// ) -// } else { -// ProgressView() -// .frame(maxWidth: .infinity, maxHeight: .infinity) -//// .background(FColors.Background.swiftUIPrimary) -// } -// } else { -// ProgressView() -// .frame(maxWidth: .infinity, maxHeight: .infinity) -//// .background(FColors.Background.swiftUIPrimary) -// } -// } -// } - -struct ProfileTimelineView: View { - let presenter: TimelinePresenter? - - var body: some View { - ScrollView { - LazyVStack(spacing: 0) { - if let presenter { - ObservePresenter(presenter: presenter) { state in - if let timelineState = state as? TimelineState, - case let .success(success) = onEnum(of: timelineState.listState) - { - ForEach(0 ..< success.itemCount, id: \.self) { index in - let statusID = "status_\(index)" - if let status = success.peek(index: index) { - StatusItemView(data: status, detailKey: nil) - .padding(.vertical, 8) - .padding(.horizontal, 16) - .id(statusID) - .onAppear { - success.get(index: index) - } - - } else { - StatusPlaceHolder() - .padding(.vertical, 8) - .padding(.horizontal, 16) - .id(statusID) - } - - if index < success.itemCount - 1 { - Divider() - .padding(.horizontal, 16) - } - } - } else if let timelineState = state as? TimelineState { - StatusTimelineComponent( - data: timelineState.listState, - detailKey: nil - ) - .padding(.horizontal, 16) - } - } - } - } - } - } -} diff --git a/iosApp/iosApp/UI/Page/ProfileNew/SwiftUIView/ProfileTabScreen.swift b/iosApp/iosApp/UI/Page/ProfileNew/SwiftUIView/ProfileTabScreen.swift deleted file mode 100644 index f8ae3badb..000000000 --- a/iosApp/iosApp/UI/Page/ProfileNew/SwiftUIView/ProfileTabScreen.swift +++ /dev/null @@ -1,213 +0,0 @@ -import AVKit -import JXPhotoBrowser -import Kingfisher -import MarkdownUI -import OrderedCollections -import shared -import SwiftUI - -struct ProfileTabScreen: View { - let toProfileMedia: (MicroBlogKey) -> Void - let accountType: AccountType - let userKey: MicroBlogKey? - - @State private var presenter: ProfileNewPresenter - @State private var mediaPresenter: ProfileMediaPresenter - @State private var selectedTabIndex: Int = 0 - @State private var scrollOffset: CGFloat = 0 - @StateObject private var tabStore: ProfileTabSettingStore - - @Environment(FlareTheme.self) private var theme - - init(accountType: AccountType, userKey: MicroBlogKey?, toProfileMedia: @escaping (MicroBlogKey) -> Void) { - self.toProfileMedia = toProfileMedia - self.accountType = accountType - self.userKey = userKey - - _presenter = .init(initialValue: ProfileNewPresenter(accountType: accountType, userKey: userKey)) - _mediaPresenter = .init(initialValue: ProfileMediaPresenter(accountType: accountType, userKey: userKey)) - - // 初始化 tabStore - let tabStore = ProfileTabSettingStore(userKey: userKey) - _tabStore = StateObject(wrappedValue: tabStore) - } - - var body: some View { - ObservePresenter(presenter: presenter) { state in - ZStack(alignment: .top) { - // 内容区 - contentView(state: state) - - // 固定TabBar - 当滚动超过header高度时固定在顶部 - if let loadedUserInfo = ProfileUserInfo.from(state: state as! ProfileNewState), - scrollOffset > 0 - { - VStack(spacing: 0) { - TabBarView( - selectedIndex: $selectedTabIndex, - tabs: tabStore.availableTabs - ) - .background(Color(UIColor.systemBackground)) - .shadow(color: Color.black.opacity(0.1), radius: 2, x: 0, y: 2) - } - .transition(.opacity) - .zIndex(100) - } - } - .ignoresSafeArea(edges: .top) - } - } - - @ViewBuilder - private func contentView(state: ProfileNewState) -> some View { - switch onEnum(of: state.userState) { - case .error: - Text("error") - case .loading: - loadingView() - case let .success(data): - if let loadedUserInfo = ProfileUserInfo.from(state: state) { - scrollableContentView(userInfo: loadedUserInfo, state: state) - } else { - ProgressView() - .frame(maxWidth: .infinity, maxHeight: .infinity) - } - } - } - - @ViewBuilder - private func loadingView() -> some View { - CommonProfileHeader( - userInfo: ProfileUserInfo( - profile: createSampleUser(), - relation: nil, - isMe: false, - followCount: "0", - fansCount: "0", - fields: [:], - canSendMessage: false - ), - state: nil, - onFollowClick: { _ in } - ) - .redacted(reason: .placeholder) - .listRowSeparator(.hidden) - .listRowInsets(EdgeInsets()) - } - - @ViewBuilder - private func scrollableContentView(userInfo: ProfileUserInfo, state: ProfileNewState) -> some View { - GeometryReader { geometry in - ScrollView { - VStack(spacing: 0) { - // 头部个人信息 - CommonProfileHeader( - userInfo: userInfo, - state: state, - onFollowClick: { _ in } - ) - .background( - GeometryReader { headerGeometry in - Color.clear - .preference(key: ScrollOffsetPreferenceKey.self, - value: headerGeometry.frame(in: .global).height) - .onAppear { - scrollOffset = headerGeometry.frame(in: .global).height - } - } - ) - - // TabBar (在滚动时会被固定的TabBar覆盖) - TabBarView( - selectedIndex: $selectedTabIndex, - tabs: tabStore.availableTabs - ) - .background(Color(UIColor.systemBackground)) - - // 支持左右滑动的TabView - TabView(selection: $selectedTabIndex) { - ForEach(0 ..< min(tabStore.availableTabs.count, 3), id: \.self) { index in - tabContent(for: tabStore.availableTabs[index]) - .tag(index) - } - } - .tabViewStyle(.page(indexDisplayMode: .never)) - .frame( - height: geometry.size.height - (geometry.safeAreaInsets.top + 40) - ) - } - } - .onPreferenceChange(ScrollOffsetPreferenceKey.self) { value in - scrollOffset = value - } - .coordinateSpace(name: "scroll") - } - } - - // 固定标签内容 - @ViewBuilder - private func tabContent(for tab: FLTabItem) -> some View { - if tab is FLProfileMediaTabItem { - ProfileMediaListScreen( - accountType: accountType, - userKey: userKey, - currentMediaPresenter: mediaPresenter - ) - } else if let presenter = tabStore.getOrCreatePresenter(for: tab) { - ProfileTimelineView(presenter: presenter) - } else { - ProgressView() - .frame(maxWidth: .infinity, maxHeight: .infinity) - } - } -} - -// 简化版标签栏视图 -struct TabBarView: View { - @Binding var selectedIndex: Int - let tabs: [FLTabItem] - - var body: some View { - HStack(spacing: 0) { - ForEach(0 ..< min(tabs.count, 3), id: \.self) { index in - Button { - withAnimation { - selectedIndex = index - } - } label: { - VStack(spacing: 4) { - Text(getTabTitle(tab: tabs[index])) - .fontWeight(selectedIndex == index ? .semibold : .regular) - .foregroundStyle(selectedIndex == index ? Color.primary : Color.gray) - - // 下划线指示器 - Rectangle() - .frame(height: 3) - .foregroundStyle(selectedIndex == index ? Color.primary : Color.clear) - } - .frame(maxWidth: .infinity) - .contentShape(Rectangle()) - } - .buttonStyle(.plain) - } - } - .padding(.vertical, 8) - } - - private func getTabTitle(tab: FLTabItem) -> String { - switch tab.metaData.title { - case let .text(title): - title - case let .localized(key): - NSLocalizedString(key, comment: "") - } - } -} - -// 用于跟踪滚动位置 -struct ScrollOffsetPreferenceKey: PreferenceKey { - static var defaultValue: CGFloat = 0 - static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { - value = nextValue() - } -} diff --git a/iosApp/iosApp/UI/Page/ProfileNew/SwiftUIView/ProfileTabScreen.swift.bak b/iosApp/iosApp/UI/Page/ProfileNew/SwiftUIView/ProfileTabScreen.swift.bak deleted file mode 100644 index 605a86079..000000000 --- a/iosApp/iosApp/UI/Page/ProfileNew/SwiftUIView/ProfileTabScreen.swift.bak +++ /dev/null @@ -1,138 +0,0 @@ -import AVKit -import JXPhotoBrowser -import Kingfisher -import MarkdownUI -import OrderedCollections -import shared -import SwiftUI - -struct ProfileTabScreen: View { - let toProfileMedia: (MicroBlogKey) -> Void - let accountType: AccountType - let userKey: MicroBlogKey? - - @State private var symbolToggle: Bool = false - @State private var presenter: ProfileNewPresenter - @State private var mediaPresenter: ProfileMediaPresenter - - @StateObject private var tabStore: ProfileTabSettingStore - - @Environment(FlareTheme.self) private var theme - - init(accountType: AccountType, userKey: MicroBlogKey?, toProfileMedia: @escaping (MicroBlogKey) -> Void) { - self.toProfileMedia = toProfileMedia - self.accountType = accountType - self.userKey = userKey - -// let host = UserManager.shared.getCurrentUser()?.key.host ?? "" -// let userName = UserManager.shared.getCurrentUser()?.name.raw ?? "" - - _presenter = .init(initialValue: ProfileNewPresenter(accountType: accountType, userKey: userKey)) - _mediaPresenter = .init(initialValue: ProfileMediaPresenter(accountType: accountType, userKey: userKey)) - - // 初始化 tabStore - let tabStore = ProfileTabSettingStore(userKey: userKey) - _tabStore = StateObject(wrappedValue: tabStore) - } - - var body: some View { - ObservePresenter(presenter: presenter) { state in - ZStack { - switch onEnum(of: state.userState) { - case .error: - Text("error") - case .loading: - - CommonProfileHeader( - userInfo: ProfileUserInfo( - profile: createSampleUser(), - relation: nil, - isMe: false, - followCount: "0", - fansCount: "0", - fields: [:], - canSendMessage: false - ), - state: nil, - onFollowClick: { _ in } - ) - .redacted(reason: .placeholder) - .listRowSeparator(.hidden) - .listRowInsets(EdgeInsets()) - - case let .success(data): - - let loadedUserInfo = ProfileUserInfo.from(state: state as! ProfileNewState) - - if loadedUserInfo != nil { - HeaderPageScrollView(displaysSymbols: symbolToggle) { - CommonProfileHeader( - userInfo: loadedUserInfo!, - state: state, - onFollowClick: { _ in } - ) - } labels: { - getTitleFLTabItem(tab: tabStore.availableTabs[0]) - getTitleFLTabItem(tab: tabStore.availableTabs[1]) - getTitleFLTabItem(tab: tabStore.availableTabs[2]) - } pages: { - DummyView(tab: tabStore.availableTabs[0]) - DummyView(tab: tabStore.availableTabs[1]) - DummyView(tab: tabStore.availableTabs[2]) - - -// DummyView2(.red, count: 50) -// DummyView2(.yellow, count: 10) -// DummyView2(.indigo, count: 5) - - - } onRefresh: { - print("Refresh Data") - } - } else { - ProgressView() - .frame(maxWidth: .infinity, maxHeight: .infinity) - } - } - }.ignoresSafeArea(edges: .top) - } - } - - private func getTitleFLTabItem(tab: FLTabItem) -> PageLabel { - switch tab.metaData.title { - case let .text(title): - PageLabel(title: title, symbolImage: "square.grid.3x3.fill") - case let .localized(key): - PageLabel(title: NSLocalizedString(key, comment: ""), symbolImage: "square.grid.3x3.fill") - } - } - - @ViewBuilder - private func DummyView2(_ color: Color, count: Int) -> some View { - LazyVGrid(columns: Array(repeating: GridItem(), count: 3)) { - ForEach(0.. some View { - if tab is FLProfileMediaTabItem { - ProfileMediaListScreen(accountType: accountType, userKey: userKey, currentMediaPresenter: mediaPresenter) - } else { - if let presenterx = tabStore.getOrCreatePresenter(for: tab) { - - ProfileTimelineView( - presenter: presenterx - ) - } else { - ProgressView() - .frame(maxWidth: .infinity, maxHeight: .infinity) - } - } - } -} diff --git a/iosApp/iosApp/UI/Page/ProfileV2/ProfileV2HeaderPageScrollView.swift b/iosApp/iosApp/UI/Page/ProfileV2/ProfileV2HeaderPageScrollView.swift deleted file mode 100644 index d46f82b06..000000000 --- a/iosApp/iosApp/UI/Page/ProfileV2/ProfileV2HeaderPageScrollView.swift +++ /dev/null @@ -1,267 +0,0 @@ - -import SwiftUI - -// -// struct ProfileV2PageLabel: Identifiable { -// var id: String = UUID().uuidString -// var title: String -// var symbolImage: String -// } -// -// @resultBuilder -// struct ProfileV2PageLabelBuilder { -// static func buildBlock(_ components: ProfileV2PageLabel...) -> [ProfileV2PageLabel] { -// components.compactMap(\.self) -// } -// } -// -// struct ProfileV2HeaderPageScrollView: View { -// private var displaysSymbols: Bool = false -// private var header: Header -// /// Labels(Tab Title or Tab Image) -// private var labels: [ProfileV2PageLabel] -// /// Tab views -// private var pages: Pages -// private var onRefresh: (Int) async -> Void -// -// init( -// displaysSymbols: Bool, -// @ViewBuilder header: @escaping () -> Header, -// @ProfileV2PageLabelBuilder labels: @escaping () -> [ProfileV2PageLabel], -// @ViewBuilder pages: @escaping () -> Pages, -// onRefresh: @escaping (Int) async -> Void = { _ in } -// ) { -// self.displaysSymbols = displaysSymbols -// self.header = header() -// self.labels = labels() -// self.pages = pages() -// self.onRefresh = onRefresh -// -// let count = labels().count -// _scrollPositions = .init(initialValue: .init(repeating: .init(), count: count)) -// _scrollGeometries = .init(initialValue: .init(repeating: .init(), count: count)) -// } -// -// /// View Properties -// @State private var activeTab: String? -// @State private var headerHeight: CGFloat = 0 -// @State private var scrollGeometries: [ScrollGeometry] -// @State private var scrollPositions: [ScrollPosition] -// /// Main Scroll Properties -// @State private var mainScrollDisabled: Bool = false -// @State private var mainScrollPhase: ScrollPhase = .idle -// @State private var mainScrollGeometry: ScrollGeometry = .init() -// -// var body: some View { -// GeometryReader { -// let size = $0.size -// -// ScrollView(.horizontal) { -// // 使用HStack允许我们维护对其他滚动视图的引用,使我们能够在必要时更新它们。 -// HStack(spacing: 0) { -// Group(subviews: pages) { collection in -// // 检查集合和标签是否相互匹配 -// if collection.count != labels.count { -// Text("Tabviews and labels does not match!") -// .frame(width: size.width, height: size.height) -// } else { -// ForEach(labels) { label in -// PageScrollView(label: label, size: size, collection: collection) -// } -// } -// } -// } -// .scrollTargetLayout() -// } -// .scrollTargetBehavior(.paging) -// .scrollPosition(id: $activeTab) -// .scrollIndicators(.hidden) -// .scrollDisabled(mainScrollDisabled) -// // 在滚动视图设置动画时禁用交互,以避免意外点击! -// .allowsHitTesting(mainScrollPhase == .idle) -// .onScrollPhaseChange { _, newPhase in -// mainScrollPhase = newPhase -// } -// .onScrollGeometryChange(for: ScrollGeometry.self) { -// $0 -// } action: { _, newValue in -// mainScrollGeometry = newValue -// } -// .mask { -// Rectangle() -// .ignoresSafeArea(.all, edges: .bottom) -// } -// .onAppear { -// guard activeTab == nil else { return } -// activeTab = labels.first?.id -// } -// } -// } -// -// @ViewBuilder -// func PageScrollView(label: ProfileV2PageLabel, size: CGSize, collection: SubviewsCollection) -> some View { -// let index = labels.firstIndex(where: { $0.id == label.id }) ?? 0 -// -// ScrollView(.vertical) { -// // 使用LazyVStack优化性能 -// LazyVStack(spacing: 0, pinnedViews: [.sectionHeaders]) { -// ZStack { -// if activeTab == label.id { -// header -// // 使其具有粘性,以便在以下情况下不会向左或向右移动 -// .visualEffect { content, proxy in -// content -// .offset(x: -proxy.frame(in: .scrollView(axis: .horizontal)).minX) -// } -// .onGeometryChange(for: CGFloat.self) { proxy in -// proxy.size.height -// } action: { newValue in -// headerHeight = newValue -// } -// .transition(.identity) -// } else { -// Rectangle() -// .foregroundStyle(.clear) -// .frame(height: headerHeight) -// .transition(.identity) -// } -// } -// .simultaneousGesture(horizontalScrollDisableGesture) -// -// // 使用固定视图将我们的标签栏固定在顶部! -// Section { -// collection[index] -// // 让我们使其可滚动到顶部,即使视图没有足够的内容 -// .frame(minHeight: size.height - 40, alignment: .top) -// } header: { -// ZStack { -// if activeTab == label.id { -// CustomTabBar() -// .visualEffect { content, proxy in -// content -// .offset(x: -proxy.frame(in: .scrollView(axis: .horizontal)).minX) -// } -// .transition(.identity) -// } else { -// Rectangle() -// .foregroundStyle(.clear) -// .frame(height: 40) -// .transition(.identity) -// } -// } -// .simultaneousGesture(horizontalScrollDisableGesture) -// } -// } -// } -// .onScrollGeometryChange(for: ScrollGeometry.self, of: { $0 }) { _, newValue in -// scrollGeometries[index] = newValue -// -// if newValue.offsetY <= 0 { -// resetScrollViews(label) -// } -// } -// .scrollPosition($scrollPositions[index]) -// .onScrollPhaseChange { _, newPhase in -// let geometry = scrollGeometries[index] -// let maxOffset = min(geometry.offsetY, headerHeight) -// if newPhase == .idle, maxOffset <= headerHeight { -// updateOtherScrollViews(label, to: maxOffset) -// } -// if newPhase == .idle, mainScrollDisabled { -// mainScrollDisabled = false -// } -// } -// .zIndex(activeTab == label.id ? 1000 : 0) -// .frame(width: size.width) -// .scrollClipDisabled() -// .refreshable { -// await onRefresh(index) -// } -// } -// -// // 自定义选项卡栏 -// @ViewBuilder -// func CustomTabBar() -> some View { -// let progress = mainScrollGeometry.offsetX / mainScrollGeometry.containerSize.width -// VStack(alignment: .leading, spacing: 5) { -// HStack(spacing: 0) { -// ForEach(labels) { label in -// Group { -// if displaysSymbols { -// Image(systemName: label.symbolImage) -// } else { -// Text(label.title) -// } -// } -// .frame(maxWidth: .infinity) -// .foregroundStyle(activeTab == label.id ? Color.primary : .gray) -// .onTapGesture { -// withAnimation(.easeInOut(duration: 0.25)) { -// activeTab = label.id -// } -// } -// } -// } -// -// Capsule() -// .frame(width: 50, height: 4) -// .containerRelativeFrame(.horizontal) { value, _ in -// value / CGFloat(labels.count) -// } -// .visualEffect { content, proxy in -// content -// .offset(x: proxy.size.width * progress) -// } -// } -// .frame(height: 40) -// .background(.background) -// } -// -// var horizontalScrollDisableGesture: some Gesture { -// DragGesture(minimumDistance: 0) -// .onChanged { _ in -// mainScrollDisabled = true -// } -// .onEnded { _ in -// mainScrollDisabled = false -// } -// } -// -// // 将页面滚动视图重置为初始位置 -// func resetScrollViews(_ from: ProfileV2PageLabel) { -// for index in labels.indices { -// let label = labels[index] -// if label.id != from.id { -// scrollPositions[index].scrollTo(y: 0) -// } -// } -// } -// -// // 更新其他滚动视图以与当前滚动视图匹配,直到达到其标题高度 -// func updateOtherScrollViews(_ from: ProfileV2PageLabel, to: CGFloat) { -// for index in labels.indices { -// let label = labels[index] -// let offset = scrollGeometries[index].offsetY -// let wantsUpdate = offset < headerHeight || to < headerHeight -// if wantsUpdate, label.id != from.id { -// scrollPositions[index].scrollTo(y: to) -// } -// } -// } -// } -// -//// - ScrollGeometry Extension -// -// private extension ScrollGeometry { -// init() { -// self.init(contentOffset: .zero, contentSize: .zero, contentInsets: .init(.zero), containerSize: .zero) -// } -// -// var offsetY: CGFloat { -// contentOffset.y + contentInsets.top -// } -// -// var offsetX: CGFloat { -// contentOffset.x + contentInsets.leading -// } -// } diff --git a/iosApp/iosApp/UI/Page/ProfileV2/ProfileV2Screen.swift b/iosApp/iosApp/UI/Page/ProfileV2/ProfileV2Screen.swift deleted file mode 100644 index fe70de9ef..000000000 --- a/iosApp/iosApp/UI/Page/ProfileV2/ProfileV2Screen.swift +++ /dev/null @@ -1,197 +0,0 @@ -import SwiftUI - -// -// struct ProfileV2Screen: View { -// let toProfileMedia: (MicroBlogKey) -> Void -// let accountType: AccountType -// let userKey: MicroBlogKey? -// -// @State private var refreshCount: Int = 0 -// -// init(accountType: AccountType, userKey: MicroBlogKey?, toProfileMedia: @escaping (MicroBlogKey) -> Void) { -// self.toProfileMedia = toProfileMedia -// self.accountType = accountType -// self.userKey = userKey -// } -// -// var body: some View { -// NavigationView { -// ProfileV2HeaderPageScrollView(displaysSymbols: false) { -// ProfileV2HeaderView() -// } labels: { -// ProfileV2PageLabel(title: "Posts", symbolImage: "square.grid.3x3") -// ProfileV2PageLabel(title: "Timeline", symbolImage: "list.bullet") -// ProfileV2PageLabel(title: "Media", symbolImage: "photo.stack") -// } pages: { -// ProfileV2PostsTabView() -// ProfileV2TimelineTabView() -// ProfileV2MediaTabView() -// } onRefresh: { index in -// await handleRefresh(for: index) -// } -// } -// } -// -// private func handleRefresh(for index: Int) async { -// print("🔄 [Profile V2] Refresh tab \(index)") -// refreshCount += 1 -// -// try? await Task.sleep(nanoseconds: 1_000_000_000) -// -// let tabNames = ["Posts", "Timeline", "Media"] -// let tabName = index < tabNames.count ? tabNames[index] : "Unknown" -// print("✅ [Profile V2] \(tabName) tab refreshed (count: \(refreshCount))") -// } -// } -// -// struct ProfileV2HeaderView: View { -// var body: some View { -// VStack(spacing: 16) { -// Circle() -// .fill(.blue.gradient) -// .frame(width: 80, height: 80) -// .overlay { -// Text("👤") -// .font(.largeTitle) -// } -// -// VStack(spacing: 8) { -// Text("Profile V2 User") -// .font(.title2) -// .fontWeight(.bold) -// -// Text("@profilev2user") -// .font(.subheadline) -// .foregroundColor(.secondary) -// -// Text("This is Profile V2 test user bio. Testing the new Instagram-style scrolling architecture.") -// .font(.caption) -// .multilineTextAlignment(.center) -// .foregroundColor(.secondary) -// .padding(.horizontal) -// } -// -// HStack(spacing: 24) { -// ProfileV2StatView(title: "Posts", count: "123") -// ProfileV2StatView(title: "Followers", count: "1.2K") -// ProfileV2StatView(title: "Following", count: "456") -// } -// -// HStack(spacing: 12) { -// Button("Follow") { -// print("Follow button tapped") -// } -// .buttonStyle(.borderedProminent) -// -// Button("Message") { -// print("Message button tapped") -// } -// .buttonStyle(.bordered) -// } -// } -// .padding() -// .background( -// RoundedRectangle(cornerRadius: 20) -// .fill(.blue.gradient.opacity(0.1)) -// ) -// .padding(.horizontal) -// } -// } -// -// struct ProfileV2StatView: View { -// let title: String -// let count: String -// -// var body: some View { -// VStack(spacing: 4) { -// Text(count) -// .font(.headline) -// .fontWeight(.bold) -// -// Text(title) -// .font(.caption) -// .foregroundColor(.secondary) -// } -// } -// } -// -// struct ProfileV2PostsTabView: View { -// var body: some View { -// ProfileV2DummyTabView( -// color: .red, -// title: "Posts", -// icon: "square.grid.3x3", -// count: 20 -// ) -// } -// } -// -// struct ProfileV2TimelineTabView: View { -// var body: some View { -// ProfileV2DummyTabView( -// color: .green, -// title: "Timeline", -// icon: "list.bullet", -// count: 15 -// ) -// } -// } -// -// struct ProfileV2MediaTabView: View { -// var body: some View { -// ProfileV2DummyTabView( -// color: .orange, -// title: "Media", -// icon: "photo.stack", -// count: 10 -// ) -// } -// } -// -// struct ProfileV2DummyTabView: View { -// let color: Color -// let title: String -// let icon: String -// let count: Int -// -// var body: some View { -// LazyVStack(spacing: 12) { -// HStack { -// Image(systemName: icon) -// .foregroundColor(color) -// Text("\(title) Content") -// .font(.headline) -// .foregroundColor(color) -// Spacer() -// Text("\(count) items") -// .font(.caption) -// .foregroundColor(.secondary) -// } -// .padding(.horizontal) -// .padding(.top) -// -// // 内容区域(颜色方块) -// ForEach(0 ..< count, id: \.self) { index in -// RoundedRectangle(cornerRadius: 12) -// .fill(color.gradient) -// .frame(height: 60) -// .overlay { -// VStack { -// Text("\(title) Item \(index + 1)") -// .font(.subheadline) -// .fontWeight(.medium) -// .foregroundColor(.white) -// -// Text("Tap to interact") -// .font(.caption) -// .foregroundColor(.white.opacity(0.8)) -// } -// } -// .onTapGesture { -// print("🔘 [Profile V2] Tapped \(title) item \(index + 1)") -// } -// } -// } -// .padding(.horizontal) -// } -// } diff --git a/iosApp/iosApp/UI/Page/Status/StatusDetailScreen.swift b/iosApp/iosApp/UI/Page/Status/StatusDetailScreen.swift index a54e7bef4..729f2bd1a 100644 --- a/iosApp/iosApp/UI/Page/Status/StatusDetailScreen.swift +++ b/iosApp/iosApp/UI/Page/Status/StatusDetailScreen.swift @@ -8,7 +8,7 @@ struct StatusDetailScreen: View { private let statusKey: MicroBlogKey @Environment(FlareRouter.self) private var router - @Environment(FlareAppState.self) private var menuState + @Environment(FlareMenuState.self) private var menuState @Environment(FlareTheme.self) private var theme init(accountType: AccountType, statusKey: MicroBlogKey) { diff --git a/iosApp/iosApp/UI/Page/Status/VVOStatusDetailScreen.swift b/iosApp/iosApp/UI/Page/Status/VVOStatusDetailScreen.swift index f0bd53f25..b824d1ef5 100644 --- a/iosApp/iosApp/UI/Page/Status/VVOStatusDetailScreen.swift +++ b/iosApp/iosApp/UI/Page/Status/VVOStatusDetailScreen.swift @@ -7,8 +7,7 @@ struct VVOStatusDetailScreen: View { @State private var type: DetailStatusType = .comment private let statusKey: MicroBlogKey - // 获取全局的AppState - @Environment(FlareAppState.self) private var menuState + @Environment(FlareMenuState.self) private var menuState init(accountType: AccountType, statusKey: MicroBlogKey) { presenter = .init(accountType: accountType, statusKey: statusKey) diff --git a/iosApp/iosApp/UI/Theme/FlareTheme.swift b/iosApp/iosApp/UI/Theme/FlareTheme.swift index ade28f4d8..5a079202a 100644 --- a/iosApp/iosApp/UI/Theme/FlareTheme.swift +++ b/iosApp/iosApp/UI/Theme/FlareTheme.swift @@ -171,16 +171,16 @@ public final class FlareTheme { let themeStorage = ThemeStorage() - private var _bodyTextStyle: FlareTextStyle.Style? - private var _captionTextStyle: FlareTextStyle.Style? + private var _flareTextBodyTextStyle: FlareTextStyle.Style? + private var _flareTextCaptionTextStyle: FlareTextStyle.Style? - public var bodyTextStyle: FlareTextStyle.Style { - if let cached = _bodyTextStyle { + public var flareTextBodyTextStyle: FlareTextStyle.Style { + if let cached = _flareTextBodyTextStyle { return cached } let style = FlareTextStyle.Style( - font: UIFont.systemFont(ofSize: 17 * fontSizeScale), // 直接计算字体大小,避免循环依赖 + font: UIFont.systemFont(ofSize: 17 * fontSizeScale), textColor: UIColor(labelColor), linkColor: UIColor(tintColor), mentionColor: UIColor(tintColor), @@ -188,17 +188,17 @@ public final class FlareTheme { cashtagColor: UIColor(tintColor) ) - _bodyTextStyle = style + _flareTextBodyTextStyle = style return style } - public var captionTextStyle: FlareTextStyle.Style { - if let cached = _captionTextStyle { + public var flareTextCaptionTextStyle: FlareTextStyle.Style { + if let cached = _flareTextCaptionTextStyle { return cached } let style = FlareTextStyle.Style( - font: UIFont.systemFont(ofSize: 13 * fontSizeScale), // 直接计算字体大小,避免循环依赖 + font: UIFont.systemFont(ofSize: 13 * fontSizeScale), textColor: UIColor(.gray), linkColor: UIColor(tintColor), mentionColor: UIColor(tintColor), @@ -206,7 +206,7 @@ public final class FlareTheme { cashtagColor: UIColor(tintColor) ) - _captionTextStyle = style + _flareTextCaptionTextStyle = style return style } @@ -403,8 +403,8 @@ public final class FlareTheme { computeContrastingTintColor() // 预初始化Markdown样式缓存 - _ = bodyTextStyle - _ = captionTextStyle + _ = flareTextBodyTextStyle + _ = flareTextCaptionTextStyle } public static var allColorSet: [ColorSet] { @@ -470,8 +470,8 @@ public final class FlareTheme { private func notifyThemeChanged() { // 清空样式缓存,确保主题变化时重新计算 - _bodyTextStyle = nil - _captionTextStyle = nil + _flareTextBodyTextStyle = nil + _flareTextCaptionTextStyle = nil DispatchQueue.main.async { NotificationCenter.default.post(name: NSNotification.Name("FlareThemeDidChange"), object: self)