From d4dee2c9c372adc7d4ffe9421dfef4a61ee3b242 Mon Sep 17 00:00:00 2001 From: khcrysalis <97859147+khcrysalis@users.noreply.github.com> Date: Tue, 4 Feb 2025 14:36:13 -0800 Subject: [PATCH] feat: fullscreen sheets for news --- .../Data/CoreData/Models/SourcesModel.swift | 2 +- feather.xcodeproj/project.pbxproj | 8 + iOS/Extensions/View+NavTransition.swift | 28 ++++ .../News/CardContextMenuView.swift | 138 +++++++++++++----- .../News/NewsCardContainerView.swift | 50 +++++++ .../SourceAppViews/News/NewsCardView.swift | 43 ++---- .../News/NewsCardsScrollView.swift | 12 +- 7 files changed, 214 insertions(+), 67 deletions(-) create mode 100644 iOS/Extensions/View+NavTransition.swift create mode 100644 iOS/Views/Sources/SourceAppViews/News/NewsCardContainerView.swift diff --git a/Shared/Data/CoreData/Models/SourcesModel.swift b/Shared/Data/CoreData/Models/SourcesModel.swift index eeb92a2..bc33a08 100644 --- a/Shared/Data/CoreData/Models/SourcesModel.swift +++ b/Shared/Data/CoreData/Models/SourcesModel.swift @@ -29,7 +29,7 @@ public struct SourcesData: Codable, Hashable { } public struct NewsData: Codable, Hashable { - public let title: String + public let title: String? public let identifier: String public let caption: String? public let tintColor: String? diff --git a/feather.xcodeproj/project.pbxproj b/feather.xcodeproj/project.pbxproj index 6474aff..7e8d89b 100644 --- a/feather.xcodeproj/project.pbxproj +++ b/feather.xcodeproj/project.pbxproj @@ -37,6 +37,8 @@ 335F0C302C5E175B00A4F0AE /* CertData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335F0C2F2C5E175B00A4F0AE /* CertData.swift */; }; 336EEE812C32367C0011188D /* ZIPFoundation in Frameworks */ = {isa = PBXBuildFile; productRef = 336EEE802C32367C0011188D /* ZIPFoundation */; }; 3386515B2D52343500A85670 /* CardContextMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3386515A2D52343500A85670 /* CardContextMenuView.swift */; }; + 3386515D2D52A51400A85670 /* NewsCardContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3386515C2D52A51400A85670 /* NewsCardContainerView.swift */; }; + 3386515F2D52A5DF00A85670 /* View+NavTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3386515E2D52A5DF00A85670 /* View+NavTransition.swift */; }; 338FFCD92C682469006C3BE0 /* PopupViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 338FFCD82C682469006C3BE0 /* PopupViewController.swift */; }; 338FFCDB2C6870E2006C3BE0 /* LibraryViewController+Import.swift in Sources */ = {isa = PBXBuildFile; fileRef = 338FFCDA2C6870E2006C3BE0 /* LibraryViewController+Import.swift */; }; 338FFCE42C695118006C3BE0 /* IconsListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 338FFCE32C695118006C3BE0 /* IconsListViewController.swift */; }; @@ -181,6 +183,8 @@ 335F0C2D2C5E0FFF00A4F0AE /* CoreDataManager+Certificates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CoreDataManager+Certificates.swift"; sourceTree = ""; }; 335F0C2F2C5E175B00A4F0AE /* CertData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CertData.swift; sourceTree = ""; }; 3386515A2D52343500A85670 /* CardContextMenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardContextMenuView.swift; sourceTree = ""; }; + 3386515C2D52A51400A85670 /* NewsCardContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewsCardContainerView.swift; sourceTree = ""; }; + 3386515E2D52A5DF00A85670 /* View+NavTransition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+NavTransition.swift"; sourceTree = ""; }; 338B0F0C2D0C0E8200C5EA11 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = ""; }; 338FFCD82C682469006C3BE0 /* PopupViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopupViewController.swift; sourceTree = ""; }; 338FFCDA2C6870E2006C3BE0 /* LibraryViewController+Import.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LibraryViewController+Import.swift"; sourceTree = ""; }; @@ -339,6 +343,7 @@ children = ( 3386515A2D52343500A85670 /* CardContextMenuView.swift */, 33089A292D51F117006E1068 /* NewsCardView.swift */, + 3386515C2D52A51400A85670 /* NewsCardContainerView.swift */, 33089A2B2D51F2CE006E1068 /* NewsCardsScrollView.swift */, ); path = News; @@ -749,6 +754,7 @@ 33E4D8352C6593F4006A1C26 /* CGSize+aspectFit.swift */, 33BE87602C6C37CE0044D245 /* UIImage+resize.swift */, 33C29F1F2C7BD73800EF7608 /* UIUserInterfaceStyle+allCases.swift */, + 3386515E2D52A5DF00A85670 /* View+NavTransition.swift */, ); path = Extensions; sourceTree = ""; @@ -1084,6 +1090,7 @@ 33BA37A82BF8168900FF530A /* TabbarController.swift in Sources */, AFAC50952C48DF9300EDEAB6 /* AppSigner.swift in Sources */, 339FE33E2CCE06A100C297BA /* AddIdentifierViewController.swift in Sources */, + 3386515F2D52A5DF00A85670 /* View+NavTransition.swift in Sources */, 33BA37AB2BF8196000FF530A /* Preferences.swift in Sources */, 3386515B2D52343500A85670 /* CardContextMenuView.swift in Sources */, D0651C392BFEEA4B00D40829 /* signing.cpp in Sources */, @@ -1123,6 +1130,7 @@ 33E5A5A22CC85CAA00532930 /* ServerOptionsViewController.swift in Sources */, 33AC87F32C3B7D24003D1175 /* CertificatesViewController.swift in Sources */, 338FFCF42C6971B2006C3BE0 /* IconsListTableViewCell.swift in Sources */, + 3386515D2D52A51400A85670 /* NewsCardContainerView.swift in Sources */, 33BA37AD2BF8197200FF530A /* Storage.swift in Sources */, 33CE81A92C4295F300C05327 /* CertImportingViewController.swift in Sources */, 33570F612C32B80E008CB560 /* AppsTableViewCell.swift in Sources */, diff --git a/iOS/Extensions/View+NavTransition.swift b/iOS/Extensions/View+NavTransition.swift new file mode 100644 index 0000000..8b1ce51 --- /dev/null +++ b/iOS/Extensions/View+NavTransition.swift @@ -0,0 +1,28 @@ +// +// View+NavTransition.swift +// Luce +// +// Created by samara on 30.01.2025. +// + +import SwiftUI + +extension View { + @ViewBuilder + func compatNavigationTransition(id: String, ns: Namespace.ID) -> some View { + if #available(iOS 18.0, *) { + self.navigationTransition(.zoom(sourceID: id, in: ns)) + } else { + self + } + } + + @ViewBuilder + func compatMatchedTransitionSource(id: String, ns: Namespace.ID) -> some View { + if #available(iOS 18.0, *) { + self.matchedTransitionSource(id: id, in: ns) + } else { + self + } + } +} diff --git a/iOS/Views/Sources/SourceAppViews/News/CardContextMenuView.swift b/iOS/Views/Sources/SourceAppViews/News/CardContextMenuView.swift index ef88c8c..4455832 100644 --- a/iOS/Views/Sources/SourceAppViews/News/CardContextMenuView.swift +++ b/iOS/Views/Sources/SourceAppViews/News/CardContextMenuView.swift @@ -8,6 +8,8 @@ import SwiftUI struct CardContextMenuView: View { + @Environment(\.dismiss) var dismiss + let news: NewsData var formattedDate: String { @@ -20,44 +22,114 @@ struct CardContextMenuView: View { } var body: some View { - VStack(alignment: .leading, spacing: 12) { - AsyncImage(url: URL(string: news.imageURL ?? "")) { image in - image.resizable() - .aspectRatio(contentMode: .fill) - } placeholder: { - Color.gray - } - .frame(width: 280, height: 160) - .clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous)) - .overlay( - LinearGradient( - gradient: Gradient(colors: [.clear, .black.opacity(0.2)]), - startPoint: .top, - endPoint: .bottom - ) - ) - - VStack(alignment: .leading, spacing: 8) { - Text(news.title) - .font(.title3) - .fontWeight(.bold) - .lineLimit(2) + NavigationView { + VStack(spacing: 12) { + if (news.imageURL != nil) { + AsyncImage(url: URL(string: news.imageURL ?? "")) { image in + Color.clear.overlay( + image + .resizable() + .aspectRatio(contentMode: .fill) + ) + .transition(.opacity.animation(.easeInOut(duration: 0.3))) + } placeholder: { + Color.black + .opacity(0.2) + .overlay( + ProgressView() + .progressViewStyle(.circular) + .tint(.white) + ) + } + .background(Color(uiColor: UIColor(hex: news.tintColor ?? "000000"))) + .clipShape( + RoundedRectangle(cornerRadius: 12, style: .continuous) + ) + .overlay( + LinearGradient( + gradient: Gradient(colors: [.clear, .black.opacity(0.2)]), + startPoint: .top, + endPoint: .bottom + ) + ) + .overlay( + RoundedRectangle(cornerRadius: 12, style: .continuous) + .stroke(Color.white.opacity(0.15), lineWidth: 2) + ) + .frame(height: 250) + } - if let caption = news.caption { - Text(caption) - .font(.subheadline) + VStack(alignment: .leading, spacing: 16) { + if let title = news.title { + Text(title) + .font(.title) + .fontWeight(.bold) + .lineLimit(2) + .foregroundStyle(.primary) + .multilineTextAlignment(.leading) + } + + if let caption = news.caption { + Text(caption) + .font(.headline) + .foregroundColor(.secondary) + .multilineTextAlignment(.leading) + } + + if (news.url != nil) { + Button(action: { + UIApplication.shared.open(news.url!) + }) { + Label("Open URL", systemImage: "arrow.up.right") + .frame(maxWidth: .infinity) + } + .padding() + .foregroundColor(.accentColor) + .background(Color(uiColor: .secondarySystemBackground)) + .cornerRadius(10) + } + + Text(formattedDate) + .font(.caption) .foregroundColor(.secondary) - .lineLimit(2) } + .frame(maxWidth: .infinity) - Text(formattedDate) - .font(.caption) - .foregroundColor(.secondary) + Spacer() } + .frame( + minWidth: 0, + maxWidth: .infinity, + minHeight: 0, + maxHeight: .infinity, + alignment: .topLeading + ) + .padding() + .background(Color(uiColor: .systemBackground)) + .clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous)) + .toolbar { + ToolbarItem(placement: .topBarLeading) { + Button(action: { + dismiss() + }) { + Image(systemName: "chevron.left") + .padding(10) + .compatFontWeight(.bold) + .background(Color(uiColor: .secondarySystemBackground)) + .clipShape(Circle()) + } + } + } + } + } +} + +extension View { + func compatFontWeight(_ _weight: Font.Weight) -> some View { + if #available(iOS 16.0, *) { + return self.fontWeight(_weight) + } else { + return self } - .frame(width: 280) - .padding() - .background(Color(uiColor: .systemBackground)) - .clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous)) } } diff --git a/iOS/Views/Sources/SourceAppViews/News/NewsCardContainerView.swift b/iOS/Views/Sources/SourceAppViews/News/NewsCardContainerView.swift new file mode 100644 index 0000000..e18cfd8 --- /dev/null +++ b/iOS/Views/Sources/SourceAppViews/News/NewsCardContainerView.swift @@ -0,0 +1,50 @@ +// +// NewsCardContainerView.swift +// feather +// +// Created by samara on 4.02.2025. +// + +import SwiftUI + +struct NewsCardContainerView: View { + @Binding var isSheetPresented: Bool + var news: NewsData + @Namespace private var namespace + + let uuid = UUID().uuidString + + var body: some View { + Button(action: { + isSheetPresented = true + }) { + NewsCardView(news: news) + .fullScreenCover(isPresented: $isSheetPresented) { + CardContextMenuView(news: news) + .compatNavigationTransition(id: uuid, ns: namespace) + } + .compatMatchedTransitionSource(id: uuid, ns: namespace) + .compactContentMenuPreview(news: news) + } + } +} + +extension View { + func compactContentMenuPreview(news: NewsData) -> some View { + if #available(iOS 16.0, *) { + return self.contextMenu { + if (news.url != nil) { + Button(action: { + UIApplication.shared.open(news.url!) + }) { + Label("Open URL", systemImage: "arrow.up.right") + } + } + } preview: { + CardContextMenuView(news: news) + } + } else { + return self + } + } +} diff --git a/iOS/Views/Sources/SourceAppViews/News/NewsCardView.swift b/iOS/Views/Sources/SourceAppViews/News/NewsCardView.swift index 44e62c6..6a4037d 100644 --- a/iOS/Views/Sources/SourceAppViews/News/NewsCardView.swift +++ b/iOS/Views/Sources/SourceAppViews/News/NewsCardView.swift @@ -8,19 +8,26 @@ import SwiftUI struct NewsCardView: View { - let news: NewsData + var news: NewsData var body: some View { ZStack(alignment: .bottomLeading) { if (news.imageURL != nil) { - AsyncImage(url: URL(string: news.imageURL!)) { image in + AsyncImage(url: URL(string: news.imageURL ?? "")) { image in Color.clear.overlay( image .resizable() .aspectRatio(contentMode: .fill) ) + .transition(.opacity.animation(.easeInOut(duration: 0.3))) } placeholder: { - Color.black.opacity(0.2) + Color.black + .opacity(0.2) + .overlay( + ProgressView() + .progressViewStyle(.circular) + .tint(.white) + ) } LinearGradient( @@ -37,11 +44,12 @@ struct NewsCardView: View { VStack { Spacer() - Text(news.title) + Text(news.title ?? "") .font(.headline) .fontWeight(.bold) .foregroundColor(.white) .lineLimit(2) + .multilineTextAlignment(.leading) .padding() } } @@ -54,32 +62,5 @@ struct NewsCardView: View { RoundedRectangle(cornerRadius: 12, style: .continuous) .stroke(Color.white.opacity(0.15), lineWidth: 2) ) - .compactContentMenuPreview(news: news) - } -} - -extension View { - func compactContentMenuPreview(news: NewsData) -> some View { - if #available(iOS 16.0, *) { - return self.contextMenu { - if (news.url != nil) { - Button(action: { - UIApplication.shared.open(news.url!) - }) { - Label("Open URL", systemImage: "arrow.up.right") - } - } else { - Button(action: { - UIApplication.shared.open(URL(string: "https://github.com/khcrysalis/feather")!) - }) { - Label("Give us a star!", systemImage: "star") - } - } - } preview: { - CardContextMenuView(news: news) - } - } else { - return self - } } } diff --git a/iOS/Views/Sources/SourceAppViews/News/NewsCardsScrollView.swift b/iOS/Views/Sources/SourceAppViews/News/NewsCardsScrollView.swift index f7d224f..772ed64 100644 --- a/iOS/Views/Sources/SourceAppViews/News/NewsCardsScrollView.swift +++ b/iOS/Views/Sources/SourceAppViews/News/NewsCardsScrollView.swift @@ -9,16 +9,24 @@ import SwiftUI struct NewsCardsScrollView: View { @State private var newsData: [NewsData] + @State private var sheetStates: [String: Bool] = [:] + @State var isSheetPresented = false init(newsData: [NewsData]) { - _newsData = State(initialValue: newsData) + _newsData = State(initialValue: newsData) + print(newsData) } var body: some View { ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 10) { ForEach(newsData.reversed(), id: \.self) { new in - NewsCardView(news: new) + let binding = Binding( + get: { sheetStates[new.identifier] ?? false }, + set: { sheetStates[new.identifier] = $0 } + ) + + NewsCardContainerView(isSheetPresented: binding, news: new) } } .padding()