Add user profile for other accounts

This commit is contained in:
Marcin Czachursk 2023-01-05 11:55:20 +01:00
parent 3c2ee8c592
commit d2d4844469
30 changed files with 618 additions and 243 deletions

View File

@ -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 = "<group>"; };
F80048072961E6DE00E6868A /* StatusDataHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusDataHandler.swift; sourceTree = "<group>"; };
F80048092961EA1900E6868A /* AttachmentDataHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentDataHandler.swift; sourceTree = "<group>"; };
F8210DCE2966B600001D9973 /* ImageRowAsync.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageRowAsync.swift; sourceTree = "<group>"; };
F8210DDC2966CF17001D9973 /* StatusData+Status.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StatusData+Status.swift"; sourceTree = "<group>"; };
F8210DDE2966CFC7001D9973 /* AttachmentData+Attachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AttachmentData+Attachment.swift"; sourceTree = "<group>"; };
F8210DE02966D0C4001D9973 /* StatusService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusService.swift; sourceTree = "<group>"; };
F8210DE22966D256001D9973 /* Status+StatusData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Status+StatusData.swift"; sourceTree = "<group>"; };
F8210DE42966E160001D9973 /* Color+SystemColors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+SystemColors.swift"; sourceTree = "<group>"; };
F8210DE62966E1D1001D9973 /* Color+Assets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+Assets.swift"; sourceTree = "<group>"; };
F8341F8F295C636C009C8EE6 /* UIImage+Exif.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+Exif.swift"; sourceTree = "<group>"; };
F8341F91295C63BB009C8EE6 /* ImageStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageStatus.swift; sourceTree = "<group>"; };
F83901A5295D8EC000456AE2 /* LabelIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabelIcon.swift; sourceTree = "<group>"; };
@ -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 = "<group>";
@ -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 = "<group>";
@ -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" */;

View File

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

View File

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

View File

@ -8,6 +8,9 @@
import Foundation
class ApplicationSettingsHandler {
public static let shared = ApplicationSettingsHandler()
private init() { }
func getDefaultSettings() -> ApplicationSettings {
var settingsList: [ApplicationSettings] = []

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -16,6 +16,7 @@ struct SignInView: View {
var body: some View {
VStack {
// TODO: Rebild signin.
HStack {
TextField(
"Server address",

View File

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

View File

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

View File

@ -29,8 +29,6 @@ struct ImageRow: View {
}.padding()
}
}
} else {
Text("Error")
}
}
}

View File

@ -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: [])
}
}

View File

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

View File

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