Merge pull request #77 from VernissageApp/feature/iPad

iPad version of the application.
This commit is contained in:
Marcin Czachurski 2023-09-24 11:53:08 +02:00 committed by GitHub
commit 3f44a6b350
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
61 changed files with 1011 additions and 300 deletions

View File

@ -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,

View File

@ -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] = []

View File

@ -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()

View File

@ -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")

View File

@ -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)

View File

@ -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: [

View File

@ -7,40 +7,86 @@
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)
private let staticImageHeight = 500.0
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 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) -> 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)
}
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 {
let divider = Double(width) / UIScreen.main.bounds.size.width
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
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)
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
)
self.memoryCacheData.insert(size, forKey: url)
return size
}
}

View File

@ -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 */; };
@ -89,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 */; };
@ -182,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 */; };
@ -190,16 +192,19 @@
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 */; };
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 */; };
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 */
@ -267,6 +272,7 @@
F84625F729FE2C2F002D3AF4 /* AccountFetcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountFetcher.swift; sourceTree = "<group>"; };
F858906A29E1CC7A00D4BDED /* UIApplication+Window.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIApplication+Window.swift"; sourceTree = "<group>"; };
F85B586C29ED169B00A16D12 /* Vernissage-010.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Vernissage-010.xcdatamodel"; sourceTree = "<group>"; };
F85D0C642ABA08F9002B3577 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
F85D4970296402DC00751DF7 /* AuthorizationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthorizationService.swift; sourceTree = "<group>"; };
F85D4974296407F100751DF7 /* HomeTimelineService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeTimelineService.swift; sourceTree = "<group>"; };
F85D497629640A5200751DF7 /* ImageRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageRow.swift; sourceTree = "<group>"; };
@ -284,7 +290,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 = "<group>"; };
F864F76529BB91B400B13921 /* PhotoWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoWidget.swift; sourceTree = "<group>"; };
F864F76729BB91B600B13921 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
F864F76929BB91B600B13921 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
F864F77129BB924D00B13921 /* PhotoWidgetEntryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoWidgetEntryView.swift; sourceTree = "<group>"; };
F864F77329BB929A00B13921 /* PhotoProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoProvider.swift; sourceTree = "<group>"; };
@ -302,6 +307,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>"; };
@ -391,13 +397,15 @@
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>"; };
F8B758DD2AB9DD85000C8068 /* ColumnData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColumnData.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>"; };
F8DF38E329DD68820047F1AA /* ViewOffsetKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewOffsetKey.swift; sourceTree = "<group>"; };
F8DF38E529DDB98A0047F1AA /* SocialsSectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SocialsSectionView.swift; sourceTree = "<group>"; };
F8DF38E729DDC3D20047F1AA /* eu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = eu; path = eu.lproj/Localizable.strings; sourceTree = "<group>"; };
F8E36E452AB8745300769C55 /* Sizable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sizable.swift; sourceTree = "<group>"; };
F8E36E472AB874A500769C55 /* StatusModel+Sizeable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StatusModel+Sizeable.swift"; sourceTree = "<group>"; };
F8E6D03229CDD52500416CCA /* EditProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditProfileView.swift; sourceTree = "<group>"; };
F8EF371429C624DA00669F45 /* Vernissage-006.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Vernissage-006.xcdatamodel"; sourceTree = "<group>"; };
F8EF3C8B29FC3A5F00CBFF7C /* Vernissage-012.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Vernissage-012.xcdatamodel"; sourceTree = "<group>"; };
@ -407,6 +415,7 @@
F8F6E44829BCC0F00004795E /* PhotoMediumWidgetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoMediumWidgetView.swift; sourceTree = "<group>"; };
F8F6E44A29BCC0FF0004795E /* PhotoLargeWidgetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoLargeWidgetView.swift; sourceTree = "<group>"; };
F8F6E45029BCE9190004795E /* UIImage+Resize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+Resize.swift"; sourceTree = "<group>"; };
F8FAA0AC2AB0BCB400FD78BD /* View+ContainerBackground.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+ContainerBackground.swift"; sourceTree = "<group>"; };
F8FB8AB929EB2ED400342C04 /* NavigationMenuButtons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationMenuButtons.swift; sourceTree = "<group>"; };
F8FFBD4929E99BEE0047EE80 /* Vernissage-009.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Vernissage-009.xcdatamodel"; sourceTree = "<group>"; };
/* End PBXFileReference section */
@ -447,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 */,
);
@ -533,7 +543,8 @@
F8DF38E329DD68820047F1AA /* ViewOffsetKey.swift */,
F871F21C29EF0D7000A351EF /* NavigationMenuItemDetails.swift */,
F8624D3C29F2D3AC00204986 /* SelectedMenuItemDetails.swift */,
F8C287A22A06B4C90072213F /* ImageScale.swift */,
F8E36E452AB8745300769C55 /* Sizable.swift */,
F8B758DD2AB9DD85000C8068 /* ColumnData.swift */,
);
path = Models;
sourceTree = "<group>";
@ -590,6 +601,7 @@
F8AFF7C329B25EF40087D083 /* ImagesGrid.swift */,
F89D6C49297196FF001DA3D4 /* ImageViewer.swift */,
F870EE5129F1645C00A2D43B /* MainNavigationOptions.swift */,
F8675DCF2A1FA40500A89959 /* WaterfallGrid.swift */,
);
path = Widgets;
sourceTree = "<group>";
@ -651,6 +663,7 @@
isa = PBXGroup;
children = (
F858906A29E1CC7A00D4BDED /* UIApplication+Window.swift */,
F8E36E472AB874A500769C55 /* StatusModel+Sizeable.swift */,
);
path = Extensions;
sourceTree = "<group>";
@ -665,7 +678,7 @@
F84625EE29FE2ABA002D3AF4 /* PhotoWidget */,
F8F6E44F29BCE9030004795E /* Extensions */,
F864F76329BB91B400B13921 /* VernissageWidgetBundle.swift */,
F864F76729BB91B600B13921 /* Assets.xcassets */,
F85D0C642ABA08F9002B3577 /* Assets.xcassets */,
);
path = VernissageWidget;
sourceTree = "<group>";
@ -909,6 +922,7 @@
children = (
F815F60B29E49CF20044566B /* Avatar.swift */,
F8F6E45029BCE9190004795E /* UIImage+Resize.swift */,
F8FAA0AC2AB0BCB400FD78BD /* View+ContainerBackground.swift */,
);
path = Extensions;
sourceTree = "<group>";
@ -969,7 +983,6 @@
F88C2465295C37B80006098B /* Frameworks */,
F88C2466295C37B80006098B /* Resources */,
F864F76D29BB91B600B13921 /* Embed Foundation Extensions */,
F8CB3DF129D8267C00CDAE5A /* ShellScript */,
);
buildRules = (
);
@ -988,6 +1001,7 @@
F88BC52629E0431D00CE6141 /* ServicesKit */,
F88BC52929E046D700CE6141 /* WidgetsKit */,
F88BC52C29E04BB600CE6141 /* EnvironmentKit */,
F8A1920F2ABB322E00C2599A /* Semaphore */,
);
productName = Vernissage;
productReference = F88C2468295C37B80006098B /* Vernissage.app */;
@ -1001,7 +1015,7 @@
attributes = {
BuildIndependentTargetsInParallel = 1;
LastSwiftUpdateCheck = 1430;
LastUpgradeCheck = 1420;
LastUpgradeCheck = 1500;
TargetAttributes = {
F864F75C29BB91B400B13921 = {
CreatedOnToolsVersion = 14.2;
@ -1031,6 +1045,7 @@
F88E4D4B297EA4290057491A /* XCRemoteSwiftPackageReference "EmojiText" */,
F89B5CBE29D019B600549F2F /* XCRemoteSwiftPackageReference "HTMLString" */,
F84625F929FE393B002D3AF4 /* XCRemoteSwiftPackageReference "QRCode" */,
F8A1920E2ABB322E00C2599A /* XCRemoteSwiftPackageReference "Semaphore" */,
);
productRefGroup = F88C2469295C37B80006098B /* Products */;
projectDirPath = "";
@ -1048,7 +1063,7 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
F864F76829BB91B600B13921 /* Assets.xcassets in Resources */,
F85D0C652ABA08F9002B3577 /* Assets.xcassets in Resources */,
F835082429BEF9C400DE3247 /* Localizable.strings in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -1076,27 +1091,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;
@ -1121,6 +1115,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 */,
@ -1181,6 +1176,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 */,
@ -1197,6 +1193,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 */,
@ -1216,6 +1213,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 */,
@ -1258,7 +1256,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 */,
@ -1278,6 +1275,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 */,
);
@ -1316,112 +1314,154 @@
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;
<<<<<<< HEAD
CURRENT_PROJECT_VERSION = 207;
=======
CURRENT_PROJECT_VERSION = 144;
>>>>>>> develop
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",
);
<<<<<<< HEAD
MARKETING_VERSION = 1.10.0;
=======
MARKETING_VERSION = 1.9.1;
>>>>>>> develop
PRODUCT_BUNDLE_IDENTIFIER = dev.mczachurski.vernissage.widget;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
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;
<<<<<<< HEAD
CURRENT_PROJECT_VERSION = 207;
=======
CURRENT_PROJECT_VERSION = 144;
>>>>>>> develop
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",
);
<<<<<<< HEAD
MARKETING_VERSION = 1.10.0;
=======
MARKETING_VERSION = 1.9.1;
>>>>>>> develop
PRODUCT_BUNDLE_IDENTIFIER = dev.mczachurski.vernissage.widget;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
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;
<<<<<<< HEAD
CURRENT_PROJECT_VERSION = 207;
=======
CURRENT_PROJECT_VERSION = 144;
>>>>>>> develop
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",
);
<<<<<<< HEAD
MARKETING_VERSION = 1.10.0;
=======
MARKETING_VERSION = 1.9.1;
>>>>>>> develop
PRODUCT_BUNDLE_IDENTIFIER = dev.mczachurski.vernissage.share;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
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;
<<<<<<< HEAD
CURRENT_PROJECT_VERSION = 207;
=======
CURRENT_PROJECT_VERSION = 144;
>>>>>>> develop
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",
);
<<<<<<< HEAD
MARKETING_VERSION = 1.10.0;
=======
MARKETING_VERSION = 1.9.1;
>>>>>>> develop
PRODUCT_BUNDLE_IDENTIFIER = dev.mczachurski.vernissage.share;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
@ -1429,6 +1469,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;
@ -1462,6 +1503,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;
@ -1476,7 +1518,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;
@ -1490,6 +1532,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;
@ -1523,6 +1566,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;
@ -1531,7 +1575,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;
@ -1547,12 +1591,17 @@
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;
<<<<<<< HEAD
CURRENT_PROJECT_VERSION = 207;
=======
CURRENT_PROJECT_VERSION = 144;
>>>>>>> develop
DEVELOPMENT_ASSET_PATHS = "\"Vernissage/Preview Content\"";
DEVELOPMENT_TEAM = B2U9FEKYP8;
ENABLE_PREVIEWS = YES;
@ -1566,12 +1615,16 @@
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",
);
<<<<<<< HEAD
MARKETING_VERSION = 1.10.0;
=======
MARKETING_VERSION = 1.9.1;
>>>>>>> develop
PRODUCT_BUNDLE_IDENTIFIER = dev.mczachurski.vernissage;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -1580,7 +1633,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;
};
@ -1590,11 +1643,16 @@
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;
<<<<<<< HEAD
CURRENT_PROJECT_VERSION = 207;
=======
CURRENT_PROJECT_VERSION = 144;
>>>>>>> develop
DEVELOPMENT_ASSET_PATHS = "\"Vernissage/Preview Content\"";
DEVELOPMENT_TEAM = B2U9FEKYP8;
ENABLE_PREVIEWS = YES;
@ -1608,12 +1666,16 @@
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",
);
<<<<<<< HEAD
MARKETING_VERSION = 1.10.0;
=======
MARKETING_VERSION = 1.9.1;
>>>>>>> develop
PRODUCT_BUNDLE_IDENTIFIER = dev.mczachurski.vernissage;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
@ -1621,7 +1683,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;
};
@ -1699,6 +1761,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 +1842,11 @@
package = F89B5CBE29D019B600549F2F /* XCRemoteSwiftPackageReference "HTMLString" */;
productName = HTMLString;
};
F8A1920F2ABB322E00C2599A /* Semaphore */ = {
isa = XCSwiftPackageProductDependency;
package = F8A1920E2ABB322E00C2599A /* XCRemoteSwiftPackageReference "Semaphore" */;
productName = Semaphore;
};
/* End XCSwiftPackageProductDependency section */
/* Begin XCVersionGroup section */

