From d2d48444690de3dddd7b81137facc14805b6b91b Mon Sep 17 00:00:00 2001 From: Marcin Czachursk Date: Thu, 5 Jan 2023 11:55:20 +0100 Subject: [PATCH] Add user profile for other accounts --- Vernissage.xcodeproj/project.pbxproj | 61 +++++++ .../Contents.json | 14 +- Vernissage/CoreData/AccountDataHandler.swift | 7 +- .../CoreData/ApplicationSettingsHandler.swift | 3 + .../CoreData/AttachmentData+Attachment.swift | 21 +++ .../CoreData/AttachmentDataHandler.swift | 3 + Vernissage/CoreData/CoreDataHandler.swift | 11 +- Vernissage/CoreData/StatusData+Status.swift | 37 ++++ Vernissage/CoreData/StatusDataHandler.swift | 19 ++ Vernissage/Extensions/Color+Assets.swift | 15 ++ .../Extensions/Color+SystemColors.swift | 61 +++++++ .../MastodonClientAuthenticated+Account.swift | 23 +++ Vernissage/Extensions/Status+StatusData.swift | 37 ++++ Vernissage/Formatters/HTMLFotmattedText.swift | 2 +- Vernissage/Services/AccountService.swift | 19 ++ .../Services/AuthorizationService.swift | 13 +- Vernissage/Services/RemoteFileService.swift | 1 + Vernissage/Services/StatusService.swift | 17 ++ Vernissage/Services/TimelineService.swift | 102 +++-------- Vernissage/VernissageApp.swift | 4 + Vernissage/Views/DetailsView.swift | 128 ++++++++----- Vernissage/Views/HomeFeedView.swift | 5 +- Vernissage/Views/MainView.swift | 10 +- Vernissage/Views/SignInView.swift | 1 + Vernissage/Views/UserProfileView.swift | 168 ++++++++++-------- Vernissage/Widgets/CommentsSection.swift | 14 +- Vernissage/Widgets/ImageRow.swift | 2 - Vernissage/Widgets/ImageRowAsync.swift | 47 +++++ Vernissage/Widgets/InteractionRow.swift | 10 +- Vernissage/Widgets/UsernameRow.swift | 6 +- 30 files changed, 618 insertions(+), 243 deletions(-) rename Vernissage/Assets.xcassets/{displayNameColor.colorset => DangerColor.colorset}/Contents.json (71%) create mode 100644 Vernissage/CoreData/AttachmentData+Attachment.swift create mode 100644 Vernissage/CoreData/StatusData+Status.swift create mode 100644 Vernissage/Extensions/Color+Assets.swift create mode 100644 Vernissage/Extensions/Color+SystemColors.swift create mode 100644 Vernissage/Extensions/Status+StatusData.swift create mode 100644 Vernissage/Services/StatusService.swift create mode 100644 Vernissage/Widgets/ImageRowAsync.swift diff --git a/Vernissage.xcodeproj/project.pbxproj b/Vernissage.xcodeproj/project.pbxproj index 816027e..967cfda 100644 --- a/Vernissage.xcodeproj/project.pbxproj +++ b/Vernissage.xcodeproj/project.pbxproj @@ -13,6 +13,16 @@ F80048062961850500E6868A /* StatusData+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = F80048022961850500E6868A /* StatusData+CoreDataProperties.swift */; }; F80048082961E6DE00E6868A /* StatusDataHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F80048072961E6DE00E6868A /* StatusDataHandler.swift */; }; F800480A2961EA1900E6868A /* AttachmentDataHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F80048092961EA1900E6868A /* AttachmentDataHandler.swift */; }; + F8210DCF2966B600001D9973 /* ImageRowAsync.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8210DCE2966B600001D9973 /* ImageRowAsync.swift */; }; + F8210DD52966BB7E001D9973 /* Nuke in Frameworks */ = {isa = PBXBuildFile; productRef = F8210DD42966BB7E001D9973 /* Nuke */; }; + F8210DD72966BB7E001D9973 /* NukeExtensions in Frameworks */ = {isa = PBXBuildFile; productRef = F8210DD62966BB7E001D9973 /* NukeExtensions */; }; + F8210DD92966BB7E001D9973 /* NukeUI in Frameworks */ = {isa = PBXBuildFile; productRef = F8210DD82966BB7E001D9973 /* NukeUI */; }; + F8210DDD2966CF17001D9973 /* StatusData+Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8210DDC2966CF17001D9973 /* StatusData+Status.swift */; }; + F8210DDF2966CFC7001D9973 /* AttachmentData+Attachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8210DDE2966CFC7001D9973 /* AttachmentData+Attachment.swift */; }; + F8210DE12966D0C4001D9973 /* StatusService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8210DE02966D0C4001D9973 /* StatusService.swift */; }; + F8210DE32966D256001D9973 /* Status+StatusData.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8210DE22966D256001D9973 /* Status+StatusData.swift */; }; + F8210DE52966E160001D9973 /* Color+SystemColors.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8210DE42966E160001D9973 /* Color+SystemColors.swift */; }; + F8210DE72966E1D1001D9973 /* Color+Assets.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8210DE62966E1D1001D9973 /* Color+Assets.swift */; }; F8341F90295C636C009C8EE6 /* UIImage+Exif.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8341F8F295C636C009C8EE6 /* UIImage+Exif.swift */; }; F8341F92295C63BB009C8EE6 /* ImageStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8341F91295C63BB009C8EE6 /* ImageStatus.swift */; }; F83901A6295D8EC000456AE2 /* LabelIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = F83901A5295D8EC000456AE2 /* LabelIcon.swift */; }; @@ -64,6 +74,13 @@ F80048022961850500E6868A /* StatusData+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StatusData+CoreDataProperties.swift"; sourceTree = ""; }; F80048072961E6DE00E6868A /* StatusDataHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusDataHandler.swift; sourceTree = ""; }; F80048092961EA1900E6868A /* AttachmentDataHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentDataHandler.swift; sourceTree = ""; }; + F8210DCE2966B600001D9973 /* ImageRowAsync.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageRowAsync.swift; sourceTree = ""; }; + F8210DDC2966CF17001D9973 /* StatusData+Status.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StatusData+Status.swift"; sourceTree = ""; }; + F8210DDE2966CFC7001D9973 /* AttachmentData+Attachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AttachmentData+Attachment.swift"; sourceTree = ""; }; + F8210DE02966D0C4001D9973 /* StatusService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusService.swift; sourceTree = ""; }; + F8210DE22966D256001D9973 /* Status+StatusData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Status+StatusData.swift"; sourceTree = ""; }; + F8210DE42966E160001D9973 /* Color+SystemColors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+SystemColors.swift"; sourceTree = ""; }; + F8210DE62966E1D1001D9973 /* Color+Assets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+Assets.swift"; sourceTree = ""; }; F8341F8F295C636C009C8EE6 /* UIImage+Exif.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+Exif.swift"; sourceTree = ""; }; F8341F91295C63BB009C8EE6 /* ImageStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageStatus.swift; sourceTree = ""; }; F83901A5295D8EC000456AE2 /* LabelIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabelIcon.swift; sourceTree = ""; }; @@ -115,6 +132,9 @@ buildActionMask = 2147483647; files = ( F866F6B729608467002E8F88 /* MastodonSwift in Frameworks */, + F8210DD52966BB7E001D9973 /* Nuke in Frameworks */, + F8210DD72966BB7E001D9973 /* NukeExtensions in Frameworks */, + F8210DD92966BB7E001D9973 /* NukeUI in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -143,6 +163,9 @@ F85D4980296417F700751DF7 /* MastodonClientAuthenticated+Context.swift */, F8A93D812965FF5D001D8331 /* MastodonClientAuthenticated+Account.swift */, F85D49862964334100751DF7 /* String+Date.swift */, + F8210DE22966D256001D9973 /* Status+StatusData.swift */, + F8210DE42966E160001D9973 /* Color+SystemColors.swift */, + F8210DE62966E1D1001D9973 /* Color+Assets.swift */, ); path = Extensions; sourceTree = ""; @@ -163,9 +186,11 @@ F80047FF2961850500E6868A /* AttachmentData+CoreDataClass.swift */, F80048002961850500E6868A /* AttachmentData+CoreDataProperties.swift */, F85D498229642FAC00751DF7 /* AttachmentData+Comperable.swift */, + F8210DDE2966CFC7001D9973 /* AttachmentData+Attachment.swift */, F80048012961850500E6868A /* StatusData+CoreDataClass.swift */, F80048022961850500E6868A /* StatusData+CoreDataProperties.swift */, F85D49842964301800751DF7 /* StatusData+Attachments.swift */, + F8210DDC2966CF17001D9973 /* StatusData+Status.swift */, F866F69E296040A8002E8F88 /* ApplicationSettings+CoreDataClass.swift */, F866F69F296040A8002E8F88 /* ApplicationSettings+CoreDataProperties.swift */, F88FAD28295F43B8009B20C9 /* AccountData+CoreDataClass.swift */, @@ -193,6 +218,7 @@ F83901A5295D8EC000456AE2 /* LabelIcon.swift */, F85D4972296406E700751DF7 /* BottomRight.swift */, F85D497629640A5200751DF7 /* ImageRow.swift */, + F8210DCE2966B600001D9973 /* ImageRowAsync.swift */, F85D497829640B9D00751DF7 /* ImagesCarousel.swift */, F85D497A29640C8200751DF7 /* UsernameRow.swift */, F85D497C29640D5900751DF7 /* InteractionRow.swift */, @@ -252,6 +278,7 @@ F85D4970296402DC00751DF7 /* AuthorizationService.swift */, F85D4974296407F100751DF7 /* TimelineService.swift */, F8A93D7F2965FED4001D8331 /* AccountService.swift */, + F8210DE02966D0C4001D9973 /* StatusService.swift */, ); path = Services; sourceTree = ""; @@ -274,6 +301,9 @@ name = Vernissage; packageProductDependencies = ( F866F6B629608467002E8F88 /* MastodonSwift */, + F8210DD42966BB7E001D9973 /* Nuke */, + F8210DD62966BB7E001D9973 /* NukeExtensions */, + F8210DD82966BB7E001D9973 /* NukeUI */, ); productName = Vernissage; productReference = F88C2468295C37B80006098B /* Vernissage.app */; @@ -305,6 +335,7 @@ mainGroup = F88C245F295C37B80006098B; packageReferences = ( F866F6B529608467002E8F88 /* XCRemoteSwiftPackageReference "Mastodon" */, + F8210DD32966BB7E001D9973 /* XCRemoteSwiftPackageReference "Nuke" */, ); productRefGroup = F88C2469295C37B80006098B /* Products */; projectDirPath = ""; @@ -333,8 +364,10 @@ buildActionMask = 2147483647; files = ( F85D497729640A5200751DF7 /* ImageRow.swift in Sources */, + F8210DDF2966CFC7001D9973 /* AttachmentData+Attachment.swift in Sources */, F80048082961E6DE00E6868A /* StatusDataHandler.swift in Sources */, F866F6A0296040A8002E8F88 /* ApplicationSettings+CoreDataClass.swift in Sources */, + F8210DE52966E160001D9973 /* Color+SystemColors.swift in Sources */, F88FAD23295F3FC4009B20C9 /* LocalFeedView.swift in Sources */, F88FAD2B295F43B8009B20C9 /* AccountData+CoreDataProperties.swift in Sources */, F85D4975296407F100751DF7 /* TimelineService.swift in Sources */, @@ -342,9 +375,12 @@ F88FAD21295F3944009B20C9 /* HomeFeedView.swift in Sources */, F88C2475295C37BB0006098B /* CoreDataHandler.swift in Sources */, F88FAD2A295F43B8009B20C9 /* AccountData+CoreDataClass.swift in Sources */, + F8210DE12966D0C4001D9973 /* StatusService.swift in Sources */, F8A93D822965FF5D001D8331 /* MastodonClientAuthenticated+Account.swift in Sources */, F85D49872964334100751DF7 /* String+Date.swift in Sources */, F8341F92295C63BB009C8EE6 /* ImageStatus.swift in Sources */, + F8210DDD2966CF17001D9973 /* StatusData+Status.swift in Sources */, + F8210DCF2966B600001D9973 /* ImageRowAsync.swift in Sources */, F85D498329642FAC00751DF7 /* AttachmentData+Comperable.swift in Sources */, F85D497B29640C8200751DF7 /* UsernameRow.swift in Sources */, F85D497929640B9D00751DF7 /* ImagesCarousel.swift in Sources */, @@ -363,7 +399,9 @@ F85D497F296416C800751DF7 /* CommentsSection.swift in Sources */, F88C2486295C48030006098B /* HTMLFotmattedText.swift in Sources */, F866F6A529604194002E8F88 /* ApplicationSettingsHandler.swift in Sources */, + F8210DE32966D256001D9973 /* Status+StatusData.swift in Sources */, F85D49852964301800751DF7 /* StatusData+Attachments.swift in Sources */, + F8210DE72966E1D1001D9973 /* Color+Assets.swift in Sources */, F85D497D29640D5900751DF7 /* InteractionRow.swift in Sources */, F866F6A729604629002E8F88 /* SignInView.swift in Sources */, F88C246C295C37B80006098B /* VernissageApp.swift in Sources */, @@ -583,6 +621,14 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ + F8210DD32966BB7E001D9973 /* XCRemoteSwiftPackageReference "Nuke" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/kean/Nuke"; + requirement = { + branch = master; + kind = branch; + }; + }; F866F6B529608467002E8F88 /* XCRemoteSwiftPackageReference "Mastodon" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/mczachurski/Mastodon.swift"; @@ -594,6 +640,21 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ + F8210DD42966BB7E001D9973 /* Nuke */ = { + isa = XCSwiftPackageProductDependency; + package = F8210DD32966BB7E001D9973 /* XCRemoteSwiftPackageReference "Nuke" */; + productName = Nuke; + }; + F8210DD62966BB7E001D9973 /* NukeExtensions */ = { + isa = XCSwiftPackageProductDependency; + package = F8210DD32966BB7E001D9973 /* XCRemoteSwiftPackageReference "Nuke" */; + productName = NukeExtensions; + }; + F8210DD82966BB7E001D9973 /* NukeUI */ = { + isa = XCSwiftPackageProductDependency; + package = F8210DD32966BB7E001D9973 /* XCRemoteSwiftPackageReference "Nuke" */; + productName = NukeUI; + }; F866F6B629608467002E8F88 /* MastodonSwift */ = { isa = XCSwiftPackageProductDependency; package = F866F6B529608467002E8F88 /* XCRemoteSwiftPackageReference "Mastodon" */; diff --git a/Vernissage/Assets.xcassets/displayNameColor.colorset/Contents.json b/Vernissage/Assets.xcassets/DangerColor.colorset/Contents.json similarity index 71% rename from Vernissage/Assets.xcassets/displayNameColor.colorset/Contents.json rename to Vernissage/Assets.xcassets/DangerColor.colorset/Contents.json index d890719..db2b413 100644 --- a/Vernissage/Assets.xcassets/displayNameColor.colorset/Contents.json +++ b/Vernissage/Assets.xcassets/DangerColor.colorset/Contents.json @@ -2,12 +2,12 @@ "colors" : [ { "color" : { - "color-space" : "srgb", + "color-space" : "display-p3", "components" : { "alpha" : "1.000", - "blue" : "0.000", - "green" : "0.000", - "red" : "0.000" + "blue" : "68", + "green" : "87", + "red" : "255" } }, "idiom" : "universal" @@ -23,9 +23,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "1.000", - "green" : "1.000", - "red" : "1.000" + "blue" : "68", + "green" : "87", + "red" : "255" } }, "idiom" : "universal" diff --git a/Vernissage/CoreData/AccountDataHandler.swift b/Vernissage/CoreData/AccountDataHandler.swift index 12e7fb2..70954d3 100644 --- a/Vernissage/CoreData/AccountDataHandler.swift +++ b/Vernissage/CoreData/AccountDataHandler.swift @@ -8,6 +8,9 @@ import Foundation class AccountDataHandler { + public static let shared = AccountDataHandler() + private init() { } + func getAccountsData() -> [AccountData] { let context = CoreDataHandler.shared.container.viewContext let fetchRequest = AccountData.fetchRequest() @@ -21,9 +24,7 @@ class AccountDataHandler { func getCurrentAccountData() -> AccountData? { let accounts = self.getAccountsData() - - let applicationSettingsHandler = ApplicationSettingsHandler() - let defaultSettings = applicationSettingsHandler.getDefaultSettings() + let defaultSettings = ApplicationSettingsHandler.shared.getDefaultSettings() let currentAccount = accounts.first { accountData in accountData.id == defaultSettings.currentAccount diff --git a/Vernissage/CoreData/ApplicationSettingsHandler.swift b/Vernissage/CoreData/ApplicationSettingsHandler.swift index 07a0932..8222bc1 100644 --- a/Vernissage/CoreData/ApplicationSettingsHandler.swift +++ b/Vernissage/CoreData/ApplicationSettingsHandler.swift @@ -8,6 +8,9 @@ import Foundation class ApplicationSettingsHandler { + public static let shared = ApplicationSettingsHandler() + private init() { } + func getDefaultSettings() -> ApplicationSettings { var settingsList: [ApplicationSettings] = [] diff --git a/Vernissage/CoreData/AttachmentData+Attachment.swift b/Vernissage/CoreData/AttachmentData+Attachment.swift new file mode 100644 index 0000000..5449ddb --- /dev/null +++ b/Vernissage/CoreData/AttachmentData+Attachment.swift @@ -0,0 +1,21 @@ +// +// https://mczachurski.dev +// Copyright © 2023 Marcin Czachurski and the repository contributors. +// Licensed under the MIT License. +// + + +import Foundation +import MastodonSwift + +extension AttachmentData { + func copyFrom(_ attachment: Attachment) { + self.id = attachment.id + self.url = attachment.url + self.blurhash = attachment.blurhash + self.previewUrl = attachment.previewUrl + self.remoteUrl = attachment.remoteUrl + self.text = attachment.description + self.type = attachment.type.rawValue + } +} diff --git a/Vernissage/CoreData/AttachmentDataHandler.swift b/Vernissage/CoreData/AttachmentDataHandler.swift index 3b75033..8e34317 100644 --- a/Vernissage/CoreData/AttachmentDataHandler.swift +++ b/Vernissage/CoreData/AttachmentDataHandler.swift @@ -9,6 +9,9 @@ import Foundation import CoreData class AttachmentDataHandler { + public static let shared = AttachmentDataHandler() + private init() { } + func getAttachmentsData() -> [AttachmentData] { let context = CoreDataHandler.shared.container.viewContext let fetchRequest = AttachmentData.fetchRequest() diff --git a/Vernissage/CoreData/CoreDataHandler.swift b/Vernissage/CoreData/CoreDataHandler.swift index 12577b0..95466e4 100644 --- a/Vernissage/CoreData/CoreDataHandler.swift +++ b/Vernissage/CoreData/CoreDataHandler.swift @@ -7,11 +7,12 @@ import CoreData -public class CoreDataHandler { +public class CoreDataHandler { public static let shared = CoreDataHandler() + public let container: NSPersistentContainer - init(inMemory: Bool = false) { + private init(inMemory: Bool = false) { container = NSPersistentContainer(name: "Vernissage") if inMemory { container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null") @@ -56,6 +57,12 @@ public class CoreDataHandler { } } +extension CoreDataHandler { + public static var memory: CoreDataHandler = { + CoreDataHandler(inMemory: true) + }() +} + extension CoreDataHandler { public static var preview: CoreDataHandler = { let result = CoreDataHandler(inMemory: true) diff --git a/Vernissage/CoreData/StatusData+Status.swift b/Vernissage/CoreData/StatusData+Status.swift new file mode 100644 index 0000000..71bd768 --- /dev/null +++ b/Vernissage/CoreData/StatusData+Status.swift @@ -0,0 +1,37 @@ +// +// https://mczachurski.dev +// Copyright © 2023 Marcin Czachurski and the repository contributors. +// Licensed under the MIT License. +// + +import Foundation +import MastodonSwift + +extension StatusData { + func copyFrom(_ status: Status) { + self.id = status.id + self.createdAt = status.createdAt + self.accountAvatar = status.account?.avatar + self.accountDisplayName = status.account?.displayName + self.accountId = status.account!.id + self.accountUsername = status.account!.username + self.applicationName = status.application?.name + self.applicationWebsite = status.application?.website + self.bookmarked = status.bookmarked + self.content = status.content + self.favourited = status.favourited + self.favouritesCount = Int32(status.favouritesCount) + self.inReplyToAccount = status.inReplyToAccount + self.inReplyToId = status.inReplyToId + self.muted = status.muted + self.pinned = status.pinned + self.reblogged = status.reblogged + self.reblogsCount = Int32(status.reblogsCount) + self.repliesCount = Int32(status.repliesCount) + self.sensitive = status.sensitive + self.spoilerText = status.spoilerText + self.uri = status.uri + self.url = status.url + self.visibility = status.visibility.rawValue + } +} diff --git a/Vernissage/CoreData/StatusDataHandler.swift b/Vernissage/CoreData/StatusDataHandler.swift index b7f5934..330fadd 100644 --- a/Vernissage/CoreData/StatusDataHandler.swift +++ b/Vernissage/CoreData/StatusDataHandler.swift @@ -7,8 +7,12 @@ import Foundation import CoreData +import MastodonSwift class StatusDataHandler { + public static let shared = StatusDataHandler() + private init() { } + func getStatusesData() -> [StatusData] { let context = CoreDataHandler.shared.container.viewContext let fetchRequest = StatusData.fetchRequest() @@ -20,6 +24,21 @@ class StatusDataHandler { } } + func getStatusData(statusId: String) -> StatusData? { + let context = CoreDataHandler.shared.container.viewContext + let fetchRequest = StatusData.fetchRequest() + + fetchRequest.fetchLimit = 1 + fetchRequest.predicate = NSPredicate(format: "id = %@", statusId) + + do { + return try context.fetch(fetchRequest).first + } catch { + print("Error during fetching accounts") + return nil + } + } + func getMaximumStatus(viewContext: NSManagedObjectContext? = nil) -> StatusData? { let context = viewContext ?? CoreDataHandler.shared.container.viewContext let fetchRequest = StatusData.fetchRequest() diff --git a/Vernissage/Extensions/Color+Assets.swift b/Vernissage/Extensions/Color+Assets.swift new file mode 100644 index 0000000..3da7550 --- /dev/null +++ b/Vernissage/Extensions/Color+Assets.swift @@ -0,0 +1,15 @@ +// +// https://mczachurski.dev +// Copyright © 2023 Marcin Czachurski and the repository contributors. +// Licensed under the MIT License. +// + +import SwiftUI + +extension Color { + + // MARK: - Text Colors + static let dangerColor = Color("DangerColor") + static let lightGrayColor = Color("LightGrayColor") + static let mainTextColor = Color("MainTextColor") +} diff --git a/Vernissage/Extensions/Color+SystemColors.swift b/Vernissage/Extensions/Color+SystemColors.swift new file mode 100644 index 0000000..602c5b9 --- /dev/null +++ b/Vernissage/Extensions/Color+SystemColors.swift @@ -0,0 +1,61 @@ +// +// https://mczachurski.dev +// Copyright © 2023 Marcin Czachurski and the repository contributors. +// Licensed under the MIT License. +// + +import SwiftUI + +extension Color { + + // MARK: - Text Colors + static let lightText = Color(UIColor.lightText) + static let darkText = Color(UIColor.darkText) + static let placeholderText = Color(UIColor.placeholderText) + + // MARK: - Label Colors + static let label = Color(UIColor.label) + static let secondaryLabel = Color(UIColor.secondaryLabel) + static let tertiaryLabel = Color(UIColor.tertiaryLabel) + static let quaternaryLabel = Color(UIColor.quaternaryLabel) + + // MARK: - Background Colors + static let systemBackground = Color(UIColor.systemBackground) + static let secondarySystemBackground = Color(UIColor.secondarySystemBackground) + static let tertiarySystemBackground = Color(UIColor.tertiarySystemBackground) + + // MARK: - Fill Colors + static let systemFill = Color(UIColor.systemFill) + static let secondarySystemFill = Color(UIColor.secondarySystemFill) + static let tertiarySystemFill = Color(UIColor.tertiarySystemFill) + static let quaternarySystemFill = Color(UIColor.quaternarySystemFill) + + // MARK: - Grouped Background Colors + static let systemGroupedBackground = Color(UIColor.systemGroupedBackground) + static let secondarySystemGroupedBackground = Color(UIColor.secondarySystemGroupedBackground) + static let tertiarySystemGroupedBackground = Color(UIColor.tertiarySystemGroupedBackground) + + // MARK: - Gray Colors + static let systemGray = Color(UIColor.systemGray) + static let systemGray2 = Color(UIColor.systemGray2) + static let systemGray3 = Color(UIColor.systemGray3) + static let systemGray4 = Color(UIColor.systemGray4) + static let systemGray5 = Color(UIColor.systemGray5) + static let systemGray6 = Color(UIColor.systemGray6) + + // MARK: - Other Colors + static let separator = Color(UIColor.separator) + static let opaqueSeparator = Color(UIColor.opaqueSeparator) + static let link = Color(UIColor.link) + + // MARK: System Colors + static let systemBlue = Color(UIColor.systemBlue) + static let systemPurple = Color(UIColor.systemPurple) + static let systemGreen = Color(UIColor.systemGreen) + static let systemYellow = Color(UIColor.systemYellow) + static let systemOrange = Color(UIColor.systemOrange) + static let systemPink = Color(UIColor.systemPink) + static let systemRed = Color(UIColor.systemRed) + static let systemTeal = Color(UIColor.systemTeal) + static let systemIndigo = Color(UIColor.systemIndigo) +} diff --git a/Vernissage/Extensions/MastodonClientAuthenticated+Account.swift b/Vernissage/Extensions/MastodonClientAuthenticated+Account.swift index 0f42715..f43adfb 100644 --- a/Vernissage/Extensions/MastodonClientAuthenticated+Account.swift +++ b/Vernissage/Extensions/MastodonClientAuthenticated+Account.swift @@ -18,4 +18,27 @@ extension MastodonClientAuthenticated { let (data, _) = try await urlSession.data(for: request) return try JSONDecoder().decode(Account.self, from: data) } + + func getRelationship(for accountId: String) async throws -> Relationship? { + let request = try Self.request( + for: baseURL, + target: Mastodon.Account.relationships([accountId]), + withBearerToken: token + ) + + let (data, _) = try await urlSession.data(for: request) + let relationships = try JSONDecoder().decode([Relationship].self, from: data) + return relationships.first + } + + func getStatuses(for accountId: String) async throws -> [Status] { + let request = try Self.request( + for: baseURL, + target: Mastodon.Account.statuses(accountId, true, true), + withBearerToken: token + ) + + let (data, _) = try await urlSession.data(for: request) + return try JSONDecoder().decode([Status].self, from: data) + } } diff --git a/Vernissage/Extensions/Status+StatusData.swift b/Vernissage/Extensions/Status+StatusData.swift new file mode 100644 index 0000000..e8a855d --- /dev/null +++ b/Vernissage/Extensions/Status+StatusData.swift @@ -0,0 +1,37 @@ +// +// https://mczachurski.dev +// Copyright © 2023 Marcin Czachurski and the repository contributors. +// Licensed under the MIT License. +// + +import Foundation +import MastodonSwift + +extension Status { + func createStatusData() async throws -> StatusData { + let statusData = StatusDataHandler.shared.createStatusDataEntity(viewContext: CoreDataHandler.memory.container.viewContext) + statusData.copyFrom(self) + + for attachment in self.mediaAttachments { + let imageData = try await RemoteFileService.shared.fetchData(url: attachment.url) + + guard let imageData = imageData else { + continue + } + + // Save attachment in database. + let attachmentData = AttachmentDataHandler.shared.createAttachmnentDataEntity(viewContext: CoreDataHandler.memory.container.viewContext) + + attachmentData.copyFrom(attachment) + attachmentData.statusId = statusData.id + attachmentData.data = imageData + + // TODO: read exif informatio + + attachmentData.statusRelation = statusData + statusData.addToAttachmentRelation(attachmentData) + } + + return statusData + } +} diff --git a/Vernissage/Formatters/HTMLFotmattedText.swift b/Vernissage/Formatters/HTMLFotmattedText.swift index 86917d3..481fb60 100644 --- a/Vernissage/Formatters/HTMLFotmattedText.swift +++ b/Vernissage/Formatters/HTMLFotmattedText.swift @@ -48,7 +48,7 @@ struct HTMLFormattedText: UIViewRepresentable { let largeAttributes = [ NSAttributedString.Key.font: UIFont.systemFont(ofSize: CGFloat(self.fontSize)), - NSAttributedString.Key.foregroundColor: UIColor(Color("MainTextColor")) + NSAttributedString.Key.foregroundColor: UIColor(Color.mainTextColor) ] let linkAttributes = [ diff --git a/Vernissage/Services/AccountService.swift b/Vernissage/Services/AccountService.swift index 867913f..5fb87a6 100644 --- a/Vernissage/Services/AccountService.swift +++ b/Vernissage/Services/AccountService.swift @@ -9,6 +9,7 @@ import MastodonSwift public class AccountService { public static let shared = AccountService() + private init() { } public func getAccount(withId accountId: String, and accountData: AccountData?) async throws -> Account? { guard let accessToken = accountData?.accessToken, let serverUrl = accountData?.serverUrl else { @@ -18,4 +19,22 @@ public class AccountService { let client = MastodonClient(baseURL: serverUrl).getAuthenticated(token: accessToken) return try await client.getAccount(for: accountId) } + + public func getRelationship(withId accountId: String, forUser accountData: AccountData?) async throws -> Relationship? { + guard let accessToken = accountData?.accessToken, let serverUrl = accountData?.serverUrl else { + return nil + } + + let client = MastodonClient(baseURL: serverUrl).getAuthenticated(token: accessToken) + return try await client.getRelationship(for: accountId) + } + + public func getStatuses(forAccountId accountId: String, andContext accountData: AccountData?) async throws -> [Status] { + guard let accessToken = accountData?.accessToken, let serverUrl = accountData?.serverUrl else { + return [] + } + + let client = MastodonClient(baseURL: serverUrl).getAuthenticated(token: accessToken) + return try await client.getStatuses(for: accountId) + } } diff --git a/Vernissage/Services/AuthorizationService.swift b/Vernissage/Services/AuthorizationService.swift index e58de7d..791b23c 100644 --- a/Vernissage/Services/AuthorizationService.swift +++ b/Vernissage/Services/AuthorizationService.swift @@ -9,10 +9,10 @@ import MastodonSwift public class AuthorizationService { public static let shared = AuthorizationService() + private init() { } public func verifyAccount(_ result: @escaping (AccountData?) -> Void) async { - let accountDataHandler = AccountDataHandler() - let currentAccount = accountDataHandler.getCurrentAccountData() + let currentAccount = AccountDataHandler.shared.getCurrentAccountData() // When we dont have even one account stored in database then we have to ask user to enter server and sign in. guard let accountData = currentAccount, let accessToken = accountData.accessToken else { @@ -65,8 +65,7 @@ public class AuthorizationService { let account = try await authenticatedClient.verifyCredentials() // Create account object in database. - let accountDataHandler = AccountDataHandler() - let accountData = accountDataHandler.createAccountDataEntity() + let accountData = AccountDataHandler.shared.createAccountDataEntity() accountData.id = account.id accountData.username = account.username @@ -100,8 +99,7 @@ public class AuthorizationService { } // Set newly created account as current. - let applicationSettingsHandler = ApplicationSettingsHandler() - let defaultSettings = applicationSettingsHandler.getDefaultSettings() + let defaultSettings = ApplicationSettingsHandler.shared.getDefaultSettings() defaultSettings.currentAccount = accountData.id // Save account data in database and in application state. @@ -158,8 +156,7 @@ public class AuthorizationService { } // We have to be sure that account id is saved as default account. - let applicationSettingsHandler = ApplicationSettingsHandler() - let defaultSettings = applicationSettingsHandler.getDefaultSettings() + let defaultSettings = ApplicationSettingsHandler.shared.getDefaultSettings() defaultSettings.currentAccount = accountData.id // Save account data in database and in application state. diff --git a/Vernissage/Services/RemoteFileService.swift b/Vernissage/Services/RemoteFileService.swift index b239260..00e3f8f 100644 --- a/Vernissage/Services/RemoteFileService.swift +++ b/Vernissage/Services/RemoteFileService.swift @@ -9,6 +9,7 @@ import Foundation public class RemoteFileService { public static let shared = RemoteFileService() + private init() { } public func fetchData(url: URL) async throws -> Data? { let urlRequest = URLRequest(url: url) diff --git a/Vernissage/Services/StatusService.swift b/Vernissage/Services/StatusService.swift new file mode 100644 index 0000000..02da639 --- /dev/null +++ b/Vernissage/Services/StatusService.swift @@ -0,0 +1,17 @@ +// +// https://mczachurski.dev +// Copyright © 2023 Marcin Czachurski and the repository contributors. +// Licensed under the MIT License. +// + +import Foundation +import MastodonSwift + +public class StatusService { + public static let shared = StatusService() + private init() { } + + func copy(from status: Status, to statusData: StatusData) { + + } +} diff --git a/Vernissage/Services/TimelineService.swift b/Vernissage/Services/TimelineService.swift index a0f3375..e4cc280 100644 --- a/Vernissage/Services/TimelineService.swift +++ b/Vernissage/Services/TimelineService.swift @@ -10,14 +10,14 @@ import MastodonSwift public class TimelineService { public static let shared = TimelineService() + private init() { } public func onBottomOfList(for accountData: AccountData) async throws { // Load data from API and operate on CoreData on background context. let backgroundContext = CoreDataHandler.shared.newBackgroundContext() // Get maximimum downloaded stauts id. - let statusDataHandler = StatusDataHandler() - let oldestStatus = statusDataHandler.getMinimumtatus(viewContext: backgroundContext) + let oldestStatus = StatusDataHandler.shared.getMinimumtatus(viewContext: backgroundContext) guard let oldestStatus = oldestStatus else { return @@ -31,40 +31,20 @@ public class TimelineService { let backgroundContext = CoreDataHandler.shared.newBackgroundContext() // Get maximimum downloaded stauts id. - let statusDataHandler = StatusDataHandler() - let newestStatus = statusDataHandler.getMaximumStatus(viewContext: backgroundContext) + let newestStatus = StatusDataHandler.shared.getMaximumStatus(viewContext: backgroundContext) try await self.loadData(for: accountData, on: backgroundContext, minId: newestStatus?.id) } - public func getStatus(withId statusId: String, and accountData: AccountData) async throws -> Status? { - guard let accessToken = accountData.accessToken else { + public func getStatus(withId statusId: String, and accountData: AccountData?) async throws -> Status? { + guard let accessToken = accountData?.accessToken, let serverUrl = accountData?.serverUrl else { return nil } - let client = MastodonClient(baseURL: accountData.serverUrl).getAuthenticated(token: accessToken) + let client = MastodonClient(baseURL: serverUrl).getAuthenticated(token: accessToken) return try await client.read(statusId: statusId) } - public func updateStatus(statusData: StatusData, and accountData: AccountData) async throws -> StatusData? { - guard let accessToken = accountData.accessToken else { - return nil - } - - // Load data from API and operate on CoreData on background context. - let backgroundContext = CoreDataHandler.shared.newBackgroundContext() - - // Get new information from API. - let client = MastodonClient(baseURL: accountData.serverUrl).getAuthenticated(token: accessToken) - let status = try await client.read(statusId: statusData.id) - - // Update status data in database. - try await self.updateStatusData(from: status, to: statusData, on: backgroundContext) - try backgroundContext.save() - - return statusData - } - public func getComments(for statusId: String, and accountData: AccountData) async throws -> Context { let client = MastodonClient(baseURL: accountData.serverUrl).getAuthenticated(token: accountData.accessToken ?? "") return try await client.getContext(for: statusId) @@ -78,46 +58,29 @@ public class TimelineService { // Retrieve statuses from API. let client = MastodonClient(baseURL: accountData.serverUrl).getAuthenticated(token: accessToken) let statuses = try await client.getHomeTimeline(maxId: maxId, minId: minId, limit: 40) - - // Create handler for managing statuses in database. - let statusDataHandler = StatusDataHandler() - + // Save status data in database. for status in statuses { - let statusData = statusDataHandler.createStatusDataEntity(viewContext: backgroundContext) - try await self.updateStatusData(from: status, to: statusData, on: backgroundContext) + let statusData = StatusDataHandler.shared.createStatusDataEntity(viewContext: backgroundContext) + try await self.copy(from: status, to: statusData, on: backgroundContext) } try backgroundContext.save() } - private func updateStatusData(from status: Status, to statusData: StatusData, on backgroundContext: NSManagedObjectContext) async throws { - statusData.id = status.id - statusData.createdAt = status.createdAt - statusData.accountAvatar = status.account?.avatar - statusData.accountDisplayName = status.account?.displayName - statusData.accountId = status.account!.id - statusData.accountUsername = status.account!.username - statusData.applicationName = status.application?.name - statusData.applicationWebsite = status.application?.website - statusData.bookmarked = status.bookmarked - statusData.content = status.content - statusData.favourited = status.favourited - statusData.favouritesCount = Int32(status.favouritesCount) - statusData.inReplyToAccount = status.inReplyToAccount - statusData.inReplyToId = status.inReplyToId - statusData.muted = status.muted - statusData.pinned = status.pinned - statusData.reblogged = status.reblogged - statusData.reblogsCount = Int32(status.reblogsCount) - statusData.repliesCount = Int32(status.repliesCount) - statusData.sensitive = status.sensitive - statusData.spoilerText = status.spoilerText - statusData.uri = status.uri - statusData.url = status.url - statusData.visibility = status.visibility.rawValue + public func updateStatus(_ statusData: StatusData, basedOn status: Status) async throws -> StatusData? { + // Load data from API and operate on CoreData on background context. + let backgroundContext = CoreDataHandler.shared.newBackgroundContext() - let attachmentDataHandler = AttachmentDataHandler() + // Update status data in database. + try await self.copy(from: status, to: statusData, on: backgroundContext) + try backgroundContext.save() + + return statusData + } + + private func copy(from status: Status, to statusData: StatusData, on backgroundContext: NSManagedObjectContext) async throws { + statusData.copyFrom(status) for attachment in status.mediaAttachments { let imageData = try await self.fetchImage(attachment: attachment) @@ -126,31 +89,16 @@ public class TimelineService { continue } - /* - var exif = image.getExifData() - if let dict = exif as? [String: AnyObject] { - dict.keys.map { key in - print(key) - print(dict[key]) - } - } - */ - // Save attachment in database. let attachmentData = statusData.attachments().first { item in item.id == attachment.id } - ?? attachmentDataHandler.createAttachmnentDataEntity(viewContext: backgroundContext) - - attachmentData.id = attachment.id - attachmentData.url = attachment.url - attachmentData.blurhash = attachment.blurhash - attachmentData.previewUrl = attachment.previewUrl - attachmentData.remoteUrl = attachment.remoteUrl - attachmentData.text = attachment.description - attachmentData.type = attachment.type.rawValue + ?? AttachmentDataHandler.shared.createAttachmnentDataEntity(viewContext: backgroundContext) + attachmentData.copyFrom(attachment) attachmentData.statusId = statusData.id attachmentData.data = imageData + // TODO: read exif information + if attachmentData.isInserted { attachmentData.statusRelation = statusData statusData.addToAttachmentRelation(attachmentData) diff --git a/Vernissage/VernissageApp.swift b/Vernissage/VernissageApp.swift index 7ee3a50..bc5ef54 100644 --- a/Vernissage/VernissageApp.swift +++ b/Vernissage/VernissageApp.swift @@ -20,6 +20,7 @@ struct VernissageApp: App { NavigationStack { switch applicationViewMode { case .loading: + // TODO: Loading splashscreen. Text("Loading") case .signIn: SignInView { viewMode in @@ -43,6 +44,9 @@ struct VernissageApp: App { self.applicationState.accountData = accountData self.applicationViewMode = .mainView }) + + URLCache.shared.memoryCapacity = 10_000_000 // ~10 MB memory space + URLCache.shared.diskCapacity = 1_000_000_000 // ~1GB disk cache space } .navigationViewStyle(.stack) } diff --git a/Vernissage/Views/DetailsView.swift b/Vernissage/Views/DetailsView.swift index 5a48260..ee55ca6 100644 --- a/Vernissage/Views/DetailsView.swift +++ b/Vernissage/Views/DetailsView.swift @@ -10,64 +10,102 @@ import AVFoundation struct DetailsView: View { @EnvironmentObject var applicationState: ApplicationState - @ObservedObject public var statusData: StatusData + @State var statusId: String + + @State private var statusData: StatusData? var body: some View { ScrollView { - VStack (alignment: .leading) { - ImagesCarousel(attachments: statusData.attachments()) - - VStack(alignment: .leading) { - NavigationLink(destination: UserProfileView( - accountId: statusData.accountId, - accountDisplayName: statusData.accountDisplayName, - accountUserName: statusData.accountUsername) - .environmentObject(applicationState)) { - UsernameRow(statusData: statusData) + if let statusData = self.statusData { + VStack (alignment: .leading) { + ImagesCarousel(attachments: statusData.attachments()) + + VStack(alignment: .leading) { + NavigationLink(destination: UserProfileView( + accountId: statusData.accountId, + accountDisplayName: statusData.accountDisplayName, + accountUserName: statusData.accountUsername) + .environmentObject(applicationState)) { + UsernameRow(statusData: statusData) + } + + HTMLFormattedText(statusData.content) + .padding(.leading, -4) + + VStack (alignment: .leading) { + LabelIcon(iconName: "camera", value: "SONY ILCE-7M3") + LabelIcon(iconName: "camera.aperture", value: "Viltrox 24mm F1.8 E") + LabelIcon(iconName: "timelapse", value: "24.0 mm, f/1.8, 1/640s, ISO 100") + LabelIcon(iconName: "calendar", value: "2 Oct 2022") } - - HTMLFormattedText(statusData.content) - .padding(.leading, -4) - - VStack (alignment: .leading) { - LabelIcon(iconName: "camera", value: "SONY ILCE-7M3") - LabelIcon(iconName: "camera.aperture", value: "Viltrox 24mm F1.8 E") - LabelIcon(iconName: "timelapse", value: "24.0 mm, f/1.8, 1/640s, ISO 100") - LabelIcon(iconName: "calendar", value: "2 Oct 2022") - } - .foregroundColor(Color("LightGrayColor")) - - HStack { - Text("Uploaded") - Text(statusData.createdAt.toRelative(.isoDateTimeMilliSec)) - .padding(.horizontal, -4) - if let applicationName = statusData.applicationName { - Text("via \(applicationName)") + .foregroundColor(Color.lightGrayColor) + + HStack { + Text("Uploaded") + Text(statusData.createdAt.toRelative(.isoDateTimeMilliSec)) + .padding(.horizontal, -4) + if let applicationName = statusData.applicationName { + Text("via \(applicationName)") + } } + .foregroundColor(Color.lightGrayColor) + .font(.footnote) + + InteractionRow(statusData: statusData) + .padding(8) } - .foregroundColor(Color("LightGrayColor")) - .font(.footnote) + .padding(8) - InteractionRow(statusData: statusData) - .padding(8) + Rectangle() + .size(width: UIScreen.main.bounds.width, height: 4) + .fill(Color.mainTextColor) + .opacity(0.1) + + CommentsSection(statusId: statusData.id) + } + } else { + VStack (alignment: .leading) { + Rectangle() + .fill(Color.placeholderText) + .frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.width) + .redacted(reason: .placeholder) + HStack (alignment: .center) { + Circle() + .fill(Color.placeholderText) + .frame(width: 48.0, height: 48.0) + .redacted(reason: .placeholder) + + VStack (alignment: .leading) { + Text("Verylong Displayname") + .foregroundColor(Color.mainTextColor) + .redacted(reason: .placeholder) + Text("@username") + .foregroundColor(Color.lightGrayColor) + .font(.footnote) + .redacted(reason: .placeholder) + } + .padding(.leading, 8) + }.padding(8) } - .padding(8) - - Rectangle() - .size(width: UIScreen.main.bounds.width, height: 4) - .fill(Color("MainTextColor")) - .opacity(0.1) - - CommentsSection(statusId: statusData.id) } } .navigationBarTitle("Details") .onAppear { Task { do { - if let accountData = self.applicationState.accountData { - let timelineService = TimelineService() - _ = try await timelineService.updateStatus(statusData: self.statusData, and: accountData) + // Get status from API. + let status = try await TimelineService.shared.getStatus(withId: self.statusId, and: self.applicationState.accountData) + + if let status { + // Get status from database. + let statusDataFromDatabase = StatusDataHandler.shared.getStatusData(statusId: self.statusId) + + // If we have status in database then we can update data. + if let statusDataFromDatabase { + self.statusData = try await TimelineService.shared.updateStatus(statusDataFromDatabase, basedOn: status) + } else { + self.statusData = try await status.createStatusData() + } } } catch { print("Error \(error.localizedDescription)") @@ -79,6 +117,6 @@ struct DetailsView: View { struct DetailsView_Previews: PreviewProvider { static var previews: some View { - DetailsView(statusData: StatusData()) + DetailsView(statusId: "123") } } diff --git a/Vernissage/Views/HomeFeedView.swift b/Vernissage/Views/HomeFeedView.swift index e40db4b..d374244 100644 --- a/Vernissage/Views/HomeFeedView.swift +++ b/Vernissage/Views/HomeFeedView.swift @@ -22,9 +22,8 @@ struct HomeFeedView: View { ScrollView { LazyVGrid(columns: gridColumns) { ForEach(dbStatuses, id: \.self) { item in - NavigationLink(destination: - DetailsView(statusData: item) - .environmentObject(applicationState)) { + NavigationLink(destination: DetailsView(statusId: item.id) + .environmentObject(applicationState)) { ImageRow(attachments: item.attachments()) } } diff --git a/Vernissage/Views/MainView.swift b/Vernissage/Views/MainView.swift index b058586..e8eeab0 100644 --- a/Vernissage/Views/MainView.swift +++ b/Vernissage/Views/MainView.swift @@ -97,7 +97,7 @@ struct MainView: View { .font(.subheadline) } .frame(width: 150) - .foregroundColor(Color("MainTextColor")) + .foregroundColor(Color.mainTextColor) } } } @@ -108,20 +108,20 @@ struct MainView: View { Menu { Button { - // Switch accounts... + // TODO: Switch accounts. } label: { HStack { Text(self.applicationState.accountData?.displayName ?? self.applicationState.accountData?.username ?? "") Image(systemName: "person.circle.fill") .resizable() - .foregroundColor(Color("MainTextColor")) + .foregroundColor(Color.mainTextColor) } } Divider() Button { - // Open settings... + // TODO: Open settings. } label: { HStack { Text("Settings") @@ -138,7 +138,7 @@ struct MainView: View { Image(systemName: "person.circle") .resizable() .frame(width: 32.0, height: 32.0) - .foregroundColor(Color("MainTextColor")) + .foregroundColor(Color.mainTextColor) } } } diff --git a/Vernissage/Views/SignInView.swift b/Vernissage/Views/SignInView.swift index ed2cc01..75e7223 100644 --- a/Vernissage/Views/SignInView.swift +++ b/Vernissage/Views/SignInView.swift @@ -16,6 +16,7 @@ struct SignInView: View { var body: some View { VStack { + // TODO: Rebild signin. HStack { TextField( "Server address", diff --git a/Vernissage/Views/UserProfileView.swift b/Vernissage/Views/UserProfileView.swift index 513aeec..592da61 100644 --- a/Vernissage/Views/UserProfileView.swift +++ b/Vernissage/Views/UserProfileView.swift @@ -13,103 +13,121 @@ struct UserProfileView: View { @State public var accountDisplayName: String? @State public var accountUserName: String @State private var account: Account? = nil + @State private var relationship: Relationship? = nil + @State private var statuses: [Status] = [] + + private static let initialColumns = 1 + @State private var gridColumns = Array(repeating: GridItem(.flexible()), count: initialColumns) var body: some View { - VStack(alignment: .leading) { + ScrollView { if let account = self.account { - - HStack(alignment: .center) { - AsyncImage(url: account.avatar) { image in - image - .resizable() - .clipShape(Circle()) - .aspectRatio(contentMode: .fit) - } placeholder: { - Image(systemName: "person.circle") - .resizable() - .foregroundColor(Color("MainTextColor")) - } - .frame(width: 96.0, height: 96.0) - - Spacer() - - VStack(alignment: .center) { - Text("\(account.statusesCount)") - .font(.title3) - Text("Posts") - .font(.subheadline) - .opacity(0.6) + VStack(alignment: .leading) { + HStack(alignment: .center) { + AsyncImage(url: account.avatar) { image in + image + .resizable() + .clipShape(Circle()) + .aspectRatio(contentMode: .fit) + } placeholder: { + Image(systemName: "person.circle") + .resizable() + .foregroundColor(Color.mainTextColor) + } + .frame(width: 96.0, height: 96.0) + + Spacer() + + VStack(alignment: .center) { + Text("\(account.statusesCount)") + .font(.title3) + Text("Posts") + .font(.subheadline) + .opacity(0.6) + } + + Spacer() + + VStack(alignment: .center) { + Text("\(account.followersCount)") + .font(.title3) + Text("Followers") + .font(.subheadline) + .opacity(0.6) + } + + Spacer() + + VStack(alignment: .center) { + Text("\(account.followingCount)") + .font(.title3) + Text("Following") + .font(.subheadline) + .opacity(0.6) + } } - Spacer() - - VStack(alignment: .center) { - Text("\(account.followersCount)") - .font(.title3) - Text("Followers") - .font(.subheadline) - .opacity(0.6) + HStack (alignment: .center) { + Text(account.displayName ?? account.username) + .foregroundColor(Color.mainTextColor) + .font(.footnote) + .fontWeight(.bold) + Text("@\(account.username)") + .foregroundColor(Color.lightGrayColor) + .font(.footnote) + + Spacer() + + Button { + // TODO: Folllow/Unfollow. + } label: { + HStack { + Image(systemName: relationship?.following == true ? "person.badge.minus" : "person.badge.plus") + Text(relationship?.following == true ? "Unfollow" : (relationship?.followedBy == true ? "Follow back" : "Follow")) + } + } + .buttonStyle(.borderedProminent) + .tint(relationship?.following == true ? Color.dangerColor : .accentColor) + } - Spacer() - - VStack(alignment: .center) { - Text("\(account.followingCount)") - .font(.title3) - Text("Following") - .font(.subheadline) - .opacity(0.6) + if let note = account.note { + HTMLFormattedText(note, withFontSize: 14, andWidth: Int(UIScreen.main.bounds.width) - 16) + .padding(.top, -10) + .padding(.leading, -4) } - } - - HStack (alignment: .center) { - Text(account.displayName ?? account.username) - .foregroundColor(Color("DisplayNameColor")) - .font(.footnote) - .fontWeight(.bold) - Text("@\(account.username)") - .foregroundColor(Color("LightGrayColor")) + + Text("Joined \(account.createdAt.toRelative(.isoDateTimeMilliSec))") + .foregroundColor(Color.lightGrayColor.opacity(0.5)) .font(.footnote) - Spacer() - - Button { - // Folllow/Unfollow - } label: { - Text("Follow") + } + .padding() + + LazyVGrid(columns: gridColumns) { + ForEach(self.statuses, id: \.id) { item in + NavigationLink(destination: DetailsView(statusId: item.id) + .environmentObject(applicationState)) { + ImageRowAsync(attachments: item.mediaAttachments) + } } - .buttonStyle(.borderedProminent) - .tint(.accentColor) - } - if let note = account.note { - HTMLFormattedText(note, withFontSize: 14, andWidth: Int(UIScreen.main.bounds.width) - 16) - .padding(.top, -10) - .padding(.leading, -4) - } - - Text("Joined \(account.createdAt.toRelative(.isoDateTimeMilliSec))") - .foregroundColor(Color("LightGrayColor").opacity(0.5)) - .font(.footnote) - - Spacer() } else { ProgressView() .progressViewStyle(CircularProgressViewStyle()) } } - .padding() .navigationBarTitle(self.accountDisplayName ?? self.accountUserName) .onAppear { Task { do { - if let account = try await AccountService.shared.getAccount( - withId: self.accountId, - and: self.applicationState.accountData - ) { - self.account = account - } + async let relationshipTask = AccountService.shared.getRelationship(withId: self.accountId, forUser: self.applicationState.accountData) + async let accountTask = AccountService.shared.getAccount(withId: self.accountId, and: self.applicationState.accountData) + + (self.relationship, self.account) = try await (relationshipTask, accountTask) + + self.statuses = try await AccountService.shared.getStatuses(forAccountId: self.accountId, andContext: self.applicationState.accountData) } catch { print("Error \(error.localizedDescription)") } diff --git a/Vernissage/Widgets/CommentsSection.swift b/Vernissage/Widgets/CommentsSection.swift index cd17e3b..8e0a036 100644 --- a/Vernissage/Widgets/CommentsSection.swift +++ b/Vernissage/Widgets/CommentsSection.swift @@ -36,7 +36,7 @@ struct CommentsSection: View { } placeholder: { Image(systemName: "person.circle") .resizable() - .foregroundColor(Color("MainTextColor")) + .foregroundColor(Color.mainTextColor) } .frame(width: 32.0, height: 32.0) } @@ -45,17 +45,17 @@ struct CommentsSection: View { VStack (alignment: .leading) { HStack (alignment: .top) { Text(status.account?.displayName ?? status.account?.username ?? "") - .foregroundColor(Color("DisplayNameColor")) + .foregroundColor(Color.mainTextColor) .font(.footnote) .fontWeight(.bold) Text("@\(status.account?.username ?? "")") - .foregroundColor(Color("LightGrayColor")) + .foregroundColor(Color.lightGrayColor) .font(.footnote) Spacer() Text(status.createdAt.toRelative(.isoDateTimeMilliSec)) - .foregroundColor(Color("LightGrayColor").opacity(0.5)) + .foregroundColor(Color.lightGrayColor.opacity(0.5)) .font(.footnote) } @@ -73,14 +73,14 @@ struct CommentsSection: View { .frame(minWidth: 0, maxWidth: .infinity) .frame(height: status.mediaAttachments.count == 1 ? 200 : 100) .cornerRadius(10) - .shadow(color: Color("MainTextColor").opacity(0.3), radius: 2) + .shadow(color: Color.mainTextColor.opacity(0.3), radius: 2) } placeholder: { Image(systemName: "photo") .resizable() .scaledToFit() .frame(minWidth: 0, maxWidth: .infinity) .frame(height: status.mediaAttachments.count == 1 ? 200 : 100) - .foregroundColor(Color("MainTextColor")) + .foregroundColor(Color.mainTextColor) .opacity(0.05) } } @@ -98,7 +98,7 @@ struct CommentsSection: View { if withDivider { Rectangle() .size(width: UIScreen.main.bounds.width, height: 4) - .fill(Color("MainTextColor")) + .fill(Color.mainTextColor) .opacity(0.1) } } diff --git a/Vernissage/Widgets/ImageRow.swift b/Vernissage/Widgets/ImageRow.swift index 6992ad7..1051ac1 100644 --- a/Vernissage/Widgets/ImageRow.swift +++ b/Vernissage/Widgets/ImageRow.swift @@ -29,8 +29,6 @@ struct ImageRow: View { }.padding() } } - } else { - Text("Error") } } } diff --git a/Vernissage/Widgets/ImageRowAsync.swift b/Vernissage/Widgets/ImageRowAsync.swift new file mode 100644 index 0000000..2bde772 --- /dev/null +++ b/Vernissage/Widgets/ImageRowAsync.swift @@ -0,0 +1,47 @@ +// +// https://mczachurski.dev +// Copyright © 2023 Marcin Czachurski and the repository contributors. +// Licensed under the MIT License. +// + +import SwiftUI +import MastodonSwift +import NukeUI + +struct ImageRowAsync: View { + @State public var attachments: [Attachment] + @State private var imageHeight = UIScreen.main.bounds.width + + var body: some View { + if let attachment = attachments.first { + ZStack { + LazyImage(url: attachment.url, resizingMode: .fill) + .onSuccess({ imageResponse in + let imgHeight = imageResponse.image.size.height + let imgWidth = imageResponse.image.size.width + + let divider = imgWidth / UIScreen.main.bounds.size.width + self.imageHeight = imgHeight / divider + }) + .frame(height: self.imageHeight <= 0 ? UIScreen.main.bounds.width : self.imageHeight) + + if let count = attachments.count, count > 1 { + BottomRight { + Text("1 / \(count)") + .padding(.horizontal, 6) + .padding(.vertical, 3) + .font(.caption2) + .foregroundColor(.black) + .background(.ultraThinMaterial, in: Capsule()) + }.padding() + } + } + } + } +} + +struct ImageRowAsync_Previews: PreviewProvider { + static var previews: some View { + ImageRow(attachments: []) + } +} diff --git a/Vernissage/Widgets/InteractionRow.swift b/Vernissage/Widgets/InteractionRow.swift index 57046dc..3da7619 100644 --- a/Vernissage/Widgets/InteractionRow.swift +++ b/Vernissage/Widgets/InteractionRow.swift @@ -12,7 +12,7 @@ struct InteractionRow: View { var body: some View { HStack (alignment: .top) { Button { - // Reply + // TODO: Reply. } label: { HStack(alignment: .center) { Image(systemName: "message") @@ -24,7 +24,7 @@ struct InteractionRow: View { Spacer() Button { - // Reboost + // TODO: Reboost. } label: { HStack(alignment: .center) { Image(systemName: statusData.reblogged ? "paperplane.fill" : "paperplane") @@ -36,7 +36,7 @@ struct InteractionRow: View { Spacer() Button { - // Favorite + // TODO: Favorite. } label: { HStack(alignment: .center) { Image(systemName: statusData.favourited ? "hand.thumbsup.fill" : "hand.thumbsup") @@ -48,7 +48,7 @@ struct InteractionRow: View { Spacer() Button { - // Bookmark + // TODO: Bookmark. } label: { Image(systemName: statusData.bookmarked ? "bookmark.fill" : "bookmark") } @@ -56,7 +56,7 @@ struct InteractionRow: View { Spacer() Button { - // Share + // TODO: Share. } label: { Image(systemName: "square.and.arrow.up") } diff --git a/Vernissage/Widgets/UsernameRow.swift b/Vernissage/Widgets/UsernameRow.swift index d269226..07a3da3 100644 --- a/Vernissage/Widgets/UsernameRow.swift +++ b/Vernissage/Widgets/UsernameRow.swift @@ -19,15 +19,15 @@ struct UsernameRow: View { } placeholder: { Image(systemName: "person.circle") .resizable() - .foregroundColor(Color("MainTextColor")) + .foregroundColor(Color.mainTextColor) } .frame(width: 48.0, height: 48.0) VStack (alignment: .leading) { Text(statusData.accountDisplayName ?? statusData.accountUsername) - .foregroundColor(Color("DisplayNameColor")) + .foregroundColor(Color.mainTextColor) Text("@\(statusData.accountUsername)") - .foregroundColor(Color("LightGrayColor")) + .foregroundColor(Color.lightGrayColor) .font(.footnote) } .padding(.leading, 8)