From 178c9759b965a2ee5aefc3cc100776736d497106 Mon Sep 17 00:00:00 2001 From: null Date: Thu, 7 Aug 2025 17:34:30 +0800 Subject: [PATCH 1/8] Optimize code --- .../UI/Navigation/FlareDestinationView.swift | 12 ++++---- .../iosApp/UI/Navigation/FlareRootView.swift | 12 ++++---- iosApp/iosApp/UI/Navigation/FlareRouter.swift | 6 ++-- .../Compose/Text/FlareMarkdownStyle.swift | 8 +----- .../UI/Page/Compose/Text/FlareText.swift | 17 +++++------ .../UI/Page/Compose/Text/FlareTextStyle.swift | 27 ------------------ .../TimelineStatus/StatusContentView.swift | 4 +-- .../StatusQuoteView/QuotedStatus.swift | 2 +- .../Compose/TimelineV2/LinkPreviewV2.swift | 7 ++--- .../Compose/TimelineV2/MediaComponentV2.swift | 17 ++--------- .../TimelineV2/MediaItemComponentV2.swift | 4 +-- .../TimelineV2/PhotoBrowserManagerV2.swift | 8 +++--- .../Compose/TimelineV2/PodcastPreviewV2.swift | 3 +- .../TimelineV2/StatusContentViewV2.swift | 4 +-- .../TimelineV2/StatusHeaderViewV2.swift | 2 +- .../TimelineV2/StatusQuoteViewV2.swift | 4 +-- .../StatusRetweetHeaderComponentV2.swift | 2 +- .../Download/View/DownloadManagerScreen.swift | 2 +- .../Page/Home/View/Home/FlareTabBarV2.swift | 2 +- .../UI/Page/Home/View/Home/FlareTabItem.swift | 4 +-- .../Home/View/Home/HomeTabViewContentV2.swift | 14 +++++----- .../Page/List/SwiftUIView/AllFeedsView.swift | 2 +- .../Page/List/SwiftUIView/AllListsView.swift | 10 ++++--- .../iosApp/UI/Page/Menu/FlareAppState.swift | 10 ------- .../UI/Page/Menu/View/FlareMenuView.swift | 16 +++++------ .../UI/Page/Message/view/MessageScreen.swift | 2 +- .../UIKitView/ProfileTabScreenUikit.swift | 2 +- .../UI/Page/Status/StatusDetailScreen.swift | 2 +- .../Page/Status/VVOStatusDetailScreen.swift | 4 +-- iosApp/iosApp/UI/Theme/FlareTheme.swift | 28 +++++++++---------- 30 files changed, 90 insertions(+), 147 deletions(-) delete mode 100644 iosApp/iosApp/UI/Page/Menu/FlareAppState.swift diff --git a/iosApp/iosApp/UI/Navigation/FlareDestinationView.swift b/iosApp/iosApp/UI/Navigation/FlareDestinationView.swift index a8ca5c3f4..32b186cce 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 @@ -125,23 +125,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 +149,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/TimelineStatus/StatusContentView.swift b/iosApp/iosApp/UI/Page/Compose/Timeline/TimelineStatus/StatusContentView.swift index d140ad5ac..4730b3b1c 100644 --- a/iosApp/iosApp/UI/Page/Compose/Timeline/TimelineStatus/StatusContentView.swift +++ b/iosApp/iosApp/UI/Page/Compose/Timeline/TimelineStatus/StatusContentView.swift @@ -195,7 +195,7 @@ struct StatusContentWarningView: View { FlareText( contentWarning.raw, contentWarning.markdown, - textType: .caption, + textType: .flareTextTypeCaption, isRTL: contentWarning.isRTL ) .onLinkTap { url in @@ -234,7 +234,7 @@ struct StatusMainContentView: 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/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/LinkPreviewV2.swift b/iosApp/iosApp/UI/Page/Compose/TimelineV2/LinkPreviewV2.swift index c7bf8221e..0902c2f97 100644 --- a/iosApp/iosApp/UI/Page/Compose/TimelineV2/LinkPreviewV2.swift +++ b/iosApp/iosApp/UI/Page/Compose/TimelineV2/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/TimelineV2/MediaComponentV2.swift b/iosApp/iosApp/UI/Page/Compose/TimelineV2/MediaComponentV2.swift index 2d0c04b41..92f3c272b 100644 --- a/iosApp/iosApp/UI/Page/Compose/TimelineV2/MediaComponentV2.swift +++ b/iosApp/iosApp/UI/Page/Compose/TimelineV2/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/TimelineV2/MediaItemComponentV2.swift b/iosApp/iosApp/UI/Page/Compose/TimelineV2/MediaItemComponentV2.swift index 346e61a50..7e603074e 100644 --- a/iosApp/iosApp/UI/Page/Compose/TimelineV2/MediaItemComponentV2.swift +++ b/iosApp/iosApp/UI/Page/Compose/TimelineV2/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/TimelineV2/PhotoBrowserManagerV2.swift index 2a68b2ce8..4a811b288 100644 --- a/iosApp/iosApp/UI/Page/Compose/TimelineV2/PhotoBrowserManagerV2.swift +++ b/iosApp/iosApp/UI/Page/Compose/TimelineV2/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/Page/Compose/TimelineV2/PodcastPreviewV2.swift b/iosApp/iosApp/UI/Page/Compose/TimelineV2/PodcastPreviewV2.swift index b88f52e5c..1d1cf736a 100644 --- a/iosApp/iosApp/UI/Page/Compose/TimelineV2/PodcastPreviewV2.swift +++ b/iosApp/iosApp/UI/Page/Compose/TimelineV2/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/TimelineV2/StatusContentViewV2.swift b/iosApp/iosApp/UI/Page/Compose/TimelineV2/StatusContentViewV2.swift index db7e7e08e..9319073fd 100644 --- a/iosApp/iosApp/UI/Page/Compose/TimelineV2/StatusContentViewV2.swift +++ b/iosApp/iosApp/UI/Page/Compose/TimelineV2/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/TimelineV2/StatusHeaderViewV2.swift b/iosApp/iosApp/UI/Page/Compose/TimelineV2/StatusHeaderViewV2.swift index 9a44ac5d8..d16cdba77 100644 --- a/iosApp/iosApp/UI/Page/Compose/TimelineV2/StatusHeaderViewV2.swift +++ b/iosApp/iosApp/UI/Page/Compose/TimelineV2/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/TimelineV2/StatusQuoteViewV2.swift b/iosApp/iosApp/UI/Page/Compose/TimelineV2/StatusQuoteViewV2.swift index f85d3ef3b..54aa64894 100644 --- a/iosApp/iosApp/UI/Page/Compose/TimelineV2/StatusQuoteViewV2.swift +++ b/iosApp/iosApp/UI/Page/Compose/TimelineV2/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/TimelineV2/StatusRetweetHeaderComponentV2.swift index c0a705213..aa2150849 100644 --- a/iosApp/iosApp/UI/Page/Compose/TimelineV2/StatusRetweetHeaderComponentV2.swift +++ b/iosApp/iosApp/UI/Page/Compose/TimelineV2/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/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..7cc16a6e7 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") @@ -95,7 +95,7 @@ struct HomeTabViewContentV2: View { toProfileMedia: { _ in } ) } - .environment(appState) + .environment(menuState) } .customizationID("tabview_profile") } @@ -104,7 +104,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/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..fbfb48c0d 100644 --- a/iosApp/iosApp/UI/Page/List/SwiftUIView/AllListsView.swift +++ b/iosApp/iosApp/UI/Page/List/SwiftUIView/AllListsView.swift @@ -4,13 +4,12 @@ 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 +35,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/FlareAppState.swift deleted file mode 100644 index b0ed68bf4..000000000 --- a/iosApp/iosApp/UI/Page/Menu/FlareAppState.swift +++ /dev/null @@ -1,10 +0,0 @@ -import Combine -import SwiftUI - -@Observable -class FlareAppState { - // hidden home custom tabbar - 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/UIKitView/ProfileTabScreenUikit.swift b/iosApp/iosApp/UI/Page/ProfileNew/UIKitView/ProfileTabScreenUikit.swift index 63ccc98ab..407b4e455 100644 --- a/iosApp/iosApp/UI/Page/ProfileNew/UIKitView/ProfileTabScreenUikit.swift +++ b/iosApp/iosApp/UI/Page/ProfileNew/UIKitView/ProfileTabScreenUikit.swift @@ -23,7 +23,7 @@ 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?, 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..f4b2abe48 100644 --- a/iosApp/iosApp/UI/Page/Status/VVOStatusDetailScreen.swift +++ b/iosApp/iosApp/UI/Page/Status/VVOStatusDetailScreen.swift @@ -7,8 +7,8 @@ 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) From d12864d7281dc665c76480f624a155619684d817 Mon Sep 17 00:00:00 2001 From: null Date: Thu, 7 Aug 2025 17:34:47 +0800 Subject: [PATCH 2/8] Optimize code --- iosApp/iosApp/UI/Page/List/SwiftUIView/AllListsView.swift | 1 - iosApp/iosApp/UI/Page/Status/VVOStatusDetailScreen.swift | 1 - 2 files changed, 2 deletions(-) diff --git a/iosApp/iosApp/UI/Page/List/SwiftUIView/AllListsView.swift b/iosApp/iosApp/UI/Page/List/SwiftUIView/AllListsView.swift index fbfb48c0d..d434ebcd3 100644 --- a/iosApp/iosApp/UI/Page/List/SwiftUIView/AllListsView.swift +++ b/iosApp/iosApp/UI/Page/List/SwiftUIView/AllListsView.swift @@ -4,7 +4,6 @@ import os import shared import SwiftUI - struct AllListsView: View { @State private var presenter: AllListPresenter @Environment(FlareRouter.self) private var router diff --git a/iosApp/iosApp/UI/Page/Status/VVOStatusDetailScreen.swift b/iosApp/iosApp/UI/Page/Status/VVOStatusDetailScreen.swift index f4b2abe48..b824d1ef5 100644 --- a/iosApp/iosApp/UI/Page/Status/VVOStatusDetailScreen.swift +++ b/iosApp/iosApp/UI/Page/Status/VVOStatusDetailScreen.swift @@ -7,7 +7,6 @@ struct VVOStatusDetailScreen: View { @State private var type: DetailStatusType = .comment private let statusKey: MicroBlogKey - @Environment(FlareMenuState.self) private var menuState init(accountType: AccountType, statusKey: MicroBlogKey) { From a4cc40e3d27fef92ac211ceddec8f0b116c6e4dd Mon Sep 17 00:00:00 2001 From: null Date: Fri, 8 Aug 2025 16:12:56 +0800 Subject: [PATCH 3/8] Optimize code --- iosApp/iosApp/UI/Page/Menu/FlareMenuState.swift | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 iosApp/iosApp/UI/Page/Menu/FlareMenuState.swift diff --git a/iosApp/iosApp/UI/Page/Menu/FlareMenuState.swift b/iosApp/iosApp/UI/Page/Menu/FlareMenuState.swift new file mode 100644 index 000000000..50f8b7e01 --- /dev/null +++ b/iosApp/iosApp/UI/Page/Menu/FlareMenuState.swift @@ -0,0 +1,9 @@ +import Combine +import SwiftUI + +@Observable +class FlareMenuState { + var isCustomTabBarHidden: Bool = false + + init() {} +} From a435461a93ee5621f1bcd27fcf0015d3231b27c7 Mon Sep 17 00:00:00 2001 From: null Date: Fri, 8 Aug 2025 16:12:59 +0800 Subject: [PATCH 4/8] Optimize code --- .../ProfileV2HeaderPageScrollView.swift | 267 ------------------ .../UI/Page/ProfileV2/ProfileV2Screen.swift | 197 ------------- 2 files changed, 464 deletions(-) delete mode 100644 iosApp/iosApp/UI/Page/ProfileV2/ProfileV2HeaderPageScrollView.swift delete mode 100644 iosApp/iosApp/UI/Page/ProfileV2/ProfileV2Screen.swift 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) -// } -// } From b69cc33e6df09632dafd7aeb1fa698b7b99e0796 Mon Sep 17 00:00:00 2001 From: null Date: Tue, 12 Aug 2025 18:09:00 +0800 Subject: [PATCH 5/8] Optimize code --- .../UI/Navigation/FlareDestinationView.swift | 6 +- .../Download/Storage/DownloadHelper.swift | 6 +- .../Home/View/Home/HomeTabViewContentV2.swift | 3 +- .../Components/MediaCollectionViewCell.swift | 8 +- .../Components/ProfileHeaderView.swift | 75 ----- .../Components/ProfileNewHeaderView.swift | 56 ++++ .../Components/ProfileTabBarView.swift | 44 --- .../Data/ProfileMediaPresenterWrapper.swift | 3 +- .../Data/ProfilePresenterWrapper.swift | 21 +- .../Data/ProfileTabSettingStore.swift | 6 +- .../ProfileNew/Model/ProfileUserInfo.swift | 25 +- .../SwiftUIView/HeaderPageScrollView.swift | 286 ------------------ .../SwiftUIView/ProfileContentView.swift | 80 ----- .../SwiftUIView/ProfileTabScreen.swift | 213 ------------- .../SwiftUIView/ProfileTabScreen.swift.bak | 138 --------- .../ProfileWithUserNameScreen.swift | 7 +- .../ProfileRefreshViewController.swift | 10 +- .../UIKitView/ProfileTabScreenUikit.swift | 23 +- 18 files changed, 94 insertions(+), 916 deletions(-) delete mode 100644 iosApp/iosApp/UI/Page/ProfileNew/Components/ProfileHeaderView.swift delete mode 100644 iosApp/iosApp/UI/Page/ProfileNew/Components/ProfileTabBarView.swift delete mode 100644 iosApp/iosApp/UI/Page/ProfileNew/SwiftUIView/HeaderPageScrollView.swift delete mode 100644 iosApp/iosApp/UI/Page/ProfileNew/SwiftUIView/ProfileContentView.swift delete mode 100644 iosApp/iosApp/UI/Page/ProfileNew/SwiftUIView/ProfileTabScreen.swift delete mode 100644 iosApp/iosApp/UI/Page/ProfileNew/SwiftUIView/ProfileTabScreen.swift.bak diff --git a/iosApp/iosApp/UI/Navigation/FlareDestinationView.swift b/iosApp/iosApp/UI/Navigation/FlareDestinationView.swift index 32b186cce..0bbfbbff0 100644 --- a/iosApp/iosApp/UI/Navigation/FlareDestinationView.swift +++ b/iosApp/iosApp/UI/Navigation/FlareDestinationView.swift @@ -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) 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/Home/View/Home/HomeTabViewContentV2.swift b/iosApp/iosApp/UI/Page/Home/View/Home/HomeTabViewContentV2.swift index 7cc16a6e7..05e9f1131 100644 --- a/iosApp/iosApp/UI/Page/Home/View/Home/HomeTabViewContentV2.swift +++ b/iosApp/iosApp/UI/Page/Home/View/Home/HomeTabViewContentV2.swift @@ -91,8 +91,7 @@ struct HomeTabViewContentV2: View { FlareTabItem(tabType: .profile) { ProfileTabScreenUikit( accountType: accountType, - userKey: nil, - toProfileMedia: { _ in } + userKey: nil ) } .environment(menuState) diff --git a/iosApp/iosApp/UI/Page/ProfileNew/Components/MediaCollectionViewCell.swift b/iosApp/iosApp/UI/Page/ProfileNew/Components/MediaCollectionViewCell.swift index 5ac4d8c38..b158fde91 100644 --- a/iosApp/iosApp/UI/Page/ProfileNew/Components/MediaCollectionViewCell.swift +++ b/iosApp/iosApp/UI/Page/ProfileNew/Components/MediaCollectionViewCell.swift @@ -22,22 +22,22 @@ 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/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/ProfileNewHeaderView.swift b/iosApp/iosApp/UI/Page/ProfileNew/Components/ProfileNewHeaderView.swift index a1d97b269..55164debd 100644 --- a/iosApp/iosApp/UI/Page/ProfileNew/Components/ProfileNewHeaderView.swift +++ b/iosApp/iosApp/UI/Page/ProfileNew/Components/ProfileNewHeaderView.swift @@ -585,3 +585,59 @@ 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/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/ProfileMediaPresenterWrapper.swift b/iosApp/iosApp/UI/Page/ProfileNew/Data/ProfileMediaPresenterWrapper.swift index c40c4effb..8ec302a67 100644 --- a/iosApp/iosApp/UI/Page/ProfileNew/Data/ProfileMediaPresenterWrapper.swift +++ b/iosApp/iosApp/UI/Page/ProfileNew/Data/ProfileMediaPresenterWrapper.swift @@ -3,8 +3,7 @@ import os.log import shared import SwiftUI -class ProfileMediaPresenterWrapper: ObservableObject { - // - Properties +class ProfileMediaPresenterWrapper: ObservableObject { let presenter: ProfileMediaPresenter // - Init diff --git a/iosApp/iosApp/UI/Page/ProfileNew/Data/ProfilePresenterWrapper.swift b/iosApp/iosApp/UI/Page/ProfileNew/Data/ProfilePresenterWrapper.swift index 25a45e5b6..f298c7a94 100644 --- a/iosApp/iosApp/UI/Page/ProfileNew/Data/ProfilePresenterWrapper.swift +++ b/iosApp/iosApp/UI/Page/ProfileNew/Data/ProfilePresenterWrapper.swift @@ -4,34 +4,25 @@ 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/Data/ProfileTabSettingStore.swift b/iosApp/iosApp/UI/Page/ProfileNew/Data/ProfileTabSettingStore.swift index 49153ac1d..5495e4a62 100644 --- a/iosApp/iosApp/UI/Page/ProfileNew/Data/ProfileTabSettingStore.swift +++ b/iosApp/iosApp/UI/Page/ProfileNew/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/ProfileNew/Model/ProfileUserInfo.swift index e37781f04..d867c17a6 100644 --- a/iosApp/iosApp/UI/Page/ProfileNew/Model/ProfileUserInfo.swift +++ b/iosApp/iosApp/UI/Page/ProfileNew/Model/ProfileUserInfo.swift @@ -1,7 +1,7 @@ import Foundation import shared -// 整合所有用户资料页需要的信息 + struct ProfileUserInfo: Equatable { let profile: UiProfile let relation: UiRelation? @@ -11,16 +11,16 @@ 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,28 +28,24 @@ struct ProfileUserInfo: Equatable { return nil }() - // 获取是否是当前用户 - let isMe: Bool = { + let isMe: Bool = { if case let .success(me) = onEnum(of: state.isMe) { return me.data as! Bool } return false }() - // 获取是否可以发送消息 - let canSendMessage: Bool = { + let canSendMessage: Bool = { if case let .success(can) = onEnum(of: state.canSendMessage) { return can.data as! Bool } return false }() - // 获取关注和粉丝数 - let followCount = profile.matrices.followsCountHumanized + let followCount = profile.matrices.followsCountHumanized let fansCount = profile.matrices.fansCountHumanized - // 获取用户字段信息 - let fields: [String: UiRichText] = { + let fields: [String: UiRichText] = { if let bottomContent = profile.bottomContent, let fieldsContent = bottomContent as? UiProfileBottomContentFields { @@ -68,11 +64,10 @@ struct ProfileUserInfo: Equatable { canSendMessage: canSendMessage ) } - - // - 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/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/ProfileNew/SwiftUIView/ProfileWithUserNameScreen.swift b/iosApp/iosApp/UI/Page/ProfileNew/SwiftUIView/ProfileWithUserNameScreen.swift index 0790e94cc..e929f4262 100644 --- a/iosApp/iosApp/UI/Page/ProfileNew/SwiftUIView/ProfileWithUserNameScreen.swift +++ b/iosApp/iosApp/UI/Page/ProfileNew/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/ProfileRefreshViewController.swift b/iosApp/iosApp/UI/Page/ProfileNew/UIKitView/ProfileRefreshViewController.swift index 73102251d..60329ce03 100644 --- a/iosApp/iosApp/UI/Page/ProfileNew/UIKitView/ProfileRefreshViewController.swift +++ b/iosApp/iosApp/UI/Page/ProfileNew/UIKitView/ProfileRefreshViewController.swift @@ -17,10 +17,8 @@ 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? @@ -57,7 +55,6 @@ class ProfileNewRefreshViewController: UIViewController { isShowAppBar: Binding, horizontalSizeClass: UserInterfaceSizeClass?, appSettings: AppSettings, - toProfileMedia: @escaping (MicroBlogKey) -> Void, accountType: AccountType, userKey: MicroBlogKey?, tabStore: ProfileTabSettingStore, @@ -70,7 +67,6 @@ class ProfileNewRefreshViewController: UIViewController { self.isShowAppBar = isShowAppBar self.horizontalSizeClass = horizontalSizeClass self.appSettings = appSettings - self.toProfileMedia = toProfileMedia self.accountType = accountType self.userKey = userKey self.tabStore = tabStore @@ -418,8 +414,7 @@ class ProfileNewRefreshViewController: UIViewController { // 离开页面时重置状态,不然 详情页会导致没appbar isShowAppBar?.wrappedValue = true - isShowsegmentedBackButton?.wrappedValue = false - + // 确保系统导航栏状态正确 navigationController?.setNavigationBarHidden(false, animated: animated) } @@ -454,8 +449,7 @@ class ProfileNewRefreshViewController: UIViewController { // 在返回前重置导航状态 // 离开页面时重置状态,不然 详情页会导致没appbar isShowAppBar?.wrappedValue = true - isShowsegmentedBackButton?.wrappedValue = false - + // 确保导航栏可见 navigationController?.setNavigationBarHidden(false, animated: true) diff --git a/iosApp/iosApp/UI/Page/ProfileNew/UIKitView/ProfileTabScreenUikit.swift b/iosApp/iosApp/UI/Page/ProfileNew/UIKitView/ProfileTabScreenUikit.swift index 407b4e455..bc10f6260 100644 --- a/iosApp/iosApp/UI/Page/ProfileNew/UIKitView/ProfileTabScreenUikit.swift +++ b/iosApp/iosApp/UI/Page/ProfileNew/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 @@ -26,10 +25,8 @@ struct ProfileTabScreenUikit: View { @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 @@ -63,7 +60,7 @@ struct ProfileTabScreenUikit: View { ) if userKey == nil { - // + ProfileNewRefreshViewControllerWrapper( userInfo: userInfo, state: state as! ProfileNewState, @@ -72,13 +69,9 @@ 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, @@ -96,13 +89,9 @@ 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, @@ -120,10 +109,8 @@ 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 @@ -139,7 +126,6 @@ struct ProfileNewRefreshViewControllerWrapper: UIViewControllerRepresentable { isShowAppBar: $isShowAppBar, horizontalSizeClass: horizontalSizeClass, appSettings: appSettings, - toProfileMedia: toProfileMedia, accountType: accountType, userKey: userKey, tabStore: tabStore, @@ -160,7 +146,6 @@ struct ProfileNewRefreshViewControllerWrapper: UIViewControllerRepresentable { isShowAppBar: $isShowAppBar, horizontalSizeClass: horizontalSizeClass, appSettings: appSettings, - toProfileMedia: toProfileMedia, accountType: accountType, userKey: userKey, tabStore: tabStore, From 0952a325f019632a6ff651c2495b908c7a84e1ab Mon Sep 17 00:00:00 2001 From: null Date: Tue, 12 Aug 2025 18:15:24 +0800 Subject: [PATCH 6/8] Optimize code --- .../Common/ProfileListViewController.swift | 103 --- .../Components/MediaCollectionViewCell.swift | 52 -- .../Components/ProfileNewHeaderView.swift | 643 ---------------- .../Data/ProfileMediaPresenterWrapper.swift | 14 - .../Data/ProfilePresenterService.swift | 110 --- .../Data/ProfilePresenterWrapper.swift | 28 - .../Data/ProfileTabSettingStore.swift | 174 ----- .../ProfileNew/Model/ProfileUserInfo.swift | 77 -- .../SwiftUIView/CommonProfileHeader.swift | 324 -------- .../SwiftUIView/ProfileMediaListScreen.swift | 443 ----------- .../ProfileWithUserNameScreen.swift | 71 -- .../ProfileMediaViewController.swift | 226 ------ .../ProfileRefreshViewController.swift | 711 ------------------ .../ProfileStretchRefreshControl.swift | 55 -- .../UIKitView/ProfileTabScreenUikit.swift | 166 ---- 15 files changed, 3197 deletions(-) delete mode 100644 iosApp/iosApp/UI/Page/ProfileNew/Common/ProfileListViewController.swift delete mode 100644 iosApp/iosApp/UI/Page/ProfileNew/Components/MediaCollectionViewCell.swift delete mode 100644 iosApp/iosApp/UI/Page/ProfileNew/Components/ProfileNewHeaderView.swift delete mode 100644 iosApp/iosApp/UI/Page/ProfileNew/Data/ProfileMediaPresenterWrapper.swift delete mode 100644 iosApp/iosApp/UI/Page/ProfileNew/Data/ProfilePresenterService.swift delete mode 100644 iosApp/iosApp/UI/Page/ProfileNew/Data/ProfilePresenterWrapper.swift delete mode 100644 iosApp/iosApp/UI/Page/ProfileNew/Data/ProfileTabSettingStore.swift delete mode 100644 iosApp/iosApp/UI/Page/ProfileNew/Model/ProfileUserInfo.swift delete mode 100644 iosApp/iosApp/UI/Page/ProfileNew/SwiftUIView/CommonProfileHeader.swift delete mode 100644 iosApp/iosApp/UI/Page/ProfileNew/SwiftUIView/ProfileMediaListScreen.swift delete mode 100644 iosApp/iosApp/UI/Page/ProfileNew/SwiftUIView/ProfileWithUserNameScreen.swift delete mode 100644 iosApp/iosApp/UI/Page/ProfileNew/UIKitView/ProfileMediaViewController.swift delete mode 100644 iosApp/iosApp/UI/Page/ProfileNew/UIKitView/ProfileRefreshViewController.swift delete mode 100644 iosApp/iosApp/UI/Page/ProfileNew/UIKitView/ProfileStretchRefreshControl.swift delete mode 100644 iosApp/iosApp/UI/Page/ProfileNew/UIKitView/ProfileTabScreenUikit.swift diff --git a/iosApp/iosApp/UI/Page/ProfileNew/Common/ProfileListViewController.swift b/iosApp/iosApp/UI/Page/ProfileNew/Common/ProfileListViewController.swift deleted file mode 100644 index 08a2dbd26..000000000 --- a/iosApp/iosApp/UI/Page/ProfileNew/Common/ProfileListViewController.swift +++ /dev/null @@ -1,103 +0,0 @@ -import JXPagingView -import MJRefresh -import UIKit - -class ProfileNewListViewController: UIViewController { - lazy var tableView: UITableView = .init(frame: CGRect.zero, style: .plain) - var dataSource: [String] = .init() - var isNeedHeader = false - var isNeedFooter = false - var isHeaderRefreshed = false - var listViewDidScrollCallback: ((UIScrollView) -> Void)? - - override func viewDidLoad() { - super.viewDidLoad() - - // tableView.backgroundColor = .systemBackground - tableView.tableFooterView = UIView() - tableView.dataSource = self - tableView.delegate = self - tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell") - view.addSubview(tableView) - - if isNeedHeader { - tableView.mj_header = MJRefreshNormalHeader(refreshingTarget: self, refreshingAction: #selector(headerRefresh)) - } - if isNeedFooter { - tableView.mj_footer = MJRefreshAutoNormalFooter(refreshingTarget: self, refreshingAction: #selector(loadMore)) - if #available(iOS 11.0, *) { - tableView.contentInsetAdjustmentBehavior = .never - } - } else { - // 列表的contentInsetAdjustmentBehavior失效,需要自己设置底部inset - tableView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) - } - beginFirstRefresh() - } - - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - tableView.frame = view.bounds - } - - func beginFirstRefresh() { - if !isHeaderRefreshed { - if isNeedHeader { - tableView.mj_header?.beginRefreshing() - } else { - isHeaderRefreshed = true - tableView.reloadData() - } - } - } - - @objc func headerRefresh() { - DispatchQueue.main.asyncAfter(deadline: .now() + 2) { - self.tableView.mj_header?.endRefreshing() - self.isHeaderRefreshed = true - self.tableView.reloadData() - } - } - - @objc func loadMore() { - DispatchQueue.main.asyncAfter(deadline: .now() + 2) { - self.dataSource.append("load more success") - self.tableView.reloadData() - self.tableView.mj_footer?.endRefreshing() - } - } -} - -// - UITableViewDataSource, UITableViewDelegate - -extension ProfileNewListViewController: UITableViewDataSource, UITableViewDelegate { - func tableView(_: UITableView, numberOfRowsInSection _: Int) -> Int { - isHeaderRefreshed ? dataSource.count : 0 - } - - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) - cell.textLabel?.text = dataSource[indexPath.row] - return cell - } - - func tableView(_: UITableView, heightForRowAt _: IndexPath) -> CGFloat { - 50 - } - - func scrollViewDidScroll(_ scrollView: UIScrollView) { - listViewDidScrollCallback?(scrollView) - } -} - -// - JXPagingViewListViewDelegate - -extension ProfileNewListViewController: JXPagingViewListViewDelegate { - func listView() -> UIView { view } - - func listScrollView() -> UIScrollView { tableView } - - func listViewDidScrollCallback(callback: @escaping (UIScrollView) -> Void) { - listViewDidScrollCallback = callback - } -} diff --git a/iosApp/iosApp/UI/Page/ProfileNew/Components/MediaCollectionViewCell.swift b/iosApp/iosApp/UI/Page/ProfileNew/Components/MediaCollectionViewCell.swift deleted file mode 100644 index b158fde91..000000000 --- a/iosApp/iosApp/UI/Page/ProfileNew/Components/MediaCollectionViewCell.swift +++ /dev/null @@ -1,52 +0,0 @@ -import shared -import SwiftUI -import UIKit - -class MediaCollectionViewCell: UICollectionViewCell { - private var hostingController: UIHostingController? - - override init(frame: CGRect) { - super.init(frame: frame) - backgroundColor = .clear - } - - @available(*, unavailable) - required init?(coder _: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func prepareForReuse() { - super.prepareForReuse() - hostingController?.view.removeFromSuperview() - hostingController = nil - } - - func configure(with media: UiMedia, appSettings: AppSettings, onTap: @escaping () -> Void) { - - let mediaView = ProfileMediaItemView( - media: media, - appSetting: appSettings, - onTap: onTap - ) - - - hostingController?.view.removeFromSuperview() - hostingController = nil - - - let controller = UIHostingController(rootView: mediaView) - hostingController = controller - - - controller.view.backgroundColor = .clear - contentView.addSubview(controller.view) - controller.view.translatesAutoresizingMaskIntoConstraints = false - - NSLayoutConstraint.activate([ - controller.view.topAnchor.constraint(equalTo: contentView.topAnchor), - controller.view.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), - controller.view.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), - controller.view.bottomAnchor.constraint(equalTo: contentView.bottomAnchor) - ]) - } -} diff --git a/iosApp/iosApp/UI/Page/ProfileNew/Components/ProfileNewHeaderView.swift b/iosApp/iosApp/UI/Page/ProfileNew/Components/ProfileNewHeaderView.swift deleted file mode 100644 index 55164debd..000000000 --- a/iosApp/iosApp/UI/Page/ProfileNew/Components/ProfileNewHeaderView.swift +++ /dev/null @@ -1,643 +0,0 @@ -// -// ProfileNewHeaderView.swift -// iosApp -// -// Created by abujj on 1/14/25. -// Copyright © 2025 orgName. All rights reserved. -// -import Generated -import JXSegmentedView -import Kingfisher -import MarkdownUI -import MJRefresh -import os.log -import shared -import SwiftUI -import UIKit - -// 头部视图 -class ProfileNewHeaderView: UIView { - private var state: ProfileNewState? - var theme: FlareTheme? - - // 添加关注按钮回调 - var onFollowClick: ((UiRelation) -> Void)? - - // 防重复设置事件的标志 - private var hasSetupEvents = false - - private let bannerImageView: UIImageView = { - let imageView = UIImageView() - imageView.contentMode = .scaleAspectFill - imageView.clipsToBounds = true - return imageView - }() - - private let blurEffectView: UIVisualEffectView = { - let blurEffect = UIBlurEffect(style: .light) - let view = UIVisualEffectView(effect: blurEffect) - view.alpha = 0 // 初始时不模糊 - return view - }() - - private let avatarView: UIImageView = { - let imageView = UIImageView() - // imageView.backgroundColor = .gray.withAlphaComponent(0.3) - imageView.layer.cornerRadius = 40 - imageView.clipsToBounds = true - imageView.contentMode = .scaleAspectFill - return imageView - }() - - private let followButton: UIButton = { - let button = UIButton(type: .system) - button.setTitle("follow", for: .normal) - button.setTitleColor(.white, for: .normal) - // button.backgroundColor = .systemBlue - button.layer.cornerRadius = 15 - return button - }() - - private let nameLabel: UILabel = { - let label = UILabel() - label.font = .boldSystemFont(ofSize: 20) - return label - }() - - private let handleLabel: UILabel = { - let label = UILabel() - label.textColor = .gray - label.font = .systemFont(ofSize: 15) - return label - }() - - private let descriptionLabel: UILabel = { - let label = UILabel() - label.numberOfLines = 0 - label.font = .systemFont(ofSize: 15) - return label - }() - - private let followsCountLabel: UILabel = { - let label = UILabel() - label.font = .systemFont(ofSize: 14) - label.textColor = .gray - return label - }() - - private let fansCountLabel: UILabel = { - let label = UILabel() - label.font = .systemFont(ofSize: 14) - label.textColor = .gray - return label - }() - - private let markStackView: UIStackView = { - let stackView = UIStackView() - stackView.axis = .horizontal - stackView.spacing = 4 - stackView.alignment = .center - return stackView - }() - - var onFollowsCountTap: (() -> Void)? - var onFansCountTap: (() -> Void)? - var onAvatarTap: (() -> Void)? - var onBannerTap: (() -> Void)? - - // 添加主题观察者 - private var themeObserver: NSObjectProtocol? - - // 添加 userInfo 属性 - private var userInfo: ProfileUserInfo? - - override init(frame: CGRect) { - super.init(frame: frame) - setupUI() - setupThemeObserver() - } - - @available(*, unavailable) - required init?(coder _: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - deinit { - // 移除主题观察者 - if let themeObserver { - NotificationCenter.default.removeObserver(themeObserver) - } - } - - private func setupUI() { - // backgroundColor = .systemBackground - - // Banner with tap gesture - addSubview(bannerImageView) - bannerImageView.frame = CGRect(x: 0, y: 0, width: frame.width, height: 150) - let bannerTap = UITapGestureRecognizer(target: self, action: #selector(bannerTapped)) - bannerImageView.addGestureRecognizer(bannerTap) - bannerImageView.isUserInteractionEnabled = true - - // Blur effect - addSubview(blurEffectView) - blurEffectView.frame = bannerImageView.frame - - // Avatar with tap gesture - addSubview(avatarView) - avatarView.frame = CGRect(x: 16, y: 110, width: 80, height: 80) - let avatarTap = UITapGestureRecognizer(target: self, action: #selector(avatarTapped)) - avatarView.addGestureRecognizer(avatarTap) - avatarView.isUserInteractionEnabled = true - - // Follow Button - addSubview(followButton) - followButton.frame = CGRect(x: frame.width - 100, y: 160, width: 80, height: 30) - - // Name Label - addSubview(nameLabel) - nameLabel.frame = CGRect(x: 16, y: avatarView.frame.maxY + 10, width: frame.width - 32, height: 24) - - // Handle Label and Mark Stack - addSubview(handleLabel) - handleLabel.frame = CGRect(x: 16, y: nameLabel.frame.maxY + 4, width: frame.width - 32, height: 20) - - addSubview(markStackView) - markStackView.frame = CGRect(x: handleLabel.frame.maxX + 4, y: nameLabel.frame.maxY + 4, width: 100, height: 20) - - // Follows/Fans Count with tap gesture - addSubview(followsCountLabel) - followsCountLabel.frame = CGRect(x: 16, y: handleLabel.frame.maxY + 6, width: 100, height: 20) - let followsTap = UITapGestureRecognizer(target: self, action: #selector(followsCountTapped)) - followsCountLabel.addGestureRecognizer(followsTap) - followsCountLabel.isUserInteractionEnabled = true - - addSubview(fansCountLabel) - fansCountLabel.frame = CGRect(x: 120, y: handleLabel.frame.maxY + 6, width: 100, height: 20) - let fansTap = UITapGestureRecognizer(target: self, action: #selector(fansCountTapped)) - fansCountLabel.addGestureRecognizer(fansTap) - fansCountLabel.isUserInteractionEnabled = true - - // Description Label - addSubview(descriptionLabel) - - descriptionLabel.frame = CGRect(x: 16, y: followsCountLabel.frame.maxY + 10, width: frame.width - 32, height: 0) - } - - // 设置主题观察者 - private func setupThemeObserver() { - // 移除旧的观察者(如果存在) - if let existingObserver = themeObserver { - NotificationCenter.default.removeObserver(existingObserver) - } - - // 添加新的观察者 - themeObserver = NotificationCenter.default.addObserver( - forName: NSNotification.Name("FlareThemeDidChange"), - object: nil, - queue: .main - ) { [weak self] _ in - self?.applyTheme() - } - - // 立即应用当前主题 - applyTheme() - } - - // 应用主题方法 - func applyTheme() { - guard let theme else { return } - - // 应用背景色 - backgroundColor = UIColor(theme.primaryBackgroundColor) - - // 可以在这里应用其他与主题相关的样式 - nameLabel.textColor = UIColor(theme.labelColor) - descriptionLabel.textColor = UIColor(theme.labelColor) - descriptionLabel.backgroundColor = UIColor(theme.primaryBackgroundColor) // 貌似没用 - // 应用按钮颜色 - followButton.backgroundColor = UIColor(theme.tintColor) - } - - private func layoutContent() { - // 计算description的高度 - let descriptionWidth = frame.width - 32 - let descriptionSize = descriptionLabel.sizeThatFits(CGSize(width: descriptionWidth, height: .greatestFiniteMagnitude)) - - // 更新description的frame - descriptionLabel.frame = CGRect(x: 16, y: 280, width: descriptionWidth, height: descriptionSize.height) - - // 获取最后一个子视图的底部位置 - var maxY: CGFloat = 0 - for subview in subviews { - let subviewBottom = subview.frame.maxY - if subviewBottom > maxY { - maxY = subviewBottom - } - } - - // 更新整体高度,添加底部padding - frame.size.height = maxY + 16 // 16是底部padding - } - - // 更新Banner拉伸效果 - func updateBannerStretch(withOffset offset: CGFloat) { - let normalHeight: CGFloat = 150 - let stretchedHeight = normalHeight + max(0, offset) - - // 更新Banner图片frame - bannerImageView.frame = CGRect(x: 0, y: min(0, -offset), width: frame.width, height: stretchedHeight) - blurEffectView.frame = bannerImageView.frame - - // 根据拉伸程度设置模糊效果 - let blurAlpha = min(offset / 100, 0.3) // 最大模糊度0.3 - blurEffectView.alpha = blurAlpha - } - - func getContentHeight() -> CGFloat { - frame.height - } - - func configure(with userInfo: ProfileUserInfo, state: ProfileNewState? = nil, theme: FlareTheme? = nil) { - self.userInfo = userInfo // 需要保存 userInfo 以便在点击时使用 - self.state = state - self.theme = theme - - // 应用主题 - if theme != nil { - applyTheme() - } - - // 设置用户名 - nameLabel.text = userInfo.profile.name.markdown - - // 设置用户handle - handleLabel.text = "\(userInfo.profile.handleWithoutFirstAt)" - - if let url = URL(string: userInfo.profile.avatar) { - avatarView.kf.setImage( - with: url, - options: FlareImageOptions.timelineAvatar(size: CGSize(width: 160, height: 160)) - ) - } - - // 设置banner - 使用 Kingfisher 缓存 - if let url = URL(string: userInfo.profile.banner ?? ""), - !(userInfo.profile.banner ?? "").isEmpty, - (userInfo.profile.banner ?? "").range(of: "^https?://.*example\\.com.*$", options: .regularExpression) == nil - { - bannerImageView.kf.setImage( - with: url, - options: FlareImageOptions.banner(size: CGSize(width: UIScreen.main.bounds.width * 2, height: 300)) - ) { result in - switch result { - case let .success(imageResult): - // 检查图片是否有效 - if imageResult.image.size.width > 10, imageResult.image.size.height > 10 { - // 图片有效,保持现状 - } else { - // 如果图片无效,使用头像作为背景 - self.setupDynamicBannerBackground(avatarUrl: userInfo.profile.avatar) - } - case .failure: - // 加载失败,使用头像作为背景 - self.setupDynamicBannerBackground(avatarUrl: userInfo.profile.avatar) - } - } - } else { - // 如果没有banner,使用头像作为背景 - setupDynamicBannerBackground(avatarUrl: userInfo.profile.avatar) - } - - // 设置关注/粉丝数 - followsCountLabel.text = "\(formatCount(Int64(userInfo.followCount) ?? 0)) \(NSLocalizedString("following_title", comment: ""))" - fansCountLabel.text = "\(formatCount(Int64(userInfo.fansCount) ?? 0)) \(NSLocalizedString("fans_title", comment: ""))" - - // 更新关注按钮状态 - updateFollowButton(with: userInfo) - - // 设置用户标记 - markStackView.arrangedSubviews.forEach { $0.removeFromSuperview() } - for mark in userInfo.profile.mark { - let imageView = UIImageView() - imageView.tintColor = .gray - imageView.alpha = 0.6 - - switch mark { - case .cat: - imageView.image = UIImage(systemName: "cat") - case .verified: - imageView.image = UIImage(systemName: "checkmark.circle.fill") - case .locked: - imageView.image = UIImage(systemName: "lock.fill") - case .bot: - imageView.image = UIImage(systemName: "cpu") - default: - continue - } - - imageView.frame = CGRect(x: 0, y: 0, width: 16, height: 16) - markStackView.addArrangedSubview(imageView) - } - - // 开始流式布局,从关注/粉丝数下方开始 - var currentY = followsCountLabel.frame.maxY + 10 - - // 设置描述文本 - if let description = userInfo.profile.description_?.markdown, !description.isEmpty { - let descriptionView = UIHostingController( - rootView: Markdown(description) - .markdownInlineImageProvider(.emoji) - ) - if let theme { - descriptionView.view.backgroundColor = UIColor(theme.primaryBackgroundColor) - } - addSubview(descriptionView.view) - descriptionView.view.frame = CGRect(x: 16, y: currentY, width: frame.width - 32, height: 0) - descriptionView.view.sizeToFit() - currentY = descriptionView.view.frame.maxY + 16 - } - - if let bottomContent = userInfo.profile.bottomContent { - switch onEnum(of: bottomContent) { - case let .fields(data): - // 设置个人的附加资料 - let fieldsView = UserInfoFieldsView(fields: data.fields) - let hostingController = UIHostingController(rootView: fieldsView) - hostingController.view.frame = CGRect(x: 16, y: currentY, width: frame.width - 32, height: 0) - addSubview(hostingController.view) - hostingController.view.sizeToFit() - currentY = hostingController.view.frame.maxY + 16 - - case let .iconify(data): - let stackView = UIStackView() - stackView.axis = .horizontal - stackView.spacing = 8 - stackView.alignment = .leading - stackView.distribution = .fill - - // 创建一个容器视图来包含所有内容 - let containerView = UIView() - if let theme { - stackView.backgroundColor = UIColor(theme.primaryBackgroundColor) - } - containerView.addSubview(stackView) - stackView.translatesAutoresizingMaskIntoConstraints = false - - // 设置 stackView 的约束 - NSLayoutConstraint.activate([ - stackView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor), - stackView.topAnchor.constraint(equalTo: containerView.topAnchor), - stackView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor), - stackView.trailingAnchor.constraint(lessThanOrEqualTo: containerView.trailingAnchor) - ]) - - // 添加 location - if let locationValue = data.items[.location] { - let locationView = createIconWithLabel( - icon: Asset.Image.Attributes.location.image, - text: locationValue.markdown - ) - if let theme { - locationView.backgroundColor = UIColor(theme.primaryBackgroundColor) - } - stackView.addArrangedSubview(locationView) - } - - // 添加 url - if let urlValue = data.items[.url] { - let urlView = createIconWithLabel( - icon: Asset.Image.Attributes.globe.image, - text: urlValue.markdown - ) - if let theme { - urlView.backgroundColor = UIColor(theme.primaryBackgroundColor) - } - stackView.addArrangedSubview(urlView) - } - - containerView.frame = CGRect(x: 16, y: currentY, width: frame.width - 32, height: 20) - addSubview(containerView) - currentY = containerView.frame.maxY + 16 - } - } - - // 更新视图总高度 - frame.size.height = currentY - - // 设置事件处理 - setupEventHandlers() - } - - private func setupDynamicBannerBackground(avatarUrl: String?) { - guard let avatarUrl, let url = URL(string: avatarUrl) else { return } - - bannerImageView.kf.setImage( - with: url, - options: FlareImageOptions.banner(size: CGSize(width: UIScreen.main.bounds.width * 2, height: 300)) - ) { [weak self] _ in - self?.blurEffectView.alpha = 0.7 // 增加模糊效果 - } - } - - private func updateFollowButton(with userInfo: ProfileUserInfo) { - // 根据用户关系更新关注按钮状态 - if userInfo.isMe { - followButton.isHidden = true - } else { - followButton.isHidden = false - if let relation = userInfo.relation { - let title = if relation.blocking { - NSLocalizedString("profile_header_button_blocked", comment: "") - } else if relation.following { - NSLocalizedString("profile_header_button_following", comment: "") - } else if relation.hasPendingFollowRequestFromYou { - NSLocalizedString("profile_header_button_requested", comment: "") - } else { - NSLocalizedString("profile_header_button_follow", comment: "") - } - followButton.setTitle(title, for: .normal) - - // 保持蓝色背景 - // followButton.backgroundColor = .systemBlue - } - } - } - - private func setupEventHandlers() { - // 防止重复设置事件 - 检查是否已经有target - let existingTargets = followButton.allTargets - if !existingTargets.isEmpty { - os_log("[ProfileNewHeaderView] Button events already setup, skipping", log: .default, type: .debug) - return - } - - // 确保按钮可以响应事件 - followButton.isEnabled = true - followButton.isUserInteractionEnabled = true - - // 添加按钮事件 - followButton.addTarget(self, action: #selector(handleFollowButtonTap), for: .touchUpInside) - - os_log("[ProfileNewHeaderView] Button events setup completed", log: .default, type: .debug) - } - - @objc private func avatarTapped() { - onAvatarTap?() - } - - @objc private func bannerTapped() { - onBannerTap?() - } - - @objc private func followsCountTapped() { - onFollowsCountTap?() - } - - @objc private func fansCountTapped() { - onFansCountTap?() - } - - @objc private func handleFollowButtonTap() { - os_log("[ProfileNewHeaderView] Follow button tapped", log: .default, type: .debug) - - // 直接调用回调,传递 relation - if let relation = userInfo?.relation { - onFollowClick?(relation) - } - } - - // 辅助方法:查找当前视图所在的 ViewController - private func findViewController() -> UIViewController? { - var responder: UIResponder? = self - while let nextResponder = responder?.next { - if let viewController = nextResponder as? UIViewController { - return viewController - } - responder = nextResponder - } - return nil - } - - // Helper function to create icon with label - private func createIconWithLabel(icon: UIImage, text: String) -> UIView { - let hostingController = UIHostingController( - rootView: Label( - title: { - Markdown(text) - .font(.caption2) - .markdownInlineImageProvider(.emoji) - .lineLimit(1) - }, - icon: { - Image(uiImage: icon.withRenderingMode(.alwaysTemplate)) - .imageScale(.small) - } - ) - .labelStyle(CompactLabelStyle()) - .padding(.horizontal, 8) - .padding(.vertical, 4) - .cornerRadius(6) - .onLongPressGesture { - // 复制文本到剪贴板 - UIPasteboard.general.string = text - - // 显示复制成功提示 - let generator = UINotificationFeedbackGenerator() - generator.notificationOccurred(.success) - - // 显示提示消息 - if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, - let window = windowScene.windows.first - { - let toast = UILabel() - toast.text = "copy to clipboard" - // toast.backgroundColor = UIColor.black.withAlphaComponent(0.7) - toast.textColor = .white - toast.textAlignment = .center - toast.font = UIFont.systemFont(ofSize: 14) - toast.layer.cornerRadius = 10 - toast.clipsToBounds = true - toast.alpha = 0 - - window.addSubview(toast) - toast.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - toast.centerXAnchor.constraint(equalTo: window.centerXAnchor), - toast.bottomAnchor.constraint(equalTo: window.safeAreaLayoutGuide.bottomAnchor, constant: -50), - toast.widthAnchor.constraint(greaterThanOrEqualToConstant: 150), - toast.heightAnchor.constraint(equalToConstant: 40) - ]) - - UIView.animate(withDuration: 0.3, animations: { - toast.alpha = 1 - }, completion: { _ in - UIView.animate(withDuration: 0.3, delay: 1.5, options: [], animations: { - toast.alpha = 0 - }, completion: { _ in - toast.removeFromSuperview() - }) - }) - } - } - ) - hostingController.view.sizeToFit() - 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/ProfileNew/Data/ProfileMediaPresenterWrapper.swift b/iosApp/iosApp/UI/Page/ProfileNew/Data/ProfileMediaPresenterWrapper.swift deleted file mode 100644 index 8ec302a67..000000000 --- a/iosApp/iosApp/UI/Page/ProfileNew/Data/ProfileMediaPresenterWrapper.swift +++ /dev/null @@ -1,14 +0,0 @@ -import Foundation -import os.log -import shared -import SwiftUI - -class ProfileMediaPresenterWrapper: ObservableObject { - 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/ProfileNew/Data/ProfilePresenterService.swift deleted file mode 100644 index 39ae4ec51..000000000 --- a/iosApp/iosApp/UI/Page/ProfileNew/Data/ProfilePresenterService.swift +++ /dev/null @@ -1,110 +0,0 @@ -import Foundation -import os -import shared -import SwiftUI - -class ProfilePresenterService { - static let shared = ProfilePresenterService() - - private let logger = Logger(subsystem: "com.flare.app", category: "ProfilePresenterService") - private let cacheLock = NSLock() - - private var presenterCache: [String: ProfilePresenterWrapper] = [:] - private var mediaPresenterCache: [String: ProfileMediaPresenterWrapper] = [:] - private var tabStoreCache: [String: ProfileTabSettingStore] = [:] - - private init() {} - - private func getCacheKey(accountType: AccountType, userKey: MicroBlogKey?) -> String { - let accountKey = (accountType as? AccountTypeSpecific)?.accountKey.description ?? String(describing: accountType) - let userKeyStr = userKey?.description ?? "self" - return "\(accountKey)_\(userKeyStr)" - } - - func getOrCreatePresenter(accountType: AccountType, userKey: MicroBlogKey?) -> ProfilePresenterWrapper { - let key = getCacheKey(accountType: accountType, userKey: userKey) - - cacheLock.lock() - defer { cacheLock.unlock() } - - if let cached = presenterCache[key] { - os_log("[📔][ProfilePresenterService] 🔄 使用缓存 ProfilePresenterWrapper: %{public}@", log: .default, type: .debug, key) - return cached - } - - let presenter = ProfilePresenterWrapper(accountType: accountType, userKey: userKey) - presenterCache[key] = presenter - os_log("[📔][ProfilePresenterService] ✨ 创建新 ProfilePresenterWrapper: %{public}@", log: .default, type: .debug, key) - return presenter - } - - func getOrCreateMediaPresenter(accountType: AccountType, userKey: MicroBlogKey?) -> ProfileMediaPresenterWrapper { - let key = getCacheKey(accountType: accountType, userKey: userKey) - - cacheLock.lock() - defer { cacheLock.unlock() } - - if let cached = mediaPresenterCache[key] { - os_log("[📔][ProfilePresenterService] 🔄 使用缓存 ProfileMediaPresenterWrapper: %{public}@", log: .default, type: .debug, key) - return cached - } - - let presenter = ProfileMediaPresenterWrapper(accountType: accountType, userKey: userKey) - mediaPresenterCache[key] = presenter - os_log("[📔][ProfilePresenterService] ✨ 创建新 ProfileMediaPresenterWrapper: %{public}@", log: .default, type: .debug, key) - return presenter - } - - func getOrCreateTabStore(userKey: MicroBlogKey?) -> ProfileTabSettingStore { - let key = userKey?.description ?? "self" - - cacheLock.lock() - defer { cacheLock.unlock() } - - if let cached = tabStoreCache[key] { - logger.debug("🔄 使用缓存 ProfileTabSettingStore: \(key)") - return cached - } - - let store = ProfileTabSettingStore(userKey: userKey) - tabStoreCache[key] = store - logger.debug("✨ 创建新 ProfileTabSettingStore: \(key)") - return store - } - - func clearCache() { - cacheLock.lock() - defer { cacheLock.unlock() } - - let presenterCount = presenterCache.count - let mediaPresenterCount = mediaPresenterCache.count - let tabStoreCount = tabStoreCache.count - - presenterCache.removeAll() - mediaPresenterCache.removeAll() - tabStoreCache.removeAll() - - logger.debug("🧹 清除所有Profile缓存 - Presenter: \(presenterCount), MediaPresenter: \(mediaPresenterCount), TabStore: \(tabStoreCount)") - } - - func clearCache(for accountType: AccountType, userKey: MicroBlogKey?) { - let key = getCacheKey(accountType: accountType, userKey: userKey) - let tabStoreKey = userKey?.description ?? "self" - - cacheLock.lock() - defer { cacheLock.unlock() } - - presenterCache.removeValue(forKey: key) - mediaPresenterCache.removeValue(forKey: key) - tabStoreCache.removeValue(forKey: tabStoreKey) - - logger.debug("🧹 清除特定Profile缓存: \(key)") - } - - func getCacheInfo() -> String { - cacheLock.lock() - defer { cacheLock.unlock() } - - return "ProfilePresenterService缓存状态 - Presenter: \(presenterCache.count), MediaPresenter: \(mediaPresenterCache.count), TabStore: \(tabStoreCache.count)" - } -} 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 f298c7a94..000000000 --- a/iosApp/iosApp/UI/Page/ProfileNew/Data/ProfilePresenterWrapper.swift +++ /dev/null @@ -1,28 +0,0 @@ -import Foundation -import os.log -import shared -import SwiftUI - -class ProfilePresenterWrapper: ObservableObject { - - let presenter: ProfileNewPresenter - @Published var isShowAppBar: Bool? = nil // nil: 初始状态, true: 显示, false: 隐藏 - - - 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 - } - - - func updateNavigationState(showAppBar: Bool?) { - os_log("[📔][ProfilePresenterWrapper]更新导航栏状态: showAppBar=%{public}@", log: .default, type: .debug, String(describing: showAppBar)) - Task { @MainActor in - isShowAppBar = showAppBar - } - } -} diff --git a/iosApp/iosApp/UI/Page/ProfileNew/Data/ProfileTabSettingStore.swift b/iosApp/iosApp/UI/Page/ProfileNew/Data/ProfileTabSettingStore.swift deleted file mode 100644 index 5495e4a62..000000000 --- a/iosApp/iosApp/UI/Page/ProfileNew/Data/ProfileTabSettingStore.swift +++ /dev/null @@ -1,174 +0,0 @@ -import Foundation -import os.log -import shared -import SwiftUI - -class ProfileTabSettingStore: ObservableObject, TabStateProvider { - @Published var availableTabs: [FLTabItem] = [] - @Published var selectedTabKey: String? - @Published var currentUser: UiUserV2? - @Published var currentPresenter: TimelinePresenter? - @Published var currentMediaPresenter: ProfileMediaPresenter? - - private var isInitializing = false - - private var presenterCache: [String: TimelinePresenter] = [:] - private var mediaPresenterCache: [String: ProfileMediaPresenter] = [:] - - var onTabChange: ((Int) -> Void)? - - var tabCount: Int { - availableTabs.count - } - - var selectedIndex: Int { - guard let selectedTabKey else { return 0 } - return availableTabs.firstIndex { $0.key == selectedTabKey } ?? 0 - } - - init(userKey: MicroBlogKey?) { - observeUser(userKey: userKey) - } - - private func observeUser(userKey: MicroBlogKey?) { - // 先检查UserManager中是否有用户 - let result = UserManager.shared.getCurrentUser() - - if let user = result.0 { - initializeWithUser(user, userKey: userKey) - return - } else if let userKey { - // 如果是未登录状态但查看他人资料,创建临时游客用户 - os_log("[📔][ProfileTabSettingStore]未登录状态查看用户:userKey=%{public}@", log: .default, type: .debug, userKey.description) - initializeWithUser(createSampleUser(), userKey: userKey) - return - } - } - - @objc private func handleUserUpdate(_ notification: Notification) { - if let user = notification.object as? UiUserV2, - let userKey = user.key as? MicroBlogKey - { - initializeWithUser(user, userKey: userKey) - } - } - - // - Public Methods - func initializeWithUser(_ user: UiUserV2, userKey: MicroBlogKey?) { - if isInitializing || currentUser?.key == user.key { - return - } - - isInitializing = true - currentUser = user - - // 更新可用标签 - updateTabs(user: user, userKey: userKey) - - // 如果没有选中的标签,选中第一个 - if let firstItem = availableTabs.first { - selectTab(firstItem.key) - } - - isInitializing = false - } - - func selectTab(_ key: String) { - selectedTabKey = key - if let selectedItem = availableTabs.first(where: { $0.key == key }) { - updateCurrentPresenter(for: selectedItem) - } - notifyTabChange() - } - - func updateCurrentPresenter(for tab: FLTabItem) { - selectedTabKey = tab.key - if tab is FLProfileMediaTabItem { - if let mediaTab = tab as? FLProfileMediaTabItem { - // 使用 userKey 作为缓存键 - let cacheKey = "\(mediaTab.userKey?.description ?? "self")" - if let cachedPresenter = mediaPresenterCache[cacheKey] { - currentMediaPresenter = cachedPresenter - } else { - let newPresenter = ProfileMediaPresenter(accountType: mediaTab.account, userKey: mediaTab.userKey) - mediaPresenterCache[cacheKey] = newPresenter - currentMediaPresenter = newPresenter - } - } - } else if let presenter = getOrCreatePresenter(for: tab) { - // 直接设置 presenter,不使用 withAnimation - currentPresenter = presenter - - // 确保 presenter 已经设置完成 - DispatchQueue.main.async { - os_log("[📔][ProfileTabSettingStore]更新当前 presenter: tab=%{public}@, presenter=%{public}@", log: .default, type: .debug, tab.key, String(describing: self.currentPresenter)) - } - } - } - - func getOrCreatePresenter(for tab: FLTabItem) -> TimelinePresenter? { - if let timelineItem = tab as? FLTimelineTabItem { - let key = tab.key - if let cachedPresenter = presenterCache[key] { - return cachedPresenter - } else { - let presenter = timelineItem.createPresenter() - presenterCache[key] = presenter - return presenter - } - } - return nil - } - - func clearCache() { - presenterCache.removeAll() - mediaPresenterCache.removeAll() - currentMediaPresenter = nil - } - - // - Private Methods - private func updateTabs(user: UiUserV2, userKey: MicroBlogKey?) { - // 检查是否是未登录模式 - let isGuestMode = user.key is AccountTypeGuest || UserManager.shared.getCurrentUser().0 == nil - - // 创建media标签 - let mediaTab = FLProfileMediaTabItem( - metaData: FLTabMetaData( - title: .localized(.profileMedia), - icon: .mixed(.media, userKey: user.key) - ), - account: AccountTypeSpecific(accountKey: user.key), - userKey: userKey - ) - - // 如果是未登录用户查看别人的资料,只显示media标签 - if isGuestMode, userKey != nil { - availableTabs = [mediaTab] - } else { - // 已登录用户显示所有标签 - var tabs = FLTabSettings.defaultThree(user: user, userKey: userKey) - - // 插入到倒数第二的位置 - if tabs.isEmpty { - tabs.append(mediaTab) - } else { - tabs.insert(mediaTab, at: max(0, tabs.count - 1)) - } - - availableTabs = tabs - } - - // 如果没有选中的标签,选中第一个 - if selectedTabKey == nil, let firstTab = availableTabs.first { - selectTab(firstTab.key) - } - } - - func notifyTabChange() { - onTabChange?(selectedIndex) - } - - deinit { - NotificationCenter.default.removeObserver(self) - } -} diff --git a/iosApp/iosApp/UI/Page/ProfileNew/Model/ProfileUserInfo.swift b/iosApp/iosApp/UI/Page/ProfileNew/Model/ProfileUserInfo.swift deleted file mode 100644 index d867c17a6..000000000 --- a/iosApp/iosApp/UI/Page/ProfileNew/Model/ProfileUserInfo.swift +++ /dev/null @@ -1,77 +0,0 @@ -import Foundation -import shared - - -struct ProfileUserInfo: Equatable { - let profile: UiProfile - let relation: UiRelation? - let isMe: Bool - let followCount: String - let fansCount: String - let fields: [String: UiRichText] - let canSendMessage: Bool - - - 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 - } - return nil - }() - - let isMe: Bool = { - if case let .success(me) = onEnum(of: state.isMe) { - return me.data as! Bool - } - return false - }() - - let canSendMessage: Bool = { - if case let .success(can) = onEnum(of: state.canSendMessage) { - return can.data as! Bool - } - 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 - { - return fieldsContent.fields - } - return [:] - }() - - return ProfileUserInfo( - profile: profile, - relation: relation, - isMe: isMe, - followCount: followCount, - fansCount: fansCount, - fields: fields, - canSendMessage: canSendMessage - ) - } - - - static func == (lhs: ProfileUserInfo, rhs: ProfileUserInfo) -> Bool { - - lhs.profile.key.description == rhs.profile.key.description && - lhs.isMe == rhs.isMe && - lhs.followCount == rhs.followCount && - lhs.fansCount == rhs.fansCount && - lhs.canSendMessage == rhs.canSendMessage - } -} diff --git a/iosApp/iosApp/UI/Page/ProfileNew/SwiftUIView/CommonProfileHeader.swift b/iosApp/iosApp/UI/Page/ProfileNew/SwiftUIView/CommonProfileHeader.swift deleted file mode 100644 index 6bd049df0..000000000 --- a/iosApp/iosApp/UI/Page/ProfileNew/SwiftUIView/CommonProfileHeader.swift +++ /dev/null @@ -1,324 +0,0 @@ -import Awesome -import Foundation -import Kingfisher -import MarkdownUI -import shared -import SwiftUI - -enum CommonProfileHeaderConstants { - static let headerHeight: CGFloat = 200 - static let avatarSize: CGFloat = 60 -} - -struct CompactLabelStyle: LabelStyle { - func makeBody(configuration: Configuration) -> some View { - HStack(spacing: 4) { - configuration.icon - configuration.title - } - } -} - -/* - * CommonProfileHeader User Profile header(banner -- avatar -- desc -- follow count -- user location/url) - */ -struct CommonProfileHeader: View { - let userInfo: ProfileUserInfo - let state: ProfileNewState? - let onFollowClick: (UiRelation) -> Void - @State private var isBannerValid: Bool = true - - var body: some View { - // banner - ZStack(alignment: .top) { - if let banner = userInfo.profile.banner, !banner.isEmpty, banner.range(of: "^https?://.*example\\.com.*$", options: .regularExpression) == nil, isBannerValid { - Color.clear.overlay { - KFImage(URL(string: banner)) - .onSuccess { result in - if result.image.size.width <= 10 || result.image.size.height <= 10 { - isBannerValid = false - } - } - .resizable() - .scaledToFill() - .frame(height: CommonProfileHeaderConstants.headerHeight) - .clipped() - } - .frame(height: CommonProfileHeaderConstants.headerHeight) - .ignoresSafeArea(edges: [.top, .horizontal]) - } else { - DynamicBannerBackground(avatarUrl: userInfo.profile.avatar) - .ignoresSafeArea(edges: [.top, .horizontal]) - } - // user avatar - VStack(alignment: .leading) { - Spacer().frame(height: 1) - - HStack(alignment: .center) { - // avatar - VStack { - Spacer() - .frame( - height: CommonProfileHeaderConstants.headerHeight - - CommonProfileHeaderConstants.avatarSize - 1 - ) - UserAvatar(data: userInfo.profile.avatar, size: CommonProfileHeaderConstants.avatarSize) - } - - // user name - VStack(alignment: .leading, spacing: 4) { - Spacer() - .frame(height: CommonProfileHeaderConstants.headerHeight - - CommonProfileHeaderConstants.avatarSize - 1) - Markdown(userInfo.profile.name.markdown) - .font(.headline) - .markdownInlineImageProvider(.emoji) - .lineLimit(1) - HStack { - Text(userInfo.profile.handleWithoutFirstAt) - .font(.subheadline) - .foregroundColor(.gray) - .lineLimit(1) - ForEach(0 ..< userInfo.profile.mark.count, id: \.self) { index in - let mark = userInfo.profile.mark[index] - switch mark { - case .cat: Awesome.Classic.Solid.cat.image.opacity(0.6) - case .verified: Awesome.Classic.Solid.circleCheck.image.opacity(0.6) - case .locked: Awesome.Classic.Solid.lock.image.opacity(0.6) - case .bot: Awesome.Classic.Solid.robot.image.opacity(0.6) - } - } - } - .frame(height: 20) - - HStack { - UserFollowsFansCount( - followCount: userInfo.followCount, - fansCount: userInfo.fansCount - ) - .frame(height: 20) - - Spacer() - if !userInfo.isMe { - if let relation = userInfo.relation { - Button(action: { - onFollowClick(relation) - }, label: { - let text = if relation.blocking { - String(localized: "profile_header_button_blockedblocked") - } else if relation.following { - String(localized: "profile_header_button_following") - } else if relation.hasPendingFollowRequestFromYou { - String(localized: "profile_header_button_requested") - } else { - String(localized: "profile_header_button_follow") - } - Text(text) - .font(.caption) - .padding(.horizontal, 12) - .padding(.vertical, 6) - .background(Color.gray.opacity(0.2)) - .clipShape(RoundedRectangle(cornerRadius: 15)) - }) - .buttonStyle(.borderless) - } - } - } - } - } - - // user desc - if let desc = userInfo.profile.description_?.markdown { - Markdown(desc) - .markdownInlineImageProvider(.emoji) - } - - // user follows - user fans -// MatrixView(followCount: userInfo.profile.matrices.followsCountHumanized, fansCount: userInfo.profile.matrices.fansCountHumanized) - - // user Location user url -// if let bottomContent = userInfo.profile.bottomContent { -// switch onEnum(of: bottomContent) { -// case .fields(let data): -// // pawoo 的 一些个人 table Info List -// UserInfoFieldsView(fields: data.fields) -// case .iconify(let data): -// HStack(spacing: 8) { -// if let locationValue = data.items[.location] { -// Label( -// title: { -// Markdown(locationValue.markdown) -// .font(.caption2) -// .markdownInlineImageProvider(.emoji) -// }, -// icon: { -// Image(uiImage: Asset.Image.Attributes.location.image -// .withRenderingMode(.alwaysTemplate)) -// .imageScale(.small) -// } -// ) -// .labelStyle(CompactLabelStyle()) -// .padding(.horizontal, 8) -// .padding(.vertical, 4) -// .background(Color(.systemGray6)) -// .cornerRadius(6) -// } - -// if let urlValue = data.items[.url] { -// Label( -// title: { -// Markdown(urlValue.markdown) -// .font(.caption2) -// .markdownInlineImageProvider(.emoji) -// }, -// icon: { -// Image(uiImage: Asset.Image.Attributes.globe.image -// .withRenderingMode(.alwaysTemplate)) -// .imageScale(.small) -// } -// ) -// .labelStyle(CompactLabelStyle()) -// .padding(.horizontal, 8) -// .padding(.vertical, 4) -// .background(Color(.systemGray6)) -// .cornerRadius(6) -// } - - // // if let verifyValue = data.items[.verify] { - // // Label( - // // title: { - // // Markdown(verifyValue.markdown) - // // .font(.footnote) - // // .markdownInlineImageProvider(.emoji) - // // }, - // // icon: { - // // Image("attributes/calendar").renderingMode(.template) - // // } - // // ) - // // .labelStyle(CompactLabelStyle()) - // // .padding(.horizontal, 8) - // // .padding(.vertical, 4) - // // .background(Color(.systemGray6)) - // // .cornerRadius(6) - // // } -// } -// } -// } - } - .padding([.horizontal]) - } - .toolbar { - if let state { - if case let .success(isMe) = onEnum(of: state.isMe), !isMe.data.boolValue { - Menu { - if case let .success(user) = onEnum(of: state.userState) { - if case let .success(relation) = onEnum(of: state.relationState), - case let .success(actions) = onEnum(of: state.actions), - actions.data.size > 0 - { - ForEach(0 ..< actions.data.size, id: \.self) { index in - let item = actions.data.get(index: index) - Button(action: { - Task { - try? await item.invoke(userKey: user.data.key, relation: relation.data) - } - }, label: { - let text = switch onEnum(of: item) { - case let .block(block): if block.relationState(relation: relation.data) { - String(localized: "unblock") - } else { - String(localized: "block") - } - case let .mute(mute): if mute.relationState(relation: relation.data) { - String(localized: "unmute") - } else { - String(localized: "mute") - } - } - let icon = switch onEnum(of: item) { - case let .block(block): if block.relationState(relation: relation.data) { - "xmark.circle" - } else { - "checkmark.circle" - } - case let .mute(mute): if mute.relationState(relation: relation.data) { - "speaker" - } else { - "speaker.slash" - } - } - Label(text, systemImage: icon) - }) - } - } - Button(action: { state.report(userKey: user.data.key) }, label: { - Label("report", systemImage: "exclamationmark.bubble") - }) - } - } label: { - Image(systemName: "ellipsis.circle") - } - } - } - } - } -} - -struct DynamicBannerBackground: View { - let avatarUrl: String - - var body: some View { - ZStack { - // 放大的头像背景 - KFImage(URL(string: avatarUrl)) - .resizable() - .scaledToFill() - .frame(height: CommonProfileHeaderConstants.headerHeight) - .blur(radius: 10) - .overlay { - // 添加渐变遮罩 - LinearGradient( - gradient: Gradient(colors: [ - Color.black.opacity(0.3), - Color.black.opacity(0.1), - Color.black.opacity(0.3) - ]), - startPoint: .leading, - endPoint: .trailing - ) - } - .clipped() - } - .frame(maxWidth: .infinity) - .frame(height: CommonProfileHeaderConstants.headerHeight) - } -} - -extension UIImage { - var averageColor: UIColor? { - guard let inputImage = CIImage(image: self) else { return nil } - let extentVector = CIVector(x: inputImage.extent.origin.x, - y: inputImage.extent.origin.y, - z: inputImage.extent.size.width, - w: inputImage.extent.size.height) - - guard let filter = CIFilter(name: "CIAreaAverage", - parameters: [kCIInputImageKey: inputImage, - kCIInputExtentKey: extentVector]) else { return nil } - guard let outputImage = filter.outputImage else { return nil } - - var bitmap = [UInt8](repeating: 0, count: 4) - let context = CIContext(options: [.workingColorSpace: kCFNull as Any]) - context.render(outputImage, - toBitmap: &bitmap, - rowBytes: 4, - bounds: CGRect(x: 0, y: 0, width: 1, height: 1), - format: .RGBA8, - colorSpace: nil) - - return UIColor(red: CGFloat(bitmap[0]) / 255, - green: CGFloat(bitmap[1]) / 255, - blue: CGFloat(bitmap[2]) / 255, - alpha: CGFloat(bitmap[3]) / 255) - } -} diff --git a/iosApp/iosApp/UI/Page/ProfileNew/SwiftUIView/ProfileMediaListScreen.swift b/iosApp/iosApp/UI/Page/ProfileNew/SwiftUIView/ProfileMediaListScreen.swift deleted file mode 100644 index 7d527636e..000000000 --- a/iosApp/iosApp/UI/Page/ProfileNew/SwiftUIView/ProfileMediaListScreen.swift +++ /dev/null @@ -1,443 +0,0 @@ -import AVKit -import JXPhotoBrowser -import Kingfisher -import MarkdownUI -import OrderedCollections -import shared -import SwiftUI - -// - ProfileMediaGridItem -struct ProfileMediaGridItem: Identifiable { - let id: Int - let media: UiMedia - let mediaState: UiTimeline -} - -// - ProfileMediaState Extension -extension ProfileMediaState { - var allMediaItems: [UiMedia] { - var items: [UiMedia] = [] - if case let .success(data) = onEnum(of: mediaState) { - for i in 0 ..< data.itemCount { - if let mediaItem = data.peek(index: i), - case let .status(statusData) = onEnum(of: mediaItem.status.content) - { - // 按照 timeline 顺序收集所有媒体 - items.append(contentsOf: statusData.images) - } - } - } - return items - } -} - -// - ProfileMediaListScreen -struct ProfileMediaListScreen: View { -// @ObservedObject var tabStore: ProfileTabSettingStore - @State private var currentMediaPresenter: ProfileMediaPresenter? - - @State private var refreshing = false - @State private var selectedMedia: (media: UiMedia, index: Int)? - @State private var showingMediaPreview = false - @Environment(\.appSettings) private var appSettings - @Environment(\.dismiss) private var dismiss - - // , tabStore: ProfileTabSettingStore - init(accountType _: AccountType, userKey _: MicroBlogKey?, currentMediaPresenter _: ProfileMediaPresenter) { -// self.tabStore = tabStore - } - - var body: some View { - if let presenter = currentMediaPresenter { - ObservePresenter(presenter: presenter) { state in - AnyView( - WaterfallCollectionView(state: state) { item in - ProfileMediaItemView(media: item.media, appSetting: appSettings) { - let allImages = state.allMediaItems - if !allImages.isEmpty, - let mediaIndex = allImages.firstIndex(where: { $0 === item.media }) - { - FlareLog.debug("ProfileMediaListScreen Opening browser with \(allImages.count) images at index \(mediaIndex)") - showPhotoBrowser(media: item.media, images: allImages, initialIndex: mediaIndex) - } - } - } - .frame(maxWidth: .infinity, maxHeight: .infinity) - ) - } - } else { - ProgressView() - .frame(maxWidth: .infinity, maxHeight: .infinity) - } - } - - private func showPhotoBrowser(media _: UiMedia, images: [UiMedia], initialIndex: Int) { - let browser = JXPhotoBrowser() - browser.scrollDirection = .horizontal - browser.numberOfItems = { images.count } - browser.pageIndex = initialIndex - - // 设置淡入淡出动画 - browser.transitionAnimator = JXPhotoBrowserFadeAnimator() - - // 根据媒体类型返回对应的 Cell - browser.cellClassAtIndex = { index in - let media = images[index] - switch onEnum(of: media) { - case .video, .gif: - return MediaBrowserVideoCell.self - default: - return JXPhotoBrowserImageCell.self - } - } - - // 加载媒体内容 - browser.reloadCellAtIndex = { context in - guard context.index >= 0, context.index < images.count else { return } - let media = images[context.index] - - switch onEnum(of: media) { - case let .video(data): - if let url = URL(string: data.url), - let cell = context.cell as? MediaBrowserVideoCell - { - cell.load(url: url, previewUrl: URL(string: data.thumbnailUrl), isGIF: false) - } - case let .gif(data): - if let url = URL(string: data.url), - let cell = context.cell as? MediaBrowserVideoCell - { - cell.load(url: url, previewUrl: URL(string: data.previewUrl), isGIF: true) - } - case let .image(data): - if let url = URL(string: data.url), - let cell = context.cell as? JXPhotoBrowserImageCell - { - cell.imageView.kf.setImage(with: url, options: [ - .transition(.fade(0.25)), - .processor(DownsamplingImageProcessor(size: UIScreen.main.bounds.size)) - ]) - } - default: - break - } - } - - // Cell 将要显示 - browser.cellWillAppear = { cell, index in - let media = images[index] - switch onEnum(of: media) { - case .video, .gif: - if let videoCell = cell as? MediaBrowserVideoCell { - videoCell.willDisplay() - } - default: - break - } - } - - // Cell 将要消失 - browser.cellWillDisappear = { cell, index in - let media = images[index] - switch onEnum(of: media) { - case .video, .gif: - if let videoCell = cell as? MediaBrowserVideoCell { - videoCell.didEndDisplaying() - } - default: - break - } - } - - // 即将关闭时的处理 - browser.willDismiss = { _ in - // 返回 true 表示执行动画 - true - } - - browser.show() - } -} - -// - WaterfallCollectionView -struct WaterfallCollectionView: UIViewRepresentable { - let state: ProfileMediaState - let content: (ProfileMediaGridItem) -> AnyView - - init(state: ProfileMediaState, @ViewBuilder content: @escaping (ProfileMediaGridItem) -> some View) { - self.state = state - self.content = { AnyView(content($0)) } - } - - func makeCoordinator() -> Coordinator { - Coordinator(self) - } - - func makeUIView(context: Context) -> UICollectionView { - let layout = ZJFlexibleLayout(delegate: context.coordinator) - let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) - collectionView.backgroundColor = .clear - collectionView.dataSource = context.coordinator - collectionView.register(HostingCell.self, forCellWithReuseIdentifier: "Cell") - - collectionView.translatesAutoresizingMaskIntoConstraints = false - - return collectionView - } - - func updateUIView(_ collectionView: UICollectionView, context: Context) { - context.coordinator.parent = self - context.coordinator.updateItems() - - DispatchQueue.main.async { - collectionView.reloadData() - collectionView.collectionViewLayout.invalidateLayout() - } - } - - class Coordinator: NSObject, UICollectionViewDataSource, ZJFlexibleDataSource { - var parent: WaterfallCollectionView - var items: [ProfileMediaGridItem] = [] - - init(_ parent: WaterfallCollectionView) { - self.parent = parent - super.init() - updateItems() - } - - func updateItems() { - if case let .success(success) = onEnum(of: parent.state.mediaState) { - items = (0 ..< success.itemCount).compactMap { index -> ProfileMediaGridItem? in - guard let item = success.peek(index: index) else { return nil } - return ProfileMediaGridItem(id: Int(index), media: item.media, mediaState: item.status) - } - FlareLog.debug("ProfileMediaListScreen Updated items count: \(items.count)") - } - } - - // - UICollectionViewDataSource - func collectionView(_: UICollectionView, numberOfItemsInSection _: Int) -> Int { - FlareLog.debug("ProfileMediaListScreen numberOfItemsInSection: \(items.count)") - return items.count - } - - func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { - let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! HostingCell - let item = items[indexPath.item] - FlareLog.debug("ProfileMediaListScreen Setting up cell at index: \(indexPath.item)") - cell.setup(with: parent.content(item)) - return cell - } - - // - ZJFlexibleDataSource - func numberOfCols(at _: Int) -> Int { - 2 - } - - func sizeOfItemAtIndexPath(at indexPath: IndexPath) -> CGSize { - let item = items[indexPath.item] - let width = (UIScreen.main.bounds.width - spaceOfCells(at: 0) * 3) / 2 - let height: CGFloat - - switch onEnum(of: item.media) { - case let .image(data): - FlareLog.debug("ProfileMediaListScreen Image size - width: \(data.width), height: \(data.height)") - let aspectRatio = CGFloat(data.width / (data.height == 0 ? 1 : data.height)).isZero ? 1 : CGFloat(data.width / data.height) - height = width / aspectRatio - case let .video(data): - FlareLog.debug("ProfileMediaListScreen Video size - width: \(data.width), height: \(data.height)") - let aspectRatio = CGFloat(data.width / (data.height == 0 ? 1 : data.height)).isZero ? 1 : CGFloat(data.width / data.height) - height = width / aspectRatio - case let .gif(data): - FlareLog.debug("ProfileMediaListScreen Gif size - width: \(data.width), height: \(data.height)") - let aspectRatio = CGFloat(data.width / (data.height == 0 ? 1 : data.height)).isZero ? 1 : CGFloat(data.width / data.height) - height = width / aspectRatio - case .audio: - FlareLog.debug("ProfileMediaListScreen Audio item") - height = width - case .video: - FlareLog.debug("ProfileMediaListScreen Video item") - height = width - } - - FlareLog.debug("ProfileMediaListScreen Calculated size - width: \(width), height: \(height)") - return CGSize(width: width, height: height) - } - - func spaceOfCells(at _: Int) -> CGFloat { - 4 - } - - func sectionInsets(at _: Int) -> UIEdgeInsets { - UIEdgeInsets(top: 4, left: 4, bottom: 4, right: 4) // 减小边距 - } - - func sizeOfHeader(at _: Int) -> CGSize { - .zero - } - - func heightOfAdditionalContent(at _: IndexPath) -> CGFloat { - 0 - } - } -} - -// - HostingCell -class HostingCell: UICollectionViewCell { - private var hostingController: UIHostingController? - - func setup(with view: AnyView) { - if let hostingController { - hostingController.rootView = view - } else { - let controller = UIHostingController(rootView: view) - hostingController = controller - controller.view.backgroundColor = .clear - - contentView.addSubview(controller.view) - controller.view.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - controller.view.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), - controller.view.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), - controller.view.topAnchor.constraint(equalTo: contentView.topAnchor), - controller.view.bottomAnchor.constraint(equalTo: contentView.bottomAnchor) - ]) - } - } -} - -struct ProfileMediaItemView: View { - let media: UiMedia - let appSetting: AppSettings - let onTap: () -> Void - @State private var hideSensitive: Bool - - init(media: UiMedia, appSetting: AppSettings, onTap: @escaping () -> Void) { - self.media = media - self.appSetting = appSetting - self.onTap = onTap - - // 初始化 hideSensitive - switch onEnum(of: media) { - case let .image(image): - _hideSensitive = State(initialValue: !appSetting.appearanceSettings.showSensitiveContent && image.sensitive) - default: - _hideSensitive = State(initialValue: false) - } - } - - var body: some View { - ZStack { - switch onEnum(of: media) { - case .audio: - ZStack { - Image(systemName: "waveform") - .font(.largeTitle) - .frame(maxWidth: .infinity, maxHeight: .infinity) - .background(Color.gray.opacity(0.2)) -// .blur(radius: media.url.sensitive ? 20 : 0) - -// if media.sensitive { -// Text("Sensitive Content") -// .foregroundColor(.white) -// .padding(8) -// .background(.ultraThinMaterial) -// .cornerRadius(8) -// } - } - case let .gif(gif): - ZStack { - KFImage(URL(string: gif.previewUrl)) - .cacheOriginalImage() - .loadDiskFileSynchronously() - .placeholder { - ProgressView() - } - .onFailure { _ in - Image(systemName: "exclamationmark.triangle") - .font(.largeTitle) - .foregroundColor(.red) - } - .resizable() - .scaledToFit() - - VStack { - HStack { - Text("GIF") - .font(.caption) - .padding(4) - .background(.ultraThinMaterial) - Spacer() - } - Spacer() - } - } - .onTapGesture { - onTap() - } - case let .image(image): - ZStack { - KFImage(URL(string: image.previewUrl)) - .cacheOriginalImage() - .loadDiskFileSynchronously() - .placeholder { - ProgressView() - } - .onFailure { _ in - Image(systemName: "exclamationmark.triangle") - .font(.largeTitle) - .foregroundColor(.red) - } - .resizable() - .scaledToFit() -// .fade(duration: 0.25) - .if(!appSetting.appearanceSettings.showSensitiveContent && image.sensitive && hideSensitive) { view in - view.blur(radius: 32) - } - - if !appSetting.appearanceSettings.showSensitiveContent, image.sensitive { - SensitiveContentButton( - hideSensitive: hideSensitive, - action: { hideSensitive.toggle() } - ) - } - } - case let .video(video): - ZStack { - KFImage(URL(string: video.thumbnailUrl)) - .cacheOriginalImage() - .loadDiskFileSynchronously() - .placeholder { - ProgressView() - } - .onFailure { _ in - Image(systemName: "exclamationmark.triangle") - .font(.largeTitle) - .foregroundColor(.red) - } - .resizable() - .scaledToFit() -// .fade(duration: 0.25) -// .blur(radius: video.sensitive ? 20 : 0) - - Image(systemName: "play.circle.fill") - .font(.largeTitle) - .foregroundColor(.white) - .shadow(radius: 2) -// -// if video.sensitive { -// Text("Sensitive Content") -// .foregroundColor(.white) -// .padding(8) -// .background(.ultraThinMaterial) -// .cornerRadius(8) -// } - } - } - } - .contentShape(Rectangle()) - .onTapGesture { - onTap() - } - } -} diff --git a/iosApp/iosApp/UI/Page/ProfileNew/SwiftUIView/ProfileWithUserNameScreen.swift b/iosApp/iosApp/UI/Page/ProfileNew/SwiftUIView/ProfileWithUserNameScreen.swift deleted file mode 100644 index e929f4262..000000000 --- a/iosApp/iosApp/UI/Page/ProfileNew/SwiftUIView/ProfileWithUserNameScreen.swift +++ /dev/null @@ -1,71 +0,0 @@ -import MarkdownUI -import OrderedCollections -import os.log -import shared -import SwiftUI - -// user profile 入口 -struct ProfileWithUserNameScreen: View { - @State private var presenter: ProfileWithUserNameAndHostPresenter - private let accountType: AccountType - @Environment(FlareRouter.self) var router - @Environment(FlareTheme.self) private var theme - - init(accountType: AccountType, userName: String, host: String) { - self.accountType = accountType - presenter = .init(userName: userName, host: host, accountType: accountType) - os_log("[📔][ProfileWithUserNameScreen - init]ProfileWithUserNameScreen: userName=%{public}@, host=%{public}@", log: .default, type: .debug, userName, host) - } - - var body: some View { - ObservePresenter(presenter: presenter) { state in - ZStack { - switch onEnum(of: state.user) { - case .error: - Text("error") - .onAppear { - FlareLog.error("ProfileWithUserNameScreen 加载用户信息失败") - } - case .loading: - List { - 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()) - } - .scrollContentBackground(.hidden) - .listRowBackground(theme.primaryBackgroundColor) - .onAppear { - FlareLog.debug("ProfileWithUserNameScreen 正在加载用户信息...") - } - case let .success(data): - // (lldb) po state dev.dimension.flare.ui.presenter.profile.ProfileWithUserNameAndHostPresenter$body$1@1d3717a0 -// (lldb) p state -// (SharedUserState) 0x0000000000000000 -// -// let loadedUserInfo = ProfileUserInfo.from(state: state as! ProfileNewState) - - ProfileTabScreenUikit( - accountType: accountType, - 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/ProfileNew/UIKitView/ProfileMediaViewController.swift deleted file mode 100644 index 5bc0e9bbb..000000000 --- a/iosApp/iosApp/UI/Page/ProfileNew/UIKitView/ProfileMediaViewController.swift +++ /dev/null @@ -1,226 +0,0 @@ -import JXPagingView -import JXPhotoBrowser -import JXSegmentedView -import Kingfisher -import MJRefresh -import shared -import SwiftUI -import UIKit - -class ProfileMediaViewController: UIViewController { - // - Properties - private var presenterWrapper: ProfileMediaPresenterWrapper? - private var scrollCallback: ((UIScrollView) -> Void)? - private var appSettings: AppSettings? - - private lazy var collectionView: UICollectionView = { - let layout = ZJFlexibleLayout(delegate: self) - let collection = UICollectionView(frame: .zero, collectionViewLayout: layout) - collection.backgroundColor = .clear - collection.delegate = self - collection.dataSource = self - collection.register(MediaCollectionViewCell.self, forCellWithReuseIdentifier: "MediaCell") - return collection - }() - - private var items: [ProfileMediaGridItem] = [] - - // - Lifecycle - override func viewDidLoad() { - super.viewDidLoad() - setupUI() - setupRefresh() - } - - deinit { - presenterWrapper = nil - scrollCallback = nil - } - - // - Setup - private func setupUI() { - view.addSubview(collectionView) - collectionView.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - collectionView.topAnchor.constraint(equalTo: view.topAnchor), - collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor), - collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor), - collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor) - ]) - } - - private func setupRefresh() { - // 下拉刷新 - collectionView.mj_header = MJRefreshNormalHeader(refreshingBlock: { [weak self] in - Task { -// if let mediaPresenterWrapper = self?.presenterWrapper, -// case .success(let data) = onEnum(of: mediaPresenterWrapper.presenter.models.value.mediaState) { -// data.retry() -// await MainActor.run { - self?.collectionView.mj_header?.endRefreshing() -// } -// } - } - }) - - // 上拉加载更多 - collectionView.mj_footer = MJRefreshAutoNormalFooter(refreshingBlock: { [weak self] in - Task { -// if let mediaPresenterWrapper = self?.presenterWrapper, -// case .success(let data) = onEnum(of: mediaPresenterWrapper.presenter.models.value.mediaState) { -// // 检查是否还有更多数据 -// let appendState = data.appendState -// if let notLoading = appendState as? Paging_commonLoadState.NotLoading, -// !notLoading.endOfPaginationReached { -// data.retry() -// } -// await MainActor.run { -// if let notLoading = appendState as? Paging_commonLoadState.NotLoading, -// notLoading.endOfPaginationReached { -// self?.collectionView.mj_footer?.endRefreshingWithNoMoreData() -// } else { - self?.collectionView.mj_footer?.endRefreshing() -// } -// } -// } - } - }) - } - - // - Public Methods - func updateMediaPresenter(presenterWrapper: ProfileMediaPresenterWrapper) { - self.presenterWrapper = presenterWrapper - // 监听数据变化 - Task { @MainActor in - let presenter = presenterWrapper.presenter - for await state in presenter.models { - self.handleState(state.mediaState) - } - } - } - - func configure(with appSettings: AppSettings) { - self.appSettings = appSettings - } - - // - Private Methods - private func handleState(_ state: PagingState) { - if case let .success(data) = onEnum(of: state) { - items = (0 ..< data.itemCount).compactMap { index -> ProfileMediaGridItem? in - guard let item = data.peek(index: index) else { return nil } - return ProfileMediaGridItem(id: Int(index), media: item.media, mediaState: item.status) - } - - collectionView.reloadData() - collectionView.mj_header?.endRefreshing() - collectionView.mj_footer?.endRefreshing() - } else { - items = [] - collectionView.reloadData() - collectionView.mj_header?.endRefreshing() - collectionView.mj_footer?.endRefreshing() - } - } - - private func showPhotoBrowser(media: UiMedia, images: [UiMedia], initialIndex: Int) { - Task { @MainActor in - PhotoBrowserManager.shared.showPhotoBrowser( - media: media, - images: images, - initialIndex: initialIndex - ) - } - } -} - -// - UICollectionViewDataSource - -extension ProfileMediaViewController: UICollectionViewDataSource { - func collectionView(_: UICollectionView, numberOfItemsInSection _: Int) -> Int { - items.count - } - - func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { - let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "MediaCell", for: indexPath) as! MediaCollectionViewCell - let item = items[indexPath.item] - - cell.configure(with: item.media, appSettings: appSettings ?? AppSettings()) { [weak self] in - guard let self else { return } - let allImages = items.map(\.media) - if !allImages.isEmpty { - showPhotoBrowser(media: item.media, images: allImages, initialIndex: indexPath.item) - } - } - - return cell - } -} - -// - UICollectionViewDelegate -extension ProfileMediaViewController: UICollectionViewDelegate { - func scrollViewDidScroll(_ scrollView: UIScrollView) { - scrollCallback?(scrollView) - } -} - -// - ZJFlexibleDataSource -extension ProfileMediaViewController: ZJFlexibleDataSource { - func numberOfCols(at _: Int) -> Int { - 2 - } - - func sizeOfItemAtIndexPath(at indexPath: IndexPath) -> CGSize { - let item = items[indexPath.item] - let width = (UIScreen.main.bounds.width - spaceOfCells(at: 0) * 3) / 2 - let height: CGFloat - - switch onEnum(of: item.media) { - case let .image(data): - let aspectRatio = CGFloat(data.width / (data.height == 0 ? 1 : data.height)).isZero ? 1 : CGFloat(data.width / data.height) - height = width / aspectRatio - case let .video(data): - let aspectRatio = CGFloat(data.width / (data.height == 0 ? 1 : data.height)).isZero ? 1 : CGFloat(data.width / data.height) - height = width / aspectRatio - case let .gif(data): - let aspectRatio = CGFloat(data.width / (data.height == 0 ? 1 : data.height)).isZero ? 1 : CGFloat(data.width / data.height) - height = width / aspectRatio - case .audio: - height = width - case .video: - height = width - } - - return CGSize(width: width, height: height) - } - - func spaceOfCells(at _: Int) -> CGFloat { - 4 - } - - func sectionInsets(at _: Int) -> UIEdgeInsets { - UIEdgeInsets(top: 4, left: 4, bottom: 4, right: 4) - } - - func sizeOfHeader(at _: Int) -> CGSize { - .zero - } - - func heightOfAdditionalContent(at _: IndexPath) -> CGFloat { - 0 - } -} - -// - JXPagingViewListViewDelegate -extension ProfileMediaViewController: JXPagingViewListViewDelegate { - func listView() -> UIView { - view - } - - func listScrollView() -> UIScrollView { - collectionView - } - - func listViewDidScrollCallback(callback: @escaping (UIScrollView) -> Void) { - scrollCallback = callback - } -} diff --git a/iosApp/iosApp/UI/Page/ProfileNew/UIKitView/ProfileRefreshViewController.swift b/iosApp/iosApp/UI/Page/ProfileNew/UIKitView/ProfileRefreshViewController.swift deleted file mode 100644 index 60329ce03..000000000 --- a/iosApp/iosApp/UI/Page/ProfileNew/UIKitView/ProfileRefreshViewController.swift +++ /dev/null @@ -1,711 +0,0 @@ -import Generated -import JXPagingView -import JXSegmentedView -import Kingfisher -import MarkdownUI -import MJRefresh -import os.log -import shared -import SwiftUI -import UIKit - -extension JXPagingListContainerView: JXSegmentedViewListContainer {} - -class ProfileNewRefreshViewController: UIViewController { - private var theme: FlareTheme? - private var userInfo: ProfileUserInfo? - private var state: ProfileNewState? - private var selectedTab: Binding? - private var isShowAppBar: Binding? - private var horizontalSizeClass: UserInterfaceSizeClass? - private var appSettings: AppSettings? - private var accountType: AccountType? - private var userKey: MicroBlogKey? - private var tabStore: ProfileTabSettingStore? - private var mediaPresenterWrapper: ProfileMediaPresenterWrapper? - private var listViewControllers: [Int: JXPagingViewListViewDelegate] = [:] - private var themeObserver: NSObjectProtocol? - - var pagingView: JXPagingView! - var userHeaderView: ProfileNewHeaderView! - var segmentedView: JXSegmentedView! - var segmentedDataSource: JXSegmentedTitleDataSource! - var isHeaderRefreshed = false - private var titles: [String] = [] - private var refreshControl: ProfileStretchRefreshControl? - - private var navigationBar: UINavigationBar = { - let nav = UINavigationBar() - return nav - }() - - private var lastContentOffset: CGFloat = 0 - private let navigationBarHeight: CGFloat = 44 - private var isNavigationBarHidden = false - - private static let BANNER_HEIGHT: CGFloat = 200 - private var isAppBarTitleVisible = false - - private var _cachedSafeAreaTop: CGFloat? - - func configure( - userInfo: ProfileUserInfo?, - state: ProfileNewState, - selectedTab: Binding, - isShowAppBar: Binding, - horizontalSizeClass: UserInterfaceSizeClass?, - appSettings: AppSettings, - accountType: AccountType, - userKey: MicroBlogKey?, - tabStore: ProfileTabSettingStore, - mediaPresenterWrapper: ProfileMediaPresenterWrapper, - theme: FlareTheme - ) { - self.userInfo = userInfo - self.state = state - self.selectedTab = selectedTab - self.isShowAppBar = isShowAppBar - self.horizontalSizeClass = horizontalSizeClass - self.appSettings = appSettings - self.accountType = accountType - self.userKey = userKey - self.tabStore = tabStore - self.mediaPresenterWrapper = mediaPresenterWrapper - self.theme = theme - - setupThemeObserver() - - let isOwnProfile = userKey == nil - - if isOwnProfile { - // 自己的Profile:根据原有逻辑控制AppBar - if let showAppBar = isShowAppBar.wrappedValue { - navigationController?.setNavigationBarHidden(!showAppBar, animated: false) - } else { - // 初始状态,显示导航栏 - navigationController?.setNavigationBarHidden(false, animated: false) - isShowAppBar.wrappedValue = true - } - } else { - // 其他用户Profile:AppBar永远显示 - navigationController?.setNavigationBarHidden(false, animated: false) - - isShowAppBar.wrappedValue = true - } - - // 🔑 设置导航按钮 - setupNavigationButtons(isOwnProfile: isOwnProfile) - - // 更新UI - updateUI() - - // 配置头部视图 - if let userInfo { - userHeaderView?.configure(with: userInfo, state: state, theme: theme) - - // 设置关注按钮回调 - userHeaderView?.onFollowClick = { [weak self] relation in - os_log("[📔][ProfileRefreshViewController]点击关注按钮: userKey=%{public}@", log: .default, type: .debug, userInfo.profile.key.description) - state.follow(userKey: userInfo.profile.key, data: relation) - } - } - - if !isOwnProfile { - navigationController?.navigationBar.alpha = 1.0 - } - } - - private func updateUI() { - guard let userInfo else { return } - - // 更新头部视图 - userHeaderView?.configure(with: userInfo) - - // 更新标签页 - if let tabStore { - // 从 tabStore.availableTabs 获取标题 - titles = tabStore.availableTabs.map { tab in - switch tab.metaData.title { - case let .text(title): - title - case let .localized(key): - NSLocalizedString(key, comment: "") - } - } - segmentedDataSource.titles = titles - segmentedView.reloadData() - - // 如果有选中的标签,更新选中状态 - if let selectedTab { - segmentedView.defaultSelectedIndex = selectedTab.wrappedValue - } - - pagingView.reloadData() - } - } - - private var cachedSafeAreaTop: CGFloat { - if let cached = _cachedSafeAreaTop { - return cached - } - let window = UIApplication.shared.windows.first { $0.isKeyWindow } - let safeAreaTop = window?.safeAreaInsets.top ?? 0 - _cachedSafeAreaTop = safeAreaTop - return safeAreaTop - } - - private func clearSafeAreaCache() { - _cachedSafeAreaTop = nil - } - - override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { - super.viewWillTransition(to: size, with: coordinator) - - // Clear safe area cache when device orientation changes - coordinator.animate(alongsideTransition: nil) { _ in - self.clearSafeAreaCache() - } - } - - override func viewDidLoad() { - super.viewDidLoad() - - // 设置导航栏 - setupNavigationBar() - - // 初始时显示系统导航栏,隐藏自定义导航栏和返回按钮 - navigationController?.setNavigationBarHidden(false, animated: false) - navigationBar.alpha = 0 - isNavigationBarHidden = false - - // 允许系统返回手势 - navigationController?.interactivePopGestureRecognizer?.isEnabled = true - - // 配置头部视图 - 只设置宽度,让高度自适应 - userHeaderView = ProfileNewHeaderView(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 0)) - - // 新的配置代码 - if let userInfo { - userHeaderView?.configure(with: userInfo, state: state, theme: theme) - } - - // 配置分段控制器 - segmentedView = JXSegmentedView(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 50)) - segmentedDataSource = JXSegmentedTitleDataSource() - segmentedDataSource.titles = titles // 使用动态titles - segmentedDataSource.titleNormalColor = .gray - segmentedDataSource.titleSelectedColor = .label - segmentedDataSource.titleNormalFont = .systemFont(ofSize: 15) - segmentedDataSource.titleSelectedFont = .systemFont(ofSize: 15) - segmentedDataSource.isTitleColorGradientEnabled = true - segmentedView.dataSource = segmentedDataSource - - // 添加选中回调 - segmentedView.delegate = self - - let indicator = JXSegmentedIndicatorLineView() - indicator.indicatorColor = theme != nil ? UIColor(theme!.tintColor) : .systemBlue - indicator.indicatorWidth = 30 - segmentedView.indicators = [indicator] - - // 添加底部分割线 - let lineWidth = 1 / UIScreen.main.scale - let bottomLineView = UIView() - bottomLineView.backgroundColor = .separator - bottomLineView.frame = CGRect(x: 0, y: segmentedView.bounds.height - lineWidth, width: segmentedView.bounds.width, height: lineWidth) - bottomLineView.autoresizingMask = .flexibleWidth - segmentedView.addSubview(bottomLineView) - - // 配置PagingView - pagingView = JXPagingView(delegate: self) - view.addSubview(pagingView) - - // 关联segmentedView和pagingView - segmentedView.listContainer = pagingView.listContainerView - - // 配置刷新控制器 - setupRefreshControl() - - // 添加滚动监听 - addScrollObserver() - } - - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - pagingView.frame = view.bounds - } - - private func setupRefreshControl() { - let refreshControl = ProfileStretchRefreshControl() - refreshControl.headerView = userHeaderView - refreshControl.refreshHandler = { [weak self] in - self?.refreshContent() - } - userHeaderView.addSubview(refreshControl) - refreshControl.frame = CGRect(x: 0, y: 0, width: view.bounds.width, height: 150) - self.refreshControl = refreshControl - } - - private func addScrollObserver() { - let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(_:))) - pagingView.mainTableView.addGestureRecognizer(panGesture) - panGesture.delegate = self - } - - @objc private func handlePanGesture(_: UIPanGestureRecognizer) { - let offset = pagingView.mainTableView.contentOffset.y - refreshControl?.scrollViewDidScroll(withOffset: offset) - - let isOwnProfile = userKey == nil - if !isOwnProfile { - updateNavigationBarVisibility(with: offset) - } - } - - private func refreshContent() { - let workItem = DispatchWorkItem { - self.isHeaderRefreshed = true - self.refreshControl?.endRefreshing() - self.pagingView.reloadData() - - if let currentList = self.pagingView.validListDict[self.segmentedView.selectedIndex] as? ProfileNewListViewController { - currentList.headerRefresh() - } - Task { - if let currentList = self.pagingView.validListDict[self.segmentedView.selectedIndex] { - if let timelineVC = currentList as? TimelineViewController, - let timelineState = timelineVC.presenter?.models.value as? TimelineState - { - // 触发时间线刷新 - try? await timelineState.refresh() - } else if let mediaVC = currentList as? ProfileMediaViewController, - let mediaPresenterWrapper = self.mediaPresenterWrapper, - case let .success(data) = onEnum(of: mediaPresenterWrapper.presenter.models.value.mediaState) - { - // 触发媒体列表刷新 - data.retry() - } - - await MainActor.run { - self.isHeaderRefreshed = true - self.refreshControl?.endRefreshing() - } - } - } - } - - DispatchQueue.main.asyncAfter(deadline: .now() + 2, execute: workItem) - } - - private func setupNavigationBar() { - // 设置导航栏frame - navigationBar.frame = CGRect(x: 0, y: 0, width: view.bounds.width, height: navigationBarHeight) - - // 设置导航栏项 - let navigationItem = UINavigationItem() - let backButton = UIBarButtonItem(image: UIImage(systemName: "chevron.left"), style: .plain, target: self, action: #selector(backButtonTapped)) - navigationItem.leftBarButtonItem = backButton - - // 添加更多按钮到导航栏右侧 - let moreButton = UIBarButtonItem(image: UIImage(systemName: "ellipsis.circle"), style: .plain, target: self, action: #selector(handleMoreMenuTap)) - navigationItem.rightBarButtonItem = moreButton - - navigationBar.items = [navigationItem] - - // 添加到视图 - view.addSubview(navigationBar) - - // 初始时隐藏 moreButton - moreButton.isEnabled = false - navigationItem.rightBarButtonItem = nil - } - - @objc private func backButtonTapped() { - // 返回上一页 - if let navigationController = parent?.navigationController { - navigationController.popViewController(animated: true) - } else { - dismiss(animated: true) - } - } - - @objc private func handleMoreMenuTap() { - os_log("[ProfileRefreshViewController] More menu button tapped", log: .default, type: .debug) - - guard let state, - case let .success(isMe) = onEnum(of: state.isMe), - !isMe.data.boolValue else { return } - - let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) - - if case let .success(user) = onEnum(of: state.userState) { - alertController.addAction(UIAlertAction(title: NSLocalizedString("report", comment: ""), style: .destructive) { [weak self] _ in - Task { - do { - try await state.report(userKey: user.data.key) - await MainActor.run { - ToastView(icon: UIImage(systemName: "checkmark.circle"), message: "Success").show() - } - } catch { - await MainActor.run { - ToastView(icon: UIImage(systemName: "exclamationmark.circle"), message: "Failed").show() - } - } - } - }) - } - - alertController.addAction(UIAlertAction(title: NSLocalizedString("cancel", comment: ""), style: .cancel)) - - present(alertController, animated: true) - } - - private func updateNavigationBarVisibility(with offset: CGFloat) { - if offset > Self.BANNER_HEIGHT, !isAppBarTitleVisible { - isAppBarTitleVisible = true - UIView.animate(withDuration: 0.25) { - self.navigationController?.navigationBar.alpha = 0.9 - } - updateAppBarTitle(showUserName: true) - } else if offset <= Self.BANNER_HEIGHT, isAppBarTitleVisible { - isAppBarTitleVisible = false - UIView.animate(withDuration: 0.25) { - self.navigationController?.navigationBar.alpha = 1.0 - } - updateAppBarTitle(showUserName: false) - } - - lastContentOffset = offset - } - - private func updateAppBarTitle(showUserName: Bool) { - let isOwnProfile = userKey == nil - - // Only process title for other user profiles - guard !isOwnProfile else { return } - - if showUserName { - // Show user name title - if let userInfo { - let displayName = userInfo.profile.name.raw.isEmpty ? userInfo.profile.handle : userInfo.profile.name.raw - navigationController?.navigationBar.topItem?.title = displayName - } - } else { - // Hide title - navigationController?.navigationBar.topItem?.title = nil - } - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - let isOwnProfile = userKey == nil - - // AppBar永远显示 - if isOwnProfile { - navigationController?.setNavigationBarHidden(false, animated: animated) - } else { - navigationController?.setNavigationBarHidden(false, animated: animated) - navigationController?.navigationBar.alpha = 1.0 - } - } - - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - - // 离开页面时重置状态,不然 详情页会导致没appbar - isShowAppBar?.wrappedValue = true - - // 确保系统导航栏状态正确 - navigationController?.setNavigationBarHidden(false, animated: animated) - } - - deinit { - cleanupListViewControllers() - - if let themeObserver { - NotificationCenter.default.removeObserver(themeObserver) - } - } - - private func cleanupListViewControllers() { - listViewControllers.removeAll() - } - - private func setupNavigationButtons(isOwnProfile: Bool) { - if isOwnProfile { - // 自己的Profile:清除所有导航按钮 - navigationController?.navigationBar.topItem?.leftBarButtonItem = nil - navigationController?.navigationBar.topItem?.rightBarButtonItem = nil - } else { - // 其他用户Profile:只设置更多按钮,使用系统默认返回按钮 - navigationController?.navigationBar.topItem?.leftBarButtonItem = nil // 使用系统默认返回按钮 - let moreButton = UIBarButtonItem(image: UIImage(systemName: "ellipsis.circle"), style: .plain, target: self, action: #selector(handleMoreMenuTap)) - navigationController?.navigationBar.topItem?.rightBarButtonItem = moreButton - } - } - - @objc private func handleBackButtonTap() { - os_log("[📔][ProfileRefreshViewController]点击返回按钮", log: .default, type: .debug) - - // 在返回前重置导航状态 // 离开页面时重置状态,不然 详情页会导致没appbar - isShowAppBar?.wrappedValue = true - - // 确保导航栏可见 - navigationController?.setNavigationBarHidden(false, animated: true) - - // 执行返回操作 - navigationController?.popViewController(animated: true) - } - - private func setupThemeObserver() { - if let existingObserver = themeObserver { - NotificationCenter.default.removeObserver(existingObserver) - } - - themeObserver = NotificationCenter.default.addObserver( - forName: NSNotification.Name("FlareThemeDidChange"), - object: nil, - queue: .main - ) { [weak self] _ in - self?.applyCurrentTheme() - } - applyCurrentTheme() - } - - private func applyCurrentTheme() { - guard let theme else { return } - - // 应用主题到视图控制器的主视图 - view.backgroundColor = UIColor(theme.primaryBackgroundColor) - - // 应用主题到 headerView - userHeaderView?.theme = theme - userHeaderView?.applyTheme() - - // 应用主题到 segmentedView - segmentedDataSource.titleSelectedColor = UIColor(theme.labelColor) - if let indicators = segmentedView.indicators as? [JXSegmentedIndicatorLineView] { - for indicator in indicators { - indicator.indicatorColor = UIColor(theme.tintColor) - } - } - segmentedView.backgroundColor = UIColor(theme.primaryBackgroundColor) - - // 应用主题到所有列表视图控制器 - for (_, listVC) in listViewControllers { - if let timelineVC = listVC as? TimelineViewController { - timelineVC.view.backgroundColor = UIColor(theme.primaryBackgroundColor) - } else if let mediaVC = listVC as? ProfileMediaViewController { - mediaVC.view.backgroundColor = UIColor(theme.primaryBackgroundColor) - } - } - } -} - -extension ProfileNewRefreshViewController: UIGestureRecognizerDelegate { - func gestureRecognizer(_: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith _: UIGestureRecognizer) -> Bool { - true - } -} - -extension ProfileNewRefreshViewController: JXPagingViewDelegate { - func tableHeaderViewHeight(in _: JXPagingView) -> Int { - // 获取内容高度并添加额外的空间 - let contentHeight = userHeaderView.getContentHeight() - // 添加额外的 padding 确保内容不会被遮挡 - let totalHeight = contentHeight // 添加 100 点的额外空间 - return Int(totalHeight) - } - - func tableHeaderView(in _: JXPagingView) -> UIView { - userHeaderView - } - - func heightForPinSectionHeader(in _: JXPagingView) -> Int { - let safeAreaTop = cachedSafeAreaTop - - // 布局常量 - let navigationBarHeight: CGFloat = 44 // AppBar标准高度 - let tabBarHeight: CGFloat = 50 // TabBar高度 - - let isOwnProfile = userKey == nil - - if isOwnProfile { - // Own profile: SafeArea + TabBar - return Int(safeAreaTop + tabBarHeight) - } else { - // Other user profile: SafeArea + AppBar + TabBar - return Int(safeAreaTop + navigationBarHeight + tabBarHeight) - } - } - - func viewForPinSectionHeader(in _: JXPagingView) -> UIView { - let containerView = UIView() - containerView.backgroundColor = .systemBackground - containerView.isUserInteractionEnabled = true - - // Use cached safe area for performance optimization - let safeAreaTop = cachedSafeAreaTop - - let navigationBarHeight: CGFloat = 44 - let tabBarHeight: CGFloat = 50 - - let isOwnProfile = userKey == nil - - let tabBarY: CGFloat = if isOwnProfile { - safeAreaTop - } else { - safeAreaTop + navigationBarHeight - 18 - } - - // 调整 segmentedView 的位置 - segmentedView.frame = CGRect(x: 0, y: tabBarY, width: view.bounds.width, height: tabBarHeight) - - // 创建一个按钮容器,确保它在 segmentedView 之上 - let buttonContainer = UIView(frame: CGRect(x: 0, y: 0, width: 80, height: 50 + safeAreaTop)) - buttonContainer.isUserInteractionEnabled = true - buttonContainer.backgroundColor = .clear - - containerView.addSubview(segmentedView) - if let theme { - containerView.backgroundColor = UIColor(theme.primaryBackgroundColor) - } - return containerView - } - - func numberOfLists(in _: JXPagingView) -> Int { - tabStore?.availableTabs.count ?? 0 - } - - func pagingView(_: JXPagingView, initListAtIndex index: Int) -> JXPagingViewListViewDelegate { - // 如果已经存在,直接返回 - if let existingVC = listViewControllers[index] { - return existingVC - } - - guard let tabStore, - index < tabStore.availableTabs.count - else { - let emptyVC = UIViewController() - return emptyVC as! JXPagingViewListViewDelegate - } - - let tab = tabStore.availableTabs[index] - - if tab is FLProfileMediaTabItem { - let mediaVC = ProfileMediaViewController() - if let mediaPresenterWrapper { - mediaVC.updateMediaPresenter(presenterWrapper: mediaPresenterWrapper) - } - if let appSettings { - mediaVC.configure(with: appSettings) - } - // 应用主题背景色 - if let theme { - mediaVC.view.backgroundColor = UIColor(theme.primaryBackgroundColor) - } - // 保存到字典中 - listViewControllers[index] = mediaVC - return mediaVC - } else { - let timelineVC = TimelineViewController() - - if let presenter = tabStore.currentPresenter { - os_log("[📔][ProfileNewRefreshViewController] updatePresenter start", log: .default, type: .debug) - - timelineVC.updatePresenter(presenter) - } - // 应用主题背景色 - if let theme { - timelineVC.view.backgroundColor = UIColor(theme.primaryBackgroundColor) - } - // 保存到字典中 - listViewControllers[index] = timelineVC - return timelineVC - } - } -} - -// 添加 JXSegmentedViewDelegate -extension ProfileNewRefreshViewController: JXSegmentedViewDelegate { - func segmentedView(_: JXSegmentedView, didSelectedItemAt index: Int) { - os_log("[📔][ProfileNewScreen]选择标签页: index=%{public}d", log: .default, type: .debug, index) - - // 更新选中状态 - selectedTab?.wrappedValue = index - - // 更新当前选中的标签页的presenter - if let tabStore, index < tabStore.availableTabs.count { - let selectedTab = tabStore.availableTabs[index] - - // 直接更新 presenter - tabStore.updateCurrentPresenter(for: selectedTab) - - // 获取当前的列表视图并更新其 presenter - if let currentList = pagingView.validListDict[index] { - if let timelineVC = currentList as? TimelineViewController, - let presenter = tabStore.currentPresenter - { - os_log("[📔][ProfileNewRefreshViewController] updatePresenter start", log: .default, type: .debug) - - // 更新 timeline presenter - timelineVC.updatePresenter(presenter) - } else if let mediaVC = currentList as? ProfileMediaViewController, - let mediaPresenterWrapper - { - os_log("[📔][ProfileNewRefreshViewController] setupUI end", log: .default, type: .debug) - - // 更新 media presenter - mediaVC.updateMediaPresenter(presenterWrapper: mediaPresenterWrapper) - } - } - } - } - - func segmentedView(_: JXSegmentedView, didClickSelectedItemAt index: Int) { - // 如果点击已选中的标签,可以触发刷新 - if let currentList = pagingView.validListDict[index] as? ProfileNewListViewController { - currentList.tableView.mj_header?.beginRefreshing() - } - } -} - -extension ProfileNewRefreshViewController { - func needsProfileUpdate( - userInfo: ProfileUserInfo?, - selectedTab: Int, - accountType: AccountType, - userKey: MicroBlogKey? - ) -> Bool { - // 1. 检查用户信息是否变化 - let userChanged = self.userInfo?.profile.key.description != userInfo?.profile.key.description - - // 2. 检查选中Tab是否变化 - let tabChanged = self.selectedTab?.wrappedValue != selectedTab - - // 3. 检查账户类型是否变化(更精确的比较) - let currentAccountKey = (self.accountType as? AccountTypeSpecific)?.accountKey.description ?? String(describing: self.accountType) - let newAccountKey = (accountType as? AccountTypeSpecific)?.accountKey.description ?? String(describing: accountType) - let accountChanged = currentAccountKey != newAccountKey - - // 4. 检查用户Key是否变化 - let userKeyChanged = self.userKey?.description != userKey?.description - - // 5. 首次配置检查(如果当前userInfo为nil,说明是首次配置) - let isFirstConfiguration = self.userInfo == nil && userInfo != nil - - let needsUpdate = userChanged || tabChanged || accountChanged || userKeyChanged || isFirstConfiguration - - if needsUpdate { - os_log("[ProfileNewRefreshViewController] Update needed: user=%{public}@, tab=%{public}@, account=%{public}@, userKey=%{public}@, first=%{public}@", - log: .default, type: .debug, - userChanged ? "changed" : "same", - tabChanged ? "changed" : "same", - accountChanged ? "changed" : "same", - userKeyChanged ? "changed" : "same", - isFirstConfiguration ? "true" : "false") - } - - return needsUpdate - } -} diff --git a/iosApp/iosApp/UI/Page/ProfileNew/UIKitView/ProfileStretchRefreshControl.swift b/iosApp/iosApp/UI/Page/ProfileNew/UIKitView/ProfileStretchRefreshControl.swift deleted file mode 100644 index a3c9a4454..000000000 --- a/iosApp/iosApp/UI/Page/ProfileNew/UIKitView/ProfileStretchRefreshControl.swift +++ /dev/null @@ -1,55 +0,0 @@ -import UIKit - -class ProfileStretchRefreshControl: UIControl { - weak var headerView: ProfileNewHeaderView? - private let activityIndicator = UIActivityIndicatorView(style: .medium) - private var isRefreshing: Bool = false - var refreshHandler: (() -> Void)? - - override init(frame: CGRect) { - super.init(frame: frame) - setupUI() - } - - @available(*, unavailable) - required init?(coder _: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupUI() { - activityIndicator.hidesWhenStopped = true - addSubview(activityIndicator) - } - - override func layoutSubviews() { - super.layoutSubviews() - // 将指示器放在Banner图片的中间 - activityIndicator.center = CGPoint(x: bounds.midX, y: 75) // Banner高度150的一半 - } - - func beginRefreshing() { - guard !isRefreshing else { return } - isRefreshing = true - activityIndicator.startAnimating() - refreshHandler?() - } - - func endRefreshing() { - isRefreshing = false - activityIndicator.stopAnimating() - // 恢复Banner图片大小 - headerView?.updateBannerStretch(withOffset: 0) - } - - func scrollViewDidScroll(withOffset offset: CGFloat) { - if offset < 0 { - // 更新Banner拉伸效果 - headerView?.updateBannerStretch(withOffset: abs(offset)) - - // 如果下拉超过阈值且没有在刷新,开始刷新 - if abs(offset) > 60, !isRefreshing { - beginRefreshing() - } - } - } -} diff --git a/iosApp/iosApp/UI/Page/ProfileNew/UIKitView/ProfileTabScreenUikit.swift b/iosApp/iosApp/UI/Page/ProfileNew/UIKitView/ProfileTabScreenUikit.swift deleted file mode 100644 index bc10f6260..000000000 --- a/iosApp/iosApp/UI/Page/ProfileNew/UIKitView/ProfileTabScreenUikit.swift +++ /dev/null @@ -1,166 +0,0 @@ -import Combine -import Kingfisher -import MarkdownUI -import OrderedCollections -import os.log -import shared -import SwiftUI - -struct ProfileTabScreenUikit: View { - let accountType: AccountType - let userKey: MicroBlogKey? - let showBackButton: Bool - - @StateObject private var presenterWrapper: ProfilePresenterWrapper - @StateObject private var mediaPresenterWrapper: ProfileMediaPresenterWrapper - @StateObject private var tabStore: ProfileTabSettingStore - @State private var selectedTab: Int = 0 - @State private var userInfo: ProfileUserInfo? - @Environment(FlareTheme.self) private var theme: FlareTheme - - // 横屏 竖屏 - @Environment(\.horizontalSizeClass) private var horizontalSizeClass - @Environment(\.appSettings) private var appSettings - @Environment(FlareRouter.self) private var router - @Environment(FlareMenuState.self) private var menuState - - init( - accountType: AccountType, userKey: MicroBlogKey?, showBackButton: Bool = true - ) { - self.accountType = accountType - self.userKey = userKey - self.showBackButton = showBackButton - - let service = ProfilePresenterService.shared - - _presenterWrapper = StateObject( - wrappedValue: service.getOrCreatePresenter(accountType: accountType, userKey: userKey)) - _mediaPresenterWrapper = StateObject( - wrappedValue: service.getOrCreateMediaPresenter(accountType: accountType, userKey: userKey)) - _tabStore = StateObject( - wrappedValue: service.getOrCreateTabStore(userKey: userKey)) - - os_log( - "[📔][ProfileNewScreen - optimized]优化初始化完成: accountType=%{public}@, userKey=%{public}@", - log: .default, type: .debug, - String(describing: accountType), userKey?.description ?? "nil" - ) - - os_log("[📔][ProfilePresenterService] %{public}@", log: .default, type: .debug, service.getCacheInfo()) - } - - var body: some View { - ObservePresenter(presenter: presenterWrapper.presenter) { state in - let userInfo = ProfileUserInfo.from(state: state as! ProfileNewState) - - // 打印 isShowAppBar 的值 - let _ = os_log( - "[📔][ProfileTabScreen] userKey=%{public}@", log: .default, type: .debug, - String(describing: userKey) - ) - - if userKey == nil { - - ProfileNewRefreshViewControllerWrapper( - userInfo: userInfo, - state: state as! ProfileNewState, - selectedTab: $selectedTab, - isShowAppBar: Binding( - get: { presenterWrapper.isShowAppBar }, - set: { presenterWrapper.updateNavigationState(showAppBar: $0) } - ), - - horizontalSizeClass: horizontalSizeClass, - appSettings: appSettings, - accountType: accountType, - userKey: userKey, - tabStore: tabStore, - mediaPresenterWrapper: mediaPresenterWrapper, - theme: theme - ) - .ignoresSafeArea(edges: .top) - - } else { - ProfileNewRefreshViewControllerWrapper( - userInfo: userInfo, - state: state as! ProfileNewState, - selectedTab: $selectedTab, - isShowAppBar: Binding( - get: { presenterWrapper.isShowAppBar }, - set: { presenterWrapper.updateNavigationState(showAppBar: $0) } - ), - - horizontalSizeClass: horizontalSizeClass, - appSettings: appSettings, - accountType: accountType, - userKey: userKey, - tabStore: tabStore, - mediaPresenterWrapper: mediaPresenterWrapper, - theme: theme - ) - .ignoresSafeArea(edges: .top) - } - } - } -} - -struct ProfileNewRefreshViewControllerWrapper: UIViewControllerRepresentable { - let userInfo: ProfileUserInfo? - let state: ProfileNewState - @Binding var selectedTab: Int - @Binding var isShowAppBar: Bool? - let horizontalSizeClass: UserInterfaceSizeClass? - let appSettings: AppSettings - let accountType: AccountType - let userKey: MicroBlogKey? - let tabStore: ProfileTabSettingStore - let mediaPresenterWrapper: ProfileMediaPresenterWrapper - let theme: FlareTheme - - func makeUIViewController(context _: Context) -> ProfileNewRefreshViewController { - let controller = ProfileNewRefreshViewController() - controller.configure( - userInfo: userInfo, - state: state, - selectedTab: $selectedTab, - isShowAppBar: $isShowAppBar, - horizontalSizeClass: horizontalSizeClass, - appSettings: appSettings, - accountType: accountType, - userKey: userKey, - tabStore: tabStore, - mediaPresenterWrapper: mediaPresenterWrapper, - theme: theme - ) - return controller - } - - func updateUIViewController( - _ uiViewController: ProfileNewRefreshViewController, context _: Context - ) { - if shouldUpdate(uiViewController) { - uiViewController.configure( - userInfo: userInfo, - state: state, - selectedTab: $selectedTab, - isShowAppBar: $isShowAppBar, - horizontalSizeClass: horizontalSizeClass, - appSettings: appSettings, - accountType: accountType, - userKey: userKey, - tabStore: tabStore, - mediaPresenterWrapper: mediaPresenterWrapper, - theme: theme - ) - } - } - - private func shouldUpdate(_ controller: ProfileNewRefreshViewController) -> Bool { - controller.needsProfileUpdate( - userInfo: userInfo, - selectedTab: selectedTab, - accountType: accountType, - userKey: userKey - ) - } -} From d4e057a60bb3da844c5c1fa192ae89961804e6ff Mon Sep 17 00:00:00 2001 From: null Date: Tue, 12 Aug 2025 20:47:17 +0800 Subject: [PATCH 7/8] Optimize code --- .../Timeline/CardPreview/LinkPreview.swift | 39 - .../CardPreview}/LinkPreviewV2.swift | 0 .../Timeline/CardPreview/PodcastPreview.swift | 37 - .../CardPreview}/PodcastPreviewV2.swift | 0 .../Timeline/ShareButton/ShareButton.swift | 336 -------- .../ShareButton}/ShareButtonV3.swift | 33 +- .../ShareButton/StatusShareAsImageView.swift | 147 ---- .../StatusShareAsImageViewV2.swift | 0 .../Compose/Timeline/StatusItemView.swift | 60 +- .../Timeline/StatusTimelineBuilder.swift | 3 +- ...sViewModel.swift => ActionProcessor.swift} | 62 +- .../TimelineStatus/StatusActionsView.swift | 302 -------- .../TimelineStatus/StatusContentView.swift | 256 ------- .../TimelineStatus}/StatusContentViewV2.swift | 0 .../TimelineStatus/StatusHeaderView.swift | 117 --- .../TimelineStatus}/StatusHeaderViewV2.swift | 0 .../StatusQuoteView}/StatusQuoteViewV2.swift | 0 .../StatusRetweetHeaderComponentV2.swift | 0 .../TimelineActionsViewV2.swift | 0 .../TimelineStatus/TimelineStatusView.swift | 96 --- .../TimelineStatusViewV2.swift | 1 - .../Compose/TimelineV2/ShareButtonV2.swift | 160 ---- .../MediaComponentV2.swift | 0 .../Compose/media/MediaItemComponent.swift | 28 +- .../MediaItemComponentV2.swift | 0 .../PhotoBrowserManagerV2.swift | 0 .../DataLayer/FlareTimelineState.swift | 0 .../DataLayer/PagingStateConverter.swift | 0 .../DataLayer/TimelineImagePrefetcher.swift | 0 .../Page/Home/View/Tabview/SearchScreen.swift | 19 +- .../TimelineItemsView.swift | 5 +- .../TimelineViewSwiftUIV4.swift | 2 +- .../Common/ProfileListViewController.swift | 103 +++ .../Components/MediaCollectionViewCell.swift | 52 ++ .../Components/ProfileNewHeaderView.swift | 643 ++++++++++++++++ .../Components}/TimelineDataManager.swift | 0 .../Components}/TimelineState.swift | 18 +- .../Components}/TimelineViewController.swift | 0 .../Data/ProfileMediaPresenterWrapper.swift | 14 + .../Data/ProfilePresenterService.swift | 125 +++ .../Data/ProfilePresenterWrapper.swift | 75 ++ .../Profile/Data/ProfileTabSettingStore.swift | 174 +++++ .../Page/Profile/Model/ProfileUserInfo.swift | 77 ++ .../SwiftUIView/CommonProfileHeader.swift | 324 ++++++++ .../SwiftUIView/ProfileMediaListScreen.swift | 443 +++++++++++ .../ProfileWithUserNameScreen.swift | 71 ++ .../ProfileMediaViewController.swift | 227 ++++++ .../ProfileRefreshViewController.swift | 723 ++++++++++++++++++ .../ProfileStretchRefreshControl.swift | 55 ++ .../UIKitView/ProfileTabScreenUikit.swift | 179 +++++ 50 files changed, 3386 insertions(+), 1620 deletions(-) delete mode 100644 iosApp/iosApp/UI/Page/Compose/Timeline/CardPreview/LinkPreview.swift rename iosApp/iosApp/UI/Page/Compose/{TimelineV2 => Timeline/CardPreview}/LinkPreviewV2.swift (100%) delete mode 100644 iosApp/iosApp/UI/Page/Compose/Timeline/CardPreview/PodcastPreview.swift rename iosApp/iosApp/UI/Page/Compose/{TimelineV2 => Timeline/CardPreview}/PodcastPreviewV2.swift (100%) delete mode 100644 iosApp/iosApp/UI/Page/Compose/Timeline/ShareButton/ShareButton.swift rename iosApp/iosApp/UI/Page/Compose/{TimelineV2 => Timeline/ShareButton}/ShareButtonV3.swift (85%) delete mode 100644 iosApp/iosApp/UI/Page/Compose/Timeline/ShareButton/StatusShareAsImageView.swift rename iosApp/iosApp/UI/Page/Compose/{TimelineV2 => Timeline/ShareButton}/StatusShareAsImageViewV2.swift (100%) rename iosApp/iosApp/UI/Page/Compose/Timeline/TimelineStatus/{StatusViewModel.swift => ActionProcessor.swift} (61%) delete mode 100644 iosApp/iosApp/UI/Page/Compose/Timeline/TimelineStatus/StatusActionsView.swift delete mode 100644 iosApp/iosApp/UI/Page/Compose/Timeline/TimelineStatus/StatusContentView.swift rename iosApp/iosApp/UI/Page/Compose/{TimelineV2 => Timeline/TimelineStatus}/StatusContentViewV2.swift (100%) delete mode 100644 iosApp/iosApp/UI/Page/Compose/Timeline/TimelineStatus/StatusHeaderView.swift rename iosApp/iosApp/UI/Page/Compose/{TimelineV2 => Timeline/TimelineStatus}/StatusHeaderViewV2.swift (100%) rename iosApp/iosApp/UI/Page/Compose/{TimelineV2 => Timeline/TimelineStatus/StatusQuoteView}/StatusQuoteViewV2.swift (100%) rename iosApp/iosApp/UI/Page/Compose/{TimelineV2 => Timeline/TimelineStatus}/StatusRetweetHeaderComponentV2.swift (100%) rename iosApp/iosApp/UI/Page/Compose/{TimelineV2 => Timeline/TimelineStatus}/TimelineActionsViewV2.swift (100%) delete mode 100644 iosApp/iosApp/UI/Page/Compose/Timeline/TimelineStatus/TimelineStatusView.swift rename iosApp/iosApp/UI/Page/Compose/{TimelineV2 => Timeline/TimelineStatus}/TimelineStatusViewV2.swift (99%) delete mode 100644 iosApp/iosApp/UI/Page/Compose/TimelineV2/ShareButtonV2.swift rename iosApp/iosApp/UI/Page/Compose/{TimelineV2 => media}/MediaComponentV2.swift (100%) rename iosApp/iosApp/UI/Page/Compose/{TimelineV2 => media}/MediaItemComponentV2.swift (100%) rename iosApp/iosApp/UI/Page/Compose/{TimelineV2 => media}/PhotoBrowserManagerV2.swift (100%) rename iosApp/iosApp/UI/{ => Page}/DataLayer/FlareTimelineState.swift (100%) rename iosApp/iosApp/UI/{ => Page}/DataLayer/PagingStateConverter.swift (100%) rename iosApp/iosApp/UI/{ => Page}/DataLayer/TimelineImagePrefetcher.swift (100%) create mode 100644 iosApp/iosApp/UI/Page/Profile/Common/ProfileListViewController.swift create mode 100644 iosApp/iosApp/UI/Page/Profile/Components/MediaCollectionViewCell.swift create mode 100644 iosApp/iosApp/UI/Page/Profile/Components/ProfileNewHeaderView.swift rename iosApp/iosApp/UI/Page/{Compose/Timeline => Profile/Components}/TimelineDataManager.swift (100%) rename iosApp/iosApp/UI/Page/{Compose/Timeline => Profile/Components}/TimelineState.swift (86%) rename iosApp/iosApp/UI/Page/{Compose/Timeline => Profile/Components}/TimelineViewController.swift (100%) create mode 100644 iosApp/iosApp/UI/Page/Profile/Data/ProfileMediaPresenterWrapper.swift create mode 100644 iosApp/iosApp/UI/Page/Profile/Data/ProfilePresenterService.swift create mode 100644 iosApp/iosApp/UI/Page/Profile/Data/ProfilePresenterWrapper.swift create mode 100644 iosApp/iosApp/UI/Page/Profile/Data/ProfileTabSettingStore.swift create mode 100644 iosApp/iosApp/UI/Page/Profile/Model/ProfileUserInfo.swift create mode 100644 iosApp/iosApp/UI/Page/Profile/SwiftUIView/CommonProfileHeader.swift create mode 100644 iosApp/iosApp/UI/Page/Profile/SwiftUIView/ProfileMediaListScreen.swift create mode 100644 iosApp/iosApp/UI/Page/Profile/SwiftUIView/ProfileWithUserNameScreen.swift create mode 100644 iosApp/iosApp/UI/Page/Profile/UIKitView/ProfileMediaViewController.swift create mode 100644 iosApp/iosApp/UI/Page/Profile/UIKitView/ProfileRefreshViewController.swift create mode 100644 iosApp/iosApp/UI/Page/Profile/UIKitView/ProfileStretchRefreshControl.swift create mode 100644 iosApp/iosApp/UI/Page/Profile/UIKitView/ProfileTabScreenUikit.swift 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 100% rename from iosApp/iosApp/UI/Page/Compose/TimelineV2/LinkPreviewV2.swift rename to iosApp/iosApp/UI/Page/Compose/Timeline/CardPreview/LinkPreviewV2.swift 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 100% rename from iosApp/iosApp/UI/Page/Compose/TimelineV2/PodcastPreviewV2.swift rename to iosApp/iosApp/UI/Page/Compose/Timeline/CardPreview/PodcastPreviewV2.swift 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..8dd1c6469 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,41 @@ 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..4751251ee 100644 --- a/iosApp/iosApp/UI/Page/Compose/Timeline/TimelineStatus/StatusViewModel.swift +++ b/iosApp/iosApp/UI/Page/Compose/Timeline/TimelineStatus/ActionProcessor.swift @@ -8,67 +8,7 @@ import shared 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 { /*** 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 4730b3b1c..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: .flareTextTypeCaption, - 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: .flareTextTypeBody, - 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 100% rename from iosApp/iosApp/UI/Page/Compose/TimelineV2/StatusContentViewV2.swift rename to iosApp/iosApp/UI/Page/Compose/Timeline/TimelineStatus/StatusContentViewV2.swift 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 100% rename from iosApp/iosApp/UI/Page/Compose/TimelineV2/StatusHeaderViewV2.swift rename to iosApp/iosApp/UI/Page/Compose/Timeline/TimelineStatus/StatusHeaderViewV2.swift diff --git a/iosApp/iosApp/UI/Page/Compose/TimelineV2/StatusQuoteViewV2.swift b/iosApp/iosApp/UI/Page/Compose/Timeline/TimelineStatus/StatusQuoteView/StatusQuoteViewV2.swift similarity index 100% rename from iosApp/iosApp/UI/Page/Compose/TimelineV2/StatusQuoteViewV2.swift rename to iosApp/iosApp/UI/Page/Compose/Timeline/TimelineStatus/StatusQuoteView/StatusQuoteViewV2.swift diff --git a/iosApp/iosApp/UI/Page/Compose/TimelineV2/StatusRetweetHeaderComponentV2.swift b/iosApp/iosApp/UI/Page/Compose/Timeline/TimelineStatus/StatusRetweetHeaderComponentV2.swift similarity index 100% rename from iosApp/iosApp/UI/Page/Compose/TimelineV2/StatusRetweetHeaderComponentV2.swift rename to iosApp/iosApp/UI/Page/Compose/Timeline/TimelineStatus/StatusRetweetHeaderComponentV2.swift 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 100% rename from iosApp/iosApp/UI/Page/Compose/TimelineV2/MediaComponentV2.swift rename to iosApp/iosApp/UI/Page/Compose/media/MediaComponentV2.swift diff --git a/iosApp/iosApp/UI/Page/Compose/media/MediaItemComponent.swift b/iosApp/iosApp/UI/Page/Compose/media/MediaItemComponent.swift index 35f16b224..166e8020f 100644 --- a/iosApp/iosApp/UI/Page/Compose/media/MediaItemComponent.swift +++ b/iosApp/iosApp/UI/Page/Compose/media/MediaItemComponent.swift @@ -2,17 +2,17 @@ import AVKit 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 100% rename from iosApp/iosApp/UI/Page/Compose/TimelineV2/MediaItemComponentV2.swift rename to iosApp/iosApp/UI/Page/Compose/media/MediaItemComponentV2.swift diff --git a/iosApp/iosApp/UI/Page/Compose/TimelineV2/PhotoBrowserManagerV2.swift b/iosApp/iosApp/UI/Page/Compose/media/PhotoBrowserManagerV2.swift similarity index 100% rename from iosApp/iosApp/UI/Page/Compose/TimelineV2/PhotoBrowserManagerV2.swift rename to iosApp/iosApp/UI/Page/Compose/media/PhotoBrowserManagerV2.swift 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/Home/View/Tabview/SearchScreen.swift b/iosApp/iosApp/UI/Page/Home/View/Tabview/SearchScreen.swift index 3541218f9..2b1bc15f5 100644 --- a/iosApp/iosApp/UI/Page/Home/View/Tabview/SearchScreen.swift +++ b/iosApp/iosApp/UI/Page/Home/View/Tabview/SearchScreen.swift @@ -36,20 +36,23 @@ 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( + StatusTimelineComponent( data: state.status, detailKey: nil - ) - .listStyle(.plain) - .listRowBackground(theme.primaryBackgroundColor) - }.listStyle(.plain).listRowBackground(theme.primaryBackgroundColor) - } + ).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..ab4395f7e 100644 --- a/iosApp/iosApp/UI/Page/Home/View/TimelineViewSwiftUI/TimelineItemsView.swift +++ b/iosApp/iosApp/UI/Page/Home/View/TimelineViewSwiftUI/TimelineItemsView.swift @@ -13,14 +13,13 @@ struct TimelineItemsView: View { TimelineStatusViewV2( item: item, timelineViewModel: viewModel - ) + ).padding(.horizontal, 16) .padding(.vertical, 4) .onAppear { // viewModel.itemDidAppear(item: item) Task { - if hasMore, - !viewModel.isLoadingMore, + if hasMore, !viewModel.isLoadingMore, items.count >= 7, item.id == items[items.count - 5].id || diff --git a/iosApp/iosApp/UI/Page/Home/View/TimelineViewSwiftUI/TimelineViewSwiftUIV4.swift b/iosApp/iosApp/UI/Page/Home/View/TimelineViewSwiftUI/TimelineViewSwiftUIV4.swift index 38433c8ac..0b7a243f8 100644 --- a/iosApp/iosApp/UI/Page/Home/View/TimelineViewSwiftUI/TimelineViewSwiftUIV4.swift +++ b/iosApp/iosApp/UI/Page/Home/View/TimelineViewSwiftUI/TimelineViewSwiftUIV4.swift @@ -37,7 +37,7 @@ struct TimelineViewSwiftUIV4: View { TimelineStatusViewV2( item: createSampleTimelineItem(), timelineViewModel: timeLineViewModel - ) + ).padding(.horizontal, 16) .redacted(reason: .placeholder) .listRowBackground(theme.primaryBackgroundColor) .listRowInsets(EdgeInsets()) diff --git a/iosApp/iosApp/UI/Page/Profile/Common/ProfileListViewController.swift b/iosApp/iosApp/UI/Page/Profile/Common/ProfileListViewController.swift new file mode 100644 index 000000000..08a2dbd26 --- /dev/null +++ b/iosApp/iosApp/UI/Page/Profile/Common/ProfileListViewController.swift @@ -0,0 +1,103 @@ +import JXPagingView +import MJRefresh +import UIKit + +class ProfileNewListViewController: UIViewController { + lazy var tableView: UITableView = .init(frame: CGRect.zero, style: .plain) + var dataSource: [String] = .init() + var isNeedHeader = false + var isNeedFooter = false + var isHeaderRefreshed = false + var listViewDidScrollCallback: ((UIScrollView) -> Void)? + + override func viewDidLoad() { + super.viewDidLoad() + + // tableView.backgroundColor = .systemBackground + tableView.tableFooterView = UIView() + tableView.dataSource = self + tableView.delegate = self + tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell") + view.addSubview(tableView) + + if isNeedHeader { + tableView.mj_header = MJRefreshNormalHeader(refreshingTarget: self, refreshingAction: #selector(headerRefresh)) + } + if isNeedFooter { + tableView.mj_footer = MJRefreshAutoNormalFooter(refreshingTarget: self, refreshingAction: #selector(loadMore)) + if #available(iOS 11.0, *) { + tableView.contentInsetAdjustmentBehavior = .never + } + } else { + // 列表的contentInsetAdjustmentBehavior失效,需要自己设置底部inset + tableView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) + } + beginFirstRefresh() + } + + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + tableView.frame = view.bounds + } + + func beginFirstRefresh() { + if !isHeaderRefreshed { + if isNeedHeader { + tableView.mj_header?.beginRefreshing() + } else { + isHeaderRefreshed = true + tableView.reloadData() + } + } + } + + @objc func headerRefresh() { + DispatchQueue.main.asyncAfter(deadline: .now() + 2) { + self.tableView.mj_header?.endRefreshing() + self.isHeaderRefreshed = true + self.tableView.reloadData() + } + } + + @objc func loadMore() { + DispatchQueue.main.asyncAfter(deadline: .now() + 2) { + self.dataSource.append("load more success") + self.tableView.reloadData() + self.tableView.mj_footer?.endRefreshing() + } + } +} + +// - UITableViewDataSource, UITableViewDelegate + +extension ProfileNewListViewController: UITableViewDataSource, UITableViewDelegate { + func tableView(_: UITableView, numberOfRowsInSection _: Int) -> Int { + isHeaderRefreshed ? dataSource.count : 0 + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) + cell.textLabel?.text = dataSource[indexPath.row] + return cell + } + + func tableView(_: UITableView, heightForRowAt _: IndexPath) -> CGFloat { + 50 + } + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + listViewDidScrollCallback?(scrollView) + } +} + +// - JXPagingViewListViewDelegate + +extension ProfileNewListViewController: JXPagingViewListViewDelegate { + func listView() -> UIView { view } + + func listScrollView() -> UIScrollView { tableView } + + func listViewDidScrollCallback(callback: @escaping (UIScrollView) -> Void) { + listViewDidScrollCallback = callback + } +} diff --git a/iosApp/iosApp/UI/Page/Profile/Components/MediaCollectionViewCell.swift b/iosApp/iosApp/UI/Page/Profile/Components/MediaCollectionViewCell.swift new file mode 100644 index 000000000..b158fde91 --- /dev/null +++ b/iosApp/iosApp/UI/Page/Profile/Components/MediaCollectionViewCell.swift @@ -0,0 +1,52 @@ +import shared +import SwiftUI +import UIKit + +class MediaCollectionViewCell: UICollectionViewCell { + private var hostingController: UIHostingController? + + override init(frame: CGRect) { + super.init(frame: frame) + backgroundColor = .clear + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func prepareForReuse() { + super.prepareForReuse() + hostingController?.view.removeFromSuperview() + hostingController = nil + } + + func configure(with media: UiMedia, appSettings: AppSettings, onTap: @escaping () -> Void) { + + let mediaView = ProfileMediaItemView( + media: media, + appSetting: appSettings, + onTap: onTap + ) + + + hostingController?.view.removeFromSuperview() + hostingController = nil + + + let controller = UIHostingController(rootView: mediaView) + hostingController = controller + + + controller.view.backgroundColor = .clear + contentView.addSubview(controller.view) + controller.view.translatesAutoresizingMaskIntoConstraints = false + + NSLayoutConstraint.activate([ + controller.view.topAnchor.constraint(equalTo: contentView.topAnchor), + controller.view.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), + controller.view.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), + controller.view.bottomAnchor.constraint(equalTo: contentView.bottomAnchor) + ]) + } +} diff --git a/iosApp/iosApp/UI/Page/Profile/Components/ProfileNewHeaderView.swift b/iosApp/iosApp/UI/Page/Profile/Components/ProfileNewHeaderView.swift new file mode 100644 index 000000000..55164debd --- /dev/null +++ b/iosApp/iosApp/UI/Page/Profile/Components/ProfileNewHeaderView.swift @@ -0,0 +1,643 @@ +// +// ProfileNewHeaderView.swift +// iosApp +// +// Created by abujj on 1/14/25. +// Copyright © 2025 orgName. All rights reserved. +// +import Generated +import JXSegmentedView +import Kingfisher +import MarkdownUI +import MJRefresh +import os.log +import shared +import SwiftUI +import UIKit + +// 头部视图 +class ProfileNewHeaderView: UIView { + private var state: ProfileNewState? + var theme: FlareTheme? + + // 添加关注按钮回调 + var onFollowClick: ((UiRelation) -> Void)? + + // 防重复设置事件的标志 + private var hasSetupEvents = false + + private let bannerImageView: UIImageView = { + let imageView = UIImageView() + imageView.contentMode = .scaleAspectFill + imageView.clipsToBounds = true + return imageView + }() + + private let blurEffectView: UIVisualEffectView = { + let blurEffect = UIBlurEffect(style: .light) + let view = UIVisualEffectView(effect: blurEffect) + view.alpha = 0 // 初始时不模糊 + return view + }() + + private let avatarView: UIImageView = { + let imageView = UIImageView() + // imageView.backgroundColor = .gray.withAlphaComponent(0.3) + imageView.layer.cornerRadius = 40 + imageView.clipsToBounds = true + imageView.contentMode = .scaleAspectFill + return imageView + }() + + private let followButton: UIButton = { + let button = UIButton(type: .system) + button.setTitle("follow", for: .normal) + button.setTitleColor(.white, for: .normal) + // button.backgroundColor = .systemBlue + button.layer.cornerRadius = 15 + return button + }() + + private let nameLabel: UILabel = { + let label = UILabel() + label.font = .boldSystemFont(ofSize: 20) + return label + }() + + private let handleLabel: UILabel = { + let label = UILabel() + label.textColor = .gray + label.font = .systemFont(ofSize: 15) + return label + }() + + private let descriptionLabel: UILabel = { + let label = UILabel() + label.numberOfLines = 0 + label.font = .systemFont(ofSize: 15) + return label + }() + + private let followsCountLabel: UILabel = { + let label = UILabel() + label.font = .systemFont(ofSize: 14) + label.textColor = .gray + return label + }() + + private let fansCountLabel: UILabel = { + let label = UILabel() + label.font = .systemFont(ofSize: 14) + label.textColor = .gray + return label + }() + + private let markStackView: UIStackView = { + let stackView = UIStackView() + stackView.axis = .horizontal + stackView.spacing = 4 + stackView.alignment = .center + return stackView + }() + + var onFollowsCountTap: (() -> Void)? + var onFansCountTap: (() -> Void)? + var onAvatarTap: (() -> Void)? + var onBannerTap: (() -> Void)? + + // 添加主题观察者 + private var themeObserver: NSObjectProtocol? + + // 添加 userInfo 属性 + private var userInfo: ProfileUserInfo? + + override init(frame: CGRect) { + super.init(frame: frame) + setupUI() + setupThemeObserver() + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + // 移除主题观察者 + if let themeObserver { + NotificationCenter.default.removeObserver(themeObserver) + } + } + + private func setupUI() { + // backgroundColor = .systemBackground + + // Banner with tap gesture + addSubview(bannerImageView) + bannerImageView.frame = CGRect(x: 0, y: 0, width: frame.width, height: 150) + let bannerTap = UITapGestureRecognizer(target: self, action: #selector(bannerTapped)) + bannerImageView.addGestureRecognizer(bannerTap) + bannerImageView.isUserInteractionEnabled = true + + // Blur effect + addSubview(blurEffectView) + blurEffectView.frame = bannerImageView.frame + + // Avatar with tap gesture + addSubview(avatarView) + avatarView.frame = CGRect(x: 16, y: 110, width: 80, height: 80) + let avatarTap = UITapGestureRecognizer(target: self, action: #selector(avatarTapped)) + avatarView.addGestureRecognizer(avatarTap) + avatarView.isUserInteractionEnabled = true + + // Follow Button + addSubview(followButton) + followButton.frame = CGRect(x: frame.width - 100, y: 160, width: 80, height: 30) + + // Name Label + addSubview(nameLabel) + nameLabel.frame = CGRect(x: 16, y: avatarView.frame.maxY + 10, width: frame.width - 32, height: 24) + + // Handle Label and Mark Stack + addSubview(handleLabel) + handleLabel.frame = CGRect(x: 16, y: nameLabel.frame.maxY + 4, width: frame.width - 32, height: 20) + + addSubview(markStackView) + markStackView.frame = CGRect(x: handleLabel.frame.maxX + 4, y: nameLabel.frame.maxY + 4, width: 100, height: 20) + + // Follows/Fans Count with tap gesture + addSubview(followsCountLabel) + followsCountLabel.frame = CGRect(x: 16, y: handleLabel.frame.maxY + 6, width: 100, height: 20) + let followsTap = UITapGestureRecognizer(target: self, action: #selector(followsCountTapped)) + followsCountLabel.addGestureRecognizer(followsTap) + followsCountLabel.isUserInteractionEnabled = true + + addSubview(fansCountLabel) + fansCountLabel.frame = CGRect(x: 120, y: handleLabel.frame.maxY + 6, width: 100, height: 20) + let fansTap = UITapGestureRecognizer(target: self, action: #selector(fansCountTapped)) + fansCountLabel.addGestureRecognizer(fansTap) + fansCountLabel.isUserInteractionEnabled = true + + // Description Label + addSubview(descriptionLabel) + + descriptionLabel.frame = CGRect(x: 16, y: followsCountLabel.frame.maxY + 10, width: frame.width - 32, height: 0) + } + + // 设置主题观察者 + private func setupThemeObserver() { + // 移除旧的观察者(如果存在) + if let existingObserver = themeObserver { + NotificationCenter.default.removeObserver(existingObserver) + } + + // 添加新的观察者 + themeObserver = NotificationCenter.default.addObserver( + forName: NSNotification.Name("FlareThemeDidChange"), + object: nil, + queue: .main + ) { [weak self] _ in + self?.applyTheme() + } + + // 立即应用当前主题 + applyTheme() + } + + // 应用主题方法 + func applyTheme() { + guard let theme else { return } + + // 应用背景色 + backgroundColor = UIColor(theme.primaryBackgroundColor) + + // 可以在这里应用其他与主题相关的样式 + nameLabel.textColor = UIColor(theme.labelColor) + descriptionLabel.textColor = UIColor(theme.labelColor) + descriptionLabel.backgroundColor = UIColor(theme.primaryBackgroundColor) // 貌似没用 + // 应用按钮颜色 + followButton.backgroundColor = UIColor(theme.tintColor) + } + + private func layoutContent() { + // 计算description的高度 + let descriptionWidth = frame.width - 32 + let descriptionSize = descriptionLabel.sizeThatFits(CGSize(width: descriptionWidth, height: .greatestFiniteMagnitude)) + + // 更新description的frame + descriptionLabel.frame = CGRect(x: 16, y: 280, width: descriptionWidth, height: descriptionSize.height) + + // 获取最后一个子视图的底部位置 + var maxY: CGFloat = 0 + for subview in subviews { + let subviewBottom = subview.frame.maxY + if subviewBottom > maxY { + maxY = subviewBottom + } + } + + // 更新整体高度,添加底部padding + frame.size.height = maxY + 16 // 16是底部padding + } + + // 更新Banner拉伸效果 + func updateBannerStretch(withOffset offset: CGFloat) { + let normalHeight: CGFloat = 150 + let stretchedHeight = normalHeight + max(0, offset) + + // 更新Banner图片frame + bannerImageView.frame = CGRect(x: 0, y: min(0, -offset), width: frame.width, height: stretchedHeight) + blurEffectView.frame = bannerImageView.frame + + // 根据拉伸程度设置模糊效果 + let blurAlpha = min(offset / 100, 0.3) // 最大模糊度0.3 + blurEffectView.alpha = blurAlpha + } + + func getContentHeight() -> CGFloat { + frame.height + } + + func configure(with userInfo: ProfileUserInfo, state: ProfileNewState? = nil, theme: FlareTheme? = nil) { + self.userInfo = userInfo // 需要保存 userInfo 以便在点击时使用 + self.state = state + self.theme = theme + + // 应用主题 + if theme != nil { + applyTheme() + } + + // 设置用户名 + nameLabel.text = userInfo.profile.name.markdown + + // 设置用户handle + handleLabel.text = "\(userInfo.profile.handleWithoutFirstAt)" + + if let url = URL(string: userInfo.profile.avatar) { + avatarView.kf.setImage( + with: url, + options: FlareImageOptions.timelineAvatar(size: CGSize(width: 160, height: 160)) + ) + } + + // 设置banner - 使用 Kingfisher 缓存 + if let url = URL(string: userInfo.profile.banner ?? ""), + !(userInfo.profile.banner ?? "").isEmpty, + (userInfo.profile.banner ?? "").range(of: "^https?://.*example\\.com.*$", options: .regularExpression) == nil + { + bannerImageView.kf.setImage( + with: url, + options: FlareImageOptions.banner(size: CGSize(width: UIScreen.main.bounds.width * 2, height: 300)) + ) { result in + switch result { + case let .success(imageResult): + // 检查图片是否有效 + if imageResult.image.size.width > 10, imageResult.image.size.height > 10 { + // 图片有效,保持现状 + } else { + // 如果图片无效,使用头像作为背景 + self.setupDynamicBannerBackground(avatarUrl: userInfo.profile.avatar) + } + case .failure: + // 加载失败,使用头像作为背景 + self.setupDynamicBannerBackground(avatarUrl: userInfo.profile.avatar) + } + } + } else { + // 如果没有banner,使用头像作为背景 + setupDynamicBannerBackground(avatarUrl: userInfo.profile.avatar) + } + + // 设置关注/粉丝数 + followsCountLabel.text = "\(formatCount(Int64(userInfo.followCount) ?? 0)) \(NSLocalizedString("following_title", comment: ""))" + fansCountLabel.text = "\(formatCount(Int64(userInfo.fansCount) ?? 0)) \(NSLocalizedString("fans_title", comment: ""))" + + // 更新关注按钮状态 + updateFollowButton(with: userInfo) + + // 设置用户标记 + markStackView.arrangedSubviews.forEach { $0.removeFromSuperview() } + for mark in userInfo.profile.mark { + let imageView = UIImageView() + imageView.tintColor = .gray + imageView.alpha = 0.6 + + switch mark { + case .cat: + imageView.image = UIImage(systemName: "cat") + case .verified: + imageView.image = UIImage(systemName: "checkmark.circle.fill") + case .locked: + imageView.image = UIImage(systemName: "lock.fill") + case .bot: + imageView.image = UIImage(systemName: "cpu") + default: + continue + } + + imageView.frame = CGRect(x: 0, y: 0, width: 16, height: 16) + markStackView.addArrangedSubview(imageView) + } + + // 开始流式布局,从关注/粉丝数下方开始 + var currentY = followsCountLabel.frame.maxY + 10 + + // 设置描述文本 + if let description = userInfo.profile.description_?.markdown, !description.isEmpty { + let descriptionView = UIHostingController( + rootView: Markdown(description) + .markdownInlineImageProvider(.emoji) + ) + if let theme { + descriptionView.view.backgroundColor = UIColor(theme.primaryBackgroundColor) + } + addSubview(descriptionView.view) + descriptionView.view.frame = CGRect(x: 16, y: currentY, width: frame.width - 32, height: 0) + descriptionView.view.sizeToFit() + currentY = descriptionView.view.frame.maxY + 16 + } + + if let bottomContent = userInfo.profile.bottomContent { + switch onEnum(of: bottomContent) { + case let .fields(data): + // 设置个人的附加资料 + let fieldsView = UserInfoFieldsView(fields: data.fields) + let hostingController = UIHostingController(rootView: fieldsView) + hostingController.view.frame = CGRect(x: 16, y: currentY, width: frame.width - 32, height: 0) + addSubview(hostingController.view) + hostingController.view.sizeToFit() + currentY = hostingController.view.frame.maxY + 16 + + case let .iconify(data): + let stackView = UIStackView() + stackView.axis = .horizontal + stackView.spacing = 8 + stackView.alignment = .leading + stackView.distribution = .fill + + // 创建一个容器视图来包含所有内容 + let containerView = UIView() + if let theme { + stackView.backgroundColor = UIColor(theme.primaryBackgroundColor) + } + containerView.addSubview(stackView) + stackView.translatesAutoresizingMaskIntoConstraints = false + + // 设置 stackView 的约束 + NSLayoutConstraint.activate([ + stackView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor), + stackView.topAnchor.constraint(equalTo: containerView.topAnchor), + stackView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor), + stackView.trailingAnchor.constraint(lessThanOrEqualTo: containerView.trailingAnchor) + ]) + + // 添加 location + if let locationValue = data.items[.location] { + let locationView = createIconWithLabel( + icon: Asset.Image.Attributes.location.image, + text: locationValue.markdown + ) + if let theme { + locationView.backgroundColor = UIColor(theme.primaryBackgroundColor) + } + stackView.addArrangedSubview(locationView) + } + + // 添加 url + if let urlValue = data.items[.url] { + let urlView = createIconWithLabel( + icon: Asset.Image.Attributes.globe.image, + text: urlValue.markdown + ) + if let theme { + urlView.backgroundColor = UIColor(theme.primaryBackgroundColor) + } + stackView.addArrangedSubview(urlView) + } + + containerView.frame = CGRect(x: 16, y: currentY, width: frame.width - 32, height: 20) + addSubview(containerView) + currentY = containerView.frame.maxY + 16 + } + } + + // 更新视图总高度 + frame.size.height = currentY + + // 设置事件处理 + setupEventHandlers() + } + + private func setupDynamicBannerBackground(avatarUrl: String?) { + guard let avatarUrl, let url = URL(string: avatarUrl) else { return } + + bannerImageView.kf.setImage( + with: url, + options: FlareImageOptions.banner(size: CGSize(width: UIScreen.main.bounds.width * 2, height: 300)) + ) { [weak self] _ in + self?.blurEffectView.alpha = 0.7 // 增加模糊效果 + } + } + + private func updateFollowButton(with userInfo: ProfileUserInfo) { + // 根据用户关系更新关注按钮状态 + if userInfo.isMe { + followButton.isHidden = true + } else { + followButton.isHidden = false + if let relation = userInfo.relation { + let title = if relation.blocking { + NSLocalizedString("profile_header_button_blocked", comment: "") + } else if relation.following { + NSLocalizedString("profile_header_button_following", comment: "") + } else if relation.hasPendingFollowRequestFromYou { + NSLocalizedString("profile_header_button_requested", comment: "") + } else { + NSLocalizedString("profile_header_button_follow", comment: "") + } + followButton.setTitle(title, for: .normal) + + // 保持蓝色背景 + // followButton.backgroundColor = .systemBlue + } + } + } + + private func setupEventHandlers() { + // 防止重复设置事件 - 检查是否已经有target + let existingTargets = followButton.allTargets + if !existingTargets.isEmpty { + os_log("[ProfileNewHeaderView] Button events already setup, skipping", log: .default, type: .debug) + return + } + + // 确保按钮可以响应事件 + followButton.isEnabled = true + followButton.isUserInteractionEnabled = true + + // 添加按钮事件 + followButton.addTarget(self, action: #selector(handleFollowButtonTap), for: .touchUpInside) + + os_log("[ProfileNewHeaderView] Button events setup completed", log: .default, type: .debug) + } + + @objc private func avatarTapped() { + onAvatarTap?() + } + + @objc private func bannerTapped() { + onBannerTap?() + } + + @objc private func followsCountTapped() { + onFollowsCountTap?() + } + + @objc private func fansCountTapped() { + onFansCountTap?() + } + + @objc private func handleFollowButtonTap() { + os_log("[ProfileNewHeaderView] Follow button tapped", log: .default, type: .debug) + + // 直接调用回调,传递 relation + if let relation = userInfo?.relation { + onFollowClick?(relation) + } + } + + // 辅助方法:查找当前视图所在的 ViewController + private func findViewController() -> UIViewController? { + var responder: UIResponder? = self + while let nextResponder = responder?.next { + if let viewController = nextResponder as? UIViewController { + return viewController + } + responder = nextResponder + } + return nil + } + + // Helper function to create icon with label + private func createIconWithLabel(icon: UIImage, text: String) -> UIView { + let hostingController = UIHostingController( + rootView: Label( + title: { + Markdown(text) + .font(.caption2) + .markdownInlineImageProvider(.emoji) + .lineLimit(1) + }, + icon: { + Image(uiImage: icon.withRenderingMode(.alwaysTemplate)) + .imageScale(.small) + } + ) + .labelStyle(CompactLabelStyle()) + .padding(.horizontal, 8) + .padding(.vertical, 4) + .cornerRadius(6) + .onLongPressGesture { + // 复制文本到剪贴板 + UIPasteboard.general.string = text + + // 显示复制成功提示 + let generator = UINotificationFeedbackGenerator() + generator.notificationOccurred(.success) + + // 显示提示消息 + if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, + let window = windowScene.windows.first + { + let toast = UILabel() + toast.text = "copy to clipboard" + // toast.backgroundColor = UIColor.black.withAlphaComponent(0.7) + toast.textColor = .white + toast.textAlignment = .center + toast.font = UIFont.systemFont(ofSize: 14) + toast.layer.cornerRadius = 10 + toast.clipsToBounds = true + toast.alpha = 0 + + window.addSubview(toast) + toast.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + toast.centerXAnchor.constraint(equalTo: window.centerXAnchor), + toast.bottomAnchor.constraint(equalTo: window.safeAreaLayoutGuide.bottomAnchor, constant: -50), + toast.widthAnchor.constraint(greaterThanOrEqualToConstant: 150), + toast.heightAnchor.constraint(equalToConstant: 40) + ]) + + UIView.animate(withDuration: 0.3, animations: { + toast.alpha = 1 + }, completion: { _ in + UIView.animate(withDuration: 0.3, delay: 1.5, options: [], animations: { + toast.alpha = 0 + }, completion: { _ in + toast.removeFromSuperview() + }) + }) + } + } + ) + hostingController.view.sizeToFit() + 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..2a81dbb81 100644 --- a/iosApp/iosApp/UI/Page/Compose/Timeline/TimelineState.swift +++ b/iosApp/iosApp/UI/Page/Profile/Components/TimelineState.swift @@ -7,27 +7,13 @@ 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/Profile/Data/ProfileMediaPresenterWrapper.swift b/iosApp/iosApp/UI/Page/Profile/Data/ProfileMediaPresenterWrapper.swift new file mode 100644 index 000000000..9c6437158 --- /dev/null +++ b/iosApp/iosApp/UI/Page/Profile/Data/ProfileMediaPresenterWrapper.swift @@ -0,0 +1,14 @@ +import Foundation +import os.log +import shared +import SwiftUI + +class ProfileMediaPresenterWrapper: ObservableObject { + let presenter: ProfileMediaPresenter + + + 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/Profile/Data/ProfilePresenterService.swift b/iosApp/iosApp/UI/Page/Profile/Data/ProfilePresenterService.swift new file mode 100644 index 000000000..8bbe7a719 --- /dev/null +++ b/iosApp/iosApp/UI/Page/Profile/Data/ProfilePresenterService.swift @@ -0,0 +1,125 @@ +import Foundation +import os +import shared +import SwiftUI + +class ProfilePresenterService { + static let shared = ProfilePresenterService() + + private let logger = Logger(subsystem: "com.flare.app", category: "ProfilePresenterService") + private let cacheLock = NSLock() + + private var presenterCache: [String: ProfilePresenterWrapper] = [:] + private var mediaPresenterCache: [String: ProfileMediaPresenterWrapper] = [:] + private var tabStoreCache: [String: ProfileTabSettingStore] = [:] + + private init() {} + + private func getCacheKey(accountType: AccountType, userKey: MicroBlogKey?) -> String { + let accountKey = (accountType as? AccountTypeSpecific)?.accountKey.description ?? String(describing: accountType) + let userKeyStr = userKey?.description ?? "self" + return "\(accountKey)_\(userKeyStr)" + } + + func getOrCreatePresenter(accountType: AccountType, userKey: MicroBlogKey?) -> ProfilePresenterWrapper { + let key = getCacheKey(accountType: accountType, userKey: userKey) + + cacheLock.lock() + defer { cacheLock.unlock() } + + if let cached = presenterCache[key] { + os_log("[📔][ProfilePresenterService] 🔄 使用缓存 ProfilePresenterWrapper: %{public}@", log: .default, type: .debug, key) + return cached + } + + let presenter = ProfilePresenterWrapper(accountType: accountType, userKey: userKey) + presenterCache[key] = presenter + os_log("[📔][ProfilePresenterService] ✨ 创建新 ProfilePresenterWrapper: %{public}@", log: .default, type: .debug, key) + return presenter + } + + func getOrCreateMediaPresenter(accountType: AccountType, userKey: MicroBlogKey?) -> ProfileMediaPresenterWrapper { + let key = getCacheKey(accountType: accountType, userKey: userKey) + + cacheLock.lock() + defer { cacheLock.unlock() } + + if let cached = mediaPresenterCache[key] { + os_log("[📔][ProfilePresenterService] 🔄 使用缓存 ProfileMediaPresenterWrapper: %{public}@", log: .default, type: .debug, key) + return cached + } + + let presenter = ProfileMediaPresenterWrapper(accountType: accountType, userKey: userKey) + mediaPresenterCache[key] = presenter + os_log("[📔][ProfilePresenterService] ✨ 创建新 ProfileMediaPresenterWrapper: %{public}@", log: .default, type: .debug, key) + return presenter + } + + func getOrCreateTabStore(userKey: MicroBlogKey?) -> ProfileTabSettingStore { + let key = userKey?.description ?? "self" + + cacheLock.lock() + defer { cacheLock.unlock() } + + if let cached = tabStoreCache[key] { + logger.debug("🔄 使用缓存 ProfileTabSettingStore: \(key)") + return cached + } + + let store = ProfileTabSettingStore(userKey: userKey) + tabStoreCache[key] = store + logger.debug("✨ 创建新 ProfileTabSettingStore: \(key)") + 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() } + + let presenterCount = presenterCache.count + let mediaPresenterCount = mediaPresenterCache.count + let tabStoreCount = tabStoreCache.count + + presenterCache.removeAll() + mediaPresenterCache.removeAll() + tabStoreCache.removeAll() + + logger.debug("🧹 清除所有Profile缓存 - Presenter: \(presenterCount), MediaPresenter: \(mediaPresenterCount), TabStore: \(tabStoreCount)") + } + + func clearCache(for accountType: AccountType, userKey: MicroBlogKey?) { + let key = getCacheKey(accountType: accountType, userKey: userKey) + let tabStoreKey = userKey?.description ?? "self" + + cacheLock.lock() + defer { cacheLock.unlock() } + + presenterCache.removeValue(forKey: key) + mediaPresenterCache.removeValue(forKey: key) + tabStoreCache.removeValue(forKey: tabStoreKey) + + logger.debug("🧹 清除特定Profile缓存: \(key)") + } + + func getCacheInfo() -> String { + cacheLock.lock() + defer { cacheLock.unlock() } + + return "ProfilePresenterService缓存状态 - Presenter: \(presenterCache.count), MediaPresenter: \(mediaPresenterCache.count), TabStore: \(tabStoreCache.count)" + } +} 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..d247ef673 --- /dev/null +++ b/iosApp/iosApp/UI/Page/Profile/Data/ProfilePresenterWrapper.swift @@ -0,0 +1,75 @@ +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() + self.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? { + return timelineViewModel + } +} diff --git a/iosApp/iosApp/UI/Page/Profile/Data/ProfileTabSettingStore.swift b/iosApp/iosApp/UI/Page/Profile/Data/ProfileTabSettingStore.swift new file mode 100644 index 000000000..5495e4a62 --- /dev/null +++ b/iosApp/iosApp/UI/Page/Profile/Data/ProfileTabSettingStore.swift @@ -0,0 +1,174 @@ +import Foundation +import os.log +import shared +import SwiftUI + +class ProfileTabSettingStore: ObservableObject, TabStateProvider { + @Published var availableTabs: [FLTabItem] = [] + @Published var selectedTabKey: String? + @Published var currentUser: UiUserV2? + @Published var currentPresenter: TimelinePresenter? + @Published var currentMediaPresenter: ProfileMediaPresenter? + + private var isInitializing = false + + private var presenterCache: [String: TimelinePresenter] = [:] + private var mediaPresenterCache: [String: ProfileMediaPresenter] = [:] + + var onTabChange: ((Int) -> Void)? + + var tabCount: Int { + availableTabs.count + } + + var selectedIndex: Int { + guard let selectedTabKey else { return 0 } + return availableTabs.firstIndex { $0.key == selectedTabKey } ?? 0 + } + + init(userKey: MicroBlogKey?) { + observeUser(userKey: userKey) + } + + private func observeUser(userKey: MicroBlogKey?) { + // 先检查UserManager中是否有用户 + let result = UserManager.shared.getCurrentUser() + + if let user = result.0 { + initializeWithUser(user, userKey: userKey) + return + } else if let userKey { + // 如果是未登录状态但查看他人资料,创建临时游客用户 + os_log("[📔][ProfileTabSettingStore]未登录状态查看用户:userKey=%{public}@", log: .default, type: .debug, userKey.description) + initializeWithUser(createSampleUser(), userKey: userKey) + return + } + } + + @objc private func handleUserUpdate(_ notification: Notification) { + if let user = notification.object as? UiUserV2, + let userKey = user.key as? MicroBlogKey + { + initializeWithUser(user, userKey: userKey) + } + } + + // - Public Methods + func initializeWithUser(_ user: UiUserV2, userKey: MicroBlogKey?) { + if isInitializing || currentUser?.key == user.key { + return + } + + isInitializing = true + currentUser = user + + // 更新可用标签 + updateTabs(user: user, userKey: userKey) + + // 如果没有选中的标签,选中第一个 + if let firstItem = availableTabs.first { + selectTab(firstItem.key) + } + + isInitializing = false + } + + func selectTab(_ key: String) { + selectedTabKey = key + if let selectedItem = availableTabs.first(where: { $0.key == key }) { + updateCurrentPresenter(for: selectedItem) + } + notifyTabChange() + } + + func updateCurrentPresenter(for tab: FLTabItem) { + selectedTabKey = tab.key + if tab is FLProfileMediaTabItem { + if let mediaTab = tab as? FLProfileMediaTabItem { + // 使用 userKey 作为缓存键 + let cacheKey = "\(mediaTab.userKey?.description ?? "self")" + if let cachedPresenter = mediaPresenterCache[cacheKey] { + currentMediaPresenter = cachedPresenter + } else { + let newPresenter = ProfileMediaPresenter(accountType: mediaTab.account, userKey: mediaTab.userKey) + mediaPresenterCache[cacheKey] = newPresenter + currentMediaPresenter = newPresenter + } + } + } else if let presenter = getOrCreatePresenter(for: tab) { + // 直接设置 presenter,不使用 withAnimation + currentPresenter = presenter + + // 确保 presenter 已经设置完成 + DispatchQueue.main.async { + os_log("[📔][ProfileTabSettingStore]更新当前 presenter: tab=%{public}@, presenter=%{public}@", log: .default, type: .debug, tab.key, String(describing: self.currentPresenter)) + } + } + } + + func getOrCreatePresenter(for tab: FLTabItem) -> TimelinePresenter? { + if let timelineItem = tab as? FLTimelineTabItem { + let key = tab.key + if let cachedPresenter = presenterCache[key] { + return cachedPresenter + } else { + let presenter = timelineItem.createPresenter() + presenterCache[key] = presenter + return presenter + } + } + return nil + } + + func clearCache() { + presenterCache.removeAll() + mediaPresenterCache.removeAll() + currentMediaPresenter = nil + } + + // - Private Methods + private func updateTabs(user: UiUserV2, userKey: MicroBlogKey?) { + // 检查是否是未登录模式 + let isGuestMode = user.key is AccountTypeGuest || UserManager.shared.getCurrentUser().0 == nil + + // 创建media标签 + let mediaTab = FLProfileMediaTabItem( + metaData: FLTabMetaData( + title: .localized(.profileMedia), + icon: .mixed(.media, userKey: user.key) + ), + account: AccountTypeSpecific(accountKey: user.key), + userKey: userKey + ) + + // 如果是未登录用户查看别人的资料,只显示media标签 + if isGuestMode, userKey != nil { + availableTabs = [mediaTab] + } else { + // 已登录用户显示所有标签 + var tabs = FLTabSettings.defaultThree(user: user, userKey: userKey) + + // 插入到倒数第二的位置 + if tabs.isEmpty { + tabs.append(mediaTab) + } else { + tabs.insert(mediaTab, at: max(0, tabs.count - 1)) + } + + availableTabs = tabs + } + + // 如果没有选中的标签,选中第一个 + if selectedTabKey == nil, let firstTab = availableTabs.first { + selectTab(firstTab.key) + } + } + + func notifyTabChange() { + onTabChange?(selectedIndex) + } + + deinit { + NotificationCenter.default.removeObserver(self) + } +} diff --git a/iosApp/iosApp/UI/Page/Profile/Model/ProfileUserInfo.swift b/iosApp/iosApp/UI/Page/Profile/Model/ProfileUserInfo.swift new file mode 100644 index 000000000..d867c17a6 --- /dev/null +++ b/iosApp/iosApp/UI/Page/Profile/Model/ProfileUserInfo.swift @@ -0,0 +1,77 @@ +import Foundation +import shared + + +struct ProfileUserInfo: Equatable { + let profile: UiProfile + let relation: UiRelation? + let isMe: Bool + let followCount: String + let fansCount: String + let fields: [String: UiRichText] + let canSendMessage: Bool + + + 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 + } + return nil + }() + + let isMe: Bool = { + if case let .success(me) = onEnum(of: state.isMe) { + return me.data as! Bool + } + return false + }() + + let canSendMessage: Bool = { + if case let .success(can) = onEnum(of: state.canSendMessage) { + return can.data as! Bool + } + 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 + { + return fieldsContent.fields + } + return [:] + }() + + return ProfileUserInfo( + profile: profile, + relation: relation, + isMe: isMe, + followCount: followCount, + fansCount: fansCount, + fields: fields, + canSendMessage: canSendMessage + ) + } + + + static func == (lhs: ProfileUserInfo, rhs: ProfileUserInfo) -> Bool { + + lhs.profile.key.description == rhs.profile.key.description && + lhs.isMe == rhs.isMe && + lhs.followCount == rhs.followCount && + lhs.fansCount == rhs.fansCount && + lhs.canSendMessage == rhs.canSendMessage + } +} diff --git a/iosApp/iosApp/UI/Page/Profile/SwiftUIView/CommonProfileHeader.swift b/iosApp/iosApp/UI/Page/Profile/SwiftUIView/CommonProfileHeader.swift new file mode 100644 index 000000000..6bd049df0 --- /dev/null +++ b/iosApp/iosApp/UI/Page/Profile/SwiftUIView/CommonProfileHeader.swift @@ -0,0 +1,324 @@ +import Awesome +import Foundation +import Kingfisher +import MarkdownUI +import shared +import SwiftUI + +enum CommonProfileHeaderConstants { + static let headerHeight: CGFloat = 200 + static let avatarSize: CGFloat = 60 +} + +struct CompactLabelStyle: LabelStyle { + func makeBody(configuration: Configuration) -> some View { + HStack(spacing: 4) { + configuration.icon + configuration.title + } + } +} + +/* + * CommonProfileHeader User Profile header(banner -- avatar -- desc -- follow count -- user location/url) + */ +struct CommonProfileHeader: View { + let userInfo: ProfileUserInfo + let state: ProfileNewState? + let onFollowClick: (UiRelation) -> Void + @State private var isBannerValid: Bool = true + + var body: some View { + // banner + ZStack(alignment: .top) { + if let banner = userInfo.profile.banner, !banner.isEmpty, banner.range(of: "^https?://.*example\\.com.*$", options: .regularExpression) == nil, isBannerValid { + Color.clear.overlay { + KFImage(URL(string: banner)) + .onSuccess { result in + if result.image.size.width <= 10 || result.image.size.height <= 10 { + isBannerValid = false + } + } + .resizable() + .scaledToFill() + .frame(height: CommonProfileHeaderConstants.headerHeight) + .clipped() + } + .frame(height: CommonProfileHeaderConstants.headerHeight) + .ignoresSafeArea(edges: [.top, .horizontal]) + } else { + DynamicBannerBackground(avatarUrl: userInfo.profile.avatar) + .ignoresSafeArea(edges: [.top, .horizontal]) + } + // user avatar + VStack(alignment: .leading) { + Spacer().frame(height: 1) + + HStack(alignment: .center) { + // avatar + VStack { + Spacer() + .frame( + height: CommonProfileHeaderConstants.headerHeight - + CommonProfileHeaderConstants.avatarSize - 1 + ) + UserAvatar(data: userInfo.profile.avatar, size: CommonProfileHeaderConstants.avatarSize) + } + + // user name + VStack(alignment: .leading, spacing: 4) { + Spacer() + .frame(height: CommonProfileHeaderConstants.headerHeight - + CommonProfileHeaderConstants.avatarSize - 1) + Markdown(userInfo.profile.name.markdown) + .font(.headline) + .markdownInlineImageProvider(.emoji) + .lineLimit(1) + HStack { + Text(userInfo.profile.handleWithoutFirstAt) + .font(.subheadline) + .foregroundColor(.gray) + .lineLimit(1) + ForEach(0 ..< userInfo.profile.mark.count, id: \.self) { index in + let mark = userInfo.profile.mark[index] + switch mark { + case .cat: Awesome.Classic.Solid.cat.image.opacity(0.6) + case .verified: Awesome.Classic.Solid.circleCheck.image.opacity(0.6) + case .locked: Awesome.Classic.Solid.lock.image.opacity(0.6) + case .bot: Awesome.Classic.Solid.robot.image.opacity(0.6) + } + } + } + .frame(height: 20) + + HStack { + UserFollowsFansCount( + followCount: userInfo.followCount, + fansCount: userInfo.fansCount + ) + .frame(height: 20) + + Spacer() + if !userInfo.isMe { + if let relation = userInfo.relation { + Button(action: { + onFollowClick(relation) + }, label: { + let text = if relation.blocking { + String(localized: "profile_header_button_blockedblocked") + } else if relation.following { + String(localized: "profile_header_button_following") + } else if relation.hasPendingFollowRequestFromYou { + String(localized: "profile_header_button_requested") + } else { + String(localized: "profile_header_button_follow") + } + Text(text) + .font(.caption) + .padding(.horizontal, 12) + .padding(.vertical, 6) + .background(Color.gray.opacity(0.2)) + .clipShape(RoundedRectangle(cornerRadius: 15)) + }) + .buttonStyle(.borderless) + } + } + } + } + } + + // user desc + if let desc = userInfo.profile.description_?.markdown { + Markdown(desc) + .markdownInlineImageProvider(.emoji) + } + + // user follows - user fans +// MatrixView(followCount: userInfo.profile.matrices.followsCountHumanized, fansCount: userInfo.profile.matrices.fansCountHumanized) + + // user Location user url +// if let bottomContent = userInfo.profile.bottomContent { +// switch onEnum(of: bottomContent) { +// case .fields(let data): +// // pawoo 的 一些个人 table Info List +// UserInfoFieldsView(fields: data.fields) +// case .iconify(let data): +// HStack(spacing: 8) { +// if let locationValue = data.items[.location] { +// Label( +// title: { +// Markdown(locationValue.markdown) +// .font(.caption2) +// .markdownInlineImageProvider(.emoji) +// }, +// icon: { +// Image(uiImage: Asset.Image.Attributes.location.image +// .withRenderingMode(.alwaysTemplate)) +// .imageScale(.small) +// } +// ) +// .labelStyle(CompactLabelStyle()) +// .padding(.horizontal, 8) +// .padding(.vertical, 4) +// .background(Color(.systemGray6)) +// .cornerRadius(6) +// } + +// if let urlValue = data.items[.url] { +// Label( +// title: { +// Markdown(urlValue.markdown) +// .font(.caption2) +// .markdownInlineImageProvider(.emoji) +// }, +// icon: { +// Image(uiImage: Asset.Image.Attributes.globe.image +// .withRenderingMode(.alwaysTemplate)) +// .imageScale(.small) +// } +// ) +// .labelStyle(CompactLabelStyle()) +// .padding(.horizontal, 8) +// .padding(.vertical, 4) +// .background(Color(.systemGray6)) +// .cornerRadius(6) +// } + + // // if let verifyValue = data.items[.verify] { + // // Label( + // // title: { + // // Markdown(verifyValue.markdown) + // // .font(.footnote) + // // .markdownInlineImageProvider(.emoji) + // // }, + // // icon: { + // // Image("attributes/calendar").renderingMode(.template) + // // } + // // ) + // // .labelStyle(CompactLabelStyle()) + // // .padding(.horizontal, 8) + // // .padding(.vertical, 4) + // // .background(Color(.systemGray6)) + // // .cornerRadius(6) + // // } +// } +// } +// } + } + .padding([.horizontal]) + } + .toolbar { + if let state { + if case let .success(isMe) = onEnum(of: state.isMe), !isMe.data.boolValue { + Menu { + if case let .success(user) = onEnum(of: state.userState) { + if case let .success(relation) = onEnum(of: state.relationState), + case let .success(actions) = onEnum(of: state.actions), + actions.data.size > 0 + { + ForEach(0 ..< actions.data.size, id: \.self) { index in + let item = actions.data.get(index: index) + Button(action: { + Task { + try? await item.invoke(userKey: user.data.key, relation: relation.data) + } + }, label: { + let text = switch onEnum(of: item) { + case let .block(block): if block.relationState(relation: relation.data) { + String(localized: "unblock") + } else { + String(localized: "block") + } + case let .mute(mute): if mute.relationState(relation: relation.data) { + String(localized: "unmute") + } else { + String(localized: "mute") + } + } + let icon = switch onEnum(of: item) { + case let .block(block): if block.relationState(relation: relation.data) { + "xmark.circle" + } else { + "checkmark.circle" + } + case let .mute(mute): if mute.relationState(relation: relation.data) { + "speaker" + } else { + "speaker.slash" + } + } + Label(text, systemImage: icon) + }) + } + } + Button(action: { state.report(userKey: user.data.key) }, label: { + Label("report", systemImage: "exclamationmark.bubble") + }) + } + } label: { + Image(systemName: "ellipsis.circle") + } + } + } + } + } +} + +struct DynamicBannerBackground: View { + let avatarUrl: String + + var body: some View { + ZStack { + // 放大的头像背景 + KFImage(URL(string: avatarUrl)) + .resizable() + .scaledToFill() + .frame(height: CommonProfileHeaderConstants.headerHeight) + .blur(radius: 10) + .overlay { + // 添加渐变遮罩 + LinearGradient( + gradient: Gradient(colors: [ + Color.black.opacity(0.3), + Color.black.opacity(0.1), + Color.black.opacity(0.3) + ]), + startPoint: .leading, + endPoint: .trailing + ) + } + .clipped() + } + .frame(maxWidth: .infinity) + .frame(height: CommonProfileHeaderConstants.headerHeight) + } +} + +extension UIImage { + var averageColor: UIColor? { + guard let inputImage = CIImage(image: self) else { return nil } + let extentVector = CIVector(x: inputImage.extent.origin.x, + y: inputImage.extent.origin.y, + z: inputImage.extent.size.width, + w: inputImage.extent.size.height) + + guard let filter = CIFilter(name: "CIAreaAverage", + parameters: [kCIInputImageKey: inputImage, + kCIInputExtentKey: extentVector]) else { return nil } + guard let outputImage = filter.outputImage else { return nil } + + var bitmap = [UInt8](repeating: 0, count: 4) + let context = CIContext(options: [.workingColorSpace: kCFNull as Any]) + context.render(outputImage, + toBitmap: &bitmap, + rowBytes: 4, + bounds: CGRect(x: 0, y: 0, width: 1, height: 1), + format: .RGBA8, + colorSpace: nil) + + return UIColor(red: CGFloat(bitmap[0]) / 255, + green: CGFloat(bitmap[1]) / 255, + blue: CGFloat(bitmap[2]) / 255, + alpha: CGFloat(bitmap[3]) / 255) + } +} diff --git a/iosApp/iosApp/UI/Page/Profile/SwiftUIView/ProfileMediaListScreen.swift b/iosApp/iosApp/UI/Page/Profile/SwiftUIView/ProfileMediaListScreen.swift new file mode 100644 index 000000000..7d527636e --- /dev/null +++ b/iosApp/iosApp/UI/Page/Profile/SwiftUIView/ProfileMediaListScreen.swift @@ -0,0 +1,443 @@ +import AVKit +import JXPhotoBrowser +import Kingfisher +import MarkdownUI +import OrderedCollections +import shared +import SwiftUI + +// - ProfileMediaGridItem +struct ProfileMediaGridItem: Identifiable { + let id: Int + let media: UiMedia + let mediaState: UiTimeline +} + +// - ProfileMediaState Extension +extension ProfileMediaState { + var allMediaItems: [UiMedia] { + var items: [UiMedia] = [] + if case let .success(data) = onEnum(of: mediaState) { + for i in 0 ..< data.itemCount { + if let mediaItem = data.peek(index: i), + case let .status(statusData) = onEnum(of: mediaItem.status.content) + { + // 按照 timeline 顺序收集所有媒体 + items.append(contentsOf: statusData.images) + } + } + } + return items + } +} + +// - ProfileMediaListScreen +struct ProfileMediaListScreen: View { +// @ObservedObject var tabStore: ProfileTabSettingStore + @State private var currentMediaPresenter: ProfileMediaPresenter? + + @State private var refreshing = false + @State private var selectedMedia: (media: UiMedia, index: Int)? + @State private var showingMediaPreview = false + @Environment(\.appSettings) private var appSettings + @Environment(\.dismiss) private var dismiss + + // , tabStore: ProfileTabSettingStore + init(accountType _: AccountType, userKey _: MicroBlogKey?, currentMediaPresenter _: ProfileMediaPresenter) { +// self.tabStore = tabStore + } + + var body: some View { + if let presenter = currentMediaPresenter { + ObservePresenter(presenter: presenter) { state in + AnyView( + WaterfallCollectionView(state: state) { item in + ProfileMediaItemView(media: item.media, appSetting: appSettings) { + let allImages = state.allMediaItems + if !allImages.isEmpty, + let mediaIndex = allImages.firstIndex(where: { $0 === item.media }) + { + FlareLog.debug("ProfileMediaListScreen Opening browser with \(allImages.count) images at index \(mediaIndex)") + showPhotoBrowser(media: item.media, images: allImages, initialIndex: mediaIndex) + } + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + ) + } + } else { + ProgressView() + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + } + + private func showPhotoBrowser(media _: UiMedia, images: [UiMedia], initialIndex: Int) { + let browser = JXPhotoBrowser() + browser.scrollDirection = .horizontal + browser.numberOfItems = { images.count } + browser.pageIndex = initialIndex + + // 设置淡入淡出动画 + browser.transitionAnimator = JXPhotoBrowserFadeAnimator() + + // 根据媒体类型返回对应的 Cell + browser.cellClassAtIndex = { index in + let media = images[index] + switch onEnum(of: media) { + case .video, .gif: + return MediaBrowserVideoCell.self + default: + return JXPhotoBrowserImageCell.self + } + } + + // 加载媒体内容 + browser.reloadCellAtIndex = { context in + guard context.index >= 0, context.index < images.count else { return } + let media = images[context.index] + + switch onEnum(of: media) { + case let .video(data): + if let url = URL(string: data.url), + let cell = context.cell as? MediaBrowserVideoCell + { + cell.load(url: url, previewUrl: URL(string: data.thumbnailUrl), isGIF: false) + } + case let .gif(data): + if let url = URL(string: data.url), + let cell = context.cell as? MediaBrowserVideoCell + { + cell.load(url: url, previewUrl: URL(string: data.previewUrl), isGIF: true) + } + case let .image(data): + if let url = URL(string: data.url), + let cell = context.cell as? JXPhotoBrowserImageCell + { + cell.imageView.kf.setImage(with: url, options: [ + .transition(.fade(0.25)), + .processor(DownsamplingImageProcessor(size: UIScreen.main.bounds.size)) + ]) + } + default: + break + } + } + + // Cell 将要显示 + browser.cellWillAppear = { cell, index in + let media = images[index] + switch onEnum(of: media) { + case .video, .gif: + if let videoCell = cell as? MediaBrowserVideoCell { + videoCell.willDisplay() + } + default: + break + } + } + + // Cell 将要消失 + browser.cellWillDisappear = { cell, index in + let media = images[index] + switch onEnum(of: media) { + case .video, .gif: + if let videoCell = cell as? MediaBrowserVideoCell { + videoCell.didEndDisplaying() + } + default: + break + } + } + + // 即将关闭时的处理 + browser.willDismiss = { _ in + // 返回 true 表示执行动画 + true + } + + browser.show() + } +} + +// - WaterfallCollectionView +struct WaterfallCollectionView: UIViewRepresentable { + let state: ProfileMediaState + let content: (ProfileMediaGridItem) -> AnyView + + init(state: ProfileMediaState, @ViewBuilder content: @escaping (ProfileMediaGridItem) -> some View) { + self.state = state + self.content = { AnyView(content($0)) } + } + + func makeCoordinator() -> Coordinator { + Coordinator(self) + } + + func makeUIView(context: Context) -> UICollectionView { + let layout = ZJFlexibleLayout(delegate: context.coordinator) + let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) + collectionView.backgroundColor = .clear + collectionView.dataSource = context.coordinator + collectionView.register(HostingCell.self, forCellWithReuseIdentifier: "Cell") + + collectionView.translatesAutoresizingMaskIntoConstraints = false + + return collectionView + } + + func updateUIView(_ collectionView: UICollectionView, context: Context) { + context.coordinator.parent = self + context.coordinator.updateItems() + + DispatchQueue.main.async { + collectionView.reloadData() + collectionView.collectionViewLayout.invalidateLayout() + } + } + + class Coordinator: NSObject, UICollectionViewDataSource, ZJFlexibleDataSource { + var parent: WaterfallCollectionView + var items: [ProfileMediaGridItem] = [] + + init(_ parent: WaterfallCollectionView) { + self.parent = parent + super.init() + updateItems() + } + + func updateItems() { + if case let .success(success) = onEnum(of: parent.state.mediaState) { + items = (0 ..< success.itemCount).compactMap { index -> ProfileMediaGridItem? in + guard let item = success.peek(index: index) else { return nil } + return ProfileMediaGridItem(id: Int(index), media: item.media, mediaState: item.status) + } + FlareLog.debug("ProfileMediaListScreen Updated items count: \(items.count)") + } + } + + // - UICollectionViewDataSource + func collectionView(_: UICollectionView, numberOfItemsInSection _: Int) -> Int { + FlareLog.debug("ProfileMediaListScreen numberOfItemsInSection: \(items.count)") + return items.count + } + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! HostingCell + let item = items[indexPath.item] + FlareLog.debug("ProfileMediaListScreen Setting up cell at index: \(indexPath.item)") + cell.setup(with: parent.content(item)) + return cell + } + + // - ZJFlexibleDataSource + func numberOfCols(at _: Int) -> Int { + 2 + } + + func sizeOfItemAtIndexPath(at indexPath: IndexPath) -> CGSize { + let item = items[indexPath.item] + let width = (UIScreen.main.bounds.width - spaceOfCells(at: 0) * 3) / 2 + let height: CGFloat + + switch onEnum(of: item.media) { + case let .image(data): + FlareLog.debug("ProfileMediaListScreen Image size - width: \(data.width), height: \(data.height)") + let aspectRatio = CGFloat(data.width / (data.height == 0 ? 1 : data.height)).isZero ? 1 : CGFloat(data.width / data.height) + height = width / aspectRatio + case let .video(data): + FlareLog.debug("ProfileMediaListScreen Video size - width: \(data.width), height: \(data.height)") + let aspectRatio = CGFloat(data.width / (data.height == 0 ? 1 : data.height)).isZero ? 1 : CGFloat(data.width / data.height) + height = width / aspectRatio + case let .gif(data): + FlareLog.debug("ProfileMediaListScreen Gif size - width: \(data.width), height: \(data.height)") + let aspectRatio = CGFloat(data.width / (data.height == 0 ? 1 : data.height)).isZero ? 1 : CGFloat(data.width / data.height) + height = width / aspectRatio + case .audio: + FlareLog.debug("ProfileMediaListScreen Audio item") + height = width + case .video: + FlareLog.debug("ProfileMediaListScreen Video item") + height = width + } + + FlareLog.debug("ProfileMediaListScreen Calculated size - width: \(width), height: \(height)") + return CGSize(width: width, height: height) + } + + func spaceOfCells(at _: Int) -> CGFloat { + 4 + } + + func sectionInsets(at _: Int) -> UIEdgeInsets { + UIEdgeInsets(top: 4, left: 4, bottom: 4, right: 4) // 减小边距 + } + + func sizeOfHeader(at _: Int) -> CGSize { + .zero + } + + func heightOfAdditionalContent(at _: IndexPath) -> CGFloat { + 0 + } + } +} + +// - HostingCell +class HostingCell: UICollectionViewCell { + private var hostingController: UIHostingController? + + func setup(with view: AnyView) { + if let hostingController { + hostingController.rootView = view + } else { + let controller = UIHostingController(rootView: view) + hostingController = controller + controller.view.backgroundColor = .clear + + contentView.addSubview(controller.view) + controller.view.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + controller.view.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), + controller.view.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), + controller.view.topAnchor.constraint(equalTo: contentView.topAnchor), + controller.view.bottomAnchor.constraint(equalTo: contentView.bottomAnchor) + ]) + } + } +} + +struct ProfileMediaItemView: View { + let media: UiMedia + let appSetting: AppSettings + let onTap: () -> Void + @State private var hideSensitive: Bool + + init(media: UiMedia, appSetting: AppSettings, onTap: @escaping () -> Void) { + self.media = media + self.appSetting = appSetting + self.onTap = onTap + + // 初始化 hideSensitive + switch onEnum(of: media) { + case let .image(image): + _hideSensitive = State(initialValue: !appSetting.appearanceSettings.showSensitiveContent && image.sensitive) + default: + _hideSensitive = State(initialValue: false) + } + } + + var body: some View { + ZStack { + switch onEnum(of: media) { + case .audio: + ZStack { + Image(systemName: "waveform") + .font(.largeTitle) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(Color.gray.opacity(0.2)) +// .blur(radius: media.url.sensitive ? 20 : 0) + +// if media.sensitive { +// Text("Sensitive Content") +// .foregroundColor(.white) +// .padding(8) +// .background(.ultraThinMaterial) +// .cornerRadius(8) +// } + } + case let .gif(gif): + ZStack { + KFImage(URL(string: gif.previewUrl)) + .cacheOriginalImage() + .loadDiskFileSynchronously() + .placeholder { + ProgressView() + } + .onFailure { _ in + Image(systemName: "exclamationmark.triangle") + .font(.largeTitle) + .foregroundColor(.red) + } + .resizable() + .scaledToFit() + + VStack { + HStack { + Text("GIF") + .font(.caption) + .padding(4) + .background(.ultraThinMaterial) + Spacer() + } + Spacer() + } + } + .onTapGesture { + onTap() + } + case let .image(image): + ZStack { + KFImage(URL(string: image.previewUrl)) + .cacheOriginalImage() + .loadDiskFileSynchronously() + .placeholder { + ProgressView() + } + .onFailure { _ in + Image(systemName: "exclamationmark.triangle") + .font(.largeTitle) + .foregroundColor(.red) + } + .resizable() + .scaledToFit() +// .fade(duration: 0.25) + .if(!appSetting.appearanceSettings.showSensitiveContent && image.sensitive && hideSensitive) { view in + view.blur(radius: 32) + } + + if !appSetting.appearanceSettings.showSensitiveContent, image.sensitive { + SensitiveContentButton( + hideSensitive: hideSensitive, + action: { hideSensitive.toggle() } + ) + } + } + case let .video(video): + ZStack { + KFImage(URL(string: video.thumbnailUrl)) + .cacheOriginalImage() + .loadDiskFileSynchronously() + .placeholder { + ProgressView() + } + .onFailure { _ in + Image(systemName: "exclamationmark.triangle") + .font(.largeTitle) + .foregroundColor(.red) + } + .resizable() + .scaledToFit() +// .fade(duration: 0.25) +// .blur(radius: video.sensitive ? 20 : 0) + + Image(systemName: "play.circle.fill") + .font(.largeTitle) + .foregroundColor(.white) + .shadow(radius: 2) +// +// if video.sensitive { +// Text("Sensitive Content") +// .foregroundColor(.white) +// .padding(8) +// .background(.ultraThinMaterial) +// .cornerRadius(8) +// } + } + } + } + .contentShape(Rectangle()) + .onTapGesture { + onTap() + } + } +} diff --git a/iosApp/iosApp/UI/Page/Profile/SwiftUIView/ProfileWithUserNameScreen.swift b/iosApp/iosApp/UI/Page/Profile/SwiftUIView/ProfileWithUserNameScreen.swift new file mode 100644 index 000000000..e929f4262 --- /dev/null +++ b/iosApp/iosApp/UI/Page/Profile/SwiftUIView/ProfileWithUserNameScreen.swift @@ -0,0 +1,71 @@ +import MarkdownUI +import OrderedCollections +import os.log +import shared +import SwiftUI + +// user profile 入口 +struct ProfileWithUserNameScreen: View { + @State private var presenter: ProfileWithUserNameAndHostPresenter + private let accountType: AccountType + @Environment(FlareRouter.self) var router + @Environment(FlareTheme.self) private var theme + + init(accountType: AccountType, userName: String, host: String) { + self.accountType = accountType + presenter = .init(userName: userName, host: host, accountType: accountType) + os_log("[📔][ProfileWithUserNameScreen - init]ProfileWithUserNameScreen: userName=%{public}@, host=%{public}@", log: .default, type: .debug, userName, host) + } + + var body: some View { + ObservePresenter(presenter: presenter) { state in + ZStack { + switch onEnum(of: state.user) { + case .error: + Text("error") + .onAppear { + FlareLog.error("ProfileWithUserNameScreen 加载用户信息失败") + } + case .loading: + List { + 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()) + } + .scrollContentBackground(.hidden) + .listRowBackground(theme.primaryBackgroundColor) + .onAppear { + FlareLog.debug("ProfileWithUserNameScreen 正在加载用户信息...") + } + case let .success(data): + // (lldb) po state dev.dimension.flare.ui.presenter.profile.ProfileWithUserNameAndHostPresenter$body$1@1d3717a0 +// (lldb) p state +// (SharedUserState) 0x0000000000000000 +// +// let loadedUserInfo = ProfileUserInfo.from(state: state as! ProfileNewState) + + ProfileTabScreenUikit( + accountType: accountType, + 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/Profile/UIKitView/ProfileMediaViewController.swift b/iosApp/iosApp/UI/Page/Profile/UIKitView/ProfileMediaViewController.swift new file mode 100644 index 000000000..2f1eb9aae --- /dev/null +++ b/iosApp/iosApp/UI/Page/Profile/UIKitView/ProfileMediaViewController.swift @@ -0,0 +1,227 @@ +import JXPagingView +import JXPhotoBrowser +import JXSegmentedView +import Kingfisher +import MJRefresh +import shared +import SwiftUI +import UIKit + +class ProfileMediaViewController: UIViewController { + // - Properties + private var presenterWrapper: ProfileMediaPresenterWrapper? + private var scrollCallback: ((UIScrollView) -> Void)? + private var appSettings: AppSettings? + + private lazy var collectionView: UICollectionView = { + let layout = ZJFlexibleLayout(delegate: self) + let collection = UICollectionView(frame: .zero, collectionViewLayout: layout) + collection.backgroundColor = .clear + collection.delegate = self + collection.dataSource = self + collection.register(MediaCollectionViewCell.self, forCellWithReuseIdentifier: "MediaCell") + return collection + }() + + private var items: [ProfileMediaGridItem] = [] + + // - Lifecycle + override func viewDidLoad() { + super.viewDidLoad() + setupUI() + setupRefresh() + } + + deinit { + presenterWrapper = nil + scrollCallback = nil + } + + // - Setup + private func setupUI() { + view.addSubview(collectionView) + collectionView.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + collectionView.topAnchor.constraint(equalTo: view.topAnchor), + collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor) + ]) + } + + private func setupRefresh() { + // 下拉刷新 + collectionView.mj_header = MJRefreshNormalHeader(refreshingBlock: { [weak self] in + Task { +// if let mediaPresenterWrapper = self?.presenterWrapper, +// case .success(let data) = onEnum(of: mediaPresenterWrapper.presenter.models.value.mediaState) { +// data.retry() +// await MainActor.run { + self?.collectionView.mj_header?.endRefreshing() +// } +// } + } + }) + + // 上拉加载更多 + collectionView.mj_footer = MJRefreshAutoNormalFooter(refreshingBlock: { [weak self] in + Task { +// if let mediaPresenterWrapper = self?.presenterWrapper, +// case .success(let data) = onEnum(of: mediaPresenterWrapper.presenter.models.value.mediaState) { +// // 检查是否还有更多数据 +// let appendState = data.appendState +// if let notLoading = appendState as? Paging_commonLoadState.NotLoading, +// !notLoading.endOfPaginationReached { +// data.retry() +// } +// await MainActor.run { +// if let notLoading = appendState as? Paging_commonLoadState.NotLoading, +// notLoading.endOfPaginationReached { +// self?.collectionView.mj_footer?.endRefreshingWithNoMoreData() +// } else { + self?.collectionView.mj_footer?.endRefreshing() +// } +// } +// } + } + }) + } + + // - Public Methods + func updateMediaPresenter(presenterWrapper: ProfileMediaPresenterWrapper) { + self.presenterWrapper = presenterWrapper + // 监听数据变化 + Task { @MainActor in + let presenter = presenterWrapper.presenter + for await state in presenter.models { + self.handleState(state.mediaState) + } + } + } + + func configure(with appSettings: AppSettings) { + self.appSettings = appSettings + } + + // - Private Methods + private func handleState(_ state: PagingState) { + if case let .success(data) = onEnum(of: state) { + items = (0 ..< data.itemCount).compactMap { index -> ProfileMediaGridItem? in + guard let item = data.peek(index: index) else { return nil } + return ProfileMediaGridItem(id: Int(index), media: item.media, mediaState: item.status) + } + + collectionView.reloadData() + collectionView.mj_header?.endRefreshing() + collectionView.mj_footer?.endRefreshing() + } else { + items = [] + collectionView.reloadData() + collectionView.mj_header?.endRefreshing() + collectionView.mj_footer?.endRefreshing() + } + } + + private func showPhotoBrowser(media: UiMedia, images: [UiMedia], initialIndex: Int) { + Task { @MainActor in + + PhotoBrowserManager.shared.showPhotoBrowser( + media: media, + images: images, + initialIndex: initialIndex + ) + } + } +} + +// - UICollectionViewDataSource + +extension ProfileMediaViewController: UICollectionViewDataSource { + func collectionView(_: UICollectionView, numberOfItemsInSection _: Int) -> Int { + items.count + } + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "MediaCell", for: indexPath) as! MediaCollectionViewCell + let item = items[indexPath.item] + + cell.configure(with: item.media, appSettings: appSettings ?? AppSettings()) { [weak self] in + guard let self else { return } + let allImages = items.map(\.media) + if !allImages.isEmpty { + showPhotoBrowser(media: item.media, images: allImages, initialIndex: indexPath.item) + } + } + + return cell + } +} + +// - UICollectionViewDelegate +extension ProfileMediaViewController: UICollectionViewDelegate { + func scrollViewDidScroll(_ scrollView: UIScrollView) { + scrollCallback?(scrollView) + } +} + +// - ZJFlexibleDataSource +extension ProfileMediaViewController: ZJFlexibleDataSource { + func numberOfCols(at _: Int) -> Int { + 2 + } + + func sizeOfItemAtIndexPath(at indexPath: IndexPath) -> CGSize { + let item = items[indexPath.item] + let width = (UIScreen.main.bounds.width - spaceOfCells(at: 0) * 3) / 2 + let height: CGFloat + + switch onEnum(of: item.media) { + case let .image(data): + let aspectRatio = CGFloat(data.width / (data.height == 0 ? 1 : data.height)).isZero ? 1 : CGFloat(data.width / data.height) + height = width / aspectRatio + case let .video(data): + let aspectRatio = CGFloat(data.width / (data.height == 0 ? 1 : data.height)).isZero ? 1 : CGFloat(data.width / data.height) + height = width / aspectRatio + case let .gif(data): + let aspectRatio = CGFloat(data.width / (data.height == 0 ? 1 : data.height)).isZero ? 1 : CGFloat(data.width / data.height) + height = width / aspectRatio + case .audio: + height = width + case .video: + height = width + } + + return CGSize(width: width, height: height) + } + + func spaceOfCells(at _: Int) -> CGFloat { + 4 + } + + func sectionInsets(at _: Int) -> UIEdgeInsets { + UIEdgeInsets(top: 4, left: 4, bottom: 4, right: 4) + } + + func sizeOfHeader(at _: Int) -> CGSize { + .zero + } + + func heightOfAdditionalContent(at _: IndexPath) -> CGFloat { + 0 + } +} + +// - JXPagingViewListViewDelegate +extension ProfileMediaViewController: JXPagingViewListViewDelegate { + func listView() -> UIView { + view + } + + func listScrollView() -> UIScrollView { + collectionView + } + + func listViewDidScrollCallback(callback: @escaping (UIScrollView) -> Void) { + scrollCallback = callback + } +} diff --git a/iosApp/iosApp/UI/Page/Profile/UIKitView/ProfileRefreshViewController.swift b/iosApp/iosApp/UI/Page/Profile/UIKitView/ProfileRefreshViewController.swift new file mode 100644 index 000000000..0cdc5dca7 --- /dev/null +++ b/iosApp/iosApp/UI/Page/Profile/UIKitView/ProfileRefreshViewController.swift @@ -0,0 +1,723 @@ +import Generated +import JXPagingView +import JXSegmentedView +import Kingfisher +import MarkdownUI +import MJRefresh +import os.log +import shared +import SwiftUI +import UIKit + +extension JXPagingListContainerView: JXSegmentedViewListContainer {} + +class ProfileNewRefreshViewController: UIViewController { + private var theme: FlareTheme? + private var userInfo: ProfileUserInfo? + private var state: ProfileNewState? + private var selectedTab: Binding? + private var isShowAppBar: Binding? + private var horizontalSizeClass: UserInterfaceSizeClass? + private var appSettings: AppSettings? + 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? + + var pagingView: JXPagingView! + var userHeaderView: ProfileNewHeaderView! + var segmentedView: JXSegmentedView! + var segmentedDataSource: JXSegmentedTitleDataSource! + var isHeaderRefreshed = false + private var titles: [String] = [] + private var refreshControl: ProfileStretchRefreshControl? + + private var navigationBar: UINavigationBar = { + let nav = UINavigationBar() + return nav + }() + + private var lastContentOffset: CGFloat = 0 + private let navigationBarHeight: CGFloat = 44 + private var isNavigationBarHidden = false + + private static let BANNER_HEIGHT: CGFloat = 200 + private var isAppBarTitleVisible = false + + private var _cachedSafeAreaTop: CGFloat? + + func configure( + userInfo: ProfileUserInfo?, + state: ProfileNewState, + selectedTab: Binding, + isShowAppBar: Binding, + horizontalSizeClass: UserInterfaceSizeClass?, + appSettings: AppSettings, + accountType: AccountType, + userKey: MicroBlogKey?, + tabStore: ProfileTabSettingStore, + mediaPresenterWrapper: ProfileMediaPresenterWrapper, + presenterWrapper: ProfilePresenterWrapper, + theme: FlareTheme + ) { + self.userInfo = userInfo + self.state = state + self.selectedTab = selectedTab + self.isShowAppBar = isShowAppBar + self.horizontalSizeClass = horizontalSizeClass + self.appSettings = appSettings + self.accountType = accountType + self.userKey = userKey + self.tabStore = tabStore + self.mediaPresenterWrapper = mediaPresenterWrapper + self.presenterWrapper = presenterWrapper + self.theme = theme + + setupThemeObserver() + + let isOwnProfile = userKey == nil + + if isOwnProfile { + // 自己的Profile:根据原有逻辑控制AppBar + if let showAppBar = isShowAppBar.wrappedValue { + navigationController?.setNavigationBarHidden(!showAppBar, animated: false) + } else { + // 初始状态,显示导航栏 + navigationController?.setNavigationBarHidden(false, animated: false) + isShowAppBar.wrappedValue = true + } + } else { + // 其他用户Profile:AppBar永远显示 + navigationController?.setNavigationBarHidden(false, animated: false) + + isShowAppBar.wrappedValue = true + } + + // 🔑 设置导航按钮 + setupNavigationButtons(isOwnProfile: isOwnProfile) + + // 更新UI + updateUI() + + // 配置头部视图 + if let userInfo { + userHeaderView?.configure(with: userInfo, state: state, theme: theme) + + // 设置关注按钮回调 + userHeaderView?.onFollowClick = { [weak self] relation in + os_log("[📔][ProfileRefreshViewController]点击关注按钮: userKey=%{public}@", log: .default, type: .debug, userInfo.profile.key.description) + state.follow(userKey: userInfo.profile.key, data: relation) + } + } + + if !isOwnProfile { + navigationController?.navigationBar.alpha = 1.0 + } + } + + private func updateUI() { + guard let userInfo else { return } + + // 更新头部视图 + userHeaderView?.configure(with: userInfo) + + // 更新标签页 + if let tabStore { + // 从 tabStore.availableTabs 获取标题 + titles = tabStore.availableTabs.map { tab in + switch tab.metaData.title { + case let .text(title): + title + case let .localized(key): + NSLocalizedString(key, comment: "") + } + } + segmentedDataSource.titles = titles + segmentedView.reloadData() + + // 如果有选中的标签,更新选中状态 + if let selectedTab { + segmentedView.defaultSelectedIndex = selectedTab.wrappedValue + } + + pagingView.reloadData() + } + } + + private var cachedSafeAreaTop: CGFloat { + if let cached = _cachedSafeAreaTop { + return cached + } + let window = UIApplication.shared.windows.first { $0.isKeyWindow } + let safeAreaTop = window?.safeAreaInsets.top ?? 0 + _cachedSafeAreaTop = safeAreaTop + return safeAreaTop + } + + private func clearSafeAreaCache() { + _cachedSafeAreaTop = nil + } + + override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { + super.viewWillTransition(to: size, with: coordinator) + + // Clear safe area cache when device orientation changes + coordinator.animate(alongsideTransition: nil) { _ in + self.clearSafeAreaCache() + } + } + + override func viewDidLoad() { + super.viewDidLoad() + + // 设置导航栏 + setupNavigationBar() + + // 初始时显示系统导航栏,隐藏自定义导航栏和返回按钮 + navigationController?.setNavigationBarHidden(false, animated: false) + navigationBar.alpha = 0 + isNavigationBarHidden = false + + // 允许系统返回手势 + navigationController?.interactivePopGestureRecognizer?.isEnabled = true + + // 配置头部视图 - 只设置宽度,让高度自适应 + userHeaderView = ProfileNewHeaderView(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 0)) + + // 新的配置代码 + if let userInfo { + userHeaderView?.configure(with: userInfo, state: state, theme: theme) + } + + // 配置分段控制器 + segmentedView = JXSegmentedView(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 50)) + segmentedDataSource = JXSegmentedTitleDataSource() + segmentedDataSource.titles = titles // 使用动态titles + segmentedDataSource.titleNormalColor = .gray + segmentedDataSource.titleSelectedColor = .label + segmentedDataSource.titleNormalFont = .systemFont(ofSize: 15) + segmentedDataSource.titleSelectedFont = .systemFont(ofSize: 15) + segmentedDataSource.isTitleColorGradientEnabled = true + segmentedView.dataSource = segmentedDataSource + + // 添加选中回调 + segmentedView.delegate = self + + let indicator = JXSegmentedIndicatorLineView() + indicator.indicatorColor = theme != nil ? UIColor(theme!.tintColor) : .systemBlue + indicator.indicatorWidth = 30 + segmentedView.indicators = [indicator] + + // 添加底部分割线 + let lineWidth = 1 / UIScreen.main.scale + let bottomLineView = UIView() + bottomLineView.backgroundColor = .separator + bottomLineView.frame = CGRect(x: 0, y: segmentedView.bounds.height - lineWidth, width: segmentedView.bounds.width, height: lineWidth) + bottomLineView.autoresizingMask = .flexibleWidth + segmentedView.addSubview(bottomLineView) + + // 配置PagingView + pagingView = JXPagingView(delegate: self) + view.addSubview(pagingView) + + // 关联segmentedView和pagingView + segmentedView.listContainer = pagingView.listContainerView + + // 配置刷新控制器 + setupRefreshControl() + + // 添加滚动监听 + addScrollObserver() + } + + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + pagingView.frame = view.bounds + } + + private func setupRefreshControl() { + let refreshControl = ProfileStretchRefreshControl() + refreshControl.headerView = userHeaderView + refreshControl.refreshHandler = { [weak self] in + self?.refreshContent() + } + userHeaderView.addSubview(refreshControl) + refreshControl.frame = CGRect(x: 0, y: 0, width: view.bounds.width, height: 150) + self.refreshControl = refreshControl + } + + private func addScrollObserver() { + let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(_:))) + pagingView.mainTableView.addGestureRecognizer(panGesture) + panGesture.delegate = self + } + + @objc private func handlePanGesture(_: UIPanGestureRecognizer) { + let offset = pagingView.mainTableView.contentOffset.y + refreshControl?.scrollViewDidScroll(withOffset: offset) + + let isOwnProfile = userKey == nil + if !isOwnProfile { + updateNavigationBarVisibility(with: offset) + } + } + + private func refreshContent() { + let workItem = DispatchWorkItem { + self.isHeaderRefreshed = true + self.refreshControl?.endRefreshing() + self.pagingView.reloadData() + + if let currentList = self.pagingView.validListDict[self.segmentedView.selectedIndex] as? ProfileNewListViewController { + currentList.headerRefresh() + } + Task { + if let currentList = self.pagingView.validListDict[self.segmentedView.selectedIndex] { + if let timelineVC = currentList as? TimelineViewController, + 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 { + self.isHeaderRefreshed = true + self.refreshControl?.endRefreshing() + } + } + } + } + + DispatchQueue.main.asyncAfter(deadline: .now() + 2, execute: workItem) + } + + private func setupNavigationBar() { + // 设置导航栏frame + navigationBar.frame = CGRect(x: 0, y: 0, width: view.bounds.width, height: navigationBarHeight) + + // 设置导航栏项 + let navigationItem = UINavigationItem() + let backButton = UIBarButtonItem(image: UIImage(systemName: "chevron.left"), style: .plain, target: self, action: #selector(backButtonTapped)) + navigationItem.leftBarButtonItem = backButton + + // 添加更多按钮到导航栏右侧 + let moreButton = UIBarButtonItem(image: UIImage(systemName: "ellipsis.circle"), style: .plain, target: self, action: #selector(handleMoreMenuTap)) + navigationItem.rightBarButtonItem = moreButton + + navigationBar.items = [navigationItem] + + // 添加到视图 + view.addSubview(navigationBar) + + // 初始时隐藏 moreButton + moreButton.isEnabled = false + navigationItem.rightBarButtonItem = nil + } + + @objc private func backButtonTapped() { + // 返回上一页 + if let navigationController = parent?.navigationController { + navigationController.popViewController(animated: true) + } else { + dismiss(animated: true) + } + } + + @objc private func handleMoreMenuTap() { + os_log("[ProfileRefreshViewController] More menu button tapped", log: .default, type: .debug) + + guard let state, + case let .success(isMe) = onEnum(of: state.isMe), + !isMe.data.boolValue else { return } + + let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) + + if case let .success(user) = onEnum(of: state.userState) { + alertController.addAction(UIAlertAction(title: NSLocalizedString("report", comment: ""), style: .destructive) { [weak self] _ in + Task { + do { + try await state.report(userKey: user.data.key) + await MainActor.run { + ToastView(icon: UIImage(systemName: "checkmark.circle"), message: "Success").show() + } + } catch { + await MainActor.run { + ToastView(icon: UIImage(systemName: "exclamationmark.circle"), message: "Failed").show() + } + } + } + }) + } + + alertController.addAction(UIAlertAction(title: NSLocalizedString("cancel", comment: ""), style: .cancel)) + + present(alertController, animated: true) + } + + private func updateNavigationBarVisibility(with offset: CGFloat) { + if offset > Self.BANNER_HEIGHT, !isAppBarTitleVisible { + isAppBarTitleVisible = true + UIView.animate(withDuration: 0.25) { + self.navigationController?.navigationBar.alpha = 0.9 + } + updateAppBarTitle(showUserName: true) + } else if offset <= Self.BANNER_HEIGHT, isAppBarTitleVisible { + isAppBarTitleVisible = false + UIView.animate(withDuration: 0.25) { + self.navigationController?.navigationBar.alpha = 1.0 + } + updateAppBarTitle(showUserName: false) + } + + lastContentOffset = offset + } + + private func updateAppBarTitle(showUserName: Bool) { + let isOwnProfile = userKey == nil + + // Only process title for other user profiles + guard !isOwnProfile else { return } + + if showUserName { + // Show user name title + if let userInfo { + let displayName = userInfo.profile.name.raw.isEmpty ? userInfo.profile.handle : userInfo.profile.name.raw + navigationController?.navigationBar.topItem?.title = displayName + } + } else { + // Hide title + navigationController?.navigationBar.topItem?.title = nil + } + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + let isOwnProfile = userKey == nil + + // AppBar永远显示 + if isOwnProfile { + navigationController?.setNavigationBarHidden(false, animated: animated) + } else { + navigationController?.setNavigationBarHidden(false, animated: animated) + navigationController?.navigationBar.alpha = 1.0 + } + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + + // 离开页面时重置状态,不然 详情页会导致没appbar + isShowAppBar?.wrappedValue = true + + // 确保系统导航栏状态正确 + navigationController?.setNavigationBarHidden(false, animated: animated) + } + + deinit { + cleanupListViewControllers() + + if let themeObserver { + NotificationCenter.default.removeObserver(themeObserver) + } + } + + private func cleanupListViewControllers() { + listViewControllers.removeAll() + } + + private func setupNavigationButtons(isOwnProfile: Bool) { + if isOwnProfile { + // 自己的Profile:清除所有导航按钮 + navigationController?.navigationBar.topItem?.leftBarButtonItem = nil + navigationController?.navigationBar.topItem?.rightBarButtonItem = nil + } else { + // 其他用户Profile:只设置更多按钮,使用系统默认返回按钮 + navigationController?.navigationBar.topItem?.leftBarButtonItem = nil // 使用系统默认返回按钮 + let moreButton = UIBarButtonItem(image: UIImage(systemName: "ellipsis.circle"), style: .plain, target: self, action: #selector(handleMoreMenuTap)) + navigationController?.navigationBar.topItem?.rightBarButtonItem = moreButton + } + } + + @objc private func handleBackButtonTap() { + os_log("[📔][ProfileRefreshViewController]点击返回按钮", log: .default, type: .debug) + + // 在返回前重置导航状态 // 离开页面时重置状态,不然 详情页会导致没appbar + isShowAppBar?.wrappedValue = true + + // 确保导航栏可见 + navigationController?.setNavigationBarHidden(false, animated: true) + + // 执行返回操作 + navigationController?.popViewController(animated: true) + } + + private func setupThemeObserver() { + if let existingObserver = themeObserver { + NotificationCenter.default.removeObserver(existingObserver) + } + + themeObserver = NotificationCenter.default.addObserver( + forName: NSNotification.Name("FlareThemeDidChange"), + object: nil, + queue: .main + ) { [weak self] _ in + self?.applyCurrentTheme() + } + applyCurrentTheme() + } + + private func applyCurrentTheme() { + guard let theme else { return } + + // 应用主题到视图控制器的主视图 + view.backgroundColor = UIColor(theme.primaryBackgroundColor) + + // 应用主题到 headerView + userHeaderView?.theme = theme + userHeaderView?.applyTheme() + + // 应用主题到 segmentedView + segmentedDataSource.titleSelectedColor = UIColor(theme.labelColor) + if let indicators = segmentedView.indicators as? [JXSegmentedIndicatorLineView] { + for indicator in indicators { + indicator.indicatorColor = UIColor(theme.tintColor) + } + } + segmentedView.backgroundColor = UIColor(theme.primaryBackgroundColor) + + // 应用主题到所有列表视图控制器 + for (_, listVC) in listViewControllers { + if let timelineVC = listVC as? TimelineViewController { + timelineVC.view.backgroundColor = UIColor(theme.primaryBackgroundColor) + } else if let mediaVC = listVC as? ProfileMediaViewController { + mediaVC.view.backgroundColor = UIColor(theme.primaryBackgroundColor) + } + } + } +} + +extension ProfileNewRefreshViewController: UIGestureRecognizerDelegate { + func gestureRecognizer(_: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith _: UIGestureRecognizer) -> Bool { + true + } +} + +extension ProfileNewRefreshViewController: JXPagingViewDelegate { + func tableHeaderViewHeight(in _: JXPagingView) -> Int { + // 获取内容高度并添加额外的空间 + let contentHeight = userHeaderView.getContentHeight() + // 添加额外的 padding 确保内容不会被遮挡 + let totalHeight = contentHeight // 添加 100 点的额外空间 + return Int(totalHeight) + } + + func tableHeaderView(in _: JXPagingView) -> UIView { + userHeaderView + } + + func heightForPinSectionHeader(in _: JXPagingView) -> Int { + let safeAreaTop = cachedSafeAreaTop + + // 布局常量 + let navigationBarHeight: CGFloat = 44 // AppBar标准高度 + let tabBarHeight: CGFloat = 50 // TabBar高度 + + let isOwnProfile = userKey == nil + + if isOwnProfile { + // Own profile: SafeArea + TabBar + return Int(safeAreaTop + tabBarHeight) + } else { + // Other user profile: SafeArea + AppBar + TabBar + return Int(safeAreaTop + navigationBarHeight + tabBarHeight) + } + } + + func viewForPinSectionHeader(in _: JXPagingView) -> UIView { + let containerView = UIView() + containerView.backgroundColor = .systemBackground + containerView.isUserInteractionEnabled = true + + // Use cached safe area for performance optimization + let safeAreaTop = cachedSafeAreaTop + + let navigationBarHeight: CGFloat = 44 + let tabBarHeight: CGFloat = 50 + + let isOwnProfile = userKey == nil + + let tabBarY: CGFloat = if isOwnProfile { + safeAreaTop + } else { + safeAreaTop + navigationBarHeight - 18 + } + + // 调整 segmentedView 的位置 + segmentedView.frame = CGRect(x: 0, y: tabBarY, width: view.bounds.width, height: tabBarHeight) + + // 创建一个按钮容器,确保它在 segmentedView 之上 + let buttonContainer = UIView(frame: CGRect(x: 0, y: 0, width: 80, height: 50 + safeAreaTop)) + buttonContainer.isUserInteractionEnabled = true + buttonContainer.backgroundColor = .clear + + containerView.addSubview(segmentedView) + if let theme { + containerView.backgroundColor = UIColor(theme.primaryBackgroundColor) + } + return containerView + } + + func numberOfLists(in _: JXPagingView) -> Int { + tabStore?.availableTabs.count ?? 0 + } + + func pagingView(_: JXPagingView, initListAtIndex index: Int) -> JXPagingViewListViewDelegate { + // 如果已经存在,直接返回 + if let existingVC = listViewControllers[index] { + return existingVC + } + + guard let tabStore, + index < tabStore.availableTabs.count + else { + let emptyVC = UIViewController() + return emptyVC as! JXPagingViewListViewDelegate + } + + let tab = tabStore.availableTabs[index] + + if tab is FLProfileMediaTabItem { + let mediaVC = ProfileMediaViewController() + if let mediaPresenterWrapper { + mediaVC.updateMediaPresenter(presenterWrapper: mediaPresenterWrapper) + } + if let appSettings { + mediaVC.configure(with: appSettings) + } + // 应用主题背景色 + if let theme { + mediaVC.view.backgroundColor = UIColor(theme.primaryBackgroundColor) + } + // 保存到字典中 + listViewControllers[index] = mediaVC + return mediaVC + } else { + let timelineVC = TimelineViewController() + + if let presenter = tabStore.currentPresenter { + os_log("[📔][ProfileNewRefreshViewController] updatePresenter start", log: .default, type: .debug) + + timelineVC.updatePresenter(presenter) + } + // 应用主题背景色 + if let theme { + timelineVC.view.backgroundColor = UIColor(theme.primaryBackgroundColor) + } + // 保存到字典中 + listViewControllers[index] = timelineVC + return timelineVC + } + } +} + +// 添加 JXSegmentedViewDelegate +extension ProfileNewRefreshViewController: JXSegmentedViewDelegate { + func segmentedView(_: JXSegmentedView, didSelectedItemAt index: Int) { + os_log("[📔][ProfileNewScreen]选择标签页: index=%{public}d", log: .default, type: .debug, index) + + // 更新选中状态 + selectedTab?.wrappedValue = index + + // 更新当前选中的标签页的presenter + if let tabStore, index < tabStore.availableTabs.count { + let selectedTab = tabStore.availableTabs[index] + + // 直接更新 presenter + tabStore.updateCurrentPresenter(for: selectedTab) + + // 获取当前的列表视图并更新其 presenter + if let currentList = pagingView.validListDict[index] { + if let timelineVC = currentList as? TimelineViewController, + let presenter = tabStore.currentPresenter + { + os_log("[📔][ProfileNewRefreshViewController] updatePresenter start", log: .default, type: .debug) + + // 更新 timeline presenter + timelineVC.updatePresenter(presenter) + } else if let mediaVC = currentList as? ProfileMediaViewController, + let mediaPresenterWrapper + { + os_log("[📔][ProfileNewRefreshViewController] setupUI end", log: .default, type: .debug) + + // 更新 media presenter + mediaVC.updateMediaPresenter(presenterWrapper: mediaPresenterWrapper) + } + } + } + } + + func segmentedView(_: JXSegmentedView, didClickSelectedItemAt index: Int) { + // 如果点击已选中的标签,可以触发刷新 + if let currentList = pagingView.validListDict[index] as? ProfileNewListViewController { + currentList.tableView.mj_header?.beginRefreshing() + } + } +} + +extension ProfileNewRefreshViewController { + func needsProfileUpdate( + userInfo: ProfileUserInfo?, + selectedTab: Int, + accountType: AccountType, + userKey: MicroBlogKey? + ) -> Bool { + // 1. 检查用户信息是否变化 + let userChanged = self.userInfo?.profile.key.description != userInfo?.profile.key.description + + // 2. 检查选中Tab是否变化 + let tabChanged = self.selectedTab?.wrappedValue != selectedTab + + // 3. 检查账户类型是否变化(更精确的比较) + let currentAccountKey = (self.accountType as? AccountTypeSpecific)?.accountKey.description ?? String(describing: self.accountType) + let newAccountKey = (accountType as? AccountTypeSpecific)?.accountKey.description ?? String(describing: accountType) + let accountChanged = currentAccountKey != newAccountKey + + // 4. 检查用户Key是否变化 + let userKeyChanged = self.userKey?.description != userKey?.description + + // 5. 首次配置检查(如果当前userInfo为nil,说明是首次配置) + let isFirstConfiguration = self.userInfo == nil && userInfo != nil + + let needsUpdate = userChanged || tabChanged || accountChanged || userKeyChanged || isFirstConfiguration + + if needsUpdate { + os_log("[ProfileNewRefreshViewController] Update needed: user=%{public}@, tab=%{public}@, account=%{public}@, userKey=%{public}@, first=%{public}@", + log: .default, type: .debug, + userChanged ? "changed" : "same", + tabChanged ? "changed" : "same", + accountChanged ? "changed" : "same", + userKeyChanged ? "changed" : "same", + isFirstConfiguration ? "true" : "false") + } + + return needsUpdate + } +} diff --git a/iosApp/iosApp/UI/Page/Profile/UIKitView/ProfileStretchRefreshControl.swift b/iosApp/iosApp/UI/Page/Profile/UIKitView/ProfileStretchRefreshControl.swift new file mode 100644 index 000000000..a3c9a4454 --- /dev/null +++ b/iosApp/iosApp/UI/Page/Profile/UIKitView/ProfileStretchRefreshControl.swift @@ -0,0 +1,55 @@ +import UIKit + +class ProfileStretchRefreshControl: UIControl { + weak var headerView: ProfileNewHeaderView? + private let activityIndicator = UIActivityIndicatorView(style: .medium) + private var isRefreshing: Bool = false + var refreshHandler: (() -> Void)? + + override init(frame: CGRect) { + super.init(frame: frame) + setupUI() + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupUI() { + activityIndicator.hidesWhenStopped = true + addSubview(activityIndicator) + } + + override func layoutSubviews() { + super.layoutSubviews() + // 将指示器放在Banner图片的中间 + activityIndicator.center = CGPoint(x: bounds.midX, y: 75) // Banner高度150的一半 + } + + func beginRefreshing() { + guard !isRefreshing else { return } + isRefreshing = true + activityIndicator.startAnimating() + refreshHandler?() + } + + func endRefreshing() { + isRefreshing = false + activityIndicator.stopAnimating() + // 恢复Banner图片大小 + headerView?.updateBannerStretch(withOffset: 0) + } + + func scrollViewDidScroll(withOffset offset: CGFloat) { + if offset < 0 { + // 更新Banner拉伸效果 + headerView?.updateBannerStretch(withOffset: abs(offset)) + + // 如果下拉超过阈值且没有在刷新,开始刷新 + if abs(offset) > 60, !isRefreshing { + beginRefreshing() + } + } + } +} diff --git a/iosApp/iosApp/UI/Page/Profile/UIKitView/ProfileTabScreenUikit.swift b/iosApp/iosApp/UI/Page/Profile/UIKitView/ProfileTabScreenUikit.swift new file mode 100644 index 000000000..693187ce2 --- /dev/null +++ b/iosApp/iosApp/UI/Page/Profile/UIKitView/ProfileTabScreenUikit.swift @@ -0,0 +1,179 @@ +import Combine +import Kingfisher +import MarkdownUI +import OrderedCollections +import os.log +import shared +import SwiftUI + +struct ProfileTabScreenUikit: View { + let accountType: AccountType + let userKey: MicroBlogKey? + let showBackButton: Bool + + @StateObject private var presenterWrapper: ProfilePresenterWrapper + @StateObject private var mediaPresenterWrapper: ProfileMediaPresenterWrapper + @StateObject private var tabStore: ProfileTabSettingStore + @State private var selectedTab: Int = 0 + @State private var userInfo: ProfileUserInfo? + @Environment(FlareTheme.self) private var theme: FlareTheme + + // 横屏 竖屏 + @Environment(\.horizontalSizeClass) private var horizontalSizeClass + @Environment(\.appSettings) private var appSettings + @Environment(FlareRouter.self) private var router + @Environment(FlareMenuState.self) private var menuState + + init( + accountType: AccountType, userKey: MicroBlogKey?, showBackButton: Bool = true + ) { + self.accountType = accountType + self.userKey = userKey + self.showBackButton = showBackButton + + let service = ProfilePresenterService.shared + + _presenterWrapper = StateObject( + wrappedValue: service.getOrCreatePresenter(accountType: accountType, userKey: userKey)) + _mediaPresenterWrapper = StateObject( + wrappedValue: service.getOrCreateMediaPresenter(accountType: accountType, userKey: userKey)) + _tabStore = StateObject( + wrappedValue: service.getOrCreateTabStore(userKey: userKey)) + + os_log( + "[📔][ProfileNewScreen - optimized]优化初始化完成: accountType=%{public}@, userKey=%{public}@", + log: .default, type: .debug, + String(describing: accountType), userKey?.description ?? "nil" + ) + + os_log("[📔][ProfilePresenterService] %{public}@", log: .default, type: .debug, service.getCacheInfo()) + } + + var body: some View { + ObservePresenter(presenter: presenterWrapper.presenter) { state in + let userInfo = ProfileUserInfo.from(state: state as! ProfileNewState) + + // 打印 isShowAppBar 的值 + let _ = os_log( + "[📔][ProfileTabScreen] userKey=%{public}@", log: .default, type: .debug, + 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, + selectedTab: $selectedTab, + isShowAppBar: Binding( + get: { presenterWrapper.isShowAppBar }, + set: { presenterWrapper.updateNavigationState(showAppBar: $0) } + ), + + horizontalSizeClass: horizontalSizeClass, + appSettings: appSettings, + accountType: accountType, + userKey: userKey, + tabStore: tabStore, + mediaPresenterWrapper: mediaPresenterWrapper, + presenterWrapper: presenterWrapper, + theme: theme + ) + .ignoresSafeArea(edges: .top) + + } else { + ProfileNewRefreshViewControllerWrapper( + userInfo: userInfo, + state: state as! ProfileNewState, + selectedTab: $selectedTab, + isShowAppBar: Binding( + get: { presenterWrapper.isShowAppBar }, + set: { presenterWrapper.updateNavigationState(showAppBar: $0) } + ), + + horizontalSizeClass: horizontalSizeClass, + appSettings: appSettings, + accountType: accountType, + userKey: userKey, + tabStore: tabStore, + mediaPresenterWrapper: mediaPresenterWrapper, + presenterWrapper: presenterWrapper, + theme: theme + ) + .ignoresSafeArea(edges: .top) + } + } + } +} + +struct ProfileNewRefreshViewControllerWrapper: UIViewControllerRepresentable { + let userInfo: ProfileUserInfo? + let state: ProfileNewState + @Binding var selectedTab: Int + @Binding var isShowAppBar: Bool? + let horizontalSizeClass: UserInterfaceSizeClass? + let appSettings: AppSettings + let accountType: AccountType + let userKey: MicroBlogKey? + let tabStore: ProfileTabSettingStore + let mediaPresenterWrapper: ProfileMediaPresenterWrapper + let presenterWrapper: ProfilePresenterWrapper + let theme: FlareTheme + + func makeUIViewController(context _: Context) -> ProfileNewRefreshViewController { + let controller = ProfileNewRefreshViewController() + controller.configure( + userInfo: userInfo, + state: state, + selectedTab: $selectedTab, + isShowAppBar: $isShowAppBar, + horizontalSizeClass: horizontalSizeClass, + appSettings: appSettings, + accountType: accountType, + userKey: userKey, + tabStore: tabStore, + mediaPresenterWrapper: mediaPresenterWrapper, + presenterWrapper: presenterWrapper, + theme: theme + ) + return controller + } + + func updateUIViewController( + _ uiViewController: ProfileNewRefreshViewController, context _: Context + ) { + if shouldUpdate(uiViewController) { + uiViewController.configure( + userInfo: userInfo, + state: state, + selectedTab: $selectedTab, + isShowAppBar: $isShowAppBar, + horizontalSizeClass: horizontalSizeClass, + appSettings: appSettings, + accountType: accountType, + userKey: userKey, + tabStore: tabStore, + mediaPresenterWrapper: mediaPresenterWrapper, + presenterWrapper: presenterWrapper, + theme: theme + ) + } + } + + private func shouldUpdate(_ controller: ProfileNewRefreshViewController) -> Bool { + controller.needsProfileUpdate( + userInfo: userInfo, + selectedTab: selectedTab, + accountType: accountType, + userKey: userKey + ) + } +} From e19c121fb76bf60f045ce2571e19ff133ca5e30d Mon Sep 17 00:00:00 2001 From: null Date: Tue, 12 Aug 2025 20:47:44 +0800 Subject: [PATCH 8/8] format --- .../Timeline/ShareButton/ShareButtonV3.swift | 30 ++++----- .../Compose/Timeline/StatusItemView.swift | 65 +++++++++---------- .../TimelineStatus/ActionProcessor.swift | 1 - .../Compose/media/MediaItemComponent.swift | 5 +- .../Page/Home/View/Tabview/SearchScreen.swift | 14 ++-- .../TimelineItemsView.swift | 32 ++++----- .../TimelineViewSwiftUIV4.swift | 8 +-- .../Components/MediaCollectionViewCell.swift | 4 -- .../Components/ProfileNewHeaderView.swift | 13 ++-- .../Profile/Components/TimelineState.swift | 3 +- .../Data/ProfileMediaPresenterWrapper.swift | 3 +- .../Data/ProfilePresenterWrapper.swift | 10 +-- .../Page/Profile/Model/ProfileUserInfo.swift | 14 ++-- .../ProfileMediaViewController.swift | 1 - .../ProfileRefreshViewController.swift | 4 +- .../UIKitView/ProfileTabScreenUikit.swift | 5 +- 16 files changed, 95 insertions(+), 117 deletions(-) diff --git a/iosApp/iosApp/UI/Page/Compose/Timeline/ShareButton/ShareButtonV3.swift b/iosApp/iosApp/UI/Page/Compose/Timeline/ShareButton/ShareButtonV3.swift index 4fed193bb..f8329256f 100644 --- a/iosApp/iosApp/UI/Page/Compose/Timeline/ShareButton/ShareButtonV3.swift +++ b/iosApp/iosApp/UI/Page/Compose/Timeline/ShareButton/ShareButtonV3.swift @@ -8,28 +8,26 @@ import SwiftDate import SwiftUI import UIKit - #if canImport(_Translation_SwiftUI) -import Translation + 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 + 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 } -} #endif private struct CaptureMode: EnvironmentKey { diff --git a/iosApp/iosApp/UI/Page/Compose/Timeline/StatusItemView.swift b/iosApp/iosApp/UI/Page/Compose/Timeline/StatusItemView.swift index 8dd1c6469..684f2d8ed 100644 --- a/iosApp/iosApp/UI/Page/Compose/Timeline/StatusItemView.swift +++ b/iosApp/iosApp/UI/Page/Compose/Timeline/StatusItemView.swift @@ -71,40 +71,39 @@ 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: { - TimelineStatusViewV2( - item: TimelineItem.from(self.data), - timelineViewModel: nil + // 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) + ) + .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/TimelineStatus/ActionProcessor.swift b/iosApp/iosApp/UI/Page/Compose/Timeline/TimelineStatus/ActionProcessor.swift index 4751251ee..85c2ca739 100644 --- a/iosApp/iosApp/UI/Page/Compose/Timeline/TimelineStatus/ActionProcessor.swift +++ b/iosApp/iosApp/UI/Page/Compose/Timeline/TimelineStatus/ActionProcessor.swift @@ -8,7 +8,6 @@ import shared import SwiftDate import SwiftUI import UIKit - enum ActionProcessor { /*** diff --git a/iosApp/iosApp/UI/Page/Compose/media/MediaItemComponent.swift b/iosApp/iosApp/UI/Page/Compose/media/MediaItemComponent.swift index 166e8020f..eb9de1ab3 100644 --- a/iosApp/iosApp/UI/Page/Compose/media/MediaItemComponent.swift +++ b/iosApp/iosApp/UI/Page/Compose/media/MediaItemComponent.swift @@ -2,8 +2,9 @@ import AVKit import Kingfisher import shared import SwiftUI + // -//struct MediaItemComponent: View { +// struct MediaItemComponent: View { // let media: UiMedia // // var body: some View { @@ -15,4 +16,4 @@ import SwiftUI // action: {} // ) // } -//} +// } diff --git a/iosApp/iosApp/UI/Page/Home/View/Tabview/SearchScreen.swift b/iosApp/iosApp/UI/Page/Home/View/Tabview/SearchScreen.swift index 2b1bc15f5..eb8250fb5 100644 --- a/iosApp/iosApp/UI/Page/Home/View/Tabview/SearchScreen.swift +++ b/iosApp/iosApp/UI/Page/Home/View/Tabview/SearchScreen.swift @@ -44,15 +44,13 @@ struct SearchScreen: View { EmptyView() .listRowSeparator(.hidden) } - StatusTimelineComponent( - data: state.status, - detailKey: nil - ).listRowBackground(theme.primaryBackgroundColor) - .listRowInsets(EdgeInsets()) - - + StatusTimelineComponent( + data: state.status, + detailKey: nil + ).listRowBackground(theme.primaryBackgroundColor) + .listRowInsets(EdgeInsets()) - } .padding(.horizontal, 16).listStyle(.plain) + }.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 ab4395f7e..d2a4f2874 100644 --- a/iosApp/iosApp/UI/Page/Home/View/TimelineViewSwiftUI/TimelineItemsView.swift +++ b/iosApp/iosApp/UI/Page/Home/View/TimelineViewSwiftUI/TimelineItemsView.swift @@ -14,28 +14,28 @@ struct TimelineItemsView: View { item: item, timelineViewModel: viewModel ).padding(.horizontal, 16) - .padding(.vertical, 4) - .onAppear { - // viewModel.itemDidAppear(item: item) + .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 0b7a243f8..4dbc287ae 100644 --- a/iosApp/iosApp/UI/Page/Home/View/TimelineViewSwiftUI/TimelineViewSwiftUIV4.swift +++ b/iosApp/iosApp/UI/Page/Home/View/TimelineViewSwiftUI/TimelineViewSwiftUIV4.swift @@ -38,10 +38,10 @@ struct TimelineViewSwiftUIV4: View { item: createSampleTimelineItem(), timelineViewModel: timeLineViewModel ).padding(.horizontal, 16) - .redacted(reason: .placeholder) - .listRowBackground(theme.primaryBackgroundColor) - .listRowInsets(EdgeInsets()) - .listRowSeparator(.hidden) + .redacted(reason: .placeholder) + .listRowBackground(theme.primaryBackgroundColor) + .listRowInsets(EdgeInsets()) + .listRowSeparator(.hidden) } case let .loaded(items, hasMore): diff --git a/iosApp/iosApp/UI/Page/Profile/Components/MediaCollectionViewCell.swift b/iosApp/iosApp/UI/Page/Profile/Components/MediaCollectionViewCell.swift index b158fde91..51d811bf6 100644 --- a/iosApp/iosApp/UI/Page/Profile/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) { - let mediaView = ProfileMediaItemView( media: media, appSetting: appSettings, onTap: onTap ) - hostingController?.view.removeFromSuperview() hostingController = nil - let controller = UIHostingController(rootView: mediaView) hostingController = controller - controller.view.backgroundColor = .clear contentView.addSubview(controller.view) controller.view.translatesAutoresizingMaskIntoConstraints = false diff --git a/iosApp/iosApp/UI/Page/Profile/Components/ProfileNewHeaderView.swift b/iosApp/iosApp/UI/Page/Profile/Components/ProfileNewHeaderView.swift index 55164debd..378ce385d 100644 --- a/iosApp/iosApp/UI/Page/Profile/Components/ProfileNewHeaderView.swift +++ b/iosApp/iosApp/UI/Page/Profile/Components/ProfileNewHeaderView.swift @@ -585,6 +585,7 @@ class ProfileNewHeaderView: UIView { return hostingController.view } } + struct UserFollowsFansCount: View { let followCount: String let fansCount: String @@ -630,12 +631,12 @@ struct UserInfoFieldsView: View { .padding(.horizontal) } .padding(.vertical) -#if os(iOS) - .background(Color(UIColor.secondarySystemBackground)) -#else - .background(Color(NSColor.windowBackgroundColor)) -#endif - .clipShape(RoundedRectangle(cornerRadius: 8)) + #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/Profile/Components/TimelineState.swift b/iosApp/iosApp/UI/Page/Profile/Components/TimelineState.swift index 2a81dbb81..672076a12 100644 --- a/iosApp/iosApp/UI/Page/Profile/Components/TimelineState.swift +++ b/iosApp/iosApp/UI/Page/Profile/Components/TimelineState.swift @@ -7,13 +7,12 @@ class TimelineLoadingState { // 当前加载中的行 private(set) var loadingRows: Set = [] private let preloadDistance: Int = 10 - + // 清空加载队列 func clearLoadingRows() { loadingRows.removeAll() } - // 检查并触发预加载 func checkAndTriggerPreload(currentRow: Int, data: PagingState) { guard case let .success(successData) = onEnum(of: data) else { return } diff --git a/iosApp/iosApp/UI/Page/Profile/Data/ProfileMediaPresenterWrapper.swift b/iosApp/iosApp/UI/Page/Profile/Data/ProfileMediaPresenterWrapper.swift index 9c6437158..575ff8d02 100644 --- a/iosApp/iosApp/UI/Page/Profile/Data/ProfileMediaPresenterWrapper.swift +++ b/iosApp/iosApp/UI/Page/Profile/Data/ProfileMediaPresenterWrapper.swift @@ -3,10 +3,9 @@ import os.log import shared import SwiftUI -class ProfileMediaPresenterWrapper: ObservableObject { +class ProfileMediaPresenterWrapper: ObservableObject { let presenter: ProfileMediaPresenter - 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/Profile/Data/ProfilePresenterWrapper.swift b/iosApp/iosApp/UI/Page/Profile/Data/ProfilePresenterWrapper.swift index d247ef673..0f30b3221 100644 --- a/iosApp/iosApp/UI/Page/Profile/Data/ProfilePresenterWrapper.swift +++ b/iosApp/iosApp/UI/Page/Profile/Data/ProfilePresenterWrapper.swift @@ -4,7 +4,6 @@ import shared import SwiftUI class ProfilePresenterWrapper: ObservableObject { - let presenter: ProfileNewPresenter @Published var isShowAppBar: Bool? = nil // nil: 初始状态, true: 显示, false: 隐藏 @@ -13,7 +12,6 @@ class ProfilePresenterWrapper: ObservableObject { 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") @@ -21,10 +19,8 @@ class ProfilePresenterWrapper: ObservableObject { 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)) @@ -45,7 +41,7 @@ class ProfilePresenterWrapper: ObservableObject { // 创建TimelineViewModel实例 let viewModel = TimelineViewModel() - self.timelineViewModel = viewModel + timelineViewModel = viewModel // 从ProfileTabSettingStore获取当前的Timeline Presenter if let timelinePresenter = tabStore.currentPresenter { @@ -70,6 +66,6 @@ class ProfilePresenterWrapper: ObservableObject { // 新增:获取TimelineViewModel(如果已初始化) func getTimelineViewModel() -> TimelineViewModel? { - return timelineViewModel + timelineViewModel } } diff --git a/iosApp/iosApp/UI/Page/Profile/Model/ProfileUserInfo.swift b/iosApp/iosApp/UI/Page/Profile/Model/ProfileUserInfo.swift index d867c17a6..b9877efd0 100644 --- a/iosApp/iosApp/UI/Page/Profile/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 - 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,24 +24,24 @@ struct ProfileUserInfo: Equatable { return nil }() - let isMe: Bool = { + let isMe: Bool = { if case let .success(me) = onEnum(of: state.isMe) { return me.data as! Bool } return false }() - let canSendMessage: Bool = { + let canSendMessage: Bool = { if case let .success(can) = onEnum(of: state.canSendMessage) { return can.data as! Bool } return false }() - let followCount = profile.matrices.followsCountHumanized + let followCount = profile.matrices.followsCountHumanized let fansCount = profile.matrices.fansCountHumanized - let fields: [String: UiRichText] = { + let fields: [String: UiRichText] = { if let bottomContent = profile.bottomContent, let fieldsContent = bottomContent as? UiProfileBottomContentFields { @@ -64,10 +60,8 @@ struct ProfileUserInfo: Equatable { canSendMessage: canSendMessage ) } - 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/Profile/UIKitView/ProfileMediaViewController.swift b/iosApp/iosApp/UI/Page/Profile/UIKitView/ProfileMediaViewController.swift index 2f1eb9aae..5bc0e9bbb 100644 --- a/iosApp/iosApp/UI/Page/Profile/UIKitView/ProfileMediaViewController.swift +++ b/iosApp/iosApp/UI/Page/Profile/UIKitView/ProfileMediaViewController.swift @@ -124,7 +124,6 @@ class ProfileMediaViewController: UIViewController { private func showPhotoBrowser(media: UiMedia, images: [UiMedia], initialIndex: Int) { Task { @MainActor in - PhotoBrowserManager.shared.showPhotoBrowser( media: media, images: images, diff --git a/iosApp/iosApp/UI/Page/Profile/UIKitView/ProfileRefreshViewController.swift b/iosApp/iosApp/UI/Page/Profile/UIKitView/ProfileRefreshViewController.swift index 0cdc5dca7..a48b391f1 100644 --- a/iosApp/iosApp/UI/Page/Profile/UIKitView/ProfileRefreshViewController.swift +++ b/iosApp/iosApp/UI/Page/Profile/UIKitView/ProfileRefreshViewController.swift @@ -426,7 +426,7 @@ class ProfileNewRefreshViewController: UIViewController { // 离开页面时重置状态,不然 详情页会导致没appbar isShowAppBar?.wrappedValue = true - + // 确保系统导航栏状态正确 navigationController?.setNavigationBarHidden(false, animated: animated) } @@ -461,7 +461,7 @@ class ProfileNewRefreshViewController: UIViewController { // 在返回前重置导航状态 // 离开页面时重置状态,不然 详情页会导致没appbar isShowAppBar?.wrappedValue = true - + // 确保导航栏可见 navigationController?.setNavigationBarHidden(false, animated: true) diff --git a/iosApp/iosApp/UI/Page/Profile/UIKitView/ProfileTabScreenUikit.swift b/iosApp/iosApp/UI/Page/Profile/UIKitView/ProfileTabScreenUikit.swift index 693187ce2..6ad390230 100644 --- a/iosApp/iosApp/UI/Page/Profile/UIKitView/ProfileTabScreenUikit.swift +++ b/iosApp/iosApp/UI/Page/Profile/UIKitView/ProfileTabScreenUikit.swift @@ -68,7 +68,6 @@ struct ProfileTabScreenUikit: View { } if userKey == nil { - ProfileNewRefreshViewControllerWrapper( userInfo: userInfo, state: state as! ProfileNewState, @@ -77,7 +76,7 @@ struct ProfileTabScreenUikit: View { get: { presenterWrapper.isShowAppBar }, set: { presenterWrapper.updateNavigationState(showAppBar: $0) } ), - + horizontalSizeClass: horizontalSizeClass, appSettings: appSettings, accountType: accountType, @@ -98,7 +97,7 @@ struct ProfileTabScreenUikit: View { get: { presenterWrapper.isShowAppBar }, set: { presenterWrapper.updateNavigationState(showAppBar: $0) } ), - + horizontalSizeClass: horizontalSizeClass, appSettings: appSettings, accountType: accountType,