View File

@ -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)
}
}

View File

@ -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<T>: Identifiable where T: Identifiable, T: Hashable, T: Sizable {
public let id = UUID().uuidString
public var data: [T] = []
public var height: Double = 0.0
}

View File

@ -6,7 +6,7 @@
import Foundation
enum ImageScale {
case orginalFullWidth
case squareHalfWidth
public protocol Sizable {
var height: Double { get }
var width: Double { get }
}

View File

@ -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,7 +44,8 @@ public class HomeTimelineService {
return allStatusesFromApi.count
}
public func refreshTimeline(for account: AccountModel) async throws -> String? {
@MainActor
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()
@ -55,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? {
@ -191,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.

View File

@ -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)
}

View File

@ -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)

View File

@ -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
}
}

View File

@ -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)
}
}

View File

@ -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())
@ -237,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()

View File

@ -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)

View File

@ -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)
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 {

View File

@ -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

View File

@ -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()

View File

@ -21,7 +21,7 @@ struct SupportView: View {
Text(product.displayName)
Text(product.description)
.font(.footnote)
.foregroundColor(.lightGrayColor)
.foregroundColor(.customGrayColor)
}
Spacer()
Button(product.displayPrice) {

View File

@ -80,7 +80,7 @@ struct InstanceRowView: View {
Spacer()
}
.padding(.top, 4)
.foregroundColor(.lightGrayColor)
.foregroundColor(.customGrayColor)
.font(.caption)
}
}

