Add new gallery component.
This commit is contained in:
parent
446fbd9b9e
commit
085abcbea1
|
@ -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] = []
|
||||
|
|
|
@ -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<URL, CGSize>(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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 = "<group>"; };
|
||||
F866F6A929605AFA002E8F88 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
|
||||
F866F6AD29606367002E8F88 /* ApplicationViewMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationViewMode.swift; sourceTree = "<group>"; };
|
||||
F8675DCF2A1FA40500A89959 /* WaterfallGrid.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaterfallGrid.swift; sourceTree = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
F86A4300299A97F500DF7645 /* ProductIdentifiers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductIdentifiers.swift; sourceTree = "<group>"; };
|
||||
|
@ -392,7 +392,6 @@
|
|||
F8B08861299435C9002AB40A /* SupportView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SupportView.swift; sourceTree = "<group>"; };
|
||||
F8B3699A29D86EB600BE3808 /* .swiftlint.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = .swiftlint.yml; sourceTree = "<group>"; };
|
||||
F8B3699B29D86EBD00BE3808 /* .gitignore */ = {isa = PBXFileReference; lastKnownFileType = text; path = .gitignore; sourceTree = "<group>"; };
|
||||
F8C287A22A06B4C90072213F /* ImageScale.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageScale.swift; sourceTree = "<group>"; };
|
||||
F8C937A929882CA90004D782 /* Vernissage-001.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Vernissage-001.xcdatamodel"; sourceTree = "<group>"; };
|
||||
F8CAE64129B8F1AF001E0372 /* Vernissage-005.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Vernissage-005.xcdatamodel"; sourceTree = "<group>"; };
|
||||
F8D5444229D4066C002225D6 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
|
@ -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 = "<group>";
|
||||
|
@ -592,6 +589,7 @@
|
|||
F8AFF7C329B25EF40087D083 /* ImagesGrid.swift */,
|
||||
F89D6C49297196FF001DA3D4 /* ImageViewer.swift */,
|
||||
F870EE5129F1645C00A2D43B /* MainNavigationOptions.swift */,
|
||||
F8675DCF2A1FA40500A89959 /* WaterfallGrid.swift */,
|
||||
);
|
||||
path = Widgets;
|
||||
sourceTree = "<group>";
|
||||
|
@ -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" */;
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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()
|
||||
|
|
|
@ -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<StatusData>
|
||||
|
||||
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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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<Int>, containerWidth: Binding<Double>, containerHeight: Binding<Double>) {
|
||||
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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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<Double>,
|
||||
clipToRectangle: Binding<Bool> = 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))
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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<Double>,
|
||||
clipToRectangle: Binding<Bool> = Binding.constant(false),
|
||||
showSpoilerText: Binding<Bool> = 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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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<Content>: 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<Int>,
|
||||
hideLoadMore: Binding<Bool>,
|
||||
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
|
||||
}
|
||||
}
|
|
@ -14,3 +14,11 @@ public extension View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension ViewModifier {
|
||||
func asyncAfter(_ time: Double, operation: @escaping () -> Void) {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + time) {
|
||||
operation()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue