From 446fbd9b9ed04ee09c53bd8c25391e9bc70ade9c Mon Sep 17 00:00:00 2001 From: Marcin Czachurski Date: Thu, 11 May 2023 20:00:39 +0200 Subject: [PATCH 01/14] Initial PoC of iPad grid --- Vernissage.xcodeproj/project.pbxproj | 29 ++++++++--- Vernissage/Views/StatusesView.swift | 50 +++++++++++-------- .../Subviews/UserProfileStatusesView.swift | 16 +++++- Vernissage/Widgets/ImageRowAsync.swift | 8 +-- Vernissage/Widgets/ImageRowItemAsync.swift | 9 ++-- .../WidgetsKit/Views/BaseComposeView.swift | 2 +- 6 files changed, 78 insertions(+), 36 deletions(-) diff --git a/Vernissage.xcodeproj/project.pbxproj b/Vernissage.xcodeproj/project.pbxproj index d84c597..d014013 100644 --- a/Vernissage.xcodeproj/project.pbxproj +++ b/Vernissage.xcodeproj/project.pbxproj @@ -27,6 +27,7 @@ F8210DEA2966E4F9001D9973 /* AnimatePlaceholderModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8210DE92966E4F9001D9973 /* AnimatePlaceholderModifier.swift */; }; F825F0C929F7A562008BD204 /* UserProfilePrivateAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F825F0C829F7A562008BD204 /* UserProfilePrivateAccountView.swift */; }; F825F0CB29F7CFC4008BD204 /* FollowRequestsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F825F0CA29F7CFC4008BD204 /* FollowRequestsView.swift */; }; + F830C3CD2A07A4020005FEF8 /* WaterfallGrid in Frameworks */ = {isa = PBXBuildFile; productRef = F830C3CC2A07A4020005FEF8 /* WaterfallGrid */; }; F835082329BEF9C400DE3247 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = F835082629BEF9C400DE3247 /* Localizable.strings */; }; F835082429BEF9C400DE3247 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = F835082629BEF9C400DE3247 /* Localizable.strings */; }; F83CBEFB298298A1002972C8 /* ImageCarouselPicture.swift in Sources */ = {isa = PBXBuildFile; fileRef = F83CBEFA298298A1002972C8 /* ImageCarouselPicture.swift */; }; @@ -449,6 +450,7 @@ F8210DD92966BB7E001D9973 /* NukeUI in Frameworks */, F89B5CC029D019B600549F2F /* HTMLString in Frameworks */, F88BC52A29E046D700CE6141 /* WidgetsKit in Frameworks */, + F830C3CD2A07A4020005FEF8 /* WaterfallGrid in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -988,6 +990,7 @@ F88BC52629E0431D00CE6141 /* ServicesKit */, F88BC52929E046D700CE6141 /* WidgetsKit */, F88BC52C29E04BB600CE6141 /* EnvironmentKit */, + F830C3CC2A07A4020005FEF8 /* WaterfallGrid */, ); productName = Vernissage; productReference = F88C2468295C37B80006098B /* Vernissage.app */; @@ -1031,6 +1034,7 @@ F88E4D4B297EA4290057491A /* XCRemoteSwiftPackageReference "EmojiText" */, F89B5CBE29D019B600549F2F /* XCRemoteSwiftPackageReference "HTMLString" */, F84625F929FE393B002D3AF4 /* XCRemoteSwiftPackageReference "QRCode" */, + F830C3CB2A07A4020005FEF8 /* XCRemoteSwiftPackageReference "WaterfallGrid" */, ); productRefGroup = F88C2469295C37B80006098B /* Products */; projectDirPath = ""; @@ -1338,7 +1342,7 @@ SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 1; + TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; }; @@ -1366,7 +1370,7 @@ SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 1; + TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; }; @@ -1394,7 +1398,7 @@ SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 1; + TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; }; @@ -1421,7 +1425,7 @@ SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 1; + TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; }; @@ -1580,7 +1584,7 @@ SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 1; + TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; }; @@ -1621,7 +1625,7 @@ SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 1; + TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; }; @@ -1675,6 +1679,14 @@ minimumVersion = 12.0.0; }; }; + F830C3CB2A07A4020005FEF8 /* XCRemoteSwiftPackageReference "WaterfallGrid" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/paololeonardi/WaterfallGrid.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.0.0; + }; + }; F84625F929FE393B002D3AF4 /* XCRemoteSwiftPackageReference "QRCode" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/dmrschmidt/QRCode"; @@ -1717,6 +1729,11 @@ package = F8210DD32966BB7E001D9973 /* XCRemoteSwiftPackageReference "Nuke" */; productName = NukeUI; }; + F830C3CC2A07A4020005FEF8 /* WaterfallGrid */ = { + isa = XCSwiftPackageProductDependency; + package = F830C3CB2A07A4020005FEF8 /* XCRemoteSwiftPackageReference "WaterfallGrid" */; + productName = WaterfallGrid; + }; F84625FA29FE393B002D3AF4 /* QRCode */ = { isa = XCSwiftPackageProductDependency; package = F84625F929FE393B002D3AF4 /* XCRemoteSwiftPackageReference "QRCode" */; diff --git a/Vernissage/Views/StatusesView.swift b/Vernissage/Views/StatusesView.swift index 12093c7..2768020 100644 --- a/Vernissage/Views/StatusesView.swift +++ b/Vernissage/Views/StatusesView.swift @@ -11,6 +11,7 @@ import ClientKit import ServicesKit import EnvironmentKit import WidgetsKit +import WaterfallGrid struct StatusesView: View { public enum ListType: Hashable { @@ -50,7 +51,7 @@ struct StatusesView: View { @State private var state: ViewState = .loading @State private var lastStatusId: String? - private let defaultLimit = 20 + private let defaultLimit = 40 private let imagePrefetcher = ImagePrefetcher(destination: .diskCache) var body: some View { @@ -88,26 +89,35 @@ struct StatusesView: View { @ViewBuilder private func list() -> some View { ScrollView { - LazyVStack(alignment: .center) { - ForEach(self.statusViewModels, id: \.id) { item in - ImageRowAsync(statusViewModel: item) - } - - if allItemsLoaded == false { - HStack { - Spacer() - LoadingIndicator() - .task { - do { - try await self.loadMoreStatuses() - } catch { - ErrorService.shared.handle(error, message: "statuses.error.loadingStatusesFailed", showToastr: !Task.isCancelled) - } - } - Spacer() - } - } + WaterfallGrid(self.statusViewModels, id: \.id) { item in + ImageRowAsync(statusViewModel: item, + withAvatar: true, + imageScale: self.applicationState.showGridOnUserProfile ? .squareHalfWidth : .orginalFullWidth) + .padding(.top, -2) } + .gridStyle(columns: 3, spacing: 4) + // .padding(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) + +// LazyVStack(alignment: .center) { +// ForEach(self.statusViewModels, id: \.id) { item in +// ImageRowAsync(statusViewModel: item) +// } +// +// if allItemsLoaded == false { +// HStack { +// Spacer() +// LoadingIndicator() +// .task { +// do { +// try await self.loadMoreStatuses() +// } catch { +// ErrorService.shared.handle(error, message: "statuses.error.loadingStatusesFailed", showToastr: !Task.isCancelled) +// } +// } +// Spacer() +// } +// } +// } } .refreshable { do { diff --git a/Vernissage/Views/UserProfileView/Subviews/UserProfileStatusesView.swift b/Vernissage/Views/UserProfileView/Subviews/UserProfileStatusesView.swift index 47e654d..4f6e8e5 100644 --- a/Vernissage/Views/UserProfileView/Subviews/UserProfileStatusesView.swift +++ b/Vernissage/Views/UserProfileView/Subviews/UserProfileStatusesView.swift @@ -11,6 +11,7 @@ import ClientKit import ServicesKit import EnvironmentKit import WidgetsKit +import WaterfallGrid struct UserProfileStatusesView: View { @EnvironmentObject private var applicationState: ApplicationState @@ -22,7 +23,7 @@ struct UserProfileStatusesView: View { @State private var firstLoadFinished = false @State private var statusViewModels: [StatusModel] = [] - private let defaultLimit = 20 + private let defaultLimit = 40 private let imagePrefetcher = ImagePrefetcher(destination: .diskCache) private let singleGrids = [GridItem(.flexible(), spacing: 10)] private let dubleGrid = [GridItem(.flexible(), spacing: 10), GridItem(.flexible(), spacing: 0)] @@ -54,7 +55,19 @@ struct UserProfileStatusesView: View { .padding(.bottom, 8) } } + + WaterfallGrid(self.statusViewModels, id: \.id) { item in + ImageRowAsync(statusViewModel: item, + withAvatar: false, + imageScale: self.applicationState.showGridOnUserProfile ? .squareHalfWidth : .orginalFullWidth) +// .if(self.applicationState.showGridOnUserProfile) { +// $0.frame(width: UIScreen.main.bounds.width / 2, height: UIScreen.main.bounds.width / 2) +// } + } + .gridStyle(columns: 3, spacing: 2) + .padding(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) + /* LazyVGrid(columns: self.applicationState.showGridOnUserProfile ? dubleGrid : singleGrids, spacing: 5) { ForEach(self.statusViewModels, id: \.id) { item in ImageRowAsync(statusViewModel: item, @@ -80,6 +93,7 @@ struct UserProfileStatusesView: View { } } } + */ } else { LoadingIndicator() .onFirstAppear { diff --git a/Vernissage/Widgets/ImageRowAsync.swift b/Vernissage/Widgets/ImageRowAsync.swift index d84afc6..5b8acc8 100644 --- a/Vernissage/Widgets/ImageRowAsync.swift +++ b/Vernissage/Widgets/ImageRowAsync.swift @@ -59,8 +59,8 @@ struct ImageRowAsync: View { } } } - .if(self.imageScale == .orginalFullWidth) { - $0.frame(width: self.imageWidth, height: self.imageHeight) + .if(self.imageScale == .squareHalfWidth) { + $0.frame(width: self.imageWidth / 3, height: self.imageHeight / 3) } } else { TabView(selection: $selected) { @@ -98,8 +98,8 @@ struct ImageRowAsync: View { } } }) - .if(self.imageScale == .orginalFullWidth) { - $0.frame(width: self.imageWidth, height: self.imageHeight) + .if(self.imageScale == .squareHalfWidth) { + $0.frame(width: self.imageWidth / 3, height: self.imageHeight / 3) } .tabViewStyle(.page(indexDisplayMode: .never)) .overlay(CustomPageTabViewStyleView(pages: self.statusViewModel.mediaAttachments, currentId: $selected)) diff --git a/Vernissage/Widgets/ImageRowItemAsync.swift b/Vernissage/Widgets/ImageRowItemAsync.swift index f660505..1dc6011 100644 --- a/Vernissage/Widgets/ImageRowItemAsync.swift +++ b/Vernissage/Widgets/ImageRowItemAsync.swift @@ -155,10 +155,11 @@ struct ImageRowItemAsync: View { private func imageView(image: Image) -> some View { image .resizable() - .scaledToFill() - .if(self.imageScale == .squareHalfWidth) { - $0.frame(width: UIScreen.main.bounds.width / 2, height: UIScreen.main.bounds.width / 2).clipped() - } + //.aspectRatio(contentMode: .fill) + .aspectRatio(contentMode: .fit) +// .if(self.imageScale == .squareHalfWidth) { +// $0.frame(width: UIScreen.main.bounds.width / 4, height: UIScreen.main.bounds.width / 4).clipped() +// } .onTapGesture(count: 2) { Task { // Update favourite in Pixelfed server. diff --git a/WidgetsKit/Sources/WidgetsKit/Views/BaseComposeView.swift b/WidgetsKit/Sources/WidgetsKit/Views/BaseComposeView.swift index 5830312..4ef011d 100644 --- a/WidgetsKit/Sources/WidgetsKit/Views/BaseComposeView.swift +++ b/WidgetsKit/Sources/WidgetsKit/Views/BaseComposeView.swift @@ -159,7 +159,7 @@ public struct BaseComposeView: View { } .photosPicker(isPresented: $photosPickerVisible, selection: $selectedItems, - maxSelectionCount: 4, + maxSelectionCount: self.applicationState.statusMaxMediaAttachments, matching: .images) .fileImporter(isPresented: $isFileImporterPresented, allowedContentTypes: [.image], From 085abcbea171229247014f4f3e35848f812754d2 Mon Sep 17 00:00:00 2001 From: Marcin Czachurski Date: Thu, 25 May 2023 17:33:04 +0200 Subject: [PATCH 02/14] Add new gallery component. --- .../ClientKit/Models/StatusModel.swift | 16 ++- .../ServicesKit/ImageSizeService.swift | 43 ++++--- Vernissage.xcodeproj/project.pbxproj | 25 +--- Vernissage/Models/ImageScale.swift | 12 -- Vernissage/Services/HomeTimelineService.swift | 2 + Vernissage/Views/HomeFeedView.swift | 66 ++++++---- Vernissage/Views/PaginableStatusesView.swift | 2 +- Vernissage/Views/StatusView/StatusView.swift | 4 +- Vernissage/Views/StatusesView.swift | 70 +++++----- Vernissage/Views/TrendStatusesView.swift | 2 +- .../Subviews/UserProfileStatusesView.swift | 120 ++++++++++-------- .../UserProfileView/UserProfileView.swift | 12 +- Vernissage/Widgets/ImageRow.swift | 17 ++- Vernissage/Widgets/ImageRowAsync.swift | 88 +++++++++---- Vernissage/Widgets/ImageRowItem.swift | 14 +- Vernissage/Widgets/ImageRowItemAsync.swift | 32 +++-- Vernissage/Widgets/ImageViewer.swift | 4 +- Vernissage/Widgets/ImagesCarousel.swift | 7 +- Vernissage/Widgets/WaterfallGrid.swift | 83 ++++++++++++ .../Extensions/View+AsyncAfter.swift | 8 ++ .../ViewModifiers/DeviceRotation.swift | 85 +++++++++++++ 21 files changed, 500 insertions(+), 212 deletions(-) delete mode 100644 Vernissage/Models/ImageScale.swift create mode 100644 Vernissage/Widgets/WaterfallGrid.swift create mode 100644 WidgetsKit/Sources/WidgetsKit/ViewModifiers/DeviceRotation.swift diff --git a/ClientKit/Sources/ClientKit/Models/StatusModel.swift b/ClientKit/Sources/ClientKit/Models/StatusModel.swift index 64e16c1..17f9252 100644 --- a/ClientKit/Sources/ClientKit/Models/StatusModel.swift +++ b/ClientKit/Sources/ClientKit/Models/StatusModel.swift @@ -8,7 +8,6 @@ import Foundation import PixelfedKit public class StatusModel: ObservableObject { - public let id: EntityId public let content: Html @@ -107,6 +106,21 @@ public extension StatusModel { } } +extension StatusModel: Equatable { + public static func == (lhs: StatusModel, rhs: StatusModel) -> Bool { + lhs.id == rhs.id + } +} + +extension StatusModel: Hashable { + public func hash(into hasher: inout Hasher) { + return hasher.combine(self.id) + } +} + +extension StatusModel: Identifiable { +} + public extension [StatusModel] { func getAllImagesUrls() -> [URL] { var urls: [URL] = [] diff --git a/ServicesKit/Sources/ServicesKit/ImageSizeService.swift b/ServicesKit/Sources/ServicesKit/ImageSizeService.swift index 7a06503..177db3a 100644 --- a/ServicesKit/Sources/ServicesKit/ImageSizeService.swift +++ b/ServicesKit/Sources/ServicesKit/ImageSizeService.swift @@ -7,40 +7,53 @@ import Foundation import SwiftUI +/// Service is storing orginal image sizes. +/// Very often images doesn't have size in metadataa (especially for services other then Pixelfed). +/// After download image from server we can check his size and remember in the cache. +/// +/// When we want to prepare placeholder for specfic image and container witdh we have to use special method. public class ImageSizeService { public static let shared = ImageSizeService() private init() { } + /// Cache with orginal image sizes. private var memoryCacheData = MemoryCache(entryLifetime: 3600) public func get(for url: URL) -> CGSize? { return self.memoryCacheData[url] } - public func calculate(for url: URL, width: Int32, height: Int32) -> CGSize { - return calculate(for: url, width: Double(width), height: Double(height)) + public func save(for url: URL, width: Int32, height: Int32) { + save(for: url, width: Double(width), height: Double(height)) } - public func calculate(for url: URL, width: Int, height: Int) -> CGSize { - return calculate(for: url, width: Double(width), height: Double(height)) + public func save(for url: URL, width: Int, height: Int) { + save(for: url, width: Double(width), height: Double(height)) } - public func calculate(width: Double, height: Double) -> CGSize { - let divider = Double(width) / UIScreen.main.bounds.size.width + public func save(for url: URL, width: Double, height: Double) { + self.memoryCacheData.insert(CGSize(width: width, height: height), forKey: url) + } +} + +extension ImageSizeService { + public func calculate(for url: URL, andContainerWidth containerWidth: Double) -> CGSize { + guard let size = self.get(for: url) else { + return CGSize(width: containerWidth, height: containerWidth) + } + + return self.calculate(width: size.width, height: size.height, andContainerWidth: containerWidth) + } + + public func calculate(width: Double, height: Double, andContainerWidth containerWidth: Double) -> CGSize { + let divider = Double(width) / containerWidth let calculatedHeight = Double(height) / divider let size = CGSize( - width: UIScreen.main.bounds.width, - height: (calculatedHeight > 0 && calculatedHeight < .infinity) ? calculatedHeight : UIScreen.main.bounds.width + width: containerWidth, + height: (calculatedHeight > 0 && calculatedHeight < .infinity) ? calculatedHeight : containerWidth ) return size } - - public func calculate(for url: URL, width: Double, height: Double) -> CGSize { - let size = self.calculate(width: width, height: height) - - self.memoryCacheData.insert(size, forKey: url) - return size - } } diff --git a/Vernissage.xcodeproj/project.pbxproj b/Vernissage.xcodeproj/project.pbxproj index d014013..0125604 100644 --- a/Vernissage.xcodeproj/project.pbxproj +++ b/Vernissage.xcodeproj/project.pbxproj @@ -27,7 +27,6 @@ F8210DEA2966E4F9001D9973 /* AnimatePlaceholderModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8210DE92966E4F9001D9973 /* AnimatePlaceholderModifier.swift */; }; F825F0C929F7A562008BD204 /* UserProfilePrivateAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F825F0C829F7A562008BD204 /* UserProfilePrivateAccountView.swift */; }; F825F0CB29F7CFC4008BD204 /* FollowRequestsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F825F0CA29F7CFC4008BD204 /* FollowRequestsView.swift */; }; - F830C3CD2A07A4020005FEF8 /* WaterfallGrid in Frameworks */ = {isa = PBXBuildFile; productRef = F830C3CC2A07A4020005FEF8 /* WaterfallGrid */; }; F835082329BEF9C400DE3247 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = F835082629BEF9C400DE3247 /* Localizable.strings */; }; F835082429BEF9C400DE3247 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = F835082629BEF9C400DE3247 /* Localizable.strings */; }; F83CBEFB298298A1002972C8 /* ImageCarouselPicture.swift in Sources */ = {isa = PBXBuildFile; fileRef = F83CBEFA298298A1002972C8 /* ImageCarouselPicture.swift */; }; @@ -90,6 +89,7 @@ F866F6A729604629002E8F88 /* SignInView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F866F6A629604629002E8F88 /* SignInView.swift */; }; F866F6AA29605AFA002E8F88 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F866F6A929605AFA002E8F88 /* SceneDelegate.swift */; }; F866F6AE29606367002E8F88 /* ApplicationViewMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = F866F6AD29606367002E8F88 /* ApplicationViewMode.swift */; }; + F8675DD02A1FA40500A89959 /* WaterfallGrid.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8675DCF2A1FA40500A89959 /* WaterfallGrid.swift */; }; F86A42FD299A8B8E00DF7645 /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F86A42FC299A8B8E00DF7645 /* StoreKit.framework */; }; F86A42FF299A8C5500DF7645 /* InAppPurchaseStoreKitConfiguration.storekit in Resources */ = {isa = PBXBuildFile; fileRef = F86A42FE299A8C5500DF7645 /* InAppPurchaseStoreKitConfiguration.storekit */; }; F86A4301299A97F500DF7645 /* ProductIdentifiers.swift in Sources */ = {isa = PBXBuildFile; fileRef = F86A4300299A97F500DF7645 /* ProductIdentifiers.swift */; }; @@ -191,7 +191,6 @@ F8B0885E29942E31002AB40A /* ThirdPartyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8B0885D29942E31002AB40A /* ThirdPartyView.swift */; }; F8B0886029943498002AB40A /* OtherSectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8B0885F29943498002AB40A /* OtherSectionView.swift */; }; F8B08862299435C9002AB40A /* SupportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8B08861299435C9002AB40A /* SupportView.swift */; }; - F8C287A32A06B4C90072213F /* ImageScale.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8C287A22A06B4C90072213F /* ImageScale.swift */; }; F8D5444329D4066C002225D6 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D5444229D4066C002225D6 /* AppDelegate.swift */; }; F8DF38E429DD68820047F1AA /* ViewOffsetKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8DF38E329DD68820047F1AA /* ViewOffsetKey.swift */; }; F8DF38E629DDB98A0047F1AA /* SocialsSectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8DF38E529DDB98A0047F1AA /* SocialsSectionView.swift */; }; @@ -303,6 +302,7 @@ F866F6A829604FFF002E8F88 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; F866F6A929605AFA002E8F88 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; F866F6AD29606367002E8F88 /* ApplicationViewMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationViewMode.swift; sourceTree = ""; }; + F8675DCF2A1FA40500A89959 /* WaterfallGrid.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaterfallGrid.swift; sourceTree = ""; }; F86A42FC299A8B8E00DF7645 /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; }; F86A42FE299A8C5500DF7645 /* InAppPurchaseStoreKitConfiguration.storekit */ = {isa = PBXFileReference; lastKnownFileType = text; path = InAppPurchaseStoreKitConfiguration.storekit; sourceTree = ""; }; F86A4300299A97F500DF7645 /* ProductIdentifiers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductIdentifiers.swift; sourceTree = ""; }; @@ -392,7 +392,6 @@ F8B08861299435C9002AB40A /* SupportView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SupportView.swift; sourceTree = ""; }; F8B3699A29D86EB600BE3808 /* .swiftlint.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = .swiftlint.yml; sourceTree = ""; }; F8B3699B29D86EBD00BE3808 /* .gitignore */ = {isa = PBXFileReference; lastKnownFileType = text; path = .gitignore; sourceTree = ""; }; - F8C287A22A06B4C90072213F /* ImageScale.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageScale.swift; sourceTree = ""; }; F8C937A929882CA90004D782 /* Vernissage-001.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Vernissage-001.xcdatamodel"; sourceTree = ""; }; F8CAE64129B8F1AF001E0372 /* Vernissage-005.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Vernissage-005.xcdatamodel"; sourceTree = ""; }; F8D5444229D4066C002225D6 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; @@ -450,7 +449,6 @@ F8210DD92966BB7E001D9973 /* NukeUI in Frameworks */, F89B5CC029D019B600549F2F /* HTMLString in Frameworks */, F88BC52A29E046D700CE6141 /* WidgetsKit in Frameworks */, - F830C3CD2A07A4020005FEF8 /* WaterfallGrid in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -535,7 +533,6 @@ F8DF38E329DD68820047F1AA /* ViewOffsetKey.swift */, F871F21C29EF0D7000A351EF /* NavigationMenuItemDetails.swift */, F8624D3C29F2D3AC00204986 /* SelectedMenuItemDetails.swift */, - F8C287A22A06B4C90072213F /* ImageScale.swift */, ); path = Models; sourceTree = ""; @@ -592,6 +589,7 @@ F8AFF7C329B25EF40087D083 /* ImagesGrid.swift */, F89D6C49297196FF001DA3D4 /* ImageViewer.swift */, F870EE5129F1645C00A2D43B /* MainNavigationOptions.swift */, + F8675DCF2A1FA40500A89959 /* WaterfallGrid.swift */, ); path = Widgets; sourceTree = ""; @@ -990,7 +988,6 @@ F88BC52629E0431D00CE6141 /* ServicesKit */, F88BC52929E046D700CE6141 /* WidgetsKit */, F88BC52C29E04BB600CE6141 /* EnvironmentKit */, - F830C3CC2A07A4020005FEF8 /* WaterfallGrid */, ); productName = Vernissage; productReference = F88C2468295C37B80006098B /* Vernissage.app */; @@ -1034,7 +1031,6 @@ F88E4D4B297EA4290057491A /* XCRemoteSwiftPackageReference "EmojiText" */, F89B5CBE29D019B600549F2F /* XCRemoteSwiftPackageReference "HTMLString" */, F84625F929FE393B002D3AF4 /* XCRemoteSwiftPackageReference "QRCode" */, - F830C3CB2A07A4020005FEF8 /* XCRemoteSwiftPackageReference "WaterfallGrid" */, ); productRefGroup = F88C2469295C37B80006098B /* Products */; projectDirPath = ""; @@ -1262,7 +1258,6 @@ F89B5CC229D01BF700549F2F /* InstanceView.swift in Sources */, F825F0CB29F7CFC4008BD204 /* FollowRequestsView.swift in Sources */, F825F0C929F7A562008BD204 /* UserProfilePrivateAccountView.swift in Sources */, - F8C287A32A06B4C90072213F /* ImageScale.swift in Sources */, F89F57B029D1C11200001EE3 /* RelationshipModel.swift in Sources */, F88AB05829B36B8200345EDE /* AccountsPhotoView.swift in Sources */, F85D4971296402DC00751DF7 /* AuthorizationService.swift in Sources */, @@ -1282,6 +1277,7 @@ F83CBEFB298298A1002972C8 /* ImageCarouselPicture.swift in Sources */, F89A46DC296EAACE0062125F /* SettingsView.swift in Sources */, F866F6AE29606367002E8F88 /* ApplicationViewMode.swift in Sources */, + F8675DD02A1FA40500A89959 /* WaterfallGrid.swift in Sources */, F85D4DFE29B78C8400345267 /* HashtagModel.swift in Sources */, F866F6AA29605AFA002E8F88 /* SceneDelegate.swift in Sources */, ); @@ -1679,14 +1675,6 @@ minimumVersion = 12.0.0; }; }; - F830C3CB2A07A4020005FEF8 /* XCRemoteSwiftPackageReference "WaterfallGrid" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/paololeonardi/WaterfallGrid.git"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 1.0.0; - }; - }; F84625F929FE393B002D3AF4 /* XCRemoteSwiftPackageReference "QRCode" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/dmrschmidt/QRCode"; @@ -1729,11 +1717,6 @@ package = F8210DD32966BB7E001D9973 /* XCRemoteSwiftPackageReference "Nuke" */; productName = NukeUI; }; - F830C3CC2A07A4020005FEF8 /* WaterfallGrid */ = { - isa = XCSwiftPackageProductDependency; - package = F830C3CB2A07A4020005FEF8 /* XCRemoteSwiftPackageReference "WaterfallGrid" */; - productName = WaterfallGrid; - }; F84625FA29FE393B002D3AF4 /* QRCode */ = { isa = XCSwiftPackageProductDependency; package = F84625F929FE393B002D3AF4 /* XCRemoteSwiftPackageReference "QRCode" */; diff --git a/Vernissage/Models/ImageScale.swift b/Vernissage/Models/ImageScale.swift deleted file mode 100644 index 01bb86f..0000000 --- a/Vernissage/Models/ImageScale.swift +++ /dev/null @@ -1,12 +0,0 @@ -// -// https://mczachurski.dev -// Copyright © 2023 Marcin Czachurski and the repository contributors. -// Licensed under the Apache License 2.0. -// - -import Foundation - -enum ImageScale { - case orginalFullWidth - case squareHalfWidth -} diff --git a/Vernissage/Services/HomeTimelineService.swift b/Vernissage/Services/HomeTimelineService.swift index d630c2e..7438637 100644 --- a/Vernissage/Services/HomeTimelineService.swift +++ b/Vernissage/Services/HomeTimelineService.swift @@ -19,6 +19,7 @@ public class HomeTimelineService { private let defaultAmountOfDownloadedStatuses = 40 private let imagePrefetcher = ImagePrefetcher(destination: .diskCache) + @MainActor public func loadOnBottom(for account: AccountModel) async throws -> Int { // Load data from API and operate on CoreData on background context. let backgroundContext = CoreDataHandler.shared.newBackgroundContext() @@ -43,6 +44,7 @@ public class HomeTimelineService { return allStatusesFromApi.count } + @MainActor public func refreshTimeline(for account: AccountModel) async throws -> String? { // Load data from API and operate on CoreData on background context. let backgroundContext = CoreDataHandler.shared.newBackgroundContext() diff --git a/Vernissage/Views/HomeFeedView.swift b/Vernissage/Views/HomeFeedView.swift index 1a3e11a..6d008c8 100644 --- a/Vernissage/Views/HomeFeedView.swift +++ b/Vernissage/Views/HomeFeedView.swift @@ -21,6 +21,11 @@ struct HomeFeedView: View { @State private var opacity = 0.0 @State private var offset = -50.0 + // Gallery parameters. + @State private var imageColumns = 3 + @State private var containerWidth: Double = UIScreen.main.bounds.width + @State private var containerHeight: Double = UIScreen.main.bounds.height + @FetchRequest var dbStatuses: FetchedResults init(accountId: String) { @@ -55,32 +60,49 @@ struct HomeFeedView: View { private func timeline() -> some View { ZStack { ScrollView { - LazyVStack { - ForEach(dbStatuses, id: \.self) { item in - if self.shouldUpToDateBeVisible(statusId: item.id) { - self.upToDatePlaceholder() - } - - ImageRow(statusData: item) - } - - if allItemsLoaded == false { - LoadingIndicator() - .task { - do { - if let account = self.applicationState.account { - let newStatusesCount = try await HomeTimelineService.shared.loadOnBottom(for: account) - if newStatusesCount == 0 { - allItemsLoaded = true - } - } - } catch { - ErrorService.shared.handle(error, message: "global.error.errorDuringDownloadStatuses", showToastr: !Task.isCancelled) - } + if self.imageColumns > 1 { +// WaterfallGrid(statusViewModel: $dbStatuses, columns: $imageColumns, hideLoadMore: $allItemsLoaded) { item in +// ImageRowAsync(statusViewModel: item, containerWidth: $containerWidth) +// } onLoadMore: { +// do { +// try await self.loadMoreStatuses() +// } catch { +// ErrorService.shared.handle(error, message: "statuses.error.loadingStatusesFailed", showToastr: !Task.isCancelled) +// } +// } + } else { + LazyVStack { + ForEach(dbStatuses, id: \.self) { item in + if self.shouldUpToDateBeVisible(statusId: item.id) { + self.upToDatePlaceholder() } + + ImageRow(statusData: item) + } + + if allItemsLoaded == false { + LoadingIndicator() + .task { + do { + if let account = self.applicationState.account { + let newStatusesCount = try await HomeTimelineService.shared.loadOnBottom(for: account) + if newStatusesCount == 0 { + allItemsLoaded = true + } + } + } catch { + ErrorService.shared.handle(error, message: "global.error.errorDuringDownloadStatuses", showToastr: !Task.isCancelled) + } + } + } } } } + .gallery { galleryProperties in + self.imageColumns = galleryProperties.imageColumns + self.containerWidth = galleryProperties.containerWidth + self.containerHeight = galleryProperties.containerHeight + } self.newPhotosView() .offset(y: self.offset) diff --git a/Vernissage/Views/PaginableStatusesView.swift b/Vernissage/Views/PaginableStatusesView.swift index ab6579b..8e03589 100644 --- a/Vernissage/Views/PaginableStatusesView.swift +++ b/Vernissage/Views/PaginableStatusesView.swift @@ -77,7 +77,7 @@ struct PaginableStatusesView: View { ScrollView { LazyVStack(alignment: .center) { ForEach(self.statusViewModels, id: \.id) { item in - ImageRowAsync(statusViewModel: item) + ImageRowAsync(statusViewModel: item, containerWidth: Binding.constant(UIScreen.main.bounds.width)) } if allItemsLoaded == false { diff --git a/Vernissage/Views/StatusView/StatusView.swift b/Vernissage/Views/StatusView/StatusView.swift index bcfcb36..09b8434 100644 --- a/Vernissage/Views/StatusView/StatusView.swift +++ b/Vernissage/Views/StatusView/StatusView.swift @@ -240,7 +240,9 @@ struct StatusView: View { } if let imageHeight = self.imageHeight, let imageWidth = self.imageWidth, imageHeight > 0 && imageWidth > 0 { - let calculatedSize = ImageSizeService.shared.calculate(width: Double(imageWidth), height: Double(imageHeight)) + let calculatedSize = ImageSizeService.shared.calculate(width: Double(imageWidth), + height: Double(imageHeight), + andContainerWidth: UIScreen.main.bounds.size.width) return calculatedSize.height } diff --git a/Vernissage/Views/StatusesView.swift b/Vernissage/Views/StatusesView.swift index 2768020..bf0c93f 100644 --- a/Vernissage/Views/StatusesView.swift +++ b/Vernissage/Views/StatusesView.swift @@ -11,7 +11,6 @@ import ClientKit import ServicesKit import EnvironmentKit import WidgetsKit -import WaterfallGrid struct StatusesView: View { public enum ListType: Hashable { @@ -51,6 +50,11 @@ struct StatusesView: View { @State private var state: ViewState = .loading @State private var lastStatusId: String? + // Gallery parameters. + @State private var imageColumns = 3 + @State private var containerWidth: Double = UIScreen.main.bounds.width + @State private var containerHeight: Double = UIScreen.main.bounds.height + private let defaultLimit = 40 private let imagePrefetcher = ImagePrefetcher(destination: .diskCache) @@ -89,35 +93,43 @@ struct StatusesView: View { @ViewBuilder private func list() -> some View { ScrollView { - WaterfallGrid(self.statusViewModels, id: \.id) { item in - ImageRowAsync(statusViewModel: item, - withAvatar: true, - imageScale: self.applicationState.showGridOnUserProfile ? .squareHalfWidth : .orginalFullWidth) - .padding(.top, -2) + if self.imageColumns > 1 { + WaterfallGrid(statusViewModel: $statusViewModels, columns: $imageColumns, hideLoadMore: $allItemsLoaded) { item in + ImageRowAsync(statusViewModel: item, containerWidth: $containerWidth) + } onLoadMore: { + do { + try await self.loadMoreStatuses() + } catch { + ErrorService.shared.handle(error, message: "statuses.error.loadingStatusesFailed", showToastr: !Task.isCancelled) + } + } + } else { + LazyVStack(alignment: .center) { + ForEach(self.statusViewModels, id: \.id) { item in + ImageRowAsync(statusViewModel: item, containerWidth: $containerWidth) + } + + if allItemsLoaded == false { + HStack { + Spacer() + LoadingIndicator() + .task { + do { + try await self.loadMoreStatuses() + } catch { + ErrorService.shared.handle(error, message: "statuses.error.loadingStatusesFailed", showToastr: !Task.isCancelled) + } + } + Spacer() + } + } + } } - .gridStyle(columns: 3, spacing: 4) - // .padding(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) - -// LazyVStack(alignment: .center) { -// ForEach(self.statusViewModels, id: \.id) { item in -// ImageRowAsync(statusViewModel: item) -// } -// -// if allItemsLoaded == false { -// HStack { -// Spacer() -// LoadingIndicator() -// .task { -// do { -// try await self.loadMoreStatuses() -// } catch { -// ErrorService.shared.handle(error, message: "statuses.error.loadingStatusesFailed", showToastr: !Task.isCancelled) -// } -// } -// Spacer() -// } -// } -// } + } + .gallery { galleryProperties in + self.imageColumns = galleryProperties.imageColumns + self.containerWidth = galleryProperties.containerWidth + self.containerHeight = galleryProperties.containerHeight } .refreshable { do { diff --git a/Vernissage/Views/TrendStatusesView.swift b/Vernissage/Views/TrendStatusesView.swift index ca041a7..1f66449 100644 --- a/Vernissage/Views/TrendStatusesView.swift +++ b/Vernissage/Views/TrendStatusesView.swift @@ -72,7 +72,7 @@ struct TrendStatusesView: View { } else { LazyVStack(alignment: .center) { ForEach(self.statusViewModels, id: \.id) { item in - ImageRowAsync(statusViewModel: item) + ImageRowAsync(statusViewModel: item, containerWidth: Binding.constant(UIScreen.main.bounds.width)) } } .refreshable { diff --git a/Vernissage/Views/UserProfileView/Subviews/UserProfileStatusesView.swift b/Vernissage/Views/UserProfileView/Subviews/UserProfileStatusesView.swift index 4f6e8e5..2e8dafb 100644 --- a/Vernissage/Views/UserProfileView/Subviews/UserProfileStatusesView.swift +++ b/Vernissage/Views/UserProfileView/Subviews/UserProfileStatusesView.swift @@ -11,7 +11,6 @@ import ClientKit import ServicesKit import EnvironmentKit import WidgetsKit -import WaterfallGrid struct UserProfileStatusesView: View { @EnvironmentObject private var applicationState: ApplicationState @@ -19,6 +18,11 @@ struct UserProfileStatusesView: View { @State public var accountId: String + // Gallery parameters. + @Binding private var imageColumns: Int + @Binding private var containerWidth: Double + @Binding private var containerHeight: Double + @State private var allItemsLoaded = false @State private var firstLoadFinished = false @State private var statusViewModels: [StatusModel] = [] @@ -28,72 +32,76 @@ struct UserProfileStatusesView: View { private let singleGrids = [GridItem(.flexible(), spacing: 10)] private let dubleGrid = [GridItem(.flexible(), spacing: 10), GridItem(.flexible(), spacing: 0)] + init(accountId: String, imageColumns: Binding, containerWidth: Binding, containerHeight: Binding) { + self.accountId = accountId + self._imageColumns = imageColumns + self._containerWidth = containerWidth + self._containerHeight = containerHeight + } + var body: some View { if firstLoadFinished == true { - HStack { - Spacer() - Button { - withAnimation { - self.applicationState.showGridOnUserProfile = false - ApplicationSettingsHandler.shared.set(showGridOnUserProfile: false) + if self.imageColumns > 1 { + WaterfallGrid(statusViewModel: $statusViewModels, columns: $imageColumns, hideLoadMore: $allItemsLoaded) { item in + ImageRowAsync(statusViewModel: item, withAvatar: false, containerWidth: $containerWidth) + } onLoadMore: { + do { + try await self.loadMoreStatuses() + } catch { + ErrorService.shared.handle(error, message: "statuses.error.loadingStatusesFailed", showToastr: !Task.isCancelled) } - } label: { - Image(systemName: "rectangle.grid.1x2.fill") - .foregroundColor(self.applicationState.showGridOnUserProfile ? .lightGrayColor : .accentColor) - .padding(.trailing, 8) - .padding(.bottom, 8) } - Button { - withAnimation { - self.applicationState.showGridOnUserProfile = true - ApplicationSettingsHandler.shared.set(showGridOnUserProfile: true) - } - } label: { - Image(systemName: "rectangle.grid.2x2.fill") - .foregroundColor(self.applicationState.showGridOnUserProfile ? .accentColor : .lightGrayColor) - .padding(.trailing, 16) - .padding(.bottom, 8) - } - } - - WaterfallGrid(self.statusViewModels, id: \.id) { item in - ImageRowAsync(statusViewModel: item, - withAvatar: false, - imageScale: self.applicationState.showGridOnUserProfile ? .squareHalfWidth : .orginalFullWidth) -// .if(self.applicationState.showGridOnUserProfile) { -// $0.frame(width: UIScreen.main.bounds.width / 2, height: UIScreen.main.bounds.width / 2) -// } - } - .gridStyle(columns: 3, spacing: 2) - .padding(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) - - /* - LazyVGrid(columns: self.applicationState.showGridOnUserProfile ? dubleGrid : singleGrids, spacing: 5) { - ForEach(self.statusViewModels, id: \.id) { item in - ImageRowAsync(statusViewModel: item, - withAvatar: false, - imageScale: self.applicationState.showGridOnUserProfile ? .squareHalfWidth : .orginalFullWidth) - .if(self.applicationState.showGridOnUserProfile) { - $0.frame(width: UIScreen.main.bounds.width / 2, height: UIScreen.main.bounds.width / 2) + } else { + HStack { + Spacer() + Button { + withAnimation { + self.applicationState.showGridOnUserProfile = false + ApplicationSettingsHandler.shared.set(showGridOnUserProfile: false) } + } label: { + Image(systemName: "rectangle.grid.1x2.fill") + .foregroundColor(self.applicationState.showGridOnUserProfile ? .lightGrayColor : .accentColor) + .padding(.trailing, 8) + .padding(.bottom, 8) + } + Button { + withAnimation { + self.applicationState.showGridOnUserProfile = true + ApplicationSettingsHandler.shared.set(showGridOnUserProfile: true) + } + } label: { + Image(systemName: "rectangle.grid.2x2.fill") + .foregroundColor(self.applicationState.showGridOnUserProfile ? .accentColor : .lightGrayColor) + .padding(.trailing, 16) + .padding(.bottom, 8) + } } - if allItemsLoaded == false && firstLoadFinished == true { - HStack { - Spacer() - LoadingIndicator() - .task { - do { - try await self.loadMoreStatuses() - } catch { - ErrorService.shared.handle(error, message: "global.error.errorDuringDownloadStatuses", showToastr: true) + LazyVGrid(columns: self.applicationState.showGridOnUserProfile ? dubleGrid : singleGrids, spacing: 5) { + ForEach(self.statusViewModels, id: \.id) { item in + ImageRowAsync(statusViewModel: item, + withAvatar: false, + containerWidth: Binding.constant(self.applicationState.showGridOnUserProfile ? self.containerWidth / 2 : self.containerWidth), + clipToRectangle: $applicationState.showGridOnUserProfile) + } + + if allItemsLoaded == false && firstLoadFinished == true { + HStack { + Spacer() + LoadingIndicator() + .task { + do { + try await self.loadMoreStatuses() + } catch { + ErrorService.shared.handle(error, message: "global.error.errorDuringDownloadStatuses", showToastr: true) + } } - } - Spacer() + Spacer() + } } } } - */ } else { LoadingIndicator() .onFirstAppear { diff --git a/Vernissage/Views/UserProfileView/UserProfileView.swift b/Vernissage/Views/UserProfileView/UserProfileView.swift index 1d4d4ee..1221724 100644 --- a/Vernissage/Views/UserProfileView/UserProfileView.swift +++ b/Vernissage/Views/UserProfileView/UserProfileView.swift @@ -27,6 +27,11 @@ struct UserProfileView: View { @State private var state: ViewState = .loading @State private var viewId = UUID().uuidString + // Gallery parameters. + @State private var imageColumns = 3 + @State private var containerWidth: Double = UIScreen.main.bounds.width + @State private var containerHeight: Double = UIScreen.main.bounds.height + var body: some View { self.mainBody() .navigationTitle(self.accountDisplayName ?? self.accountUserName) @@ -68,11 +73,16 @@ struct UserProfileView: View { .id(self.viewId) if self.applicationState.account?.id == account.id || self.relationship.haveAccessToPhotos(account: account) { - UserProfileStatusesView(accountId: account.id) + UserProfileStatusesView(accountId: account.id, imageColumns: $imageColumns, containerWidth: $containerWidth, containerHeight: $containerHeight) } else { UserProfilePrivateAccountView() } } + .gallery { galleryProperties in + self.imageColumns = galleryProperties.imageColumns + self.containerWidth = galleryProperties.containerWidth + self.containerHeight = galleryProperties.containerHeight + } .onAppear { if let updatedProfile = self.applicationState.updatedProfile { self.account = nil diff --git a/Vernissage/Widgets/ImageRow.swift b/Vernissage/Widgets/ImageRow.swift index b39f774..afd0ae9 100644 --- a/Vernissage/Widgets/ImageRow.swift +++ b/Vernissage/Widgets/ImageRow.swift @@ -25,12 +25,15 @@ struct ImageRow: View { // Calculate size of frame (first from cache, then from real image, then from metadata). if let firstAttachment, let size = ImageSizeService.shared.get(for: firstAttachment.url) { - self.imageWidth = size.width - self.imageHeight = size.height + let calculatedSize = ImageSizeService.shared.calculate(width: size.width, height: size.height, andContainerWidth: UIScreen.main.bounds.size.width) + self.imageWidth = calculatedSize.width + self.imageHeight = calculatedSize.height } else if let firstAttachment, firstAttachment.metaImageWidth > 0 && firstAttachment.metaImageHeight > 0 { - let size = ImageSizeService.shared.calculate(for: firstAttachment.url, - width: firstAttachment.metaImageWidth, - height: firstAttachment.metaImageHeight) + ImageSizeService.shared.save(for: firstAttachment.url, + width: firstAttachment.metaImageWidth, + height: firstAttachment.metaImageHeight) + + let size = ImageSizeService.shared.calculate(for: firstAttachment.url, andContainerWidth: UIScreen.main.bounds.size.width) self.imageWidth = size.width self.imageHeight = size.height } else { @@ -73,7 +76,9 @@ struct ImageRow: View { } .onChange(of: selected, perform: { attachmentId in if let attachment = attachmentsData.first(where: { item in item.id == attachmentId }) { - let size = ImageSizeService.shared.calculate(width: Double(attachment.metaImageWidth), height: Double(attachment.metaImageHeight)) + let size = ImageSizeService.shared.calculate(width: Double(attachment.metaImageWidth), + height: Double(attachment.metaImageHeight), + andContainerWidth: UIScreen.main.bounds.size.width) if size.width != self.imageWidth || size.height != self.imageHeight { withAnimation(.linear(duration: 0.4)) { diff --git a/Vernissage/Widgets/ImageRowAsync.swift b/Vernissage/Widgets/ImageRowAsync.swift index 5b8acc8..811f78b 100644 --- a/Vernissage/Widgets/ImageRowAsync.swift +++ b/Vernissage/Widgets/ImageRowAsync.swift @@ -9,38 +9,50 @@ import PixelfedKit import ClientKit import ServicesKit import WidgetsKit +import EnvironmentKit struct ImageRowAsync: View { private let statusViewModel: StatusModel private let firstAttachment: AttachmentModel? private let showAvatar: Bool - private let imageScale: ImageScale + + @Binding private var containerWidth: Double + @Binding private var clipToRectangle: Bool @State private var selected: String @State private var imageHeight: Double @State private var imageWidth: Double - init(statusViewModel: StatusModel, withAvatar showAvatar: Bool = true, imageScale: ImageScale = .orginalFullWidth) { + init(statusViewModel: StatusModel, + withAvatar showAvatar: Bool = true, + containerWidth: Binding, + clipToRectangle: Binding = Binding.constant(false)) { self.showAvatar = showAvatar - self.imageScale = imageScale self.statusViewModel = statusViewModel self.firstAttachment = statusViewModel.mediaAttachments.first self.selected = String.empty() + self._containerWidth = containerWidth + self._clipToRectangle = clipToRectangle + // Calculate size of frame (first from cache, then from metadata). if let firstAttachment, let size = ImageSizeService.shared.get(for: firstAttachment.url) { - self.imageWidth = size.width - self.imageHeight = size.height + let calculatedSize = ImageSizeService.shared.calculate(width: size.width, height: size.height, andContainerWidth: containerWidth.wrappedValue) + + self.imageWidth = calculatedSize.width + self.imageHeight = calculatedSize.height } else if let firstAttachment, let imgHeight = (firstAttachment.meta as? ImageMetadata)?.original?.height, let imgWidth = (firstAttachment.meta as? ImageMetadata)?.original?.width { - let size = ImageSizeService.shared.calculate(for: firstAttachment.url, width: imgWidth, height: imgHeight) - self.imageWidth = size.width - self.imageHeight = size.height + ImageSizeService.shared.save(for: firstAttachment.url, width: imgWidth, height: imgHeight) + let calculatedSize = ImageSizeService.shared.calculate(for: firstAttachment.url, andContainerWidth: containerWidth.wrappedValue) + + self.imageWidth = calculatedSize.width + self.imageHeight = calculatedSize.height } else { - self.imageWidth = UIScreen.main.bounds.width - self.imageHeight = UIScreen.main.bounds.width + self.imageWidth = containerWidth.wrappedValue + self.imageHeight = containerWidth.wrappedValue } } @@ -49,18 +61,28 @@ struct ImageRowAsync: View { ImageRowItemAsync(statusViewModel: self.statusViewModel, attachment: firstAttachment, withAvatar: self.showAvatar, - imageScale: self.imageScale) { (imageWidth, imageHeight) in + containerWidth: $containerWidth, + clipToRectangle: $clipToRectangle, + showSpoilerText: Binding.constant(self.containerWidth > 300)) { (imageWidth, imageHeight) in // When we download image and calculate real size we have to change view size. - if imageWidth != self.imageWidth || imageHeight != self.imageHeight { + let calculatedSize = ImageSizeService.shared.calculate(width: imageWidth, height: imageHeight, andContainerWidth: self.containerWidth) + + if calculatedSize.width != self.imageWidth || calculatedSize.height != self.imageHeight { withAnimation(.linear(duration: 0.4)) { - self.imageWidth = imageWidth - self.imageHeight = imageHeight + self.imageWidth = calculatedSize.width + self.imageHeight = calculatedSize.height } } } - .if(self.imageScale == .squareHalfWidth) { - $0.frame(width: self.imageWidth / 3, height: self.imageHeight / 3) + .frame(width: self.clipToRectangle ? self.containerWidth : self.imageWidth, + height: self.clipToRectangle ? self.containerWidth : self.imageHeight) + .onChange(of: self.containerWidth) { newContainerWidth in + let calculatedSize = ImageSizeService.shared.calculate(width: self.imageWidth, + height: self.imageHeight, + andContainerWidth: newContainerWidth) + self.imageWidth = calculatedSize.width + self.imageHeight = calculatedSize.height } } else { TabView(selection: $selected) { @@ -68,14 +90,18 @@ struct ImageRowAsync: View { ImageRowItemAsync(statusViewModel: self.statusViewModel, attachment: attachment, withAvatar: self.showAvatar, - imageScale: self.imageScale) { (imageWidth, imageHeight) in + containerWidth: $containerWidth, + clipToRectangle: $clipToRectangle, + showSpoilerText: Binding.constant(self.containerWidth > 300)) { (imageWidth, imageHeight) in // When we download image and calculate real size we have to change view size (only when image is now visible). + let calculatedSize = ImageSizeService.shared.calculate(width: imageWidth, height: imageHeight, andContainerWidth: self.containerWidth) + if attachment.id == self.selected { - if imageWidth != self.imageWidth || imageHeight != self.imageHeight { + if calculatedSize.width != self.imageWidth || calculatedSize.height != self.imageHeight { withAnimation(.linear(duration: 0.4)) { - self.imageWidth = imageWidth - self.imageHeight = imageHeight + self.imageWidth = calculatedSize.width + self.imageHeight = calculatedSize.height } } } @@ -83,24 +109,34 @@ struct ImageRowAsync: View { .tag(attachment.id) } } + .onChange(of: self.containerWidth) { newContainerWidth in + let calculatedSize = ImageSizeService.shared.calculate(width: self.imageWidth, + height: self.imageHeight, + andContainerWidth: newContainerWidth) + self.imageWidth = calculatedSize.width + self.imageHeight = calculatedSize.height + } .onFirstAppear { self.selected = self.statusViewModel.mediaAttachments.first?.id ?? String.empty() } .onChange(of: selected, perform: { attachmentId in if let attachment = self.statusViewModel.mediaAttachments.first(where: { item in item.id == attachmentId }) { if let size = ImageSizeService.shared.get(for: attachment.url) { - if size.width != self.imageWidth || size.height != self.imageHeight { + let calculatedSize = ImageSizeService.shared.calculate(width: size.width, + height: size.height, + andContainerWidth: self.containerWidth) + + if calculatedSize.width != self.imageWidth || calculatedSize.height != self.imageHeight { withAnimation(.linear(duration: 0.4)) { - self.imageWidth = size.width - self.imageHeight = size.height + self.imageWidth = calculatedSize.width + self.imageHeight = calculatedSize.height } } } } }) - .if(self.imageScale == .squareHalfWidth) { - $0.frame(width: self.imageWidth / 3, height: self.imageHeight / 3) - } + .frame(width: self.clipToRectangle ? self.containerWidth : self.imageWidth, + height: self.clipToRectangle ? self.containerWidth : self.imageHeight) .tabViewStyle(.page(indexDisplayMode: .never)) .overlay(CustomPageTabViewStyleView(pages: self.statusViewModel.mediaAttachments, currentId: $selected)) } diff --git a/Vernissage/Widgets/ImageRowItem.swift b/Vernissage/Widgets/ImageRowItem.swift index b82df11..af57c70 100644 --- a/Vernissage/Widgets/ImageRowItem.swift +++ b/Vernissage/Widgets/ImageRowItem.swift @@ -210,14 +210,18 @@ struct ImageRowItem: View { } private func setVariables(imageData: Data, downloadedImage: UIImage) { - let size = ImageSizeService.shared.calculate(for: attachmentData.url, - width: downloadedImage.size.width, - height: downloadedImage.size.height) + ImageSizeService.shared.save(for: attachmentData.url, + width: downloadedImage.size.width, + height: downloadedImage.size.height) + let size = ImageSizeService.shared.calculate(for: attachmentData.url, andContainerWidth: UIScreen.main.bounds.size.width) self.onImageDownloaded(size.width, size.height) - self.uiImage = downloadedImage - HomeTimelineService.shared.update(attachment: attachmentData, withData: imageData, imageWidth: size.width, imageHeight: size.height) + self.uiImage = downloadedImage + HomeTimelineService.shared.update(attachment: attachmentData, + withData: imageData, + imageWidth: downloadedImage.size.width, + imageHeight: downloadedImage.size.height) self.error = nil self.cancelled = false } diff --git a/Vernissage/Widgets/ImageRowItemAsync.swift b/Vernissage/Widgets/ImageRowItemAsync.swift index 1dc6011..2540d3d 100644 --- a/Vernissage/Widgets/ImageRowItemAsync.swift +++ b/Vernissage/Widgets/ImageRowItemAsync.swift @@ -22,7 +22,10 @@ struct ImageRowItemAsync: View { private var attachment: AttachmentModel private let showAvatar: Bool private let imageFromCache: Bool - private let imageScale: ImageScale + + @Binding private var containerWidth: Double + @Binding private var showSpoilerText: Bool + @Binding private var clipToRectangle: Bool @State private var showThumbImage = false @State private var opacity = 1.0 @@ -33,14 +36,19 @@ struct ImageRowItemAsync: View { init(statusViewModel: StatusModel, attachment: AttachmentModel, withAvatar showAvatar: Bool = true, - imageScale: ImageScale = .orginalFullWidth, + containerWidth: Binding, + clipToRectangle: Binding = Binding.constant(false), + showSpoilerText: Binding = Binding.constant(true), onImageDownloaded: @escaping (_: Double, _: Double) -> Void) { self.showAvatar = showAvatar - self.imageScale = imageScale self.statusViewModel = statusViewModel self.attachment = attachment self.onImageDownloaded = onImageDownloaded + self._containerWidth = containerWidth + self._showSpoilerText = showSpoilerText + self._clipToRectangle = clipToRectangle + self.imageFromCache = ImagePipeline.shared.cache.containsCachedImage(for: ImageRequest(url: attachment.url)) } @@ -49,7 +57,7 @@ struct ImageRowItemAsync: View { if let image = state.image { if self.statusViewModel.sensitive && !self.applicationState.showSensitive { ZStack { - ContentWarning(spoilerText: self.imageScale == .orginalFullWidth ? self.statusViewModel.spoilerText : nil) { + ContentWarning(spoilerText: self.showSpoilerText ? self.statusViewModel.spoilerText : nil) { self.imageContainerView(image: image) .imageContextMenu(statusModel: self.statusViewModel, attachmentModel: self.attachment, @@ -155,11 +163,10 @@ struct ImageRowItemAsync: View { private func imageView(image: Image) -> some View { image .resizable() - //.aspectRatio(contentMode: .fill) - .aspectRatio(contentMode: .fit) -// .if(self.imageScale == .squareHalfWidth) { -// $0.frame(width: UIScreen.main.bounds.width / 4, height: UIScreen.main.bounds.width / 4).clipped() -// } + .aspectRatio(contentMode: self.clipToRectangle ? .fill : .fit) + .if(self.clipToRectangle) { + $0.frame(width: self.containerWidth, height: self.containerWidth).clipped() + } .onTapGesture(count: 2) { Task { // Update favourite in Pixelfed server. @@ -195,10 +202,11 @@ struct ImageRowItemAsync: View { } private func recalculateSizeOfDownloadedImage(uiImage: UIImage) { - let size = ImageSizeService.shared.calculate(for: attachment.url, - width: uiImage.size.width, - height: uiImage.size.height) + ImageSizeService.shared.save(for: attachment.url, + width: uiImage.size.width, + height: uiImage.size.height) + let size = ImageSizeService.shared.calculate(for: attachment.url, andContainerWidth: UIScreen.main.bounds.size.width) self.onImageDownloaded(size.width, size.height) } } diff --git a/Vernissage/Widgets/ImageViewer.swift b/Vernissage/Widgets/ImageViewer.swift index 7c25f9a..e734d6a 100644 --- a/Vernissage/Widgets/ImageViewer.swift +++ b/Vernissage/Widgets/ImageViewer.swift @@ -224,7 +224,9 @@ struct ImageViewer: View { private func calculateStartingOffset() -> CGSize { // Image size on the screen. - let calculatedSize = ImageSizeService.shared.calculate(width: self.imageWidth, height: self.imageHeight) + let calculatedSize = ImageSizeService.shared.calculate(width: self.imageWidth, + height: self.imageHeight, + andContainerWidth: UIScreen.main.bounds.size.width) let imageOnScreenHeight = calculatedSize.height // Calculate full space for image. diff --git a/Vernissage/Widgets/ImagesCarousel.swift b/Vernissage/Widgets/ImagesCarousel.swift index aeb54b0..239b734 100644 --- a/Vernissage/Widgets/ImagesCarousel.swift +++ b/Vernissage/Widgets/ImagesCarousel.swift @@ -52,7 +52,8 @@ struct ImagesCarousel: View { self.heightWasPrecalculated = true } else if let highestImage, imgHeight > 0 && imgWidth > 0 { - let size = ImageSizeService.shared.calculate(for: highestImage.url, width: imgWidth, height: imgHeight) + ImageSizeService.shared.save(for: highestImage.url, width: imgWidth, height: imgHeight) + let size = ImageSizeService.shared.calculate(for: highestImage.url, andContainerWidth: UIScreen.main.bounds.size.width) self.imageWidth = size.width self.imageHeight = size.height @@ -107,6 +108,8 @@ struct ImagesCarousel: View { for item in attachments { if let data = item.data, let image = UIImage(data: data) { + ImageSizeService.shared.save(for: attachment.url, width: image.size.width, height: image.size.height) + if image.size.height > imageHeight { imageHeight = image.size.height imageWidth = image.size.width @@ -121,7 +124,7 @@ struct ImagesCarousel: View { } } - let size = ImageSizeService.shared.calculate(for: attachment.url, width: imageWidth, height: imageHeight) + let size = ImageSizeService.shared.calculate(for: attachment.url, andContainerWidth: UIScreen.main.bounds.size.width) self.imageWidth = size.width self.imageHeight = size.height } diff --git a/Vernissage/Widgets/WaterfallGrid.swift b/Vernissage/Widgets/WaterfallGrid.swift new file mode 100644 index 0000000..c5c185a --- /dev/null +++ b/Vernissage/Widgets/WaterfallGrid.swift @@ -0,0 +1,83 @@ +// +// https://mczachurski.dev +// Copyright © 2023 Marcin Czachurski and the repository contributors. +// Licensed under the Apache License 2.0. +// + +import SwiftUI +import WidgetsKit +import ClientKit + +struct WaterfallGrid: View where Content: View { + @Binding private var statusViewModels: [StatusModel] + @Binding private var columns: Int + @Binding private var hideLoadMore: Bool + + @State private var data: [[StatusModel]] = [] + + private let onLoadMore: () async -> Void + private let content: (StatusModel) -> Content + + init(statusViewModel: Binding<[StatusModel]>, + columns: Binding, + hideLoadMore: Binding, + content: @escaping (StatusModel) -> Content, + onLoadMore: @escaping () async -> Void) { + self._statusViewModels = statusViewModel + self._columns = columns + self._hideLoadMore = hideLoadMore + self.content = content + self.onLoadMore = onLoadMore + } + + var body: some View { + HStack(alignment: .top, spacing: 20) { + ForEach(self.data, id: \.self) { array in + LazyVStack(spacing: 8) { + ForEach(array, id: \.id) { item in + self.content(item) + } + + if self.shouldShowSpinner(array: array) { + LoadingIndicator() + .task { + await self.onLoadMore() + } + } + } + } + } + .onFirstAppear { + self.recalculateArrays() + } + .onChange(of: self.statusViewModels) { _ in + self.recalculateArrays() + } + .onChange(of: self.columns) { _ in + self.recalculateArrays() + } + } + + private func recalculateArrays() { + var internalArray: [[StatusModel]] = [] + + for _ in 0 ..< self.columns { + internalArray.append([]) + } + + for (index, item) in self.statusViewModels.enumerated() { + let arrayIndex = index % self.columns + internalArray[arrayIndex].append(item) + } + + self.data = internalArray + } + + private func shouldShowSpinner(array: [StatusModel]) -> Bool { + if self.hideLoadMore { + return false + } + + return self.data[1].first == array.first + } +} diff --git a/WidgetsKit/Sources/WidgetsKit/Extensions/View+AsyncAfter.swift b/WidgetsKit/Sources/WidgetsKit/Extensions/View+AsyncAfter.swift index 9b0261b..f9a570b 100644 --- a/WidgetsKit/Sources/WidgetsKit/Extensions/View+AsyncAfter.swift +++ b/WidgetsKit/Sources/WidgetsKit/Extensions/View+AsyncAfter.swift @@ -14,3 +14,11 @@ public extension View { } } } + +public extension ViewModifier { + func asyncAfter(_ time: Double, operation: @escaping () -> Void) { + DispatchQueue.main.asyncAfter(deadline: .now() + time) { + operation() + } + } +} diff --git a/WidgetsKit/Sources/WidgetsKit/ViewModifiers/DeviceRotation.swift b/WidgetsKit/Sources/WidgetsKit/ViewModifiers/DeviceRotation.swift new file mode 100644 index 0000000..6d7330c --- /dev/null +++ b/WidgetsKit/Sources/WidgetsKit/ViewModifiers/DeviceRotation.swift @@ -0,0 +1,85 @@ +// +// https://mczachurski.dev +// Copyright © 2023 Marcin Czachurski and the repository contributors. +// Licensed under the Apache License 2.0. +// + +import Foundation +import SwiftUI + +struct DeviceRotationViewModifier: ViewModifier { + let action: (UIDeviceOrientation) -> Void + + func body(content: Content) -> some View { + content + .onAppear() + .onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in + action(UIDevice.current.orientation) + } + } +} + +public extension View { + func onRotate(perform action: @escaping (UIDeviceOrientation) -> Void) -> some View { + self.modifier(DeviceRotationViewModifier(action: action)) + } +} + +public struct GalleryProperties { + public let imageColumns: Int + public let containerWidth: Double + public let containerHeight: Double +} + +struct DeviceImageGallery: ViewModifier { + @Environment(\.horizontalSizeClass) var horizontalSizeClass: UserInterfaceSizeClass? + + let action: (GalleryProperties) -> Void + + func body(content: Content) -> some View { + GeometryReader { geometry in + content + .onRotate { _ in + asyncAfter(0.1) { + let galleryProperties = self.getGalleryProperties(geometry: geometry, horizontalSize: self.horizontalSizeClass ?? .compact) + self.action(galleryProperties) + } + } + .onChange(of: self.horizontalSizeClass) { horizontalSize in + asyncAfter(0.1) { + let galleryProperties = self.getGalleryProperties(geometry: geometry, horizontalSize: horizontalSize ?? .compact) + self.action(galleryProperties) + } + } + .onAppear { + asyncAfter(0.1) { + let galleryProperties = self.getGalleryProperties(geometry: geometry, horizontalSize: self.horizontalSizeClass ?? .compact) + self.action(galleryProperties) + } + } + } + } + + private func getGalleryProperties(geometry: GeometryProxy, horizontalSize: UserInterfaceSizeClass) -> GalleryProperties { + if horizontalSize == .compact { + // View like on iPhone. + return GalleryProperties(imageColumns: 1, + containerWidth: geometry.size.width, + containerHeight: geometry.size.height) + } else { + // View like on iPad. + let imageColumns = geometry.size.width > geometry.size.height ? 3 : 2 + print("\(geometry.size.width ):\(geometry.size.height)") + + return GalleryProperties(imageColumns: imageColumns, + containerWidth: geometry.size.width / Double(imageColumns), + containerHeight: geometry.size.height / Double(imageColumns)) + } + } +} + +public extension View { + func gallery(perform action: @escaping (GalleryProperties) -> Void) -> some View { + self.modifier(DeviceImageGallery(action: action)) + } +} From d3aa3b7099f83881a245be167331c980ed47107d Mon Sep 17 00:00:00 2001 From: Marcin Czachurski Date: Fri, 26 May 2023 16:06:38 +0200 Subject: [PATCH 03/14] Timelines with waterfall --- .../Sources/ClientKit/Client+Timeline.swift | 7 ++ Vernissage/Views/HomeFeedView.swift | 60 ++++++----------- Vernissage/Views/MainView.swift | 9 ++- Vernissage/Views/PaginableStatusesView.swift | 50 ++++++++++---- Vernissage/Views/StatusesView.swift | 12 +++- Vernissage/Views/TrendStatusesView.swift | 40 ++++++++---- .../Subviews/UserProfileStatusesView.swift | 2 +- Vernissage/Widgets/WaterfallGrid.swift | 65 ++++++++++++------- .../Extensions/UIDevice+Device.swift | 18 +++++ 9 files changed, 168 insertions(+), 95 deletions(-) create mode 100644 WidgetsKit/Sources/WidgetsKit/Extensions/UIDevice+Device.swift diff --git a/ClientKit/Sources/ClientKit/Client+Timeline.swift b/ClientKit/Sources/ClientKit/Client+Timeline.swift index a2ac89f..41b1383 100644 --- a/ClientKit/Sources/ClientKit/Client+Timeline.swift +++ b/ClientKit/Sources/ClientKit/Client+Timeline.swift @@ -9,6 +9,13 @@ import PixelfedKit extension Client { public class PublicTimeline: BaseClient { + public func getHomeTimeline(maxId: String? = nil, + sinceId: String? = nil, + minId: String? = nil, + limit: Int = 40) async throws -> [Status] { + return try await pixelfedClient.getHomeTimeline(maxId: maxId, sinceId: sinceId, minId: minId, limit: limit) + } + public func getStatuses(local: Bool? = nil, remote: Bool? = nil, maxId: String? = nil, diff --git a/Vernissage/Views/HomeFeedView.swift b/Vernissage/Views/HomeFeedView.swift index 6d008c8..1a3e11a 100644 --- a/Vernissage/Views/HomeFeedView.swift +++ b/Vernissage/Views/HomeFeedView.swift @@ -21,11 +21,6 @@ struct HomeFeedView: View { @State private var opacity = 0.0 @State private var offset = -50.0 - // Gallery parameters. - @State private var imageColumns = 3 - @State private var containerWidth: Double = UIScreen.main.bounds.width - @State private var containerHeight: Double = UIScreen.main.bounds.height - @FetchRequest var dbStatuses: FetchedResults init(accountId: String) { @@ -60,49 +55,32 @@ struct HomeFeedView: View { private func timeline() -> some View { ZStack { ScrollView { - if self.imageColumns > 1 { -// WaterfallGrid(statusViewModel: $dbStatuses, columns: $imageColumns, hideLoadMore: $allItemsLoaded) { item in -// ImageRowAsync(statusViewModel: item, containerWidth: $containerWidth) -// } onLoadMore: { -// do { -// try await self.loadMoreStatuses() -// } catch { -// ErrorService.shared.handle(error, message: "statuses.error.loadingStatusesFailed", showToastr: !Task.isCancelled) -// } -// } - } else { - LazyVStack { - ForEach(dbStatuses, id: \.self) { item in - if self.shouldUpToDateBeVisible(statusId: item.id) { - self.upToDatePlaceholder() - } - - ImageRow(statusData: item) + LazyVStack { + ForEach(dbStatuses, id: \.self) { item in + if self.shouldUpToDateBeVisible(statusId: item.id) { + self.upToDatePlaceholder() } - - if allItemsLoaded == false { - LoadingIndicator() - .task { - do { - if let account = self.applicationState.account { - let newStatusesCount = try await HomeTimelineService.shared.loadOnBottom(for: account) - if newStatusesCount == 0 { - allItemsLoaded = true - } + + ImageRow(statusData: item) + } + + if allItemsLoaded == false { + LoadingIndicator() + .task { + do { + if let account = self.applicationState.account { + let newStatusesCount = try await HomeTimelineService.shared.loadOnBottom(for: account) + if newStatusesCount == 0 { + allItemsLoaded = true } - } catch { - ErrorService.shared.handle(error, message: "global.error.errorDuringDownloadStatuses", showToastr: !Task.isCancelled) } + } catch { + ErrorService.shared.handle(error, message: "global.error.errorDuringDownloadStatuses", showToastr: !Task.isCancelled) } - } + } } } } - .gallery { galleryProperties in - self.imageColumns = galleryProperties.imageColumns - self.containerWidth = galleryProperties.containerWidth - self.containerHeight = galleryProperties.containerHeight - } self.newPhotosView() .offset(y: self.offset) diff --git a/Vernissage/Views/MainView.swift b/Vernissage/Views/MainView.swift index 6662783..1961456 100644 --- a/Vernissage/Views/MainView.swift +++ b/Vernissage/Views/MainView.swift @@ -116,8 +116,13 @@ struct MainView: View { private func getMainView() -> some View { switch self.viewMode { case .home: - HomeFeedView(accountId: applicationState.account?.id ?? String.empty()) - .id(applicationState.account?.id ?? String.empty()) + if UIDevice.isIPhone { + HomeFeedView(accountId: applicationState.account?.id ?? String.empty()) + .id(applicationState.account?.id ?? String.empty()) + } else { + StatusesView(listType: .home) + .id(applicationState.account?.id ?? String.empty()) + } case .trendingPhotos: TrendStatusesView(accountId: applicationState.account?.id ?? String.empty()) .id(applicationState.account?.id ?? String.empty()) diff --git a/Vernissage/Views/PaginableStatusesView.swift b/Vernissage/Views/PaginableStatusesView.swift index 8e03589..6e6e29e 100644 --- a/Vernissage/Views/PaginableStatusesView.swift +++ b/Vernissage/Views/PaginableStatusesView.swift @@ -38,6 +38,11 @@ struct PaginableStatusesView: View { @State private var state: ViewState = .loading @State private var page = 1 + // Gallery parameters. + @State private var imageColumns = 3 + @State private var containerWidth: Double = UIScreen.main.bounds.width + @State private var containerHeight: Double = UIScreen.main.bounds.height + private let defaultLimit = 10 private let imagePrefetcher = ImagePrefetcher(destination: .diskCache) @@ -75,27 +80,44 @@ struct PaginableStatusesView: View { @ViewBuilder private func list() -> some View { ScrollView { - LazyVStack(alignment: .center) { - ForEach(self.statusViewModels, id: \.id) { item in - ImageRowAsync(statusViewModel: item, containerWidth: Binding.constant(UIScreen.main.bounds.width)) + if self.imageColumns > 1 { + WaterfallGrid($statusViewModels, columns: $imageColumns, hideLoadMore: $allItemsLoaded) { item in + ImageRowAsync(statusViewModel: item, containerWidth: $containerWidth) + } onLoadMore: { + do { + try await self.loadMoreStatuses() + } catch { + ErrorService.shared.handle(error, message: "statuses.error.loadingStatusesFailed", showToastr: !Task.isCancelled) + } } + } else { + LazyVStack(alignment: .center) { + ForEach(self.statusViewModels, id: \.id) { item in + ImageRowAsync(statusViewModel: item, containerWidth: $containerWidth) + } - if allItemsLoaded == false { - HStack { - Spacer() - LoadingIndicator() - .task { - do { - try await self.loadMoreStatuses() - } catch { - ErrorService.shared.handle(error, message: "statuses.error.loadingStatusesFailed", showToastr: !Task.isCancelled) + if allItemsLoaded == false { + HStack { + Spacer() + LoadingIndicator() + .task { + do { + try await self.loadMoreStatuses() + } catch { + ErrorService.shared.handle(error, message: "statuses.error.loadingStatusesFailed", showToastr: !Task.isCancelled) + } } - } - Spacer() + Spacer() + } } } } } + .gallery { galleryProperties in + self.imageColumns = galleryProperties.imageColumns + self.containerWidth = galleryProperties.containerWidth + self.containerHeight = galleryProperties.containerHeight + } } private func loadData() async { diff --git a/Vernissage/Views/StatusesView.swift b/Vernissage/Views/StatusesView.swift index bf0c93f..25e5674 100644 --- a/Vernissage/Views/StatusesView.swift +++ b/Vernissage/Views/StatusesView.swift @@ -14,6 +14,7 @@ import WidgetsKit struct StatusesView: View { public enum ListType: Hashable { + case home case local case federated case favourites @@ -22,6 +23,8 @@ struct StatusesView: View { public var title: LocalizedStringKey { switch self { + case .home: + return "mainview.tab.homeTimeline" case .local: return "statuses.navigationBar.localTimeline" case .federated: @@ -94,10 +97,11 @@ struct StatusesView: View { private func list() -> some View { ScrollView { if self.imageColumns > 1 { - WaterfallGrid(statusViewModel: $statusViewModels, columns: $imageColumns, hideLoadMore: $allItemsLoaded) { item in + WaterfallGrid($statusViewModels, columns: $imageColumns, hideLoadMore: $allItemsLoaded) { item in ImageRowAsync(statusViewModel: item, containerWidth: $containerWidth) } onLoadMore: { do { + print("load more......") try await self.loadMoreStatuses() } catch { ErrorService.shared.handle(error, message: "statuses.error.loadingStatusesFailed", showToastr: !Task.isCancelled) @@ -232,6 +236,12 @@ struct StatusesView: View { private func loadFromApi(maxId: String? = nil, sinceId: String? = nil, minId: String? = nil) async throws -> [Status] { switch self.listType { + case .home: + return try await self.client.publicTimeline?.getHomeTimeline( + maxId: maxId, + sinceId: sinceId, + minId: minId, + limit: self.defaultLimit) ?? [] case .local: return try await self.client.publicTimeline?.getStatuses( local: true, diff --git a/Vernissage/Views/TrendStatusesView.swift b/Vernissage/Views/TrendStatusesView.swift index 1f66449..a0ccc9d 100644 --- a/Vernissage/Views/TrendStatusesView.swift +++ b/Vernissage/Views/TrendStatusesView.swift @@ -21,6 +21,11 @@ struct TrendStatusesView: View { @State private var statusViewModels: [StatusModel] = [] @State private var state: ViewState = .loading + // Gallery parameters. + @State private var imageColumns = 3 + @State private var containerWidth: Double = UIScreen.main.bounds.width + @State private var containerHeight: Double = UIScreen.main.bounds.height + var body: some View { ScrollView { Picker(selection: $tabSelectedValue, label: Text("")) { @@ -45,6 +50,20 @@ struct TrendStatusesView: View { self.mainBody() } + .gallery { galleryProperties in + self.imageColumns = galleryProperties.imageColumns + self.containerWidth = galleryProperties.containerWidth + self.containerHeight = galleryProperties.containerHeight + } + .refreshable { + do { + HapticService.shared.fireHaptic(of: .dataRefresh(intensity: 0.3)) + try await self.loadStatuses() + HapticService.shared.fireHaptic(of: .dataRefresh(intensity: 0.7)) + } catch { + ErrorService.shared.handle(error, message: "trendingStatuses.error.loadingStatusesFailed", showToastr: !Task.isCancelled) + } + } .navigationTitle("trendingStatuses.navigationBar.title") } @@ -70,18 +89,15 @@ struct TrendStatusesView: View { if self.statusViewModels.isEmpty { NoDataView(imageSystemName: "photo.on.rectangle.angled", text: "trendingStatuses.title.noPhotos") } else { - LazyVStack(alignment: .center) { - ForEach(self.statusViewModels, id: \.id) { item in - ImageRowAsync(statusViewModel: item, containerWidth: Binding.constant(UIScreen.main.bounds.width)) - } - } - .refreshable { - do { - HapticService.shared.fireHaptic(of: .dataRefresh(intensity: 0.3)) - try await self.loadStatuses() - HapticService.shared.fireHaptic(of: .dataRefresh(intensity: 0.7)) - } catch { - ErrorService.shared.handle(error, message: "trendingStatuses.error.loadingStatusesFailed", showToastr: !Task.isCancelled) + if self.imageColumns > 1 { + WaterfallGrid($statusViewModels, columns: $imageColumns, hideLoadMore: Binding.constant(true)) { item in + ImageRowAsync(statusViewModel: item, containerWidth: $containerWidth) + } onLoadMore: { } + } else { + LazyVStack(alignment: .center) { + ForEach(self.statusViewModels, id: \.id) { item in + ImageRowAsync(statusViewModel: item, containerWidth: Binding.constant(UIScreen.main.bounds.width)) + } } } } diff --git a/Vernissage/Views/UserProfileView/Subviews/UserProfileStatusesView.swift b/Vernissage/Views/UserProfileView/Subviews/UserProfileStatusesView.swift index 2e8dafb..05286c8 100644 --- a/Vernissage/Views/UserProfileView/Subviews/UserProfileStatusesView.swift +++ b/Vernissage/Views/UserProfileView/Subviews/UserProfileStatusesView.swift @@ -42,7 +42,7 @@ struct UserProfileStatusesView: View { var body: some View { if firstLoadFinished == true { if self.imageColumns > 1 { - WaterfallGrid(statusViewModel: $statusViewModels, columns: $imageColumns, hideLoadMore: $allItemsLoaded) { item in + WaterfallGrid($statusViewModels, columns: $imageColumns, hideLoadMore: $allItemsLoaded) { item in ImageRowAsync(statusViewModel: item, withAvatar: false, containerWidth: $containerWidth) } onLoadMore: { do { diff --git a/Vernissage/Widgets/WaterfallGrid.swift b/Vernissage/Widgets/WaterfallGrid.swift index c5c185a..9d622ca 100644 --- a/Vernissage/Widgets/WaterfallGrid.swift +++ b/Vernissage/Widgets/WaterfallGrid.swift @@ -6,33 +6,23 @@ import SwiftUI import WidgetsKit -import ClientKit -struct WaterfallGrid: View where Content: View { - @Binding private var statusViewModels: [StatusModel] +struct WaterfallGrid: View where Data: RandomAccessCollection, Data: Equatable, Content: View, + ID: Hashable, Data.Element: Equatable, Data.Element: Identifiable, Data.Element: Hashable { @Binding private var columns: Int @Binding private var hideLoadMore: Bool - @State private var data: [[StatusModel]] = [] + @Binding private var data: Data + private let dataId: KeyPath + private let content: (Data.Element) -> Content + + @State private var columnsData: [[Data.Element]] = [] private let onLoadMore: () async -> Void - private let content: (StatusModel) -> Content - - init(statusViewModel: Binding<[StatusModel]>, - columns: Binding, - hideLoadMore: Binding, - content: @escaping (StatusModel) -> Content, - onLoadMore: @escaping () async -> Void) { - self._statusViewModels = statusViewModel - self._columns = columns - self._hideLoadMore = hideLoadMore - self.content = content - self.onLoadMore = onLoadMore - } var body: some View { HStack(alignment: .top, spacing: 20) { - ForEach(self.data, id: \.self) { array in + ForEach(self.columnsData, id: \.self) { array in LazyVStack(spacing: 8) { ForEach(array, id: \.id) { item in self.content(item) @@ -50,7 +40,7 @@ struct WaterfallGrid: View where Content: View { .onFirstAppear { self.recalculateArrays() } - .onChange(of: self.statusViewModels) { _ in + .onChange(of: self.data) { _ in self.recalculateArrays() } .onChange(of: self.columns) { _ in @@ -59,25 +49,52 @@ struct WaterfallGrid: View where Content: View { } private func recalculateArrays() { - var internalArray: [[StatusModel]] = [] + var internalArray: [[Data.Element]] = [] for _ in 0 ..< self.columns { internalArray.append([]) } - for (index, item) in self.statusViewModels.enumerated() { + for (index, item) in self.data.enumerated() { let arrayIndex = index % self.columns internalArray[arrayIndex].append(item) } - self.data = internalArray + self.columnsData = internalArray } - private func shouldShowSpinner(array: [StatusModel]) -> Bool { + private func shouldShowSpinner(array: [Data.Element]) -> Bool { if self.hideLoadMore { return false } - return self.data[1].first == array.first + return self.columnsData[1].first == array.first + } + +} + +extension WaterfallGrid { + init(_ data: Binding, id: KeyPath, columns: Binding, + hideLoadMore: Binding, content: @escaping (Data.Element) -> Content, onLoadMore: @escaping () async -> Void) { + self._data = data + self.dataId = id + self.content = content + + self._columns = columns + self._hideLoadMore = hideLoadMore + self.onLoadMore = onLoadMore + } +} + +extension WaterfallGrid where ID == Data.Element.ID, Data.Element: Identifiable { + init(_ data: Binding, columns: Binding, + hideLoadMore: Binding, content: @escaping (Data.Element) -> Content, onLoadMore: @escaping () async -> Void) { + self._data = data + self.dataId = \Data.Element.id + self.content = content + + self._columns = columns + self._hideLoadMore = hideLoadMore + self.onLoadMore = onLoadMore } } diff --git a/WidgetsKit/Sources/WidgetsKit/Extensions/UIDevice+Device.swift b/WidgetsKit/Sources/WidgetsKit/Extensions/UIDevice+Device.swift new file mode 100644 index 0000000..1704cbf --- /dev/null +++ b/WidgetsKit/Sources/WidgetsKit/Extensions/UIDevice+Device.swift @@ -0,0 +1,18 @@ +// +// https://mczachurski.dev +// Copyright © 2023 Marcin Czachurski and the repository contributors. +// Licensed under the Apache License 2.0. +// + +import Foundation +import UIKit + +public extension UIDevice { + static var isIPad: Bool { + UIDevice.current.userInterfaceIdiom == .pad + } + + static var isIPhone: Bool { + UIDevice.current.userInterfaceIdiom == .phone + } +} From ed94179b1d67251d3c8e0e458e780c1f6889cc59 Mon Sep 17 00:00:00 2001 From: Marcin Czachurski Date: Sun, 10 Sep 2023 12:03:08 +0200 Subject: [PATCH 04/14] Fix image height in status view --- Vernissage/Widgets/ImagesCarousel.swift | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Vernissage/Widgets/ImagesCarousel.swift b/Vernissage/Widgets/ImagesCarousel.swift index 239b734..99359a9 100644 --- a/Vernissage/Widgets/ImagesCarousel.swift +++ b/Vernissage/Widgets/ImagesCarousel.swift @@ -47,8 +47,12 @@ struct ImagesCarousel: View { // Calculate size of frame (first from cache, then from metadata). if let highestImage, let size = ImageSizeService.shared.get(for: highestImage.url) { - self.imageWidth = size.width - self.imageHeight = size.height + let calculatedSize = ImageSizeService.shared.calculate(width: size.width, + height: size.height, + andContainerWidth: UIScreen.main.bounds.size.width) + + self.imageWidth = calculatedSize.width + self.imageHeight = calculatedSize.height self.heightWasPrecalculated = true } else if let highestImage, imgHeight > 0 && imgWidth > 0 { From 317102287082e3b8d3dc5eb276751e2043528f7a Mon Sep 17 00:00:00 2001 From: Marcin Czachurski Date: Sat, 16 Sep 2023 10:39:21 +0200 Subject: [PATCH 05/14] Fix isues on iPad --- .../ServicesKit/ImageSizeService.swift | 33 +++++++ Vernissage.xcodeproj/project.pbxproj | 12 ++- Vernissage/Views/StatusView/StatusView.swift | 24 +++-- .../Subviews/StatusPlaceholderView.swift | 5 +- Vernissage/Views/StatusesView.swift | 1 - .../Subviews/UserProfileHeaderView.swift | 2 + Vernissage/Widgets/ImageCarouselPicture.swift | 19 ++++ Vernissage/Widgets/ImagesCarousel.swift | 90 +++++++++++-------- .../Extensions/View+ContainerBackground.swift | 29 ++++++ .../PhotoWidget/PhotoWidget.swift | 1 + .../Views/PhotoLargeWidgetView.swift | 2 +- .../Views/PhotoMediumWidgetView.swift | 2 +- .../Views/PhotoSmallWidgetView.swift | 2 +- .../QRCodeWidget/QRCodeWidget.swift | 1 + .../Views/QRCodeLargeWidgetView.swift | 2 + .../Views/QRCodeMediumWidgetView.swift | 2 + .../Views/QRCodeSmallWidgetView.swift | 2 + .../ViewModifiers/DeviceRotation.swift | 5 +- 18 files changed, 179 insertions(+), 55 deletions(-) create mode 100644 VernissageWidget/Extensions/View+ContainerBackground.swift diff --git a/ServicesKit/Sources/ServicesKit/ImageSizeService.swift b/ServicesKit/Sources/ServicesKit/ImageSizeService.swift index 177db3a..a6e57a6 100644 --- a/ServicesKit/Sources/ServicesKit/ImageSizeService.swift +++ b/ServicesKit/Sources/ServicesKit/ImageSizeService.swift @@ -18,6 +18,7 @@ public class ImageSizeService { /// Cache with orginal image sizes. private var memoryCacheData = MemoryCache(entryLifetime: 3600) + private let staticImageHeight = 500.0 public func get(for url: URL) -> CGSize? { return self.memoryCacheData[url] @@ -37,6 +38,12 @@ public class ImageSizeService { } extension ImageSizeService { + public func calculate(for url: URL) -> CGSize { + return UIDevice.current.userInterfaceIdiom == .phone + ? ImageSizeService.shared.calculate(for: url, andContainerWidth: UIScreen.main.bounds.size.width) + : ImageSizeService.shared.calculate(for: url, andContainerHeight: self.staticImageHeight) + } + public func calculate(for url: URL, andContainerWidth containerWidth: Double) -> CGSize { guard let size = self.get(for: url) else { return CGSize(width: containerWidth, height: containerWidth) @@ -45,6 +52,20 @@ extension ImageSizeService { return self.calculate(width: size.width, height: size.height, andContainerWidth: containerWidth) } + public func calculate(for url: URL, andContainerHeight containerHeight: Double) -> CGSize { + guard let size = self.get(for: url) else { + return CGSize(width: containerHeight, height: containerHeight) + } + + return self.calculate(width: size.width, height: size.height, andContainerHeight: containerHeight) + } + + public func calculate(width: Double, height: Double) -> CGSize { + return UIDevice.current.userInterfaceIdiom == .phone + ? ImageSizeService.shared.calculate(width: width, height: height, andContainerWidth: UIScreen.main.bounds.size.width) + : ImageSizeService.shared.calculate(width: width, height: height, andContainerHeight: self.staticImageHeight) + } + public func calculate(width: Double, height: Double, andContainerWidth containerWidth: Double) -> CGSize { let divider = Double(width) / containerWidth let calculatedHeight = Double(height) / divider @@ -56,4 +77,16 @@ extension ImageSizeService { return size } + + public func calculate(width: Double, height: Double, andContainerHeight containerHeight: Double) -> CGSize { + let divider = Double(height) / containerHeight + let calculatedWidth = Double(width) / divider + + let size = CGSize( + width: (calculatedWidth > 0 && calculatedWidth < .infinity) ? calculatedWidth : containerHeight, + height: containerHeight + ) + + return size + } } diff --git a/Vernissage.xcodeproj/project.pbxproj b/Vernissage.xcodeproj/project.pbxproj index 0125604..5d35434 100644 --- a/Vernissage.xcodeproj/project.pbxproj +++ b/Vernissage.xcodeproj/project.pbxproj @@ -200,6 +200,7 @@ F8F6E44D29BCC1F90004795E /* PhotoMediumWidgetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8F6E44829BCC0F00004795E /* PhotoMediumWidgetView.swift */; }; F8F6E44E29BCC1FB0004795E /* PhotoLargeWidgetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8F6E44A29BCC0FF0004795E /* PhotoLargeWidgetView.swift */; }; F8F6E45129BCE9190004795E /* UIImage+Resize.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8F6E45029BCE9190004795E /* UIImage+Resize.swift */; }; + F8FAA0AD2AB0BCB400FD78BD /* View+ContainerBackground.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8FAA0AC2AB0BCB400FD78BD /* View+ContainerBackground.swift */; }; F8FB8ABA29EB2ED400342C04 /* NavigationMenuButtons.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8FB8AB929EB2ED400342C04 /* NavigationMenuButtons.swift */; }; /* End PBXBuildFile section */ @@ -407,6 +408,7 @@ F8F6E44829BCC0F00004795E /* PhotoMediumWidgetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoMediumWidgetView.swift; sourceTree = ""; }; F8F6E44A29BCC0FF0004795E /* PhotoLargeWidgetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoLargeWidgetView.swift; sourceTree = ""; }; F8F6E45029BCE9190004795E /* UIImage+Resize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+Resize.swift"; sourceTree = ""; }; + F8FAA0AC2AB0BCB400FD78BD /* View+ContainerBackground.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+ContainerBackground.swift"; sourceTree = ""; }; F8FB8AB929EB2ED400342C04 /* NavigationMenuButtons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationMenuButtons.swift; sourceTree = ""; }; F8FFBD4929E99BEE0047EE80 /* Vernissage-009.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Vernissage-009.xcdatamodel"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -909,6 +911,7 @@ children = ( F815F60B29E49CF20044566B /* Avatar.swift */, F8F6E45029BCE9190004795E /* UIImage+Resize.swift */, + F8FAA0AC2AB0BCB400FD78BD /* View+ContainerBackground.swift */, ); path = Extensions; sourceTree = ""; @@ -1121,6 +1124,7 @@ F864F78229BB9A6500B13921 /* StatusData+CoreDataClass.swift in Sources */, F864F78329BB9A6800B13921 /* StatusData+CoreDataProperties.swift in Sources */, F864F78429BB9A6E00B13921 /* ApplicationSettings+CoreDataClass.swift in Sources */, + F8FAA0AD2AB0BCB400FD78BD /* View+ContainerBackground.swift in Sources */, F864F78629BB9A7400B13921 /* AccountData+CoreDataClass.swift in Sources */, F8705A7B29FF872F00DA818A /* QRCodeGenerator.swift in Sources */, F865B4CE2A024AD8008ACDFC /* StatusData+Faulty.swift in Sources */, @@ -1552,7 +1556,7 @@ CODE_SIGN_ENTITLEMENTS = Vernissage/Vernissage.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 143; + CURRENT_PROJECT_VERSION = 200; DEVELOPMENT_ASSET_PATHS = "\"Vernissage/Preview Content\""; DEVELOPMENT_TEAM = B2U9FEKYP8; ENABLE_PREVIEWS = YES; @@ -1571,7 +1575,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.9.0; + MARKETING_VERSION = 1.10.0; PRODUCT_BUNDLE_IDENTIFIER = dev.mczachurski.vernissage; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1594,7 +1598,7 @@ ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO; CODE_SIGN_ENTITLEMENTS = Vernissage/Vernissage.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 143; + CURRENT_PROJECT_VERSION = 200; DEVELOPMENT_ASSET_PATHS = "\"Vernissage/Preview Content\""; DEVELOPMENT_TEAM = B2U9FEKYP8; ENABLE_PREVIEWS = YES; @@ -1613,7 +1617,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.9.0; + MARKETING_VERSION = 1.10.0; PRODUCT_BUNDLE_IDENTIFIER = dev.mczachurski.vernissage; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; diff --git a/Vernissage/Views/StatusView/StatusView.swift b/Vernissage/Views/StatusView/StatusView.swift index 09b8434..2ec5170 100644 --- a/Vernissage/Views/StatusView/StatusView.swift +++ b/Vernissage/Views/StatusView/StatusView.swift @@ -56,7 +56,7 @@ struct StatusView: View { private func mainBody() -> some View { switch state { case .loading: - StatusPlaceholderView(imageHeight: self.getImageHeight(), imageBlurhash: self.imageBlurhash) + StatusPlaceholderView(imageWidth: self.getImageWidth(), imageHeight: self.getImageHeight(), imageBlurhash: self.imageBlurhash) .task { await self.loadData() } @@ -236,13 +236,12 @@ struct StatusView: View { private func getImageHeight() -> Double { if let highestImageUrl = self.highestImageUrl, let imageSize = ImageSizeService.shared.get(for: highestImageUrl) { - return imageSize.height + let calculatedSize = ImageSizeService.shared.calculate(width: imageSize.width, height: imageSize.height) + return calculatedSize.height } if let imageHeight = self.imageHeight, let imageWidth = self.imageWidth, imageHeight > 0 && imageWidth > 0 { - let calculatedSize = ImageSizeService.shared.calculate(width: Double(imageWidth), - height: Double(imageHeight), - andContainerWidth: UIScreen.main.bounds.size.width) + let calculatedSize = ImageSizeService.shared.calculate(width: Double(imageWidth), height: Double(imageHeight)) return calculatedSize.height } @@ -250,6 +249,21 @@ struct StatusView: View { return UIScreen.main.bounds.width * 0.75 } + private func getImageWidth() -> Double { + if let highestImageUrl = self.highestImageUrl, let imageSize = ImageSizeService.shared.get(for: highestImageUrl) { + let calculatedSize = ImageSizeService.shared.calculate(width: imageSize.width, height: imageSize.height) + return calculatedSize.width + } + + if let imageHeight = self.imageHeight, let imageWidth = self.imageWidth, imageHeight > 0 && imageWidth > 0 { + let calculatedSize = ImageSizeService.shared.calculate(width: Double(imageWidth), height: Double(imageHeight)) + return calculatedSize.width + } + + // If we don't have image height and width in metadata, we have to use some constant height. + return UIScreen.main.bounds.width * 0.75 + } + private func getMainStatus(status: StatusModel) async throws -> StatusModel { guard let inReplyToId = status.inReplyToId else { return status diff --git a/Vernissage/Views/StatusView/Subviews/StatusPlaceholderView.swift b/Vernissage/Views/StatusView/Subviews/StatusPlaceholderView.swift index 545a76f..b25d460 100644 --- a/Vernissage/Views/StatusView/Subviews/StatusPlaceholderView.swift +++ b/Vernissage/Views/StatusView/Subviews/StatusPlaceholderView.swift @@ -8,6 +8,7 @@ import SwiftUI import WidgetsKit struct StatusPlaceholderView: View { + @State var imageWidth: Double @State var imageHeight: Double @State var imageBlurhash: String? @@ -17,11 +18,11 @@ struct StatusPlaceholderView: View { if let imageBlurhash, let uiImage = UIImage(blurHash: imageBlurhash, size: CGSize(width: 32, height: 32)) { Image(uiImage: uiImage) .resizable() - .frame(width: UIScreen.main.bounds.width, height: imageHeight) + .frame(width: self.imageWidth, height: self.imageHeight) } else { Rectangle() .fill(Color.placeholderText) - .frame(width: UIScreen.main.bounds.width, height: imageHeight) + .frame(width: self.imageWidth, height: self.imageHeight) .redacted(reason: .placeholder) } diff --git a/Vernissage/Views/StatusesView.swift b/Vernissage/Views/StatusesView.swift index 25e5674..c8de36d 100644 --- a/Vernissage/Views/StatusesView.swift +++ b/Vernissage/Views/StatusesView.swift @@ -101,7 +101,6 @@ struct StatusesView: View { ImageRowAsync(statusViewModel: item, containerWidth: $containerWidth) } onLoadMore: { do { - print("load more......") try await self.loadMoreStatuses() } catch { ErrorService.shared.handle(error, message: "statuses.error.loadingStatusesFailed", showToastr: !Task.isCancelled) diff --git a/Vernissage/Views/UserProfileView/Subviews/UserProfileHeaderView.swift b/Vernissage/Views/UserProfileView/Subviews/UserProfileHeaderView.swift index 24c73ea..3f876a6 100644 --- a/Vernissage/Views/UserProfileView/Subviews/UserProfileHeaderView.swift +++ b/Vernissage/Views/UserProfileView/Subviews/UserProfileHeaderView.swift @@ -57,6 +57,8 @@ struct UserProfileHeaderView: View { .opacity(0.6) } }.foregroundColor(.mainTextColor) + + Spacer() } HStack(alignment: .center) { diff --git a/Vernissage/Widgets/ImageCarouselPicture.swift b/Vernissage/Widgets/ImageCarouselPicture.swift index 38bfb18..08be887 100644 --- a/Vernissage/Widgets/ImageCarouselPicture.swift +++ b/Vernissage/Widgets/ImageCarouselPicture.swift @@ -12,11 +12,29 @@ import WidgetsKit struct ImageCarouselPicture: View { @ObservedObject public var attachment: AttachmentModel + @State private var blurredImageHeight: Double + @State private var blurredImageWidth: Double + private let onImageDownloaded: (AttachmentModel, Data) -> Void init(attachment: AttachmentModel, onImageDownloaded: @escaping (_: AttachmentModel, _: Data) -> Void) { self.attachment = attachment self.onImageDownloaded = onImageDownloaded + + if let size = ImageSizeService.shared.get(for: attachment.url) { + let imageSize = ImageSizeService.shared.calculate(width: size.width, height: size.height) + + self.blurredImageHeight = imageSize.height + self.blurredImageWidth = imageSize.width + } else if let imageWidth = attachment.metaImageWidth, let imageHeight = attachment.metaImageHeight { + let imageSize = ImageSizeService.shared.calculate(width: Double(imageWidth), height: Double(imageHeight)) + + self.blurredImageHeight = imageSize.height + self.blurredImageWidth = imageSize.width + } else { + self.blurredImageHeight = 100.0 + self.blurredImageWidth = 100.0 + } } var body: some View { @@ -26,6 +44,7 @@ struct ImageCarouselPicture: View { .aspectRatio(contentMode: .fit) } else { BlurredImage(blurhash: attachment.blurhash) + .frame(width: self.blurredImageWidth, height: self.blurredImageHeight) .task { do { // Download image and recalculate exif data. diff --git a/Vernissage/Widgets/ImagesCarousel.swift b/Vernissage/Widgets/ImagesCarousel.swift index 99359a9..f92473c 100644 --- a/Vernissage/Widgets/ImagesCarousel.swift +++ b/Vernissage/Widgets/ImagesCarousel.swift @@ -47,9 +47,7 @@ struct ImagesCarousel: View { // Calculate size of frame (first from cache, then from metadata). if let highestImage, let size = ImageSizeService.shared.get(for: highestImage.url) { - let calculatedSize = ImageSizeService.shared.calculate(width: size.width, - height: size.height, - andContainerWidth: UIScreen.main.bounds.size.width) + let calculatedSize = ImageSizeService.shared.calculate(width: size.width, height: size.height) self.imageWidth = calculatedSize.width self.imageHeight = calculatedSize.height @@ -57,7 +55,8 @@ struct ImagesCarousel: View { self.heightWasPrecalculated = true } else if let highestImage, imgHeight > 0 && imgWidth > 0 { ImageSizeService.shared.save(for: highestImage.url, width: imgWidth, height: imgHeight) - let size = ImageSizeService.shared.calculate(for: highestImage.url, andContainerWidth: UIScreen.main.bounds.size.width) + let size = ImageSizeService.shared.calculate(for: highestImage.url) + self.imageWidth = size.width self.imageHeight = size.height @@ -70,33 +69,37 @@ struct ImagesCarousel: View { } var body: some View { - TabView(selection: $selected) { - ForEach(attachments, id: \.id) { attachment in - ImageCarouselPicture(attachment: attachment) { (attachment, imageData) in - withAnimation { - self.recalculateImageHeight(attachment: attachment, imageData: imageData) - } + HStack { + Spacer() + TabView(selection: $selected) { + ForEach(attachments, id: \.id) { attachment in + ImageCarouselPicture(attachment: attachment) { (attachment, imageData) in + withAnimation { + self.recalculateImageHeight(attachment: attachment, imageData: imageData) + } - self.asyncAfter(0.4) { - attachment.set(data: imageData) - } + self.asyncAfter(0.4) { + attachment.set(data: imageData) + } + } + .tag(attachment.id) } - .tag(attachment.id) } + .frame(height: self.imageHeight) + .tabViewStyle(PageTabViewStyle()) + .onChange(of: selected, perform: { index in + if let attachment = attachments.first(where: { item in item.id == index }) { + self.selectedAttachment = attachment + self.exifCamera = attachment.exifCamera + self.exifExposure = attachment.exifExposure + self.exifCreatedDate = attachment.exifCreatedDate + self.exifLens = attachment.exifLens + self.description = attachment.description + } + }) + Spacer() } - .frame(height: self.imageHeight) - .tabViewStyle(PageTabViewStyle()) - .onChange(of: selected, perform: { index in - if let attachment = attachments.first(where: { item in item.id == index }) { - self.selectedAttachment = attachment - self.exifCamera = attachment.exifCamera - self.exifExposure = attachment.exifExposure - self.exifCreatedDate = attachment.exifCreatedDate - self.exifLens = attachment.exifLens - self.description = attachment.description - } - }) .onAppear { self.selected = self.attachments.first?.id ?? String.empty() } @@ -107,28 +110,41 @@ struct ImagesCarousel: View { return } - var imageHeight = 0.0 - var imageWidth = 0.0 + var maxImageHeight = 0.0 + var maxImageWidth = 0.0 for item in attachments { - if let data = item.data, let image = UIImage(data: data) { - ImageSizeService.shared.save(for: attachment.url, width: image.size.width, height: image.size.height) + // Get attachment sizes from cache. + if let attachmentSize = ImageSizeService.shared.get(for: item.url) { + if attachmentSize.height > maxImageHeight { + maxImageHeight = attachmentSize.height + maxImageWidth = attachmentSize.width + } - if image.size.height > imageHeight { - imageHeight = image.size.height - imageWidth = image.size.width + continue + } + + // When we don't have in cache read from data and add to cache. + if let data = item.data, let image = UIImage(data: data) { + ImageSizeService.shared.save(for: item.url, width: image.size.width, height: image.size.height) + + if image.size.height > maxImageHeight { + maxImageHeight = image.size.height + maxImageWidth = image.size.width } } } if let image = UIImage(data: imageData) { - if image.size.height > imageHeight { - imageHeight = image.size.height - imageWidth = image.size.width + ImageSizeService.shared.save(for: attachment.url, width: image.size.width, height: image.size.height) + + if image.size.height > maxImageHeight { + maxImageHeight = image.size.height + maxImageWidth = image.size.width } } - let size = ImageSizeService.shared.calculate(for: attachment.url, andContainerWidth: UIScreen.main.bounds.size.width) + let size = ImageSizeService.shared.calculate(width: maxImageWidth, height: maxImageHeight) self.imageWidth = size.width self.imageHeight = size.height } diff --git a/VernissageWidget/Extensions/View+ContainerBackground.swift b/VernissageWidget/Extensions/View+ContainerBackground.swift new file mode 100644 index 0000000..014ee74 --- /dev/null +++ b/VernissageWidget/Extensions/View+ContainerBackground.swift @@ -0,0 +1,29 @@ +// +// https://mczachurski.dev +// Copyright © 2023 Marcin Czachurski and the repository contributors. +// Licensed under the Apache License 2.0. +// + +import SwiftUI + +extension View { +// func widgetBackground(backgroundView: some View) -> some View { +// if #available(iOSApplicationExtension 17.0, *) { +// return containerBackground(for: .widget) { +// backgroundView +// } +// } else { +// return background(backgroundView) +// } +// } + + func widgetBackground(@ViewBuilder content: @escaping () -> V) -> some View where V: View { + if #available(iOSApplicationExtension 17.0, *) { + return containerBackground(for: .widget) { + content() + } + } else { + return background(content()) + } + } +} diff --git a/VernissageWidget/PhotoWidget/PhotoWidget.swift b/VernissageWidget/PhotoWidget/PhotoWidget.swift index 310ddea..be096e4 100644 --- a/VernissageWidget/PhotoWidget/PhotoWidget.swift +++ b/VernissageWidget/PhotoWidget/PhotoWidget.swift @@ -17,5 +17,6 @@ struct PhotoWidget: Widget { .configurationDisplayName("Vernissage") .description("widget.title.photoDescription") .supportedFamilies([.systemSmall, .systemMedium, .systemLarge]) + .contentMarginsDisabled() } } diff --git a/VernissageWidget/PhotoWidget/Views/PhotoLargeWidgetView.swift b/VernissageWidget/PhotoWidget/Views/PhotoLargeWidgetView.swift index c45a2f6..983ef16 100644 --- a/VernissageWidget/PhotoWidget/Views/PhotoLargeWidgetView.swift +++ b/VernissageWidget/PhotoWidget/Views/PhotoLargeWidgetView.swift @@ -38,7 +38,7 @@ struct PhotoLargeWidgetView: View { .padding(.leading, 8) .padding(.bottom, 8) } - .background { + .widgetBackground { uiImage .resizable() .aspectRatio(contentMode: .fill) diff --git a/VernissageWidget/PhotoWidget/Views/PhotoMediumWidgetView.swift b/VernissageWidget/PhotoWidget/Views/PhotoMediumWidgetView.swift index e498839..fc9f75e 100644 --- a/VernissageWidget/PhotoWidget/Views/PhotoMediumWidgetView.swift +++ b/VernissageWidget/PhotoWidget/Views/PhotoMediumWidgetView.swift @@ -38,7 +38,7 @@ struct PhotoMediumWidgetView: View { .padding(.leading, 8) .padding(.bottom, 8) } - .background { + .widgetBackground { uiImage .resizable() .aspectRatio(contentMode: .fill) diff --git a/VernissageWidget/PhotoWidget/Views/PhotoSmallWidgetView.swift b/VernissageWidget/PhotoWidget/Views/PhotoSmallWidgetView.swift index 2cb195e..a4cf584 100644 --- a/VernissageWidget/PhotoWidget/Views/PhotoSmallWidgetView.swift +++ b/VernissageWidget/PhotoWidget/Views/PhotoSmallWidgetView.swift @@ -33,7 +33,7 @@ struct PhotoSmallWidgetView: View { .padding(.leading, 8) .padding(.bottom, 8) } - .background { + .widgetBackground { uiImage .resizable() .aspectRatio(contentMode: .fill) diff --git a/VernissageWidget/QRCodeWidget/QRCodeWidget.swift b/VernissageWidget/QRCodeWidget/QRCodeWidget.swift index c7850f6..312c1f9 100644 --- a/VernissageWidget/QRCodeWidget/QRCodeWidget.swift +++ b/VernissageWidget/QRCodeWidget/QRCodeWidget.swift @@ -17,5 +17,6 @@ struct QRCodeWidget: Widget { .configurationDisplayName("Vernissage") .description("widget.title.qrCodeDescription") .supportedFamilies([.systemSmall, .systemMedium, .systemLarge]) + .contentMarginsDisabled() } } diff --git a/VernissageWidget/QRCodeWidget/Views/QRCodeLargeWidgetView.swift b/VernissageWidget/QRCodeWidget/Views/QRCodeLargeWidgetView.swift index 5e0d890..442d78c 100644 --- a/VernissageWidget/QRCodeWidget/Views/QRCodeLargeWidgetView.swift +++ b/VernissageWidget/QRCodeWidget/Views/QRCodeLargeWidgetView.swift @@ -82,6 +82,8 @@ struct QRCodeLargeWidgetView: View { .offset(y: -8) } } + .widgetBackground { + } .padding([.leading, .trailing, .top], 24) } } diff --git a/VernissageWidget/QRCodeWidget/Views/QRCodeMediumWidgetView.swift b/VernissageWidget/QRCodeWidget/Views/QRCodeMediumWidgetView.swift index 90eb168..f77ed0b 100644 --- a/VernissageWidget/QRCodeWidget/Views/QRCodeMediumWidgetView.swift +++ b/VernissageWidget/QRCodeWidget/Views/QRCodeMediumWidgetView.swift @@ -92,6 +92,8 @@ struct QRCodeMediumWidgetView: View { .padding(.leading, 3) .offset(y: -4) } + .widgetBackground { + } .padding([.leading, .trailing, .top], 12) } } diff --git a/VernissageWidget/QRCodeWidget/Views/QRCodeSmallWidgetView.swift b/VernissageWidget/QRCodeWidget/Views/QRCodeSmallWidgetView.swift index 8616c5f..bfd3c42 100644 --- a/VernissageWidget/QRCodeWidget/Views/QRCodeSmallWidgetView.swift +++ b/VernissageWidget/QRCodeWidget/Views/QRCodeSmallWidgetView.swift @@ -61,6 +61,8 @@ struct QRCodeSmallWidgetView: View { .frame(height: 24) } } + .widgetBackground { + } .padding(8) } } diff --git a/WidgetsKit/Sources/WidgetsKit/ViewModifiers/DeviceRotation.swift b/WidgetsKit/Sources/WidgetsKit/ViewModifiers/DeviceRotation.swift index 6d7330c..fb58fd2 100644 --- a/WidgetsKit/Sources/WidgetsKit/ViewModifiers/DeviceRotation.swift +++ b/WidgetsKit/Sources/WidgetsKit/ViewModifiers/DeviceRotation.swift @@ -52,10 +52,10 @@ struct DeviceImageGallery: ViewModifier { } } .onAppear { - asyncAfter(0.1) { + // asyncAfter(0.1) { let galleryProperties = self.getGalleryProperties(geometry: geometry, horizontalSize: self.horizontalSizeClass ?? .compact) self.action(galleryProperties) - } + // } } } } @@ -69,7 +69,6 @@ struct DeviceImageGallery: ViewModifier { } else { // View like on iPad. let imageColumns = geometry.size.width > geometry.size.height ? 3 : 2 - print("\(geometry.size.width ):\(geometry.size.height)") return GalleryProperties(imageColumns: imageColumns, containerWidth: geometry.size.width / Double(imageColumns), From 4daad7dec746d1b862901a95a6ce96b748f6aacd Mon Sep 17 00:00:00 2001 From: Marcin Czachurski Date: Sun, 17 Sep 2023 08:27:45 +0200 Subject: [PATCH 06/14] Fix search and border on status screen --- .../PixelfedKit/Extensions/Primitive+AsString.swift | 2 +- Vernissage/Widgets/ImagesCarousel.swift | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/PixelfedKit/Sources/PixelfedKit/Extensions/Primitive+AsString.swift b/PixelfedKit/Sources/PixelfedKit/Extensions/Primitive+AsString.swift index 364b667..ed9f937 100644 --- a/PixelfedKit/Sources/PixelfedKit/Extensions/Primitive+AsString.swift +++ b/PixelfedKit/Sources/PixelfedKit/Extensions/Primitive+AsString.swift @@ -8,7 +8,7 @@ import Foundation extension Bool { var asString: String { - return self == true ? "true" : "false" + return self == true ? "1" : "0" } } diff --git a/Vernissage/Widgets/ImagesCarousel.swift b/Vernissage/Widgets/ImagesCarousel.swift index f92473c..810bf68 100644 --- a/Vernissage/Widgets/ImagesCarousel.swift +++ b/Vernissage/Widgets/ImagesCarousel.swift @@ -69,8 +69,8 @@ struct ImagesCarousel: View { } var body: some View { - HStack { - Spacer() + HStack(spacing: 0) { + Spacer(minLength: 0) TabView(selection: $selected) { ForEach(attachments, id: \.id) { attachment in ImageCarouselPicture(attachment: attachment) { (attachment, imageData) in @@ -86,6 +86,7 @@ struct ImagesCarousel: View { .tag(attachment.id) } } + .padding(0) .frame(height: self.imageHeight) .tabViewStyle(PageTabViewStyle()) .onChange(of: selected, perform: { index in @@ -98,8 +99,9 @@ struct ImagesCarousel: View { self.description = attachment.description } }) - Spacer() + Spacer(minLength: 0) } + .padding(0) .onAppear { self.selected = self.attachments.first?.id ?? String.empty() } From e92226f4fda0cfc02263d0ae82107902f7453979 Mon Sep 17 00:00:00 2001 From: Marcin Czachurski Date: Sun, 17 Sep 2023 08:28:40 +0200 Subject: [PATCH 07/14] Change verson to 1.10.0 build 201 --- Vernissage.xcodeproj/project.pbxproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Vernissage.xcodeproj/project.pbxproj b/Vernissage.xcodeproj/project.pbxproj index 5d35434..f3c2995 100644 --- a/Vernissage.xcodeproj/project.pbxproj +++ b/Vernissage.xcodeproj/project.pbxproj @@ -1556,7 +1556,7 @@ CODE_SIGN_ENTITLEMENTS = Vernissage/Vernissage.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 200; + CURRENT_PROJECT_VERSION = 201; DEVELOPMENT_ASSET_PATHS = "\"Vernissage/Preview Content\""; DEVELOPMENT_TEAM = B2U9FEKYP8; ENABLE_PREVIEWS = YES; @@ -1598,7 +1598,7 @@ ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO; CODE_SIGN_ENTITLEMENTS = Vernissage/Vernissage.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 200; + CURRENT_PROJECT_VERSION = 201; DEVELOPMENT_ASSET_PATHS = "\"Vernissage/Preview Content\""; DEVELOPMENT_TEAM = B2U9FEKYP8; ENABLE_PREVIEWS = YES; From 80b2d2f4e95a3760d4fcd3668d1e265f5c8c1700 Mon Sep 17 00:00:00 2001 From: Marcin Czachurski Date: Tue, 19 Sep 2023 18:43:00 +0200 Subject: [PATCH 08/14] Fix issues with waterfall widget --- Vernissage.xcodeproj/project.pbxproj | 46 ++++++++---- .../Extensions/StatusModel+Sizeable.swift | 18 +++++ Vernissage/Models/ColumnData.swift | 13 ++++ Vernissage/Models/Sizable.swift | 12 +++ Vernissage/Widgets/WaterfallGrid.swift | 73 ++++++++++++------- 5 files changed, 119 insertions(+), 43 deletions(-) create mode 100644 Vernissage/Extensions/StatusModel+Sizeable.swift create mode 100644 Vernissage/Models/ColumnData.swift create mode 100644 Vernissage/Models/Sizable.swift diff --git a/Vernissage.xcodeproj/project.pbxproj b/Vernissage.xcodeproj/project.pbxproj index f3c2995..b636915 100644 --- a/Vernissage.xcodeproj/project.pbxproj +++ b/Vernissage.xcodeproj/project.pbxproj @@ -191,9 +191,12 @@ F8B0885E29942E31002AB40A /* ThirdPartyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8B0885D29942E31002AB40A /* ThirdPartyView.swift */; }; F8B0886029943498002AB40A /* OtherSectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8B0885F29943498002AB40A /* OtherSectionView.swift */; }; F8B08862299435C9002AB40A /* SupportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8B08861299435C9002AB40A /* SupportView.swift */; }; + F8B758DE2AB9DD85000C8068 /* ColumnData.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8B758DD2AB9DD85000C8068 /* ColumnData.swift */; }; F8D5444329D4066C002225D6 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D5444229D4066C002225D6 /* AppDelegate.swift */; }; F8DF38E429DD68820047F1AA /* ViewOffsetKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8DF38E329DD68820047F1AA /* ViewOffsetKey.swift */; }; F8DF38E629DDB98A0047F1AA /* SocialsSectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8DF38E529DDB98A0047F1AA /* SocialsSectionView.swift */; }; + F8E36E462AB8745300769C55 /* Sizable.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E36E452AB8745300769C55 /* Sizable.swift */; }; + F8E36E482AB874A500769C55 /* StatusModel+Sizeable.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E36E472AB874A500769C55 /* StatusModel+Sizeable.swift */; }; F8E6D03329CDD52500416CCA /* EditProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E6D03229CDD52500416CCA /* EditProfileView.swift */; }; F8F6E44229BC58F20004795E /* Vernissage.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = F88C2476295C37BB0006098B /* Vernissage.xcdatamodeld */; }; F8F6E44C29BCC1F70004795E /* PhotoSmallWidgetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8F6E44629BCC0DC0004795E /* PhotoSmallWidgetView.swift */; }; @@ -393,12 +396,15 @@ F8B08861299435C9002AB40A /* SupportView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SupportView.swift; sourceTree = ""; }; F8B3699A29D86EB600BE3808 /* .swiftlint.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = .swiftlint.yml; sourceTree = ""; }; F8B3699B29D86EBD00BE3808 /* .gitignore */ = {isa = PBXFileReference; lastKnownFileType = text; path = .gitignore; sourceTree = ""; }; + F8B758DD2AB9DD85000C8068 /* ColumnData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColumnData.swift; sourceTree = ""; }; F8C937A929882CA90004D782 /* Vernissage-001.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Vernissage-001.xcdatamodel"; sourceTree = ""; }; F8CAE64129B8F1AF001E0372 /* Vernissage-005.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Vernissage-005.xcdatamodel"; sourceTree = ""; }; F8D5444229D4066C002225D6 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; F8DF38E329DD68820047F1AA /* ViewOffsetKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewOffsetKey.swift; sourceTree = ""; }; F8DF38E529DDB98A0047F1AA /* SocialsSectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SocialsSectionView.swift; sourceTree = ""; }; F8DF38E729DDC3D20047F1AA /* eu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = eu; path = eu.lproj/Localizable.strings; sourceTree = ""; }; + F8E36E452AB8745300769C55 /* Sizable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sizable.swift; sourceTree = ""; }; + F8E36E472AB874A500769C55 /* StatusModel+Sizeable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StatusModel+Sizeable.swift"; sourceTree = ""; }; F8E6D03229CDD52500416CCA /* EditProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditProfileView.swift; sourceTree = ""; }; F8EF371429C624DA00669F45 /* Vernissage-006.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Vernissage-006.xcdatamodel"; sourceTree = ""; }; F8EF3C8B29FC3A5F00CBFF7C /* Vernissage-012.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Vernissage-012.xcdatamodel"; sourceTree = ""; }; @@ -535,6 +541,8 @@ F8DF38E329DD68820047F1AA /* ViewOffsetKey.swift */, F871F21C29EF0D7000A351EF /* NavigationMenuItemDetails.swift */, F8624D3C29F2D3AC00204986 /* SelectedMenuItemDetails.swift */, + F8E36E452AB8745300769C55 /* Sizable.swift */, + F8B758DD2AB9DD85000C8068 /* ColumnData.swift */, ); path = Models; sourceTree = ""; @@ -653,6 +661,7 @@ isa = PBXGroup; children = ( F858906A29E1CC7A00D4BDED /* UIApplication+Window.swift */, + F8E36E472AB874A500769C55 /* StatusModel+Sizeable.swift */, ); path = Extensions; sourceTree = ""; @@ -1185,6 +1194,7 @@ F87AEB922986C44E00434FB6 /* AuthorizationSession.swift in Sources */, F86A4301299A97F500DF7645 /* ProductIdentifiers.swift in Sources */, F89D6C4229717FDC001DA3D4 /* AccountsSectionView.swift in Sources */, + F8E36E482AB874A500769C55 /* StatusModel+Sizeable.swift in Sources */, F80048082961E6DE00E6868A /* StatusDataHandler.swift in Sources */, F866F6A0296040A8002E8F88 /* ApplicationSettings+CoreDataClass.swift in Sources */, F8210DEA2966E4F9001D9973 /* AnimatePlaceholderModifier.swift in Sources */, @@ -1201,6 +1211,7 @@ F883402029B62AE900C3E096 /* SearchView.swift in Sources */, F88FAD2A295F43B8009B20C9 /* AccountData+CoreDataClass.swift in Sources */, F871F21D29EF0D7000A351EF /* NavigationMenuItemDetails.swift in Sources */, + F8E36E462AB8745300769C55 /* Sizable.swift in Sources */, F85DBF8F296732E20069BF89 /* AccountsView.swift in Sources */, F805DCF129DBEF83006A1FD9 /* ReportView.swift in Sources */, F8B0886029943498002AB40A /* OtherSectionView.swift in Sources */, @@ -1220,6 +1231,7 @@ F891E7CE29C35BF50022C449 /* ImageRowItem.swift in Sources */, F86B7221296C49A300EE59EC /* EmptyButtonStyle.swift in Sources */, F80048042961850500E6868A /* AttachmentData+CoreDataProperties.swift in Sources */, + F8B758DE2AB9DD85000C8068 /* ColumnData.swift in Sources */, F88E4D4A297EA0490057491A /* RouterPath.swift in Sources */, F88E4D48297E90CD0057491A /* TrendStatusesView.swift in Sources */, F800480A2961EA1900E6868A /* AttachmentDataHandler.swift in Sources */, @@ -1325,18 +1337,19 @@ CODE_SIGN_ENTITLEMENTS = VernissageWidget/VernissageWidgetExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 143; + CURRENT_PROJECT_VERSION = 204; DEVELOPMENT_TEAM = B2U9FEKYP8; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = VernissageWidget/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = VernissageWidget; INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 16.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.9.0; + MARKETING_VERSION = 1.10.0; PRODUCT_BUNDLE_IDENTIFIER = dev.mczachurski.vernissage.widget; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -1353,18 +1366,19 @@ ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; CODE_SIGN_ENTITLEMENTS = VernissageWidget/VernissageWidgetExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 143; + CURRENT_PROJECT_VERSION = 204; DEVELOPMENT_TEAM = B2U9FEKYP8; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = VernissageWidget/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = VernissageWidget; INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 16.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.9.0; + MARKETING_VERSION = 1.10.0; PRODUCT_BUNDLE_IDENTIFIER = dev.mczachurski.vernissage.widget; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -1380,19 +1394,19 @@ CODE_SIGN_ENTITLEMENTS = VernissageShare/VernissageShareExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 143; + CURRENT_PROJECT_VERSION = 204; DEVELOPMENT_TEAM = B2U9FEKYP8; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = VernissageShare/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = VernissageShareExtension; INFOPLIST_KEY_NSHumanReadableCopyright = ""; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 16.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.9.0; + MARKETING_VERSION = 1.10.0; PRODUCT_BUNDLE_IDENTIFIER = dev.mczachurski.vernissage.share; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -1407,19 +1421,19 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = VernissageShare/VernissageShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 143; + CURRENT_PROJECT_VERSION = 204; DEVELOPMENT_TEAM = B2U9FEKYP8; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = VernissageShare/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = VernissageShareExtension; INFOPLIST_KEY_NSHumanReadableCopyright = ""; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 16.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.9.0; + MARKETING_VERSION = 1.10.0; PRODUCT_BUNDLE_IDENTIFIER = dev.mczachurski.vernissage.share; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -1480,7 +1494,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.2; + IPHONEOS_DEPLOYMENT_TARGET = 16.6; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -1535,7 +1549,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.2; + IPHONEOS_DEPLOYMENT_TARGET = 16.6; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = iphoneos; @@ -1556,7 +1570,7 @@ CODE_SIGN_ENTITLEMENTS = Vernissage/Vernissage.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 201; + CURRENT_PROJECT_VERSION = 204; DEVELOPMENT_ASSET_PATHS = "\"Vernissage/Preview Content\""; DEVELOPMENT_TEAM = B2U9FEKYP8; ENABLE_PREVIEWS = YES; @@ -1570,7 +1584,7 @@ INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; - IPHONEOS_DEPLOYMENT_TARGET = 16.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1598,7 +1612,7 @@ ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO; CODE_SIGN_ENTITLEMENTS = Vernissage/Vernissage.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 201; + CURRENT_PROJECT_VERSION = 204; DEVELOPMENT_ASSET_PATHS = "\"Vernissage/Preview Content\""; DEVELOPMENT_TEAM = B2U9FEKYP8; ENABLE_PREVIEWS = YES; @@ -1612,7 +1626,7 @@ INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; - IPHONEOS_DEPLOYMENT_TARGET = 16.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/Vernissage/Extensions/StatusModel+Sizeable.swift b/Vernissage/Extensions/StatusModel+Sizeable.swift new file mode 100644 index 0000000..013b132 --- /dev/null +++ b/Vernissage/Extensions/StatusModel+Sizeable.swift @@ -0,0 +1,18 @@ +// +// https://mczachurski.dev +// Copyright © 2023 Marcin Czachurski and the repository contributors. +// Licensed under the Apache License 2.0. +// + +import Foundation +import ClientKit + +extension StatusModel: Sizable { + public var height: Double { + return Double(self.mediaAttachments.first?.metaImageHeight ?? 500) + } + + public var width: Double { + return Double(self.mediaAttachments.first?.metaImageWidth ?? 500) + } +} diff --git a/Vernissage/Models/ColumnData.swift b/Vernissage/Models/ColumnData.swift new file mode 100644 index 0000000..fd40c4b --- /dev/null +++ b/Vernissage/Models/ColumnData.swift @@ -0,0 +1,13 @@ +// +// https://mczachurski.dev +// Copyright © 2023 Marcin Czachurski and the repository contributors. +// Licensed under the Apache License 2.0. +// + +import Foundation + +class ColumnData: Identifiable where T: Identifiable, T: Hashable, T: Sizable { + public let id = UUID().uuidString + public var data: [T] = [] + public var height: Double = 0.0 +} diff --git a/Vernissage/Models/Sizable.swift b/Vernissage/Models/Sizable.swift new file mode 100644 index 0000000..e084785 --- /dev/null +++ b/Vernissage/Models/Sizable.swift @@ -0,0 +1,12 @@ +// +// https://mczachurski.dev +// Copyright © 2023 Marcin Czachurski and the repository contributors. +// Licensed under the Apache License 2.0. +// + +import Foundation + +public protocol Sizable { + var height: Double { get } + var width: Double { get } +} diff --git a/Vernissage/Widgets/WaterfallGrid.swift b/Vernissage/Widgets/WaterfallGrid.swift index 9d622ca..5cb732d 100644 --- a/Vernissage/Widgets/WaterfallGrid.swift +++ b/Vernissage/Widgets/WaterfallGrid.swift @@ -8,30 +8,35 @@ import SwiftUI import WidgetsKit struct WaterfallGrid: View where Data: RandomAccessCollection, Data: Equatable, Content: View, - ID: Hashable, Data.Element: Equatable, Data.Element: Identifiable, Data.Element: Hashable { + ID: Hashable, Data.Element: Equatable, Data.Element: Identifiable, Data.Element: Hashable, Data.Element: Sizable { @Binding private var columns: Int @Binding private var hideLoadMore: Bool - @Binding private var data: Data - private let dataId: KeyPath + private let content: (Data.Element) -> Content - @State private var columnsData: [[Data.Element]] = [] + @State private var columnsData: [ColumnData] = [] + @State private var processedItems: [Data.Element.ID] = [] + @State private var isDuringLoading: Bool = false private let onLoadMore: () async -> Void var body: some View { HStack(alignment: .top, spacing: 20) { - ForEach(self.columnsData, id: \.self) { array in + ForEach(self.columnsData, id: \.id) { columnData in LazyVStack(spacing: 8) { - ForEach(array, id: \.id) { item in + ForEach(columnData.data, id: \.id) { item in self.content(item) } - if self.shouldShowSpinner(array: array) { + if self.hideLoadMore == false { LoadingIndicator() .task { - await self.onLoadMore() + if isDuringLoading == false { + isDuringLoading = true + await self.onLoadMore() + isDuringLoading = false + } } } } @@ -41,7 +46,7 @@ struct WaterfallGrid: View where Data: RandomAccessCollection self.recalculateArrays() } .onChange(of: self.data) { _ in - self.recalculateArrays() + self.appendToArrays() } .onChange(of: self.columns) { _ in self.recalculateArrays() @@ -49,52 +54,66 @@ struct WaterfallGrid: View where Data: RandomAccessCollection } private func recalculateArrays() { - var internalArray: [[Data.Element]] = [] + self.columnsData = [] + self.processedItems = [] for _ in 0 ..< self.columns { - internalArray.append([]) + self.columnsData.append(ColumnData()) } - for (index, item) in self.data.enumerated() { - let arrayIndex = index % self.columns - internalArray[arrayIndex].append(item) - } + for item in self.data { + let index = self.minimumHeightIndex() - self.columnsData = internalArray + self.columnsData[index].data.append(item) + self.columnsData[index].height = self.columnsData[index].height + self.calculateHeight(item: item) + self.processedItems.append(item.id) + } } - private func shouldShowSpinner(array: [Data.Element]) -> Bool { - if self.hideLoadMore { - return false - } + private func appendToArrays() { + for item in self.data where self.processedItems.contains(where: { $0 == item.id }) == false { + let index = self.minimumHeightIndex() - return self.columnsData[1].first == array.first + self.columnsData[index].data.append(item) + self.columnsData[index].height = self.columnsData[index].height + self.calculateHeight(item: item) + self.processedItems.append(item.id) + } } + private func calculateHeight(item: Sizable) -> Double { + return item.height / item.width + } + + private func minimumHeight() -> Double { + return self.columnsData.map({ $0.height }).min() ?? .zero + } + + private func minimumHeightIndex() -> Int { + let minimumHeight = self.minimumHeight() + return self.columnsData.lastIndex(where: { $0.height == minimumHeight }) ?? 0 + } } extension WaterfallGrid { init(_ data: Binding, id: KeyPath, columns: Binding, hideLoadMore: Binding, content: @escaping (Data.Element) -> Content, onLoadMore: @escaping () async -> Void) { - self._data = data - self.dataId = id self.content = content + self.onLoadMore = onLoadMore + self._data = data self._columns = columns self._hideLoadMore = hideLoadMore - self.onLoadMore = onLoadMore } } extension WaterfallGrid where ID == Data.Element.ID, Data.Element: Identifiable { init(_ data: Binding, columns: Binding, hideLoadMore: Binding, content: @escaping (Data.Element) -> Content, onLoadMore: @escaping () async -> Void) { - self._data = data - self.dataId = \Data.Element.id self.content = content + self.onLoadMore = onLoadMore + self._data = data self._columns = columns self._hideLoadMore = hideLoadMore - self.onLoadMore = onLoadMore } } From bcff3adbc7b359924922acb46e7512531fb7b011 Mon Sep 17 00:00:00 2001 From: Marcin Czachurski Date: Tue, 19 Sep 2023 19:32:27 +0200 Subject: [PATCH 09/14] Enable automatic resource generator --- .../Extensions/Color+Assets.swift | 2 +- ServicesKit/Package.swift | 1 - .../Contents.json | 0 Vernissage.xcodeproj/project.pbxproj | 36 +++++------------- Vernissage/Views/EditProfileView.swift | 4 +- Vernissage/Views/FollowRequestsView.swift | 2 +- Vernissage/Views/InstanceView.swift | 4 +- Vernissage/Views/MainView.swift | 2 +- .../Subviews/NotificationRowView.swift | 4 +- .../Subviews/MediaSettingsView.swift | 12 +++--- .../Subviews/SocialsSectionView.swift | 6 +-- .../SettingsView/Subviews/SupportView.swift | 2 +- .../SignInView/Subviews/InstanceRowView.swift | 2 +- Vernissage/Views/StatusView/StatusView.swift | 4 +- .../StatusView/Subviews/CommentBodyView.swift | 2 +- .../Subviews/CommentsSectionView.swift | 2 +- .../Subviews/StatusPlaceholderView.swift | 4 +- .../Subviews/UserProfileHeaderView.swift | 4 +- .../Subviews/UserProfileStatusesView.swift | 4 +- .../AccentColor.colorset/Contents.json | 9 +++++ .../AppIcon.appiconset/Contents.json | 1 + .../AppIcon.appiconset/icon.png | Bin 0 -> 45724 bytes .../WidgetBackground.colorset/Contents.json | 9 +++++ .../WidgetsKit/Views/BaseComposeView.swift | 6 +-- .../WidgetsKit/Views/PlaceSelectorView.swift | 2 +- .../WidgetsKit/Widgets/NoDataView.swift | 2 +- .../WidgetsKit/Widgets/UsernameRow.swift | 2 +- 27 files changed, 64 insertions(+), 64 deletions(-) rename SharedAssets.xcassets/{LightGrayColor.colorset => CustomGrayColor.colorset}/Contents.json (100%) create mode 100644 VernissageWidget/Assets.xcassets/AppIcon.appiconset/icon.png diff --git a/EnvironmentKit/Sources/EnvironmentKit/Extensions/Color+Assets.swift b/EnvironmentKit/Sources/EnvironmentKit/Extensions/Color+Assets.swift index b22b6a1..ca6813b 100644 --- a/EnvironmentKit/Sources/EnvironmentKit/Extensions/Color+Assets.swift +++ b/EnvironmentKit/Sources/EnvironmentKit/Extensions/Color+Assets.swift @@ -10,7 +10,7 @@ public extension Color { // MARK: - Text Colors static let dangerColor = Color("DangerColor") - static let lightGrayColor = Color("LightGrayColor") + static let customGrayColor = Color("CustomGrayColor") static let mainTextColor = Color("MainTextColor") static let selectedRowColor = Color("SelectedRowColor") static let viewBackgroundColor = Color("ViewBackgroundColor") diff --git a/ServicesKit/Package.swift b/ServicesKit/Package.swift index 035dd13..082a630 100644 --- a/ServicesKit/Package.swift +++ b/ServicesKit/Package.swift @@ -20,7 +20,6 @@ let package = Package( // Dependencies declare other packages that this package depends on. .package(url: "https://github.com/omaralbeik/Drops", .upToNextMajor(from: "1.6.1")), .package(url: "https://github.com/kean/Nuke", .upToNextMajor(from: "12.0.0")), - .package(name: "PixelfedKit", path: "../PixelfedKit"), .package(name: "EnvironmentKit", path: "../EnvironmentKit") ], targets: [ diff --git a/SharedAssets.xcassets/LightGrayColor.colorset/Contents.json b/SharedAssets.xcassets/CustomGrayColor.colorset/Contents.json similarity index 100% rename from SharedAssets.xcassets/LightGrayColor.colorset/Contents.json rename to SharedAssets.xcassets/CustomGrayColor.colorset/Contents.json diff --git a/Vernissage.xcodeproj/project.pbxproj b/Vernissage.xcodeproj/project.pbxproj index b636915..7994331 100644 --- a/Vernissage.xcodeproj/project.pbxproj +++ b/Vernissage.xcodeproj/project.pbxproj @@ -38,6 +38,7 @@ F84625F829FE2C2F002D3AF4 /* AccountFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = F84625F729FE2C2F002D3AF4 /* AccountFetcher.swift */; }; F84625FB29FE393B002D3AF4 /* QRCode in Frameworks */ = {isa = PBXBuildFile; productRef = F84625FA29FE393B002D3AF4 /* QRCode */; }; F858906B29E1CC7A00D4BDED /* UIApplication+Window.swift in Sources */ = {isa = PBXBuildFile; fileRef = F858906A29E1CC7A00D4BDED /* UIApplication+Window.swift */; }; + F85D0C652ABA08F9002B3577 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F85D0C642ABA08F9002B3577 /* Assets.xcassets */; }; F85D4971296402DC00751DF7 /* AuthorizationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85D4970296402DC00751DF7 /* AuthorizationService.swift */; }; F85D4975296407F100751DF7 /* HomeTimelineService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85D4974296407F100751DF7 /* HomeTimelineService.swift */; }; F85D497729640A5200751DF7 /* ImageRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85D497629640A5200751DF7 /* ImageRow.swift */; }; @@ -54,7 +55,6 @@ F864F76129BB91B400B13921 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F864F76029BB91B400B13921 /* SwiftUI.framework */; }; F864F76429BB91B400B13921 /* VernissageWidgetBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = F864F76329BB91B400B13921 /* VernissageWidgetBundle.swift */; }; F864F76629BB91B400B13921 /* PhotoWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = F864F76529BB91B400B13921 /* PhotoWidget.swift */; }; - F864F76829BB91B600B13921 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F864F76729BB91B600B13921 /* Assets.xcassets */; }; F864F76C29BB91B600B13921 /* VernissageWidgetExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = F864F75D29BB91B400B13921 /* VernissageWidgetExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; F864F77529BB92CE00B13921 /* PhotoProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = F864F77329BB929A00B13921 /* PhotoProvider.swift */; }; F864F77629BB92CE00B13921 /* PhotoWidgetEntryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F864F77129BB924D00B13921 /* PhotoWidgetEntryView.swift */; }; @@ -271,6 +271,7 @@ F84625F729FE2C2F002D3AF4 /* AccountFetcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountFetcher.swift; sourceTree = ""; }; F858906A29E1CC7A00D4BDED /* UIApplication+Window.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIApplication+Window.swift"; sourceTree = ""; }; F85B586C29ED169B00A16D12 /* Vernissage-010.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Vernissage-010.xcdatamodel"; sourceTree = ""; }; + F85D0C642ABA08F9002B3577 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; F85D4970296402DC00751DF7 /* AuthorizationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthorizationService.swift; sourceTree = ""; }; F85D4974296407F100751DF7 /* HomeTimelineService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeTimelineService.swift; sourceTree = ""; }; F85D497629640A5200751DF7 /* ImageRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageRow.swift; sourceTree = ""; }; @@ -288,7 +289,6 @@ F864F76029BB91B400B13921 /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; }; F864F76329BB91B400B13921 /* VernissageWidgetBundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VernissageWidgetBundle.swift; sourceTree = ""; }; F864F76529BB91B400B13921 /* PhotoWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoWidget.swift; sourceTree = ""; }; - F864F76729BB91B600B13921 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; F864F76929BB91B600B13921 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; F864F77129BB924D00B13921 /* PhotoWidgetEntryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoWidgetEntryView.swift; sourceTree = ""; }; F864F77329BB929A00B13921 /* PhotoProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoProvider.swift; sourceTree = ""; }; @@ -676,7 +676,7 @@ F84625EE29FE2ABA002D3AF4 /* PhotoWidget */, F8F6E44F29BCE9030004795E /* Extensions */, F864F76329BB91B400B13921 /* VernissageWidgetBundle.swift */, - F864F76729BB91B600B13921 /* Assets.xcassets */, + F85D0C642ABA08F9002B3577 /* Assets.xcassets */, ); path = VernissageWidget; sourceTree = ""; @@ -981,7 +981,6 @@ F88C2465295C37B80006098B /* Frameworks */, F88C2466295C37B80006098B /* Resources */, F864F76D29BB91B600B13921 /* Embed Foundation Extensions */, - F8CB3DF129D8267C00CDAE5A /* ShellScript */, ); buildRules = ( ); @@ -1013,7 +1012,7 @@ attributes = { BuildIndependentTargetsInParallel = 1; LastSwiftUpdateCheck = 1430; - LastUpgradeCheck = 1420; + LastUpgradeCheck = 1500; TargetAttributes = { F864F75C29BB91B400B13921 = { CreatedOnToolsVersion = 14.2; @@ -1060,7 +1059,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - F864F76829BB91B600B13921 /* Assets.xcassets in Resources */, + F85D0C652ABA08F9002B3577 /* Assets.xcassets in Resources */, F835082429BEF9C400DE3247 /* Localizable.strings in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1088,27 +1087,6 @@ }; /* End PBXResourcesBuildPhase section */ -/* Begin PBXShellScriptBuildPhase section */ - F8CB3DF129D8267C00CDAE5A /* ShellScript */ = { - isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - ); - outputFileListPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "# Type a script or drag a script file from your workspace to insert its path.\nif which swiftlint >/dev/null; then\n swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https: //github.com/realm/SwiftLint\"\nfi\n"; - }; -/* End PBXShellScriptBuildPhase section */ - /* Begin PBXSourcesBuildPhase section */ F864F75929BB91B400B13921 /* Sources */ = { isa = PBXSourcesBuildPhase; @@ -1447,6 +1425,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; @@ -1480,6 +1459,7 @@ DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; @@ -1508,6 +1488,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; @@ -1541,6 +1522,7 @@ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; diff --git a/Vernissage/Views/EditProfileView.swift b/Vernissage/Views/EditProfileView.swift index 120f3d4..22353c7 100644 --- a/Vernissage/Views/EditProfileView.swift +++ b/Vernissage/Views/EditProfileView.swift @@ -142,7 +142,7 @@ struct EditProfileView: View { Text("@\(account.acct)") .font(.headline) - .foregroundColor(.lightGrayColor) + .foregroundColor(.customGrayColor) if self.avatarData != nil { HStack { @@ -151,7 +151,7 @@ struct EditProfileView: View { .foregroundColor(.accentColor) Text("editProfile.title.photoInfo") .font(.footnote) - .foregroundColor(.lightGrayColor) + .foregroundColor(.customGrayColor) } .padding(.top, 4) } diff --git a/Vernissage/Views/FollowRequestsView.swift b/Vernissage/Views/FollowRequestsView.swift index 7c109a6..7d73fc2 100644 --- a/Vernissage/Views/FollowRequestsView.swift +++ b/Vernissage/Views/FollowRequestsView.swift @@ -71,7 +71,7 @@ struct FollowRequestsView: View { Text(account.displayName ?? account.username) .foregroundColor(.mainTextColor) Text("@\(account.acct)") - .foregroundColor(.lightGrayColor) + .foregroundColor(.customGrayColor) .font(.footnote) } .padding(.leading, 8) diff --git a/Vernissage/Views/InstanceView.swift b/Vernissage/Views/InstanceView.swift index 5100e61..e5b0d60 100644 --- a/Vernissage/Views/InstanceView.swift +++ b/Vernissage/Views/InstanceView.swift @@ -68,7 +68,7 @@ struct InstanceView: View { if let shortDescription = instance.shortDescription { Text(shortDescription) .font(.footnote) - .foregroundColor(.lightGrayColor) + .foregroundColor(.customGrayColor) } } @@ -119,7 +119,7 @@ struct InstanceView: View { Text(title, comment: "Title") Spacer() Text(value) - .foregroundColor(.lightGrayColor) + .foregroundColor(.customGrayColor) .font(.subheadline) } } diff --git a/Vernissage/Views/MainView.swift b/Vernissage/Views/MainView.swift index 1961456..48d02a9 100644 --- a/Vernissage/Views/MainView.swift +++ b/Vernissage/Views/MainView.swift @@ -242,7 +242,7 @@ struct MainView: View { .frame(width: 16, height: 16) .foregroundColor(.white) .padding(8) - .background(Color.lightGrayColor) + .background(Color.customGrayColor) .clipShape(AvatarShape.circle.shape()) .background( AvatarShape.circle.shape() diff --git a/Vernissage/Views/NotificationsView/Subviews/NotificationRowView.swift b/Vernissage/Views/NotificationsView/Subviews/NotificationRowView.swift index ce6423c..ff76551 100644 --- a/Vernissage/Views/NotificationsView/Subviews/NotificationRowView.swift +++ b/Vernissage/Views/NotificationsView/Subviews/NotificationRowView.swift @@ -58,13 +58,13 @@ struct NotificationRowView: View { if let createdAt = self.notification.createdAt.toDate(.isoDateTimeMilliSec) { RelativeTime(date: createdAt) - .foregroundColor(.lightGrayColor) + .foregroundColor(.customGrayColor) .font(.footnote) } } Text(self.getTitle(), comment: "Notification type") - .foregroundColor(.lightGrayColor) + .foregroundColor(.customGrayColor) .font(.footnote) .fontWeight(.light) diff --git a/Vernissage/Views/SettingsView/Subviews/MediaSettingsView.swift b/Vernissage/Views/SettingsView/Subviews/MediaSettingsView.swift index 23fd3b9..ffdd5e9 100644 --- a/Vernissage/Views/SettingsView/Subviews/MediaSettingsView.swift +++ b/Vernissage/Views/SettingsView/Subviews/MediaSettingsView.swift @@ -19,7 +19,7 @@ struct MediaSettingsView: View { Text("settings.title.alwaysShowSensitiveTitle", comment: "Always show NSFW") Text("settings.title.alwaysShowSensitiveDescription", comment: "Force show all NFSW (sensitive) media without warnings") .font(.footnote) - .foregroundColor(.lightGrayColor) + .foregroundColor(.customGrayColor) } } .onChange(of: self.applicationState.showSensitive) { newValue in @@ -31,7 +31,7 @@ struct MediaSettingsView: View { Text("settings.title.alwaysShowAltTitle", comment: "Show alternative text") Text("settings.title.alwaysShowAltDescription", comment: "Show alternative text if present on status details screen") .font(.footnote) - .foregroundColor(.lightGrayColor) + .foregroundColor(.customGrayColor) } } .onChange(of: self.applicationState.showPhotoDescription) { newValue in @@ -43,7 +43,7 @@ struct MediaSettingsView: View { Text("settings.title.showAvatars", comment: "Show avatars") Text("settings.title.showAvatarsOnTimeline", comment: "Show avatars on timeline") .font(.footnote) - .foregroundColor(.lightGrayColor) + .foregroundColor(.customGrayColor) } } .onChange(of: self.applicationState.showAvatarsOnTimeline) { newValue in @@ -55,7 +55,7 @@ struct MediaSettingsView: View { Text("settings.title.showFavourite", comment: "Show favourites") Text("settings.title.showFavouriteOnTimeline", comment: "Show favourites on timeline") .font(.footnote) - .foregroundColor(.lightGrayColor) + .foregroundColor(.customGrayColor) } } .onChange(of: self.applicationState.showFavouritesOnTimeline) { newValue in @@ -67,7 +67,7 @@ struct MediaSettingsView: View { Text("settings.title.showAltText", comment: "Show ALT icon") Text("settings.title.showAltTextOnTimeline", comment: "ALT icon will be displayed on timelines") .font(.footnote) - .foregroundColor(.lightGrayColor) + .foregroundColor(.customGrayColor) } } .onChange(of: self.applicationState.showAltIconOnTimeline) { newValue in @@ -79,7 +79,7 @@ struct MediaSettingsView: View { Text("settings.title.warnAboutMissingAltTitle", comment: "Warn of missing ALT text") Text("settings.title.warnAboutMissingAltDescription", comment: "A warning about missing ALT texts will be displayed before publishing new post.") .font(.footnote) - .foregroundColor(.lightGrayColor) + .foregroundColor(.customGrayColor) } } .onChange(of: self.applicationState.warnAboutMissingAlt) { newValue in diff --git a/Vernissage/Views/SettingsView/Subviews/SocialsSectionView.swift b/Vernissage/Views/SettingsView/Subviews/SocialsSectionView.swift index 03c9d8b..14d7f0e 100644 --- a/Vernissage/Views/SettingsView/Subviews/SocialsSectionView.swift +++ b/Vernissage/Views/SettingsView/Subviews/SocialsSectionView.swift @@ -14,7 +14,7 @@ struct SocialsSectionView: View { Text("settings.title.followVernissage", comment: "Follow Vernissage") Text("settings.title.mastodonAccount", comment: "Mastodon account") .font(.footnote) - .foregroundColor(.lightGrayColor) + .foregroundColor(.customGrayColor) } Spacer() @@ -27,7 +27,7 @@ struct SocialsSectionView: View { Text("settings.title.follow", comment: "Follow me") Text("settings.title.mastodonAccount", comment: "Mastodon account") .font(.footnote) - .foregroundColor(.lightGrayColor) + .foregroundColor(.customGrayColor) } Spacer() @@ -40,7 +40,7 @@ struct SocialsSectionView: View { Text("settings.title.follow", comment: "Follow me") Text("settings.title.pixelfedAccount", comment: "Pixelfed account") .font(.footnote) - .foregroundColor(.lightGrayColor) + .foregroundColor(.customGrayColor) } Spacer() diff --git a/Vernissage/Views/SettingsView/Subviews/SupportView.swift b/Vernissage/Views/SettingsView/Subviews/SupportView.swift index 969684d..f414609 100644 --- a/Vernissage/Views/SettingsView/Subviews/SupportView.swift +++ b/Vernissage/Views/SettingsView/Subviews/SupportView.swift @@ -21,7 +21,7 @@ struct SupportView: View { Text(product.displayName) Text(product.description) .font(.footnote) - .foregroundColor(.lightGrayColor) + .foregroundColor(.customGrayColor) } Spacer() Button(product.displayPrice) { diff --git a/Vernissage/Views/SignInView/Subviews/InstanceRowView.swift b/Vernissage/Views/SignInView/Subviews/InstanceRowView.swift index 76f941b..c42b93e 100644 --- a/Vernissage/Views/SignInView/Subviews/InstanceRowView.swift +++ b/Vernissage/Views/SignInView/Subviews/InstanceRowView.swift @@ -80,7 +80,7 @@ struct InstanceRowView: View { Spacer() } .padding(.top, 4) - .foregroundColor(.lightGrayColor) + .foregroundColor(.customGrayColor) .font(.caption) } } diff --git a/Vernissage/Views/StatusView/StatusView.swift b/Vernissage/Views/StatusView/StatusView.swift index 2ec5170..054ba32 100644 --- a/Vernissage/Views/StatusView/StatusView.swift +++ b/Vernissage/Views/StatusView/StatusView.swift @@ -134,7 +134,7 @@ struct StatusView: View { } } .padding(.bottom, 2) - .foregroundColor(.lightGrayColor) + .foregroundColor(.customGrayColor) HStack { Text("status.title.uploaded", comment: "Uploaded") @@ -151,7 +151,7 @@ struct StatusView: View { Text(String(format: NSLocalizedString("status.title.via", comment: "via"), applicationName)) } } - .foregroundColor(.lightGrayColor) + .foregroundColor(.customGrayColor) .font(.footnote) InteractionRow(statusModel: statusViewModel) { diff --git a/Vernissage/Views/StatusView/Subviews/CommentBodyView.swift b/Vernissage/Views/StatusView/Subviews/CommentBodyView.swift index e4b88de..c0e4e4a 100644 --- a/Vernissage/Views/StatusView/Subviews/CommentBodyView.swift +++ b/Vernissage/Views/StatusView/Subviews/CommentBodyView.swift @@ -36,7 +36,7 @@ struct CommentBodyView: View { if let createdAt = self.statusViewModel.createdAt.toDate(.isoDateTimeMilliSec) { RelativeTime(date: createdAt) - .foregroundColor(.lightGrayColor) + .foregroundColor(.customGrayColor) .font(.footnote) } } diff --git a/Vernissage/Views/StatusView/Subviews/CommentsSectionView.swift b/Vernissage/Views/StatusView/Subviews/CommentsSectionView.swift index fa3f9b6..2c8eab4 100644 --- a/Vernissage/Views/StatusView/Subviews/CommentsSectionView.swift +++ b/Vernissage/Views/StatusView/Subviews/CommentsSectionView.swift @@ -41,7 +41,7 @@ struct CommentsSectionView: View { .padding(.horizontal, 16) .padding(.vertical, 8) } - .background(Color.lightGrayColor.opacity(0.5)) + .background(Color.customGrayColor.opacity(0.5)) .transition(AnyTransition.move(edge: .top).combined(with: .opacity)) } } diff --git a/Vernissage/Views/StatusView/Subviews/StatusPlaceholderView.swift b/Vernissage/Views/StatusView/Subviews/StatusPlaceholderView.swift index b25d460..b7dbe5b 100644 --- a/Vernissage/Views/StatusView/Subviews/StatusPlaceholderView.swift +++ b/Vernissage/Views/StatusView/Subviews/StatusPlaceholderView.swift @@ -32,10 +32,10 @@ struct StatusPlaceholderView: View { accountUsername: "@username") Text("Lorem ispum text something") - .foregroundColor(.lightGrayColor) + .foregroundColor(.customGrayColor) .font(.footnote) Text("Lorem ispum text something sdf sdfsdf sdfdsfsdfsdf") - .foregroundColor(.lightGrayColor) + .foregroundColor(.customGrayColor) .font(.footnote) LabelIcon(iconName: "mappin.and.ellipse", value: "Wroclaw, Poland") diff --git a/Vernissage/Views/UserProfileView/Subviews/UserProfileHeaderView.swift b/Vernissage/Views/UserProfileView/Subviews/UserProfileHeaderView.swift index 3f876a6..e5030eb 100644 --- a/Vernissage/Views/UserProfileView/Subviews/UserProfileHeaderView.swift +++ b/Vernissage/Views/UserProfileView/Subviews/UserProfileHeaderView.swift @@ -68,7 +68,7 @@ struct UserProfileHeaderView: View { .font(.title3) .fontWeight(.bold) Text("@\(account.acct)") - .foregroundColor(.lightGrayColor) + .foregroundColor(.customGrayColor) .font(.subheadline) } @@ -101,7 +101,7 @@ struct UserProfileHeaderView: View { self.accountRelationshipPanel() Text(String(format: NSLocalizedString("userProfile.title.joined", comment: "Joined"), account.createdAt.toRelative(.isoDateTimeMilliSec))) - .foregroundColor(.lightGrayColor.opacity(0.5)) + .foregroundColor(.customGrayColor.opacity(0.5)) .font(.footnote) } .padding([.top, .leading, .trailing]) diff --git a/Vernissage/Views/UserProfileView/Subviews/UserProfileStatusesView.swift b/Vernissage/Views/UserProfileView/Subviews/UserProfileStatusesView.swift index 05286c8..6bdf139 100644 --- a/Vernissage/Views/UserProfileView/Subviews/UserProfileStatusesView.swift +++ b/Vernissage/Views/UserProfileView/Subviews/UserProfileStatusesView.swift @@ -61,7 +61,7 @@ struct UserProfileStatusesView: View { } } label: { Image(systemName: "rectangle.grid.1x2.fill") - .foregroundColor(self.applicationState.showGridOnUserProfile ? .lightGrayColor : .accentColor) + .foregroundColor(self.applicationState.showGridOnUserProfile ? .customGrayColor : .accentColor) .padding(.trailing, 8) .padding(.bottom, 8) } @@ -72,7 +72,7 @@ struct UserProfileStatusesView: View { } } label: { Image(systemName: "rectangle.grid.2x2.fill") - .foregroundColor(self.applicationState.showGridOnUserProfile ? .accentColor : .lightGrayColor) + .foregroundColor(self.applicationState.showGridOnUserProfile ? .accentColor : .customGrayColor) .padding(.trailing, 16) .padding(.bottom, 8) } diff --git a/VernissageWidget/Assets.xcassets/AccentColor.colorset/Contents.json b/VernissageWidget/Assets.xcassets/AccentColor.colorset/Contents.json index eb87897..3fd47e5 100644 --- a/VernissageWidget/Assets.xcassets/AccentColor.colorset/Contents.json +++ b/VernissageWidget/Assets.xcassets/AccentColor.colorset/Contents.json @@ -1,6 +1,15 @@ { "colors" : [ { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.969", + "green" : "0.514", + "red" : "0.204" + } + }, "idiom" : "universal" } ], diff --git a/VernissageWidget/Assets.xcassets/AppIcon.appiconset/Contents.json b/VernissageWidget/Assets.xcassets/AppIcon.appiconset/Contents.json index 13613e3..a657e33 100644 --- a/VernissageWidget/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/VernissageWidget/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,6 +1,7 @@ { "images" : [ { + "filename" : "icon.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" diff --git a/VernissageWidget/Assets.xcassets/AppIcon.appiconset/icon.png b/VernissageWidget/Assets.xcassets/AppIcon.appiconset/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..2e4cd9607e66eec82d58d0b1eec087e421849e6b GIT binary patch literal 45724 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 Kx4!%Fr+)#o(v*(? literal 0 HcmV?d00001 diff --git a/VernissageWidget/Assets.xcassets/WidgetBackground.colorset/Contents.json b/VernissageWidget/Assets.xcassets/WidgetBackground.colorset/Contents.json index eb87897..1b51e1e 100644 --- a/VernissageWidget/Assets.xcassets/WidgetBackground.colorset/Contents.json +++ b/VernissageWidget/Assets.xcassets/WidgetBackground.colorset/Contents.json @@ -1,6 +1,15 @@ { "colors" : [ { + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "0.267", + "green" : "0.267", + "red" : "0.267" + } + }, "idiom" : "universal" } ], diff --git a/WidgetsKit/Sources/WidgetsKit/Views/BaseComposeView.swift b/WidgetsKit/Sources/WidgetsKit/Views/BaseComposeView.swift index 4ef011d..e9cac5b 100644 --- a/WidgetsKit/Sources/WidgetsKit/Views/BaseComposeView.swift +++ b/WidgetsKit/Sources/WidgetsKit/Views/BaseComposeView.swift @@ -400,7 +400,7 @@ public struct BaseComposeView: View { Image(systemName: "mappin.and.ellipse") Text("\(name), \(country)") } - .foregroundColor(.lightGrayColor) + .foregroundColor(.customGrayColor) .padding(.trailing, 8) } } @@ -425,7 +425,7 @@ public struct BaseComposeView: View { Text(account.displayNameWithoutEmojis) .foregroundColor(.mainTextColor) Text("@\(account.acct)") - .foregroundColor(.lightGrayColor) + .foregroundColor(.customGrayColor) } .padding(.leading, 8) } @@ -540,7 +540,7 @@ public struct BaseComposeView: View { Spacer() Text("\(self.applicationState.statusMaxCharacters - textModel.text.string.utf16.count)") - .foregroundColor(.lightGrayColor) + .foregroundColor(.customGrayColor) .font(.system(size: self.keyboardFontTextSize)) } .padding(8) diff --git a/WidgetsKit/Sources/WidgetsKit/Views/PlaceSelectorView.swift b/WidgetsKit/Sources/WidgetsKit/Views/PlaceSelectorView.swift index bdb3f4c..f636289 100644 --- a/WidgetsKit/Sources/WidgetsKit/Views/PlaceSelectorView.swift +++ b/WidgetsKit/Sources/WidgetsKit/Views/PlaceSelectorView.swift @@ -79,7 +79,7 @@ public struct PlaceSelectorView: View { .foregroundColor(.mainTextColor) Text(place.country ?? String.empty()) .font(.subheadline) - .foregroundColor(.lightGrayColor) + .foregroundColor(.customGrayColor) } Spacer() diff --git a/WidgetsKit/Sources/WidgetsKit/Widgets/NoDataView.swift b/WidgetsKit/Sources/WidgetsKit/Widgets/NoDataView.swift index 43ac694..96e77e4 100644 --- a/WidgetsKit/Sources/WidgetsKit/Widgets/NoDataView.swift +++ b/WidgetsKit/Sources/WidgetsKit/Widgets/NoDataView.swift @@ -24,7 +24,7 @@ public struct NoDataView: View { Text(self.text, comment: "No data message") .font(.title3) } - .foregroundColor(.lightGrayColor) + .foregroundColor(.customGrayColor) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) } } diff --git a/WidgetsKit/Sources/WidgetsKit/Widgets/UsernameRow.swift b/WidgetsKit/Sources/WidgetsKit/Widgets/UsernameRow.swift index e994524..079c8f4 100644 --- a/WidgetsKit/Sources/WidgetsKit/Widgets/UsernameRow.swift +++ b/WidgetsKit/Sources/WidgetsKit/Widgets/UsernameRow.swift @@ -34,7 +34,7 @@ public struct UsernameRow: View { Text(accountDisplayName ?? accountUsername) .foregroundColor(.mainTextColor) Text("@\(accountUsername)") - .foregroundColor(.lightGrayColor) + .foregroundColor(.customGrayColor) .font(.footnote) } .padding(.leading, 8) From 3664464224568906e1033f8659f3bf574ff5fdd4 Mon Sep 17 00:00:00 2001 From: Marcin Czachurski Date: Tue, 19 Sep 2023 19:39:23 +0200 Subject: [PATCH 10/14] Change accent color --- .../AccentColor.colorset/Contents.json | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/VernissageWidget/Assets.xcassets/AccentColor.colorset/Contents.json b/VernissageWidget/Assets.xcassets/AccentColor.colorset/Contents.json index 3fd47e5..96e2586 100644 --- a/VernissageWidget/Assets.xcassets/AccentColor.colorset/Contents.json +++ b/VernissageWidget/Assets.xcassets/AccentColor.colorset/Contents.json @@ -11,6 +11,24 @@ } }, "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.973", + "green" : "0.655", + "red" : "0.290" + } + }, + "idiom" : "universal" } ], "info" : { From b2655e419f4e041b4c93addc15ceea343ac03af0 Mon Sep 17 00:00:00 2001 From: Marcin Czachurski Date: Wed, 20 Sep 2023 16:22:15 +0200 Subject: [PATCH 11/14] Fix loading data on waterfall widget --- README.md | 6 +++ Vernissage.xcodeproj/project.pbxproj | 43 +++++++++++++---- Vernissage/Widgets/WaterfallGrid.swift | 66 ++++++++++++++++---------- 3 files changed, 83 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index abc4299..7cf5c52 100644 --- a/README.md +++ b/README.md @@ -32,3 +32,9 @@ In the name of the folders you have to put the code of the new language ([here]( Then you have to open files in these folders and translate them 🇯🇵🇫🇷🇨🇮🇧🇪. After translation create a Pull Request 👍. From time to time you have to come back and translate lines which has been added since the last translation. + +## Technical debt + - Use auto generated resources (Color/Images) instead static extensions (how to do this in separete Swift Packages?) + - Enable swiftlint (https://github.com/realm/SwiftLint/issues/5053) + - Investigate new (iOS 17) Observables + - Check how to migrate to SwiftData (iOS 17) diff --git a/Vernissage.xcodeproj/project.pbxproj b/Vernissage.xcodeproj/project.pbxproj index 7994331..b338a77 100644 --- a/Vernissage.xcodeproj/project.pbxproj +++ b/Vernissage.xcodeproj/project.pbxproj @@ -183,6 +183,7 @@ F89D6C4629718193001DA3D4 /* GeneralSectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F89D6C4529718193001DA3D4 /* GeneralSectionView.swift */; }; F89D6C4A297196FF001DA3D4 /* ImageViewer.swift in Sources */ = {isa = PBXBuildFile; fileRef = F89D6C49297196FF001DA3D4 /* ImageViewer.swift */; }; F89F57B029D1C11200001EE3 /* RelationshipModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F89F57AF29D1C11200001EE3 /* RelationshipModel.swift */; }; + F8A192102ABB322E00C2599A /* Semaphore in Frameworks */ = {isa = PBXBuildFile; productRef = F8A1920F2ABB322E00C2599A /* Semaphore */; }; F8A93D7E2965FD89001D8331 /* UserProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8A93D7D2965FD89001D8331 /* UserProfileView.swift */; }; F8AFF7C129B259150087D083 /* HashtagsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8AFF7C029B259150087D083 /* HashtagsView.swift */; }; F8AFF7C429B25EF40087D083 /* ImagesGrid.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8AFF7C329B25EF40087D083 /* ImagesGrid.swift */; }; @@ -455,6 +456,7 @@ F8210DD72966BB7E001D9973 /* NukeExtensions in Frameworks */, F88BC52D29E04BB600CE6141 /* EnvironmentKit in Frameworks */, F8210DD92966BB7E001D9973 /* NukeUI in Frameworks */, + F8A192102ABB322E00C2599A /* Semaphore in Frameworks */, F89B5CC029D019B600549F2F /* HTMLString in Frameworks */, F88BC52A29E046D700CE6141 /* WidgetsKit in Frameworks */, ); @@ -999,6 +1001,7 @@ F88BC52629E0431D00CE6141 /* ServicesKit */, F88BC52929E046D700CE6141 /* WidgetsKit */, F88BC52C29E04BB600CE6141 /* EnvironmentKit */, + F8A1920F2ABB322E00C2599A /* Semaphore */, ); productName = Vernissage; productReference = F88C2468295C37B80006098B /* Vernissage.app */; @@ -1042,6 +1045,7 @@ F88E4D4B297EA4290057491A /* XCRemoteSwiftPackageReference "EmojiText" */, F89B5CBE29D019B600549F2F /* XCRemoteSwiftPackageReference "HTMLString" */, F84625F929FE393B002D3AF4 /* XCRemoteSwiftPackageReference "QRCode" */, + F8A1920E2ABB322E00C2599A /* XCRemoteSwiftPackageReference "Semaphore" */, ); productRefGroup = F88C2469295C37B80006098B /* Products */; projectDirPath = ""; @@ -1310,12 +1314,14 @@ F864F76E29BB91B600B13921 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_ASSET_SYMBOLS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = NO; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; CODE_SIGN_ENTITLEMENTS = VernissageWidget/VernissageWidgetExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 204; + CURRENT_PROJECT_VERSION = 205; DEVELOPMENT_TEAM = B2U9FEKYP8; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = VernissageWidget/Info.plist; @@ -1340,11 +1346,13 @@ F864F76F29BB91B600B13921 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_ASSET_SYMBOLS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = NO; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; CODE_SIGN_ENTITLEMENTS = VernissageWidget/VernissageWidgetExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 204; + CURRENT_PROJECT_VERSION = 205; DEVELOPMENT_TEAM = B2U9FEKYP8; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = VernissageWidget/Info.plist; @@ -1369,10 +1377,12 @@ F88BC50D29E02F3900CE6141 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_ASSET_SYMBOLS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = NO; CODE_SIGN_ENTITLEMENTS = VernissageShare/VernissageShareExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 204; + CURRENT_PROJECT_VERSION = 205; DEVELOPMENT_TEAM = B2U9FEKYP8; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = VernissageShare/Info.plist; @@ -1397,9 +1407,11 @@ F88BC50E29E02F3900CE6141 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_ASSET_SYMBOLS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = NO; CODE_SIGN_ENTITLEMENTS = VernissageShare/VernissageShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 204; + CURRENT_PROJECT_VERSION = 205; DEVELOPMENT_TEAM = B2U9FEKYP8; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = VernissageShare/Info.plist; @@ -1547,12 +1559,13 @@ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES = "Violet Blue Pride Pride-Camera Blue-Camera Violet-Camera Orange-Camera Orange Yellow-Camera Yellow Gradient-Camera Gradient"; ASSETCATALOG_COMPILER_APPICON_NAME = Default; + ASSETCATALOG_COMPILER_GENERATE_ASSET_SYMBOLS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = NO; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO; CODE_SIGN_ENTITLEMENTS = Vernissage/Vernissage.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 204; + CURRENT_PROJECT_VERSION = 205; DEVELOPMENT_ASSET_PATHS = "\"Vernissage/Preview Content\""; DEVELOPMENT_TEAM = B2U9FEKYP8; ENABLE_PREVIEWS = YES; @@ -1590,11 +1603,12 @@ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES = "Violet Blue Pride Pride-Camera Blue-Camera Violet-Camera Orange-Camera Orange Yellow-Camera Yellow Gradient-Camera Gradient"; ASSETCATALOG_COMPILER_APPICON_NAME = Default; + ASSETCATALOG_COMPILER_GENERATE_ASSET_SYMBOLS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = NO; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO; CODE_SIGN_ENTITLEMENTS = Vernissage/Vernissage.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 204; + CURRENT_PROJECT_VERSION = 205; DEVELOPMENT_ASSET_PATHS = "\"Vernissage/Preview Content\""; DEVELOPMENT_TEAM = B2U9FEKYP8; ENABLE_PREVIEWS = YES; @@ -1699,6 +1713,14 @@ minimumVersion = 6.0.0; }; }; + F8A1920E2ABB322E00C2599A /* XCRemoteSwiftPackageReference "Semaphore" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/groue/Semaphore"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 0.0.8; + }; + }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ @@ -1772,6 +1794,11 @@ package = F89B5CBE29D019B600549F2F /* XCRemoteSwiftPackageReference "HTMLString" */; productName = HTMLString; }; + F8A1920F2ABB322E00C2599A /* Semaphore */ = { + isa = XCSwiftPackageProductDependency; + package = F8A1920E2ABB322E00C2599A /* XCRemoteSwiftPackageReference "Semaphore" */; + productName = Semaphore; + }; /* End XCSwiftPackageProductDependency section */ /* Begin XCVersionGroup section */ diff --git a/Vernissage/Widgets/WaterfallGrid.swift b/Vernissage/Widgets/WaterfallGrid.swift index 5cb732d..e24ec4f 100644 --- a/Vernissage/Widgets/WaterfallGrid.swift +++ b/Vernissage/Widgets/WaterfallGrid.swift @@ -6,6 +6,7 @@ import SwiftUI import WidgetsKit +import Semaphore struct WaterfallGrid: View where Data: RandomAccessCollection, Data: Equatable, Content: View, ID: Hashable, Data.Element: Equatable, Data.Element: Identifiable, Data.Element: Hashable, Data.Element: Sizable { @@ -17,9 +18,9 @@ struct WaterfallGrid: View where Data: RandomAccessCollection @State private var columnsData: [ColumnData] = [] @State private var processedItems: [Data.Element.ID] = [] - @State private var isDuringLoading: Bool = false private let onLoadMore: () async -> Void + private let semaphore = AsyncSemaphore(value: 1) var body: some View { HStack(alignment: .top, spacing: 20) { @@ -30,12 +31,12 @@ struct WaterfallGrid: View where Data: RandomAccessCollection } if self.hideLoadMore == false { + // We can show multiple loading indicators. Each indicator can run loading feature in pararell. + // Thus we have to be sure that loading will exeute one by one. LoadingIndicator() .task { - if isDuringLoading == false { - isDuringLoading = true - await self.onLoadMore() - isDuringLoading = false + Task { @MainActor in + await self.loadMoreData() } } } @@ -52,31 +53,48 @@ struct WaterfallGrid: View where Data: RandomAccessCollection self.recalculateArrays() } } + + private func loadMoreData() async { + await semaphore.wait() + defer { semaphore.signal() } + + await self.onLoadMore() + } private func recalculateArrays() { - self.columnsData = [] - self.processedItems = [] - - for _ in 0 ..< self.columns { - self.columnsData.append(ColumnData()) - } - - for item in self.data { - let index = self.minimumHeightIndex() - - self.columnsData[index].data.append(item) - self.columnsData[index].height = self.columnsData[index].height + self.calculateHeight(item: item) - self.processedItems.append(item.id) + Task { @MainActor in + await semaphore.wait() + defer { semaphore.signal() } + + self.columnsData = [] + self.processedItems = [] + + for _ in 0 ..< self.columns { + self.columnsData.append(ColumnData()) + } + + for item in self.data { + let index = self.minimumHeightIndex() + + self.columnsData[index].data.append(item) + self.columnsData[index].height = self.columnsData[index].height + self.calculateHeight(item: item) + self.processedItems.append(item.id) + } } } private func appendToArrays() { - for item in self.data where self.processedItems.contains(where: { $0 == item.id }) == false { - let index = self.minimumHeightIndex() - - self.columnsData[index].data.append(item) - self.columnsData[index].height = self.columnsData[index].height + self.calculateHeight(item: item) - self.processedItems.append(item.id) + Task { @MainActor in + await semaphore.wait() + defer { semaphore.signal() } + + for item in self.data where self.processedItems.contains(where: { $0 == item.id }) == false { + let index = self.minimumHeightIndex() + + self.columnsData[index].data.append(item) + self.columnsData[index].height = self.columnsData[index].height + self.calculateHeight(item: item) + self.processedItems.append(item.id) + } } } From 77e6f26cfb52975b560bcf73efcd69b5348d3f52 Mon Sep 17 00:00:00 2001 From: Marcin Czachurski Date: Fri, 22 Sep 2023 15:22:48 +0200 Subject: [PATCH 12/14] Fix image resizing on screen opening --- Vernissage.xcodeproj/project.pbxproj | 12 ++++++------ Vernissage/Views/StatusesView.swift | 4 ++-- .../WidgetsKit/ViewModifiers/DeviceRotation.swift | 4 ++-- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Vernissage.xcodeproj/project.pbxproj b/Vernissage.xcodeproj/project.pbxproj index b338a77..1dfd8d3 100644 --- a/Vernissage.xcodeproj/project.pbxproj +++ b/Vernissage.xcodeproj/project.pbxproj @@ -1321,7 +1321,7 @@ CODE_SIGN_ENTITLEMENTS = VernissageWidget/VernissageWidgetExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 205; + CURRENT_PROJECT_VERSION = 206; DEVELOPMENT_TEAM = B2U9FEKYP8; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = VernissageWidget/Info.plist; @@ -1352,7 +1352,7 @@ ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; CODE_SIGN_ENTITLEMENTS = VernissageWidget/VernissageWidgetExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 205; + CURRENT_PROJECT_VERSION = 206; DEVELOPMENT_TEAM = B2U9FEKYP8; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = VernissageWidget/Info.plist; @@ -1382,7 +1382,7 @@ CODE_SIGN_ENTITLEMENTS = VernissageShare/VernissageShareExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 205; + CURRENT_PROJECT_VERSION = 206; DEVELOPMENT_TEAM = B2U9FEKYP8; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = VernissageShare/Info.plist; @@ -1411,7 +1411,7 @@ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = NO; CODE_SIGN_ENTITLEMENTS = VernissageShare/VernissageShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 205; + CURRENT_PROJECT_VERSION = 206; DEVELOPMENT_TEAM = B2U9FEKYP8; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = VernissageShare/Info.plist; @@ -1565,7 +1565,7 @@ CODE_SIGN_ENTITLEMENTS = Vernissage/Vernissage.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 205; + CURRENT_PROJECT_VERSION = 206; DEVELOPMENT_ASSET_PATHS = "\"Vernissage/Preview Content\""; DEVELOPMENT_TEAM = B2U9FEKYP8; ENABLE_PREVIEWS = YES; @@ -1608,7 +1608,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = Vernissage/Vernissage.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 205; + CURRENT_PROJECT_VERSION = 206; DEVELOPMENT_ASSET_PATHS = "\"Vernissage/Preview Content\""; DEVELOPMENT_TEAM = B2U9FEKYP8; ENABLE_PREVIEWS = YES; diff --git a/Vernissage/Views/StatusesView.swift b/Vernissage/Views/StatusesView.swift index c8de36d..31935e1 100644 --- a/Vernissage/Views/StatusesView.swift +++ b/Vernissage/Views/StatusesView.swift @@ -55,8 +55,8 @@ struct StatusesView: View { // Gallery parameters. @State private var imageColumns = 3 - @State private var containerWidth: Double = UIScreen.main.bounds.width - @State private var containerHeight: Double = UIScreen.main.bounds.height + @State private var containerWidth: Double = UIDevice.isIPad ? UIScreen.main.bounds.width / 3 : UIScreen.main.bounds.width + @State private var containerHeight: Double = UIDevice.isIPad ? UIScreen.main.bounds.height / 3 : UIScreen.main.bounds.height private let defaultLimit = 40 private let imagePrefetcher = ImagePrefetcher(destination: .diskCache) diff --git a/WidgetsKit/Sources/WidgetsKit/ViewModifiers/DeviceRotation.swift b/WidgetsKit/Sources/WidgetsKit/ViewModifiers/DeviceRotation.swift index fb58fd2..058bc51 100644 --- a/WidgetsKit/Sources/WidgetsKit/ViewModifiers/DeviceRotation.swift +++ b/WidgetsKit/Sources/WidgetsKit/ViewModifiers/DeviceRotation.swift @@ -52,10 +52,10 @@ struct DeviceImageGallery: ViewModifier { } } .onAppear { - // asyncAfter(0.1) { + asyncAfter(0.1) { let galleryProperties = self.getGalleryProperties(geometry: geometry, horizontalSize: self.horizontalSizeClass ?? .compact) self.action(galleryProperties) - // } + } } } } From 2c855eaf16f52a88f6f5ad5d905c969a316bba14 Mon Sep 17 00:00:00 2001 From: Marcin Czachurski Date: Sun, 24 Sep 2023 11:39:37 +0200 Subject: [PATCH 13/14] Improve refresh timeline --- CoreData/StatusDataHandler.swift | 14 ----------- Vernissage/Services/HomeTimelineService.swift | 25 ++++++++++--------- Vernissage/Views/HomeFeedView.swift | 6 ++--- 3 files changed, 15 insertions(+), 30 deletions(-) diff --git a/CoreData/StatusDataHandler.swift b/CoreData/StatusDataHandler.swift index 2928e2b..1d4b880 100644 --- a/CoreData/StatusDataHandler.swift +++ b/CoreData/StatusDataHandler.swift @@ -120,20 +120,6 @@ class StatusDataHandler { } } - func remove(accountId: String, statuses: [StatusData], viewContext: NSManagedObjectContext? = nil) { - let context = viewContext ?? CoreDataHandler.shared.container.viewContext - - for status in statuses { - context.delete(status) - } - - do { - try context.save() - } catch { - CoreDataError.shared.handle(error, message: "Error during deleting status (remove).") - } - } - func setFavourited(accountId: String, statusId: String) { let backgroundContext = CoreDataHandler.shared.newBackgroundContext() diff --git a/Vernissage/Services/HomeTimelineService.swift b/Vernissage/Services/HomeTimelineService.swift index 7438637..a2b78c9 100644 --- a/Vernissage/Services/HomeTimelineService.swift +++ b/Vernissage/Services/HomeTimelineService.swift @@ -45,7 +45,7 @@ public class HomeTimelineService { } @MainActor - public func refreshTimeline(for account: AccountModel) async throws -> String? { + public func refreshTimeline(for account: AccountModel, updateLastSeenStatus: Bool = false) async throws -> String? { // Load data from API and operate on CoreData on background context. let backgroundContext = CoreDataHandler.shared.newBackgroundContext() @@ -57,29 +57,28 @@ public class HomeTimelineService { // When Apple introduce good way to show new items without scroll to top then we can change that method. let allStatusesFromApi = try await self.refresh(for: account, on: backgroundContext) - // Save data into database. - CoreDataHandler.shared.save(viewContext: backgroundContext) - + // Update last seen status. + if let lastSeenStatusId, updateLastSeenStatus == true { + try self.update(lastSeenStatusId: lastSeenStatusId, for: account, on: backgroundContext) + } + // Start prefetching images. self.prefetch(statuses: allStatusesFromApi) + + // Save data into database. + CoreDataHandler.shared.save(viewContext: backgroundContext) // Return id of last seen status. return lastSeenStatusId } - public func save(lastSeenStatusId: String, for account: AccountModel) async throws { - // Load data from API and operate on CoreData on background context. - let backgroundContext = CoreDataHandler.shared.newBackgroundContext() - + private func update(lastSeenStatusId: String, for account: AccountModel, on backgroundContext: NSManagedObjectContext) throws { // Save information about last seen status. guard let accountDataFromDb = AccountDataHandler.shared.getAccountData(accountId: account.id, viewContext: backgroundContext) else { throw DatabaseError.cannotDownloadAccount } accountDataFromDb.lastSeenStatusId = lastSeenStatusId - - // Save data into database. - CoreDataHandler.shared.save(viewContext: backgroundContext) } public func update(status statusData: StatusData, basedOn status: Status, for account: AccountModel) async throws -> StatusData? { @@ -193,7 +192,9 @@ public class HomeTimelineService { // Delete statuses from database. if !dbStatusesToRemove.isEmpty { - StatusDataHandler.shared.remove(accountId: account.id, statuses: dbStatusesToRemove, viewContext: backgroundContext) + for dbStatusToRemove in dbStatusesToRemove { + backgroundContext.delete(dbStatusToRemove) + } } // Save statuses in database. diff --git a/Vernissage/Views/HomeFeedView.swift b/Vernissage/Views/HomeFeedView.swift index 1a3e11a..b79623d 100644 --- a/Vernissage/Views/HomeFeedView.swift +++ b/Vernissage/Views/HomeFeedView.swift @@ -101,12 +101,10 @@ struct HomeFeedView: View { private func refreshData() async { do { if let account = self.applicationState.account { - if let lastSeenStatusId = try await HomeTimelineService.shared.refreshTimeline(for: account) { - try await HomeTimelineService.shared.save(lastSeenStatusId: lastSeenStatusId, for: account) - self.applicationState.lastSeenStatusId = lastSeenStatusId - } + let lastSeenStatusId = try await HomeTimelineService.shared.refreshTimeline(for: account, updateLastSeenStatus: true) asyncAfter(0.35) { + self.applicationState.lastSeenStatusId = lastSeenStatusId self.applicationState.amountOfNewStatuses = 0 } } From 3dd456f28cdd9c274a1f64413eb9b7e4e4b61109 Mon Sep 17 00:00:00 2001 From: Marcin Czachurski Date: Sun, 24 Sep 2023 11:48:00 +0200 Subject: [PATCH 14/14] Change version to 1.10.0 (207) --- Vernissage.xcodeproj/project.pbxproj | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Vernissage.xcodeproj/project.pbxproj b/Vernissage.xcodeproj/project.pbxproj index 1dfd8d3..e8d9de6 100644 --- a/Vernissage.xcodeproj/project.pbxproj +++ b/Vernissage.xcodeproj/project.pbxproj @@ -1321,7 +1321,7 @@ CODE_SIGN_ENTITLEMENTS = VernissageWidget/VernissageWidgetExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 206; + CURRENT_PROJECT_VERSION = 207; DEVELOPMENT_TEAM = B2U9FEKYP8; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = VernissageWidget/Info.plist; @@ -1352,7 +1352,7 @@ ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; CODE_SIGN_ENTITLEMENTS = VernissageWidget/VernissageWidgetExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 206; + CURRENT_PROJECT_VERSION = 207; DEVELOPMENT_TEAM = B2U9FEKYP8; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = VernissageWidget/Info.plist; @@ -1382,7 +1382,7 @@ CODE_SIGN_ENTITLEMENTS = VernissageShare/VernissageShareExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 206; + CURRENT_PROJECT_VERSION = 207; DEVELOPMENT_TEAM = B2U9FEKYP8; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = VernissageShare/Info.plist; @@ -1411,7 +1411,7 @@ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = NO; CODE_SIGN_ENTITLEMENTS = VernissageShare/VernissageShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 206; + CURRENT_PROJECT_VERSION = 207; DEVELOPMENT_TEAM = B2U9FEKYP8; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = VernissageShare/Info.plist; @@ -1565,7 +1565,7 @@ CODE_SIGN_ENTITLEMENTS = Vernissage/Vernissage.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 206; + CURRENT_PROJECT_VERSION = 207; DEVELOPMENT_ASSET_PATHS = "\"Vernissage/Preview Content\""; DEVELOPMENT_TEAM = B2U9FEKYP8; ENABLE_PREVIEWS = YES; @@ -1608,7 +1608,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = Vernissage/Vernissage.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 206; + CURRENT_PROJECT_VERSION = 207; DEVELOPMENT_ASSET_PATHS = "\"Vernissage/Preview Content\""; DEVELOPMENT_TEAM = B2U9FEKYP8; ENABLE_PREVIEWS = YES;