View File

@ -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()
}
@ -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) {
@ -236,7 +236,8 @@ 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 {
@ -248,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

View File

@ -36,7 +36,7 @@ struct CommentBodyView: View {
if let createdAt = self.statusViewModel.createdAt.toDate(.isoDateTimeMilliSec) {
RelativeTime(date: createdAt)
.foregroundColor(.lightGrayColor)
.foregroundColor(.customGrayColor)
.font(.footnote)
}
}

View File

@ -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))
}
}

View File

@ -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)
}
@ -31,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")

View File

@ -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:
@ -50,7 +53,12 @@ struct StatusesView: View {
@State private var state: ViewState = .loading
@State private var lastStatusId: String?
private let defaultLimit = 20
// Gallery parameters.
@State private var imageColumns = 3
@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)
var body: some View {
@ -88,27 +96,44 @@ struct StatusesView: View {
@ViewBuilder
private func list() -> some View {
ScrollView {
LazyVStack(alignment: .center) {
ForEach(self.statusViewModels, id: \.id) { item in
ImageRowAsync(statusViewModel: item)
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
}
.refreshable {
do {
HapticService.shared.fireHaptic(of: .dataRefresh(intensity: 0.3))
@ -210,6 +235,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,

View File

@ -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)
}
}
.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))
}
}
}
}

View File

@ -57,6 +57,8 @@ struct UserProfileHeaderView: View {
.opacity(0.6)
}
}.foregroundColor(.mainTextColor)
Spacer()
}
HStack(alignment: .center) {
@ -66,7 +68,7 @@ struct UserProfileHeaderView: View {
.font(.title3)
.fontWeight(.bold)
Text("@\(account.acct)")
.foregroundColor(.lightGrayColor)
.foregroundColor(.customGrayColor)
.font(.subheadline)
}
@ -99,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])

View File

@ -18,65 +18,87 @@ 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] = []
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)]
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($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)
}
}
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 ? .customGrayColor : .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 : .customGrayColor)
.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()
}
}
}
}

View File

@ -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

View File

@ -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.

View File

@ -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)) {

View File

@ -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 == .orginalFullWidth) {
$0.frame(width: self.imageWidth, height: self.imageHeight)
.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 == .orginalFullWidth) {
$0.frame(width: self.imageWidth, height: self.imageHeight)
}
.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))
}

View File

@ -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
}

View File

@ -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,9 +163,9 @@ 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: self.clipToRectangle ? .fill : .fit)
.if(self.clipToRectangle) {
$0.frame(width: self.containerWidth, height: self.containerWidth).clipped()
}
.onTapGesture(count: 2) {
Task {
@ -194,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)
}
}

View File

@ -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.

View File

@ -47,12 +47,16 @@ 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)
self.imageWidth = calculatedSize.width
self.imageHeight = calculatedSize.height
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)
self.imageWidth = size.width
self.imageHeight = size.height
@ -65,33 +69,39 @@ 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(spacing: 0) {
Spacer(minLength: 0)
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)
}
.padding(0)
.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(minLength: 0)
}
.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
}
})
.padding(0)
.onAppear {
self.selected = self.attachments.first?.id ?? String.empty()
}
@ -102,26 +112,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 {
// Get attachment sizes from cache.
if let attachmentSize = ImageSizeService.shared.get(for: item.url) {
if attachmentSize.height > maxImageHeight {
maxImageHeight = attachmentSize.height
maxImageWidth = attachmentSize.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) {
if image.size.height > imageHeight {
imageHeight = image.size.height
imageWidth = image.size.width
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, width: imageWidth, height: imageHeight)
let size = ImageSizeService.shared.calculate(width: maxImageWidth, height: maxImageHeight)
self.imageWidth = size.width
self.imageHeight = size.height
}

View File

@ -0,0 +1,137 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the Apache License 2.0.
//
import SwiftUI
import WidgetsKit
import Semaphore
struct WaterfallGrid<Data, ID, Content>: View where Data: RandomAccessCollection, Data: Equatable, Content: View,
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 content: (Data.Element) -> Content
@State private var columnsData: [ColumnData<Data.Element>] = []
@State private var processedItems: [Data.Element.ID] = []
private let onLoadMore: () async -> Void
private let semaphore = AsyncSemaphore(value: 1)
var body: some View {
HStack(alignment: .top, spacing: 20) {
ForEach(self.columnsData, id: \.id) { columnData in
LazyVStack(spacing: 8) {
ForEach(columnData.data, id: \.id) { item in
self.content(item)
}
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 {
Task { @MainActor in
await self.loadMoreData()
}
}
}
}
}
}
.onFirstAppear {
self.recalculateArrays()
}
.onChange(of: self.data) { _ in
self.appendToArrays()
}
.onChange(of: self.columns) { _ in
self.recalculateArrays()
}
}
private func loadMoreData() async {
await semaphore.wait()
defer { semaphore.signal() }
await self.onLoadMore()
}
private func recalculateArrays() {
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() {
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)
}
}
}
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<Data>, id: KeyPath<Data.Element, ID>, columns: Binding<Int>,
hideLoadMore: Binding<Bool>, content: @escaping (Data.Element) -> Content, onLoadMore: @escaping () async -> Void) {
self.content = content
self.onLoadMore = onLoadMore
self._data = data
self._columns = columns
self._hideLoadMore = hideLoadMore
}
}
extension WaterfallGrid where ID == Data.Element.ID, Data.Element: Identifiable {
init(_ data: Binding<Data>, columns: Binding<Int>,
hideLoadMore: Binding<Bool>, content: @escaping (Data.Element) -> Content, onLoadMore: @escaping () async -> Void) {
self.content = content
self.onLoadMore = onLoadMore
self._data = data
self._columns = columns
self._hideLoadMore = hideLoadMore
}
}

View File

@ -1,6 +1,33 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.969",
"green" : "0.514",
"red" : "0.204"
}
},
"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"
}
],

View File

@ -1,6 +1,7 @@
{
"images" : [
{
"filename" : "icon.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

View File

@ -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"
}
],

View File

@ -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<V>(@ViewBuilder content: @escaping () -> V) -> some View where V: View {
if #available(iOSApplicationExtension 17.0, *) {
return containerBackground(for: .widget) {
content()
}
} else {
return background(content())
}
}
}

View File

@ -17,5 +17,6 @@ struct PhotoWidget: Widget {
.configurationDisplayName("Vernissage")
.description("widget.title.photoDescription")
.supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
.contentMarginsDisabled()
}
}

View File

@ -38,7 +38,7 @@ struct PhotoLargeWidgetView: View {
.padding(.leading, 8)
.padding(.bottom, 8)
}
.background {
.widgetBackground {
uiImage
.resizable()
.aspectRatio(contentMode: .fill)

View File

@ -38,7 +38,7 @@ struct PhotoMediumWidgetView: View {
.padding(.leading, 8)
.padding(.bottom, 8)
}
.background {
.widgetBackground {
uiImage
.resizable()
.aspectRatio(contentMode: .fill)

View File

@ -33,7 +33,7 @@ struct PhotoSmallWidgetView: View {
.padding(.leading, 8)
.padding(.bottom, 8)
}
.background {
.widgetBackground {
uiImage
.resizable()
.aspectRatio(contentMode: .fill)

View File

@ -17,5 +17,6 @@ struct QRCodeWidget: Widget {
.configurationDisplayName("Vernissage")
.description("widget.title.qrCodeDescription")
.supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
.contentMarginsDisabled()
}
}

View File

@ -82,6 +82,8 @@ struct QRCodeLargeWidgetView: View {
.offset(y: -8)
}
}
.widgetBackground {
}
.padding([.leading, .trailing, .top], 24)
}
}

View File

@ -92,6 +92,8 @@ struct QRCodeMediumWidgetView: View {
.padding(.leading, 3)
.offset(y: -4)
}
.widgetBackground {
}
.padding([.leading, .trailing, .top], 12)
}
}

View File

@ -61,6 +61,8 @@ struct QRCodeSmallWidgetView: View {
.frame(height: 24)
}
}
.widgetBackground {
}
.padding(8)
}
}

View File

@ -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
}
}

View File

@ -14,3 +14,11 @@ public extension View {
}
}
}
public extension ViewModifier {
func asyncAfter(_ time: Double, operation: @escaping () -> Void) {
DispatchQueue.main.asyncAfter(deadline: .now() + time) {
operation()
}
}
}

View File

@ -0,0 +1,84 @@
//
// 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
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))
}
}

View File

@ -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)

View File

@ -79,7 +79,7 @@ public struct PlaceSelectorView: View {
.foregroundColor(.mainTextColor)
Text(place.country ?? String.empty())
.font(.subheadline)
.foregroundColor(.lightGrayColor)
.foregroundColor(.customGrayColor)
}
Spacer()

View File

@ -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)
}
}

View File

@ -34,7 +34,7 @@ public struct UsernameRow: View {
Text(accountDisplayName ?? accountUsername)
.foregroundColor(.mainTextColor)
Text("@\(accountUsername)")
.foregroundColor(.lightGrayColor)
.foregroundColor(.customGrayColor)
.font(.footnote)
}
.padding(.leading, 8)