A lot of fixes.
This commit is contained in:
parent
0daf234784
commit
50c26e9e4d
|
@ -44,20 +44,19 @@ public class Application: BaseApplication {
|
|||
|
||||
public required init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
self.id = try container.decode(String.self, forKey: .id)
|
||||
self.redirectUri = try container.decode(String.self, forKey: .redirectUri)
|
||||
self.clientId = try container.decode(String.self, forKey: .clientId)
|
||||
self.clientSecret = try container.decode(String.self, forKey: .clientSecret)
|
||||
self.vapidKey = try? container.decode(String.self, forKey: .vapidKey)
|
||||
self.vapidKey = try? container.decodeIfPresent(String.self, forKey: .vapidKey)
|
||||
|
||||
let superDecoder = try container.superDecoder()
|
||||
try super.init(from: superDecoder)
|
||||
try super.init(from: decoder)
|
||||
}
|
||||
|
||||
public override func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try super.encode(to: encoder)
|
||||
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(id, forKey: .id)
|
||||
try container.encode(redirectUri, forKey: .redirectUri)
|
||||
try container.encode(clientId, forKey: .clientId)
|
||||
|
@ -66,8 +65,5 @@ public class Application: BaseApplication {
|
|||
if let vapidKey {
|
||||
try container.encode(vapidKey, forKey: .vapidKey)
|
||||
}
|
||||
|
||||
let superEncoder = container.superEncoder()
|
||||
try super.encode(to: superEncoder)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,7 +30,7 @@ public class BaseApplication: Codable {
|
|||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
self.name = try container.decode(String.self, forKey: .name)
|
||||
self.website = try? container.decode(URL.self, forKey: .website)
|
||||
self.website = try? container.decodeIfPresent(URL.self, forKey: .website)
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
|
|
|
@ -9,69 +9,75 @@ import Foundation
|
|||
/// Represents the software instance of Mastodon running on this domain.
|
||||
public struct Instance: Codable {
|
||||
/// The domain name of the instance.
|
||||
public let domain: String
|
||||
public let uri: String
|
||||
|
||||
/// The title of the website.
|
||||
public let title: String?
|
||||
|
||||
/// The version of Mastodon installed on the instance.
|
||||
public let version: String
|
||||
|
||||
/// The title of the website.
|
||||
public let title: String?
|
||||
|
||||
/// The URL for the source code of the software running on this instance, in keeping with AGPL license requirements.
|
||||
public let sourceUrl: URL?
|
||||
|
||||
/// A short, plain-text description defined by the admin.
|
||||
public let shortDescription: String?
|
||||
|
||||
/// A plain-text description defined by the admin.
|
||||
public let description: String?
|
||||
|
||||
/// Usage data for this instance.
|
||||
public let usage: Usage?
|
||||
|
||||
/// The URL for the thumbnail image.
|
||||
public let thumbnail: Thumbnail?
|
||||
public let thumbnail: URL?
|
||||
|
||||
/// Primary languages of the website and its staff. Array of String (ISO 639-1 two-letter code).
|
||||
public let languages: [String]?
|
||||
|
||||
/// Configured values and limits for this website.
|
||||
public let configuration: Configuration?
|
||||
|
||||
/// If registration new accounts on server is enabled
|
||||
public let registrations: Bool
|
||||
|
||||
/// Information about registering for this website.
|
||||
public let registrations: Registration?
|
||||
|
||||
/// Hints related to contacting a representative of the website.
|
||||
public let contact: Contact?
|
||||
/// If approval for registration account is mandatory.
|
||||
public let approvalRequired: Bool
|
||||
|
||||
/// An itemized list of rules for this website.
|
||||
public let rules: [Rule]?
|
||||
|
||||
/// Main contact email.
|
||||
public let email: String?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case domain
|
||||
case uri
|
||||
case title
|
||||
case version
|
||||
case sourceUrl = "source_url"
|
||||
case shortDescription = "short_description"
|
||||
case description
|
||||
case usage
|
||||
case thumbnail
|
||||
case languages
|
||||
case configuration
|
||||
case registrations
|
||||
case contact
|
||||
case rules
|
||||
case email
|
||||
case registrations
|
||||
case approvalRequired = "approval_required"
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
self.domain = try container.decode(String.self, forKey: .domain)
|
||||
self.title = try? container.decodeIfPresent(String.self, forKey: .title)
|
||||
self.uri = try container.decode(String.self, forKey: .uri)
|
||||
self.version = try container.decode(String.self, forKey: .version)
|
||||
|
||||
self.title = try? container.decodeIfPresent(String.self, forKey: .title)
|
||||
self.sourceUrl = try? container.decodeIfPresent(URL.self, forKey: .sourceUrl)
|
||||
self.shortDescription = try? container.decodeIfPresent(String.self, forKey: .shortDescription)
|
||||
self.description = try? container.decodeIfPresent(String.self, forKey: .description)
|
||||
self.usage = try? container.decodeIfPresent(Usage.self, forKey: .usage)
|
||||
self.thumbnail = try? container.decodeIfPresent(Thumbnail.self, forKey: .thumbnail)
|
||||
self.thumbnail = try? container.decodeIfPresent(URL.self, forKey: .thumbnail)
|
||||
self.languages = try? container.decodeIfPresent([String].self, forKey: .languages)
|
||||
self.configuration = try? container.decodeIfPresent(Configuration.self, forKey: .configuration)
|
||||
self.registrations = try? container.decodeIfPresent(Registration.self, forKey: .registrations)
|
||||
self.contact = try? container.decodeIfPresent(Contact.self, forKey: .contact)
|
||||
self.rules = try? container.decodeIfPresent([Rule].self, forKey: .rules)
|
||||
self.email = try? container.decodeIfPresent(String.self, forKey: .email)
|
||||
self.registrations = (try? container.decodeIfPresent(Bool.self, forKey: .registrations)) ?? false
|
||||
self.approvalRequired = (try? container.decodeIfPresent(Bool.self, forKey: .approvalRequired)) ?? false
|
||||
}
|
||||
}
|
||||
|
|
|
@ -70,7 +70,18 @@ public class MastodonClient: MastodonClientProtocol {
|
|||
throw NetworkError.notSuccessResponse(response)
|
||||
}
|
||||
|
||||
return try JSONDecoder().decode(type, from: data)
|
||||
#if DEBUG
|
||||
do {
|
||||
return try JSONDecoder().decode(type, from: data)
|
||||
} catch {
|
||||
let json = String(data: data, encoding: .utf8)!
|
||||
print(json)
|
||||
|
||||
throw error
|
||||
}
|
||||
#else
|
||||
return try JSONDecoder().decode(type, from: data)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -29,6 +29,7 @@
|
|||
F8210DEA2966E4F9001D9973 /* AnimatePlaceholderModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8210DE92966E4F9001D9973 /* AnimatePlaceholderModifier.swift */; };
|
||||
F8341F90295C636C009C8EE6 /* Data+Exif.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8341F8F295C636C009C8EE6 /* Data+Exif.swift */; };
|
||||
F83901A6295D8EC000456AE2 /* LabelIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = F83901A5295D8EC000456AE2 /* LabelIcon.swift */; };
|
||||
F83CBEFB298298A1002972C8 /* ImageCarouselPicture.swift in Sources */ = {isa = PBXBuildFile; fileRef = F83CBEFA298298A1002972C8 /* ImageCarouselPicture.swift */; };
|
||||
F857F9FD297D8ED3002C109C /* ActionMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = F857F9FC297D8ED3002C109C /* ActionMenu.swift */; };
|
||||
F85D4971296402DC00751DF7 /* AuthorizationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85D4970296402DC00751DF7 /* AuthorizationService.swift */; };
|
||||
F85D4973296406E700751DF7 /* BottomRight.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85D4972296406E700751DF7 /* BottomRight.swift */; };
|
||||
|
@ -135,6 +136,8 @@
|
|||
F8210DE92966E4F9001D9973 /* AnimatePlaceholderModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnimatePlaceholderModifier.swift; sourceTree = "<group>"; };
|
||||
F8341F8F295C636C009C8EE6 /* Data+Exif.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+Exif.swift"; sourceTree = "<group>"; };
|
||||
F83901A5295D8EC000456AE2 /* LabelIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabelIcon.swift; sourceTree = "<group>"; };
|
||||
F83CBEFA298298A1002972C8 /* ImageCarouselPicture.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCarouselPicture.swift; sourceTree = "<group>"; };
|
||||
F85183232981C2CE001BB950 /* Vernissage20230125-001.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Vernissage20230125-001.xcdatamodel"; sourceTree = "<group>"; };
|
||||
F857F9FC297D8ED3002C109C /* ActionMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionMenu.swift; sourceTree = "<group>"; };
|
||||
F85D4970296402DC00751DF7 /* AuthorizationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthorizationService.swift; sourceTree = "<group>"; };
|
||||
F85D4972296406E700751DF7 /* BottomRight.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BottomRight.swift; sourceTree = "<group>"; };
|
||||
|
@ -201,6 +204,7 @@
|
|||
F8984E4C296B648000A2610F /* UIImage+Blurhash.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+Blurhash.swift"; sourceTree = "<group>"; };
|
||||
F898DE6F2972868A004B4A6A /* String+Empty.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Empty.swift"; sourceTree = "<group>"; };
|
||||
F898DE7129728CB2004B4A6A /* CommentViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentViewModel.swift; sourceTree = "<group>"; };
|
||||
F8995E432982865C004C191C /* Vernissage20230126-001.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Vernissage20230126-001.xcdatamodel"; sourceTree = "<group>"; };
|
||||
F8996DEA2971D29D0043EEC6 /* View+Transition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+Transition.swift"; sourceTree = "<group>"; };
|
||||
F89992C8296D6DC7005994BF /* CommentBody.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentBody.swift; sourceTree = "<group>"; };
|
||||
F89992CB296D9231005994BF /* StatusViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusViewModel.swift; sourceTree = "<group>"; };
|
||||
|
@ -351,6 +355,7 @@
|
|||
F85D497629640A5200751DF7 /* ImageRow.swift */,
|
||||
F8210DCE2966B600001D9973 /* ImageRowAsync.swift */,
|
||||
F85D497829640B9D00751DF7 /* ImagesCarousel.swift */,
|
||||
F83CBEFA298298A1002972C8 /* ImageCarouselPicture.swift */,
|
||||
F85D497A29640C8200751DF7 /* UsernameRow.swift */,
|
||||
F85D497C29640D5900751DF7 /* InteractionRow.swift */,
|
||||
F897978729681B9C00B22335 /* UserAvatar.swift */,
|
||||
|
@ -685,6 +690,7 @@
|
|||
F88E4D54297EA7EE0057491A /* MarkdownFormattedText.swift in Sources */,
|
||||
F866F6A1296040A8002E8F88 /* ApplicationSettings+CoreDataProperties.swift in Sources */,
|
||||
F8C14394296AF21B001FE31D /* Double+Round.swift in Sources */,
|
||||
F83CBEFB298298A1002972C8 /* ImageCarouselPicture.swift in Sources */,
|
||||
F89A46DC296EAACE0062125F /* SettingsView.swift in Sources */,
|
||||
F866F6AE29606367002E8F88 /* ApplicationViewMode.swift in Sources */,
|
||||
F8A93D802965FED4001D8331 /* AccountService.swift in Sources */,
|
||||
|
@ -993,12 +999,14 @@
|
|||
F88C2476295C37BB0006098B /* Vernissage.xcdatamodeld */ = {
|
||||
isa = XCVersionGroup;
|
||||
children = (
|
||||
F8995E432982865C004C191C /* Vernissage20230126-001.xcdatamodel */,
|
||||
F85183232981C2CE001BB950 /* Vernissage20230125-001.xcdatamodel */,
|
||||
F86167C9297FF423004D1F67 /* Vernissage20230121-001.xcdatamodel */,
|
||||
F89D6C3B29716DBC001DA3D4 /* Vernissage20230113-001.xcdatamodel */,
|
||||
F8AF2A61297073FE00D2DA3F /* Vernissage20230112-001.xcdatamodel */,
|
||||
F88C2477295C37BB0006098B /* Vernissage.xcdatamodel */,
|
||||
);
|
||||
currentVersion = F86167C9297FF423004D1F67 /* Vernissage20230121-001.xcdatamodel */;
|
||||
currentVersion = F8995E432982865C004C191C /* Vernissage20230126-001.xcdatamodel */;
|
||||
path = Vernissage.xcdatamodeld;
|
||||
sourceTree = "<group>";
|
||||
versionGroupType = wrapper.xcdatamodel;
|
||||
|
|
|
@ -25,6 +25,9 @@ class ApplicationSettingsHandler {
|
|||
return settings
|
||||
} else {
|
||||
let settings = self.createApplicationSettingsEntity()
|
||||
settings.avatarShape = Int32(AvatarShape.circle.rawValue)
|
||||
settings.theme = Int32(Theme.system.rawValue)
|
||||
settings.tintColor = Int32(TintColor.accentColor2.rawValue)
|
||||
CoreDataHandler.shared.save()
|
||||
|
||||
return settings
|
||||
|
|
|
@ -39,7 +39,12 @@ extension StatusData {
|
|||
@NSManaged public var visibility: String
|
||||
@NSManaged public var attachmentRelation: Set<AttachmentData>?
|
||||
@NSManaged public var pixelfedAccount: AccountData
|
||||
|
||||
|
||||
@NSManaged public var rebloggedStatusId: String?
|
||||
@NSManaged public var rebloggedAccountAvatar: URL?
|
||||
@NSManaged public var rebloggedAccountDisplayName: String?
|
||||
@NSManaged public var rebloggedAccountId: String?
|
||||
@NSManaged public var rebloggedAccountUsername: String?
|
||||
}
|
||||
|
||||
// MARK: Generated accessors for attachmentRelation
|
||||
|
|
|
@ -9,29 +9,39 @@ import MastodonKit
|
|||
|
||||
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.acct
|
||||
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
|
||||
if let reblog = status.reblog {
|
||||
self.copyFrom(reblog)
|
||||
|
||||
self.rebloggedStatusId = status.id
|
||||
self.rebloggedAccountAvatar = status.account.avatar
|
||||
self.rebloggedAccountDisplayName = status.account.displayName
|
||||
self.rebloggedAccountId = status.account.id
|
||||
self.rebloggedAccountUsername = status.account.acct
|
||||
} else {
|
||||
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.acct
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,9 +10,22 @@ import MastodonKit
|
|||
extension [Status] {
|
||||
func getStatusesWithImagesOnly() -> [Status] {
|
||||
return self.filter { status in
|
||||
status.mediaAttachments.contains { mediaAttachment in
|
||||
mediaAttachment.type == .image
|
||||
}
|
||||
status.statusContainsImage()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Status {
|
||||
func statusContainsImage() -> Bool {
|
||||
return getAllImageMediaAttachments().isEmpty == false
|
||||
}
|
||||
|
||||
func getAllImageMediaAttachments() -> [MediaAttachment] {
|
||||
if let reblog = self.reblog {
|
||||
// If status is rebloged the we have to check if orginal status contains image.
|
||||
return reblog.mediaAttachments.filter { mediaAttachment in mediaAttachment.type == .image }
|
||||
}
|
||||
|
||||
return self.mediaAttachments.filter { mediaAttachment in mediaAttachment.type == .image }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -43,7 +43,6 @@ public class AuthorizationService {
|
|||
|
||||
// Verify address.
|
||||
let instanceInformation = try await client.readInstanceInformation()
|
||||
print(instanceInformation)
|
||||
|
||||
// Create application (we will get clientId amd clientSecret).
|
||||
let oAuthApp = try await client.createApp(
|
||||
|
|
|
@ -23,10 +23,10 @@ public class HomeTimelineService {
|
|||
return 0
|
||||
}
|
||||
|
||||
let statuses = try await self.loadData(for: accountData, on: backgroundContext, maxId: oldestStatus.id)
|
||||
let newStatuses = try await self.loadData(for: accountData, on: backgroundContext, maxId: oldestStatus.id)
|
||||
|
||||
try backgroundContext.save()
|
||||
return statuses.count
|
||||
return newStatuses.count
|
||||
}
|
||||
|
||||
public func onTopOfList(for accountData: AccountData) async throws -> Int {
|
||||
|
@ -77,7 +77,7 @@ public class HomeTimelineService {
|
|||
|
||||
// Save statuses in database (and download images).
|
||||
if !statusesToAdd.isEmpty {
|
||||
try await self.save(statuses: statusesToAdd, accountData: accountData, on: backgroundContext)
|
||||
_ = try await self.save(statuses: statusesToAdd, accountData: accountData, on: backgroundContext)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -91,42 +91,33 @@ public class HomeTimelineService {
|
|||
let statuses = try await client.getHomeTimeline(maxId: maxId, minId: minId, limit: 20)
|
||||
|
||||
// Save statuses in database (and download images).
|
||||
try await self.save(statuses: statuses, accountData: accountData, on: backgroundContext)
|
||||
|
||||
return statuses
|
||||
return try await self.save(statuses: statuses, accountData: accountData, on: backgroundContext)
|
||||
}
|
||||
|
||||
public func updateStatus(_ statusData: StatusData, accountData: AccountData, basedOn status: Status) async throws -> StatusData? {
|
||||
// Load data from API and operate on CoreData on background context.
|
||||
let backgroundContext = CoreDataHandler.shared.newBackgroundContext()
|
||||
|
||||
// Download all images from server.
|
||||
let attachmentsData = await self.fetchAllImages(statuses: [status])
|
||||
|
||||
|
||||
// Update status data in database.
|
||||
try await self.copy(from: status, to: statusData, attachmentsData: attachmentsData, on: backgroundContext)
|
||||
self.copy(from: status, to: statusData, on: backgroundContext)
|
||||
try backgroundContext.save()
|
||||
|
||||
return statusData
|
||||
}
|
||||
|
||||
private func save(statuses: [Status], accountData: AccountData, on backgroundContext: NSManagedObjectContext) async throws {
|
||||
// Download all images from server.
|
||||
let attachmentsData = await self.fetchAllImages(statuses: statuses)
|
||||
public func updateAttachmentDataImage(attachmentData: AttachmentData, imageData: Data) {
|
||||
attachmentData.data = imageData
|
||||
self.setExifProperties(in: attachmentData, from: imageData)
|
||||
|
||||
CoreDataHandler.shared.save()
|
||||
}
|
||||
|
||||
private func save(statuses: [Status], accountData: AccountData, on backgroundContext: NSManagedObjectContext) async throws -> [Status] {
|
||||
// Proceed statuses with images only.
|
||||
let statusesWithImages = statuses.getStatusesWithImagesOnly()
|
||||
|
||||
// Save status data in database.
|
||||
for status in statuses {
|
||||
let contains = attachmentsData.contains { (key: String, value: Data) in
|
||||
status.mediaAttachments.contains { attachment in
|
||||
attachment.id == key
|
||||
}
|
||||
}
|
||||
|
||||
// We are adding status only when we have at least one image for status.
|
||||
if contains == false {
|
||||
continue
|
||||
}
|
||||
|
||||
for status in statusesWithImages {
|
||||
guard let dbAccount = AccountDataHandler.shared.getAccountData(accountId: accountData.id, viewContext: backgroundContext) else {
|
||||
throw DatabaseError.cannotDownloadAccount
|
||||
}
|
||||
|
@ -136,17 +127,19 @@ public class HomeTimelineService {
|
|||
statusData.pixelfedAccount = dbAccount
|
||||
dbAccount.addToStatuses(statusData)
|
||||
|
||||
try await self.copy(from: status, to: statusData, attachmentsData: attachmentsData, on: backgroundContext)
|
||||
self.copy(from: status, to: statusData, on: backgroundContext)
|
||||
}
|
||||
|
||||
return statusesWithImages
|
||||
}
|
||||
|
||||
private func copy(from status: Status, to statusData: StatusData, attachmentsData: Dictionary<String, Data>, on backgroundContext: NSManagedObjectContext) async throws {
|
||||
private func copy(from status: Status,
|
||||
to statusData: StatusData,
|
||||
on backgroundContext: NSManagedObjectContext
|
||||
) {
|
||||
statusData.copyFrom(status)
|
||||
|
||||
for attachment in status.mediaAttachments {
|
||||
guard let imageData = attachmentsData[attachment.id] else {
|
||||
continue
|
||||
}
|
||||
for attachment in status.getAllImageMediaAttachments() {
|
||||
|
||||
// Save attachment in database.
|
||||
let attachmentData = statusData.attachments().first { item in item.id == attachment.id }
|
||||
|
@ -154,31 +147,7 @@ public class HomeTimelineService {
|
|||
|
||||
attachmentData.copyFrom(attachment)
|
||||
attachmentData.statusId = statusData.id
|
||||
attachmentData.data = imageData
|
||||
|
||||
// Read exif information.
|
||||
if let exifProperties = imageData.getExifData() {
|
||||
if let make = exifProperties.getExifValue("Make"), let model = exifProperties.getExifValue("Model") {
|
||||
attachmentData.exifCamera = "\(make) \(model)"
|
||||
}
|
||||
|
||||
// "Lens" or "Lens Model"
|
||||
if let lens = exifProperties.getExifValue("Lens") {
|
||||
attachmentData.exifLens = lens
|
||||
}
|
||||
|
||||
if let createData = exifProperties.getExifValue("CreateDate") {
|
||||
attachmentData.exifCreatedDate = createData
|
||||
}
|
||||
|
||||
if let focalLenIn35mmFilm = exifProperties.getExifValue("FocalLenIn35mmFilm"),
|
||||
let fNumber = exifProperties.getExifValue("FNumber")?.calculateExifNumber(),
|
||||
let exposureTime = exifProperties.getExifValue("ExposureTime"),
|
||||
let photographicSensitivity = exifProperties.getExifValue("PhotographicSensitivity") {
|
||||
attachmentData.exifExposure = "\(focalLenIn35mmFilm)mm, f/\(fNumber), \(exposureTime)s, ISO \(photographicSensitivity)"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if attachmentData.isInserted {
|
||||
attachmentData.statusRelation = statusData
|
||||
statusData.addToAttachmentRelation(attachmentData)
|
||||
|
@ -189,10 +158,11 @@ public class HomeTimelineService {
|
|||
public func fetchAllImages(statuses: [Status]) async -> Dictionary<String, Data> {
|
||||
var attachmentUrls: Dictionary<String, URL> = [:]
|
||||
|
||||
let statusesWithImages = statuses.getStatusesWithImagesOnly()
|
||||
statusesWithImages.forEach { status in
|
||||
statuses.forEach { status in
|
||||
status.mediaAttachments.forEach { attachment in
|
||||
attachmentUrls[attachment.id] = attachment.url
|
||||
if attachment.type == .image {
|
||||
attachmentUrls[attachment.id] = attachment.url
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -200,13 +170,15 @@ public class HomeTimelineService {
|
|||
for attachmentUrl in attachmentUrls {
|
||||
taskGroup.addTask {
|
||||
do {
|
||||
print("Fetching image \(attachmentUrl.value)")
|
||||
if let imageData = try await self.fetchImage(attachmentUrl: attachmentUrl.value) {
|
||||
print("Image fetched \(attachmentUrl.value)")
|
||||
return (attachmentUrl.key, imageData)
|
||||
}
|
||||
|
||||
return (attachmentUrl.key, nil)
|
||||
} catch {
|
||||
ErrorService.shared.handle(error, message: "Fatching all images failed.")
|
||||
ErrorService.shared.handle(error, message: "Fatching image '\(attachmentUrl.value)' failed.")
|
||||
return (attachmentUrl.key, nil)
|
||||
}
|
||||
}
|
||||
|
@ -225,6 +197,31 @@ public class HomeTimelineService {
|
|||
}
|
||||
}
|
||||
|
||||
private func setExifProperties(in attachmentData: AttachmentData, from imageData: Data) {
|
||||
// Read exif information.
|
||||
if let exifProperties = imageData.getExifData() {
|
||||
if let make = exifProperties.getExifValue("Make"), let model = exifProperties.getExifValue("Model") {
|
||||
attachmentData.exifCamera = "\(make) \(model)"
|
||||
}
|
||||
|
||||
// "Lens" or "Lens Model"
|
||||
if let lens = exifProperties.getExifValue("Lens") {
|
||||
attachmentData.exifLens = lens
|
||||
}
|
||||
|
||||
if let createData = exifProperties.getExifValue("CreateDate") {
|
||||
attachmentData.exifCreatedDate = createData
|
||||
}
|
||||
|
||||
if let focalLenIn35mmFilm = exifProperties.getExifValue("FocalLenIn35mmFilm"),
|
||||
let fNumber = exifProperties.getExifValue("FNumber")?.calculateExifNumber(),
|
||||
let exposureTime = exifProperties.getExifValue("ExposureTime"),
|
||||
let photographicSensitivity = exifProperties.getExifValue("PhotographicSensitivity") {
|
||||
attachmentData.exifExposure = "\(focalLenIn35mmFilm)mm, f/\(fNumber), \(exposureTime)s, ISO \(photographicSensitivity)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func fetchImage(attachmentUrl: URL) async throws -> Data? {
|
||||
guard let data = try await RemoteFileService.shared.fetchData(url: attachmentUrl) else {
|
||||
return nil
|
||||
|
|
|
@ -3,6 +3,6 @@
|
|||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>_XCCurrentVersionName</key>
|
||||
<string>Vernissage20230121-001.xcdatamodel</string>
|
||||
<string>Vernissage20230126-001.xcdatamodel</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="21513" systemVersion="22C65" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||
<entity name="AccountData" representedClassName="AccountData" syncable="YES">
|
||||
<attribute name="accessToken" optional="YES" attributeType="String"/>
|
||||
<attribute name="acct" attributeType="String"/>
|
||||
<attribute name="avatar" optional="YES" attributeType="URI"/>
|
||||
<attribute name="avatarData" optional="YES" attributeType="Binary"/>
|
||||
<attribute name="clientId" attributeType="String"/>
|
||||
<attribute name="clientSecret" attributeType="String"/>
|
||||
<attribute name="clientVapidKey" attributeType="String"/>
|
||||
<attribute name="createdAt" attributeType="String"/>
|
||||
<attribute name="displayName" optional="YES" attributeType="String"/>
|
||||
<attribute name="followersCount" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="followingCount" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="header" optional="YES" attributeType="URI"/>
|
||||
<attribute name="id" attributeType="String"/>
|
||||
<attribute name="locked" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="note" optional="YES" attributeType="String"/>
|
||||
<attribute name="serverUrl" attributeType="URI"/>
|
||||
<attribute name="statusesCount" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="url" optional="YES" attributeType="URI"/>
|
||||
<attribute name="username" attributeType="String"/>
|
||||
<relationship name="statuses" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="StatusData" inverseName="pixelfedAccount" inverseEntity="StatusData"/>
|
||||
</entity>
|
||||
<entity name="ApplicationSettings" representedClassName="ApplicationSettings" syncable="YES">
|
||||
<attribute name="avatarShape" attributeType="Integer 32" defaultValueString="1" usesScalarValueType="YES"/>
|
||||
<attribute name="currentAccount" optional="YES" attributeType="String"/>
|
||||
<attribute name="theme" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="tintColor" attributeType="Integer 32" defaultValueString="2" usesScalarValueType="YES"/>
|
||||
</entity>
|
||||
<entity name="AttachmentData" representedClassName="AttachmentData" syncable="YES">
|
||||
<attribute name="blurhash" optional="YES" attributeType="String"/>
|
||||
<attribute name="data" attributeType="Binary" allowsExternalBinaryDataStorage="YES"/>
|
||||
<attribute name="exifCamera" optional="YES" attributeType="String"/>
|
||||
<attribute name="exifCreatedDate" optional="YES" attributeType="String"/>
|
||||
<attribute name="exifExposure" optional="YES" attributeType="String"/>
|
||||
<attribute name="exifLens" optional="YES" attributeType="String"/>
|
||||
<attribute name="id" attributeType="String"/>
|
||||
<attribute name="metaImageHeight" optional="YES" attributeType="Integer 32" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="metaImageWidth" optional="YES" attributeType="Integer 32" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="previewUrl" optional="YES" attributeType="URI"/>
|
||||
<attribute name="remoteUrl" optional="YES" attributeType="URI"/>
|
||||
<attribute name="statusId" attributeType="String"/>
|
||||
<attribute name="text" optional="YES" attributeType="String"/>
|
||||
<attribute name="type" attributeType="String"/>
|
||||
<attribute name="url" attributeType="URI"/>
|
||||
<relationship name="statusRelation" maxCount="1" deletionRule="Cascade" destinationEntity="StatusData" inverseName="attachmentRelation" inverseEntity="StatusData"/>
|
||||
</entity>
|
||||
<entity name="StatusData" representedClassName="StatusData" syncable="YES">
|
||||
<attribute name="accountAvatar" optional="YES" attributeType="URI"/>
|
||||
<attribute name="accountDisplayName" optional="YES" attributeType="String"/>
|
||||
<attribute name="accountId" attributeType="String"/>
|
||||
<attribute name="accountUsername" optional="YES" attributeType="String"/>
|
||||
<attribute name="applicationName" optional="YES" attributeType="String"/>
|
||||
<attribute name="applicationWebsite" optional="YES" attributeType="URI"/>
|
||||
<attribute name="bookmarked" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="content" attributeType="String"/>
|
||||
<attribute name="createdAt" attributeType="String"/>
|
||||
<attribute name="favourited" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="favouritesCount" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="id" attributeType="String"/>
|
||||
<attribute name="inReplyToAccount" optional="YES" attributeType="String"/>
|
||||
<attribute name="inReplyToId" optional="YES" attributeType="String"/>
|
||||
<attribute name="muted" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="pinned" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="reblogged" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="rebloggedAccountAvatar" optional="YES" attributeType="URI"/>
|
||||
<attribute name="rebloggedAccountDisplayName" optional="YES" attributeType="String"/>
|
||||
<attribute name="rebloggedAccountId" optional="YES" attributeType="String"/>
|
||||
<attribute name="rebloggedAccountUsername" optional="YES" attributeType="String"/>
|
||||
<attribute name="rebloggedStatusId" optional="YES" attributeType="String"/>
|
||||
<attribute name="reblogsCount" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="repliesCount" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="sensitive" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="spoilerText" optional="YES" attributeType="String"/>
|
||||
<attribute name="uri" attributeType="String"/>
|
||||
<attribute name="url" optional="YES" attributeType="URI"/>
|
||||
<attribute name="visibility" attributeType="String"/>
|
||||
<relationship name="attachmentRelation" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="AttachmentData" inverseName="statusRelation" inverseEntity="AttachmentData"/>
|
||||
<relationship name="pixelfedAccount" maxCount="1" deletionRule="No Action" destinationEntity="AccountData" inverseName="statuses" inverseEntity="AccountData"/>
|
||||
</entity>
|
||||
</model>
|
|
@ -0,0 +1,82 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="21513" systemVersion="22C65" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||
<entity name="AccountData" representedClassName="AccountData" syncable="YES">
|
||||
<attribute name="accessToken" optional="YES" attributeType="String"/>
|
||||
<attribute name="acct" attributeType="String"/>
|
||||
<attribute name="avatar" optional="YES" attributeType="URI"/>
|
||||
<attribute name="avatarData" optional="YES" attributeType="Binary"/>
|
||||
<attribute name="clientId" attributeType="String"/>
|
||||
<attribute name="clientSecret" attributeType="String"/>
|
||||
<attribute name="clientVapidKey" attributeType="String"/>
|
||||
<attribute name="createdAt" attributeType="String"/>
|
||||
<attribute name="displayName" optional="YES" attributeType="String"/>
|
||||
<attribute name="followersCount" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="followingCount" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="header" optional="YES" attributeType="URI"/>
|
||||
<attribute name="id" attributeType="String"/>
|
||||
<attribute name="locked" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="note" optional="YES" attributeType="String"/>
|
||||
<attribute name="serverUrl" attributeType="URI"/>
|
||||
<attribute name="statusesCount" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="url" optional="YES" attributeType="URI"/>
|
||||
<attribute name="username" attributeType="String"/>
|
||||
<relationship name="statuses" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="StatusData" inverseName="pixelfedAccount" inverseEntity="StatusData"/>
|
||||
</entity>
|
||||
<entity name="ApplicationSettings" representedClassName="ApplicationSettings" syncable="YES">
|
||||
<attribute name="avatarShape" attributeType="Integer 32" defaultValueString="1" usesScalarValueType="YES"/>
|
||||
<attribute name="currentAccount" optional="YES" attributeType="String"/>
|
||||
<attribute name="theme" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="tintColor" attributeType="Integer 32" defaultValueString="2" usesScalarValueType="YES"/>
|
||||
</entity>
|
||||
<entity name="AttachmentData" representedClassName="AttachmentData" syncable="YES">
|
||||
<attribute name="blurhash" optional="YES" attributeType="String"/>
|
||||
<attribute name="data" optional="YES" attributeType="Binary" allowsExternalBinaryDataStorage="YES"/>
|
||||
<attribute name="exifCamera" optional="YES" attributeType="String"/>
|
||||
<attribute name="exifCreatedDate" optional="YES" attributeType="String"/>
|
||||
<attribute name="exifExposure" optional="YES" attributeType="String"/>
|
||||
<attribute name="exifLens" optional="YES" attributeType="String"/>
|
||||
<attribute name="id" attributeType="String"/>
|
||||
<attribute name="metaImageHeight" optional="YES" attributeType="Integer 32" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="metaImageWidth" optional="YES" attributeType="Integer 32" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="previewUrl" optional="YES" attributeType="URI"/>
|
||||
<attribute name="remoteUrl" optional="YES" attributeType="URI"/>
|
||||
<attribute name="statusId" attributeType="String"/>
|
||||
<attribute name="text" optional="YES" attributeType="String"/>
|
||||
<attribute name="type" attributeType="String"/>
|
||||
<attribute name="url" attributeType="URI"/>
|
||||
<relationship name="statusRelation" maxCount="1" deletionRule="Cascade" destinationEntity="StatusData" inverseName="attachmentRelation" inverseEntity="StatusData"/>
|
||||
</entity>
|
||||
<entity name="StatusData" representedClassName="StatusData" syncable="YES">
|
||||
<attribute name="accountAvatar" optional="YES" attributeType="URI"/>
|
||||
<attribute name="accountDisplayName" optional="YES" attributeType="String"/>
|
||||
<attribute name="accountId" attributeType="String"/>
|
||||
<attribute name="accountUsername" optional="YES" attributeType="String"/>
|
||||
<attribute name="applicationName" optional="YES" attributeType="String"/>
|
||||
<attribute name="applicationWebsite" optional="YES" attributeType="URI"/>
|
||||
<attribute name="bookmarked" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="content" attributeType="String"/>
|
||||
<attribute name="createdAt" attributeType="String"/>
|
||||
<attribute name="favourited" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="favouritesCount" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="id" attributeType="String"/>
|
||||
<attribute name="inReplyToAccount" optional="YES" attributeType="String"/>
|
||||
<attribute name="inReplyToId" optional="YES" attributeType="String"/>
|
||||
<attribute name="muted" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="pinned" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="reblogged" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="rebloggedAccountAvatar" optional="YES" attributeType="URI"/>
|
||||
<attribute name="rebloggedAccountDisplayName" optional="YES" attributeType="String"/>
|
||||
<attribute name="rebloggedAccountId" optional="YES" attributeType="String"/>
|
||||
<attribute name="rebloggedAccountUsername" optional="YES" attributeType="String"/>
|
||||
<attribute name="rebloggedStatusId" optional="YES" attributeType="String"/>
|
||||
<attribute name="reblogsCount" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="repliesCount" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="sensitive" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="spoilerText" optional="YES" attributeType="String"/>
|
||||
<attribute name="uri" attributeType="String"/>
|
||||
<attribute name="url" optional="YES" attributeType="URI"/>
|
||||
<attribute name="visibility" attributeType="String"/>
|
||||
<relationship name="attachmentRelation" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="AttachmentData" inverseName="statusRelation" inverseEntity="AttachmentData"/>
|
||||
<relationship name="pixelfedAccount" maxCount="1" deletionRule="No Action" destinationEntity="AccountData" inverseName="statuses" inverseEntity="AccountData"/>
|
||||
</entity>
|
||||
</model>
|
|
@ -7,7 +7,7 @@
|
|||
import Foundation
|
||||
import MastodonKit
|
||||
|
||||
public class AttachmentViewModel {
|
||||
public class AttachmentViewModel: ObservableObject {
|
||||
public let id: String
|
||||
public let type: MediaAttachment.MediaAttachmentType
|
||||
public let url: URL
|
||||
|
@ -21,11 +21,11 @@ public class AttachmentViewModel {
|
|||
public let metaImageWidth: Int32?
|
||||
public let metaImageHeight: Int32?
|
||||
|
||||
public var data: Data?
|
||||
public var exifCamera: String?
|
||||
public var exifCreatedDate: String?
|
||||
public var exifExposure: String?
|
||||
public var exifLens: String?
|
||||
@Published public var exifCamera: String?
|
||||
@Published public var exifCreatedDate: String?
|
||||
@Published public var exifExposure: String?
|
||||
@Published public var exifLens: String?
|
||||
@Published public var data: Data?
|
||||
|
||||
init(id: String,
|
||||
type: MediaAttachment.MediaAttachmentType,
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
import Foundation
|
||||
import MastodonKit
|
||||
|
||||
public class StatusViewModel {
|
||||
public class StatusViewModel: ObservableObject {
|
||||
|
||||
public let uniqueId: UUID
|
||||
public let id: EntityId
|
||||
|
@ -18,7 +18,7 @@ public class StatusViewModel {
|
|||
public let account: Account
|
||||
public let inReplyToId: EntityId?
|
||||
public let inReplyToAccount: EntityId?
|
||||
public let reblog: Status?
|
||||
|
||||
public let createdAt: String
|
||||
public let reblogsCount: Int
|
||||
public let favouritesCount: Int
|
||||
|
@ -31,13 +31,16 @@ public class StatusViewModel {
|
|||
public let muted: Bool
|
||||
public let spoilerText: String?
|
||||
public let visibility: Status.Visibility
|
||||
public let mediaAttachments: [AttachmentViewModel]
|
||||
public let card: PreviewCard?
|
||||
public let mentions: [Mention]
|
||||
public let tags: [Tag]
|
||||
public let application: BaseApplication?
|
||||
public let place: Place?
|
||||
|
||||
public let reblogStatus: Status?
|
||||
|
||||
@Published public var mediaAttachments: [AttachmentViewModel]
|
||||
|
||||
public init(
|
||||
id: EntityId,
|
||||
content: Html,
|
||||
|
@ -75,7 +78,6 @@ public class StatusViewModel {
|
|||
self.application = application
|
||||
self.inReplyToId = inReplyToId
|
||||
self.inReplyToAccount = inReplyToAccount
|
||||
self.reblog = reblog
|
||||
self.createdAt = createdAt ?? Date().formatted(.iso8601)
|
||||
self.reblogsCount = reblogsCount
|
||||
self.favouritesCount = favouritesCount
|
||||
|
@ -93,42 +95,52 @@ public class StatusViewModel {
|
|||
self.mentions = mentions
|
||||
self.tags = tags
|
||||
self.place = place
|
||||
self.reblogStatus = nil
|
||||
}
|
||||
|
||||
init(status: Status) {
|
||||
|
||||
// If status has been rebloged we are saving orginal status here.
|
||||
let orginalStatus = status.reblog ?? status
|
||||
|
||||
self.uniqueId = UUID()
|
||||
self.id = status.id
|
||||
self.content = status.content
|
||||
self.uri = status.uri
|
||||
self.url = status.url
|
||||
self.account = status.account
|
||||
self.inReplyToId = status.inReplyToId
|
||||
self.inReplyToAccount = status.inReplyToAccount
|
||||
self.reblog = status.reblog
|
||||
self.createdAt = status.createdAt
|
||||
self.reblogsCount = status.reblogsCount
|
||||
self.favouritesCount = status.favouritesCount
|
||||
self.repliesCount = status.repliesCount
|
||||
self.reblogged = status.reblogged
|
||||
self.favourited = status.favourited
|
||||
self.sensitive = status.sensitive
|
||||
self.bookmarked = status.bookmarked
|
||||
self.pinned = status.pinned
|
||||
self.muted = status.muted
|
||||
self.spoilerText = status.spoilerText
|
||||
self.visibility = status.visibility
|
||||
self.card = status.card
|
||||
self.mentions = status.mentions
|
||||
self.tags = status.tags
|
||||
self.application = status.application
|
||||
self.place = status.place
|
||||
self.id = orginalStatus.id
|
||||
self.content = orginalStatus.content
|
||||
self.uri = orginalStatus.uri
|
||||
self.url = orginalStatus.url
|
||||
self.account = orginalStatus.account
|
||||
self.inReplyToId = orginalStatus.inReplyToId
|
||||
self.inReplyToAccount = orginalStatus.inReplyToAccount
|
||||
self.createdAt = orginalStatus.createdAt
|
||||
self.reblogsCount = orginalStatus.reblogsCount
|
||||
self.favouritesCount = orginalStatus.favouritesCount
|
||||
self.repliesCount = orginalStatus.repliesCount
|
||||
self.reblogged = orginalStatus.reblogged
|
||||
self.favourited = orginalStatus.favourited
|
||||
self.sensitive = orginalStatus.sensitive
|
||||
self.bookmarked = orginalStatus.bookmarked
|
||||
self.pinned = orginalStatus.pinned
|
||||
self.muted = orginalStatus.muted
|
||||
self.spoilerText = orginalStatus.spoilerText
|
||||
self.visibility = orginalStatus.visibility
|
||||
self.card = orginalStatus.card
|
||||
self.mentions = orginalStatus.mentions
|
||||
self.tags = orginalStatus.tags
|
||||
self.application = orginalStatus.application
|
||||
self.place = orginalStatus.place
|
||||
|
||||
var mediaAttachments: [AttachmentViewModel] = []
|
||||
for item in status.mediaAttachments {
|
||||
for item in orginalStatus.mediaAttachments {
|
||||
mediaAttachments.append(AttachmentViewModel(attachment: item))
|
||||
}
|
||||
|
||||
self.mediaAttachments = mediaAttachments
|
||||
|
||||
if status.reblog != nil {
|
||||
self.reblogStatus = status
|
||||
} else {
|
||||
self.reblogStatus = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -30,7 +30,7 @@ struct HomeFeedView: View {
|
|||
LazyVGrid(columns: gridColumns) {
|
||||
ForEach(dbStatuses, id: \.self) { item in
|
||||
NavigationLink(value: RouteurDestinations.status(
|
||||
id: item.id,
|
||||
id: item.rebloggedStatusId ?? item.id,
|
||||
blurhash: item.attachments().first?.blurhash,
|
||||
metaImageWidth: item.attachments().first?.metaImageWidth,
|
||||
metaImageHeight: item.attachments().first?.metaImageHeight)
|
||||
|
|
|
@ -74,8 +74,6 @@ struct NotificationsView: View {
|
|||
maxId: maxId,
|
||||
minId: minId,
|
||||
limit: 5)
|
||||
|
||||
await self.downloadAllImages(notifications: linkable.data)
|
||||
|
||||
self.minId = linkable.link?.minId
|
||||
self.maxId = linkable.link?.maxId
|
||||
|
@ -98,8 +96,6 @@ struct NotificationsView: View {
|
|||
andContext: self.applicationState.accountData,
|
||||
maxId: self.maxId,
|
||||
limit: self.defaultPageSize)
|
||||
|
||||
await self.downloadAllImages(notifications: linkable.data)
|
||||
|
||||
self.maxId = linkable.link?.maxId
|
||||
self.notifications.append(contentsOf: linkable.data)
|
||||
|
@ -125,8 +121,6 @@ struct NotificationsView: View {
|
|||
return
|
||||
}
|
||||
|
||||
await self.downloadAllImages(notifications: linkable.data)
|
||||
|
||||
self.minId = linkable.link?.minId
|
||||
self.notifications.insert(contentsOf: linkable.data, at: 0)
|
||||
} catch {
|
||||
|
|
|
@ -30,13 +30,17 @@ struct SignInView: View {
|
|||
|
||||
Button("Go") {
|
||||
Task {
|
||||
try await AuthorizationService.shared.signIn(serverAddress: serverAddress, { accountData in
|
||||
DispatchQueue.main.async {
|
||||
self.applicationState.accountData = accountData
|
||||
onSignInStateChenge?(.mainView)
|
||||
dismiss()
|
||||
}
|
||||
})
|
||||
do {
|
||||
try await AuthorizationService.shared.signIn(serverAddress: serverAddress, { accountData in
|
||||
DispatchQueue.main.async {
|
||||
self.applicationState.accountData = accountData
|
||||
onSignInStateChenge?(.mainView)
|
||||
dismiss()
|
||||
}
|
||||
})
|
||||
} catch {
|
||||
ErrorService.shared.handle(error, message: "Error during communication with server", showToastr: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,7 +33,7 @@ struct StatusView: View {
|
|||
var body: some View {
|
||||
ScrollView {
|
||||
if let statusViewModel = self.statusViewModel {
|
||||
VStack (alignment: .leading) {
|
||||
VStack (alignment: .leading) {
|
||||
ImagesCarousel(attachments: statusViewModel.mediaAttachments,
|
||||
selectedAttachmentId: $selectedAttachmentId,
|
||||
exifCamera: $exifCamera,
|
||||
|
@ -47,15 +47,16 @@ struct StatusView: View {
|
|||
}
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
NavigationLink(value: RouteurDestinations.userProfile(
|
||||
accountId: statusViewModel.account.id,
|
||||
accountDisplayName: statusViewModel.account.displayNameWithoutEmojis,
|
||||
accountUserName: statusViewModel.account.acct)
|
||||
) {
|
||||
UsernameRow(accountId: statusViewModel.account.id,
|
||||
accountAvatar: statusViewModel.account.avatar,
|
||||
accountDisplayName: statusViewModel.account.displayNameWithoutEmojis,
|
||||
accountUsername: statusViewModel.account.username)
|
||||
self.reblogInformation()
|
||||
|
||||
UsernameRow(accountId: statusViewModel.account.id,
|
||||
accountAvatar: statusViewModel.account.avatar,
|
||||
accountDisplayName: statusViewModel.account.displayNameWithoutEmojis,
|
||||
accountUsername: statusViewModel.account.acct)
|
||||
.onTapGesture {
|
||||
self.routerPath.navigate(to: .userProfile(accountId: statusViewModel.account.id,
|
||||
accountDisplayName: statusViewModel.account.displayNameWithoutEmojis,
|
||||
accountUserName: statusViewModel.account.acct))
|
||||
}
|
||||
|
||||
MarkdownFormattedText(statusViewModel.content.asMarkdown)
|
||||
|
@ -114,15 +115,7 @@ struct StatusView: View {
|
|||
// Get status from API.
|
||||
if let status = try await StatusService.shared.status(withId: self.statusId, and: self.applicationState.accountData) {
|
||||
let statusViewModel = StatusViewModel(status: status)
|
||||
|
||||
// Download images and recalculate exif data.
|
||||
let allImages = await HomeTimelineService.shared.fetchAllImages(statuses: [status])
|
||||
for attachment in statusViewModel.mediaAttachments {
|
||||
if let data = allImages[attachment.id] {
|
||||
attachment.set(data: data)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
self.statusViewModel = statusViewModel
|
||||
self.selectedAttachmentId = statusViewModel.mediaAttachments.first?.id ?? String.empty()
|
||||
self.firstLoadFinished = true
|
||||
|
@ -146,6 +139,21 @@ struct StatusView: View {
|
|||
}
|
||||
}
|
||||
|
||||
@ViewBuilder func reblogInformation() -> some View {
|
||||
if let reblogStatus = self.statusViewModel?.reblogStatus {
|
||||
HStack(alignment: .center, spacing: 4) {
|
||||
UserAvatar(accountAvatar: reblogStatus.account.avatar, size: .mini)
|
||||
Text(reblogStatus.account.displayNameWithoutEmojis)
|
||||
Image(systemName: "paperplane")
|
||||
.padding(.trailing, 8)
|
||||
}
|
||||
.font(.footnote)
|
||||
.foregroundColor(Color.mainTextColor.opacity(0.4))
|
||||
.background(Color.mainTextColor.opacity(0.1))
|
||||
.clipShape(Capsule())
|
||||
}
|
||||
}
|
||||
|
||||
private func setAttachment(_ attachmentData: AttachmentData) {
|
||||
exifCamera = attachmentData.exifCamera
|
||||
exifExposure = attachmentData.exifExposure
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
//
|
||||
// https://mczachurski.dev
|
||||
// Copyright © 2023 Marcin Czachurski and the repository contributors.
|
||||
// Licensed under the MIT License.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ImageCarouselPicture: View {
|
||||
@ObservedObject public var attachment: AttachmentViewModel
|
||||
|
||||
private let onImageDownloaded: (AttachmentViewModel, Data) -> Void
|
||||
|
||||
init(attachment: AttachmentViewModel, onImageDownloaded: @escaping (_: AttachmentViewModel, _: Data) -> Void) {
|
||||
self.attachment = attachment
|
||||
self.onImageDownloaded = onImageDownloaded
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
if let data = attachment.data, let image = UIImage(data: data) {
|
||||
Image(uiImage: image)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
} else {
|
||||
BlurredImage(blurhash: attachment.blurhash)
|
||||
.task {
|
||||
do {
|
||||
// Download image and recalculate exif data.
|
||||
if let imageData = try await RemoteFileService.shared.fetchData(url: attachment.url) {
|
||||
self.onImageDownloaded(attachment, imageData)
|
||||
}
|
||||
} catch {
|
||||
ErrorService.shared.handle(error, message: "Connot download image for status")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -10,14 +10,15 @@ struct ImageRow: View {
|
|||
private let status: StatusData
|
||||
private let imageHeight: Double
|
||||
private let imageWidth: Double
|
||||
private let uiImage:UIImage?
|
||||
private let attachmentData: AttachmentData?
|
||||
|
||||
@State private var uiImage:UIImage?
|
||||
|
||||
init(statusData: StatusData) {
|
||||
self.status = statusData
|
||||
self.attachmentData = statusData.attachments().first
|
||||
|
||||
if let attachmenData = self.attachmentData, let uiImage = UIImage(data: attachmenData.data) {
|
||||
if let imageData = self.attachmentData?.data, let uiImage = UIImage(data: imageData) {
|
||||
self.uiImage = uiImage
|
||||
|
||||
let imgHeight = uiImage.size.height
|
||||
|
@ -27,6 +28,13 @@ struct ImageRow: View {
|
|||
|
||||
self.imageWidth = UIScreen.main.bounds.width
|
||||
self.imageHeight = (calculatedHeight > 0 && calculatedHeight < .infinity) ? calculatedHeight : UIScreen.main.bounds.width
|
||||
} else if let imgWidth = attachmentData?.metaImageWidth, let imgHeight = attachmentData?.metaImageHeight {
|
||||
let divider = Double(imgWidth) / UIScreen.main.bounds.size.width
|
||||
let calculatedHeight = Double(imgHeight) / divider
|
||||
|
||||
self.uiImage = nil
|
||||
self.imageWidth = UIScreen.main.bounds.width
|
||||
self.imageHeight = (calculatedHeight > 0 && calculatedHeight < .infinity) ? calculatedHeight : UIScreen.main.bounds.width
|
||||
} else {
|
||||
self.uiImage = nil
|
||||
self.imageHeight = UIScreen.main.bounds.width
|
||||
|
@ -35,40 +43,48 @@ struct ImageRow: View {
|
|||
}
|
||||
|
||||
var body: some View {
|
||||
if let uiImage, let attachmentData {
|
||||
ZStack {
|
||||
if self.status.sensitive {
|
||||
ContentWarning(blurhash: attachmentData.blurhash, spoilerText: self.status.spoilerText) {
|
||||
if let attachmentData {
|
||||
if let uiImage {
|
||||
ZStack {
|
||||
if self.status.sensitive {
|
||||
ContentWarning(blurhash: attachmentData.blurhash, spoilerText: self.status.spoilerText) {
|
||||
Image(uiImage: uiImage)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.transition(.opacity)
|
||||
}
|
||||
} else {
|
||||
Image(uiImage: uiImage)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.transition(.opacity)
|
||||
}
|
||||
} else {
|
||||
Image(uiImage: uiImage)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
}
|
||||
|
||||
if let count = self.status.attachments().count, count > 1 {
|
||||
BottomRight {
|
||||
Text("1 / \(count)")
|
||||
.padding(.horizontal, 6)
|
||||
.padding(.vertical, 3)
|
||||
.font(.caption2)
|
||||
.foregroundColor(.black)
|
||||
.background(.ultraThinMaterial, in: Capsule())
|
||||
}.padding()
|
||||
|
||||
if let count = self.status.attachments().count, count > 1 {
|
||||
BottomRight {
|
||||
Text("1 / \(count)")
|
||||
.padding(.horizontal, 6)
|
||||
.padding(.vertical, 3)
|
||||
.font(.caption2)
|
||||
.foregroundColor(.black)
|
||||
.background(.ultraThinMaterial, in: Capsule())
|
||||
}.padding()
|
||||
}
|
||||
}
|
||||
.frame(width: self.imageWidth, height: self.imageHeight)
|
||||
} else {
|
||||
BlurredImage(blurhash: attachmentData.blurhash)
|
||||
.frame(width: self.imageWidth, height: self.imageHeight)
|
||||
.task {
|
||||
do {
|
||||
if let imageData = try await RemoteFileService.shared.fetchData(url: attachmentData.url) {
|
||||
HomeTimelineService.shared.updateAttachmentDataImage(attachmentData: attachmentData, imageData: imageData)
|
||||
self.uiImage = UIImage(data: imageData)
|
||||
}
|
||||
} catch {
|
||||
ErrorService.shared.handle(error, message: "Cannot download the image.")
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(width: self.imageWidth, height: self.imageHeight)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ImageRow_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
Text("")
|
||||
// ImageRow(status: [])
|
||||
}
|
||||
}
|
||||
|
|
|
@ -91,12 +91,14 @@ struct ImageRowAsync: View {
|
|||
}
|
||||
|
||||
private func recalculateSizeOfDownloadedImage(imageResponse: ImageResponse) {
|
||||
if heightWasPrecalculated == false {
|
||||
let imgHeight = imageResponse.image.size.height
|
||||
let imgWidth = imageResponse.image.size.width
|
||||
let calculatedHeight = self.calculateHeight(width: imgWidth, height: imgHeight)
|
||||
self.imageHeight = (calculatedHeight > 0 && calculatedHeight < .infinity) ? calculatedHeight : UIScreen.main.bounds.width
|
||||
guard heightWasPrecalculated == false else {
|
||||
return
|
||||
}
|
||||
|
||||
let imgHeight = imageResponse.image.size.height
|
||||
let imgWidth = imageResponse.image.size.width
|
||||
let calculatedHeight = self.calculateHeight(width: imgWidth, height: imgHeight)
|
||||
self.imageHeight = (calculatedHeight > 0 && calculatedHeight < .infinity) ? calculatedHeight : UIScreen.main.bounds.width
|
||||
}
|
||||
|
||||
private func calculateHeight(width: Double, height: Double) -> CGFloat {
|
||||
|
|
|
@ -5,11 +5,14 @@
|
|||
//
|
||||
|
||||
import SwiftUI
|
||||
import MastodonKit
|
||||
|
||||
struct ImagesCarousel: View {
|
||||
@State public var attachments: [AttachmentViewModel]
|
||||
@State private var height: Double = 0.0
|
||||
@State private var selected = String.empty()
|
||||
@State private var imageHeight: Double
|
||||
@State private var imageWidth: Double
|
||||
@State private var selected: String
|
||||
@State private var heightWasPrecalculated: Bool
|
||||
|
||||
@Binding public var selectedAttachmentId: String?
|
||||
@Binding public var exifCamera: String?
|
||||
|
@ -17,18 +20,64 @@ struct ImagesCarousel: View {
|
|||
@Binding public var exifCreatedDate: String?
|
||||
@Binding public var exifLens: String?
|
||||
|
||||
init(attachments: [AttachmentViewModel],
|
||||
selectedAttachmentId: Binding<String?>,
|
||||
exifCamera: Binding<String?>,
|
||||
exifExposure: Binding<String?>,
|
||||
exifCreatedDate: Binding<String?>,
|
||||
exifLens: Binding<String?>
|
||||
) {
|
||||
_selectedAttachmentId = selectedAttachmentId
|
||||
_exifCamera = exifCamera
|
||||
_exifExposure = exifExposure
|
||||
_exifCreatedDate = exifCreatedDate
|
||||
_exifLens = exifLens
|
||||
|
||||
self.attachments = attachments
|
||||
self.selected = String.empty()
|
||||
|
||||
var imgHeight = 0.0
|
||||
var imgWidth = 0.0
|
||||
|
||||
for item in attachments {
|
||||
let attachmentheight = Double((item.meta as? ImageMetadata)?.original?.height ?? 0)
|
||||
if attachmentheight > imgHeight {
|
||||
imgHeight = attachmentheight
|
||||
imgWidth = Double((item.meta as? ImageMetadata)?.original?.width ?? 0)
|
||||
}
|
||||
}
|
||||
|
||||
if imgHeight > 0 && imgWidth > 0 {
|
||||
let divider = Double(imgWidth) / UIScreen.main.bounds.size.width
|
||||
let calculatedHeight = Double(imgHeight) / divider
|
||||
|
||||
self.imageWidth = UIScreen.main.bounds.width
|
||||
self.imageHeight = (calculatedHeight > 0 && calculatedHeight < .infinity) ? calculatedHeight : UIScreen.main.bounds.width
|
||||
self.heightWasPrecalculated = true
|
||||
} else {
|
||||
self.imageWidth = UIScreen.main.bounds.width
|
||||
self.imageHeight = UIScreen.main.bounds.width * 0.75
|
||||
self.heightWasPrecalculated = false
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
TabView(selection: $selected) {
|
||||
ForEach(attachments, id: \.id) { attachment in
|
||||
if let data = attachment.data, let image = UIImage(data: data) {
|
||||
Image(uiImage: image)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.tag(attachment.id)
|
||||
ImageCarouselPicture(attachment: attachment) { (attachment, imageData) in
|
||||
withAnimation {
|
||||
self.recalculateImageHeight(imageData: imageData)
|
||||
}
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
|
||||
attachment.set(data: imageData)
|
||||
}
|
||||
|
||||
}
|
||||
.tag(attachment.id)
|
||||
}
|
||||
}
|
||||
.frame(height: CGFloat(self.height))
|
||||
.frame(height: CGFloat(self.imageHeight))
|
||||
.tabViewStyle(PageTabViewStyle())
|
||||
.onChange(of: selected, perform: { index in
|
||||
self.selectedAttachmentId = selected
|
||||
|
@ -42,11 +91,14 @@ struct ImagesCarousel: View {
|
|||
})
|
||||
.onAppear {
|
||||
self.selected = self.attachments.first?.id ?? String.empty()
|
||||
self.calculateImageHeight()
|
||||
}
|
||||
}
|
||||
|
||||
private func calculateImageHeight() {
|
||||
private func recalculateImageHeight(imageData: Data) {
|
||||
guard heightWasPrecalculated == false else {
|
||||
return
|
||||
}
|
||||
|
||||
var imageHeight = 0.0
|
||||
var imageWidth = 0.0
|
||||
|
||||
|
@ -59,14 +111,14 @@ struct ImagesCarousel: View {
|
|||
}
|
||||
}
|
||||
|
||||
if let image = UIImage(data: imageData) {
|
||||
if image.size.height > imageHeight {
|
||||
imageHeight = image.size.height
|
||||
imageWidth = image.size.width
|
||||
}
|
||||
}
|
||||
|
||||
let divider = imageWidth / UIScreen.main.bounds.size.width
|
||||
self.height = imageHeight / divider
|
||||
}
|
||||
}
|
||||
|
||||
struct ImagesCarousel_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
Text("")
|
||||
// ImagesCarousel(attachments: [], exifCamera: .constant(""), exifExposure: .constant(""), exifCreatedDate: .constant(""), exifLens: .constant(""))
|
||||
self.imageHeight = imageHeight / divider
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,10 +12,21 @@ struct NotificationRow: View {
|
|||
@EnvironmentObject var applicationState: ApplicationState
|
||||
@EnvironmentObject var routerPath: RouterPath
|
||||
|
||||
@State public var notification: MastodonKit.Notification
|
||||
@State private var image: SwiftUI.Image?
|
||||
|
||||
private var attachment: MediaAttachment?
|
||||
private var notification: MastodonKit.Notification
|
||||
private let contentWidth = Int(UIScreen.main.bounds.width) - 150
|
||||
|
||||
public init(notification: MastodonKit.Notification) {
|
||||
self.notification = notification
|
||||
self.attachment = notification.status?.getAllImageMediaAttachments().first
|
||||
|
||||
if let attachment, let previewUrl = attachment.previewUrl, let imageFromCache = CacheImageService.shared.getImage(for: previewUrl) {
|
||||
self.image = imageFromCache
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
HStack (alignment: .top, spacing: 8) {
|
||||
ZStack {
|
||||
|
@ -54,23 +65,27 @@ struct NotificationRow: View {
|
|||
|
||||
switch self.notification.type {
|
||||
case .favourite, .reblog, .mention, .status, .poll, .update:
|
||||
if let status = self.notification.status, let statusViewModel = StatusViewModel(status: status) {
|
||||
HStack(alignment: .top) {
|
||||
Spacer()
|
||||
if let attachment = statusViewModel.mediaAttachments.filter({ attachment in
|
||||
attachment.type == MediaAttachment.MediaAttachmentType.image
|
||||
}).first {
|
||||
if let cachedImage = CacheImageService.shared.getImage(for: attachment.url) {
|
||||
cachedImage
|
||||
.resizable()
|
||||
.frame(width: 50, height: 50)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 8))
|
||||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
HStack(alignment: .top) {
|
||||
Spacer()
|
||||
if let attachment {
|
||||
if let cachedImage = self.image {
|
||||
cachedImage
|
||||
.resizable()
|
||||
.frame(width: 50, height: 50)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 8))
|
||||
} else {
|
||||
EmptyView()
|
||||
BlurredImage(blurhash: attachment.blurhash)
|
||||
.frame(width: 50, height: 50)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 8))
|
||||
.task {
|
||||
await CacheImageService.shared.downloadImage(url: attachment.previewUrl)
|
||||
if let previewUrl = attachment.previewUrl, let imageFromCache = CacheImageService.shared.getImage(for: previewUrl) {
|
||||
self.image = imageFromCache
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
case .follow, .followRequest, .adminSignUp:
|
||||
|
|
|
@ -11,14 +11,16 @@ struct UserAvatar: View {
|
|||
@EnvironmentObject var applicationState: ApplicationState
|
||||
|
||||
public enum Size {
|
||||
case list, comment, profile
|
||||
case mini, list, comment, profile
|
||||
|
||||
public var size: CGSize {
|
||||
switch self {
|
||||
case .list:
|
||||
return .init(width: 48, height: 48)
|
||||
case .mini:
|
||||
return .init(width: 20, height: 20)
|
||||
case .comment:
|
||||
return .init(width: 32, height: 32)
|
||||
case .list:
|
||||
return .init(width: 48, height: 48)
|
||||
case .profile:
|
||||
return .init(width: 96, height: 96)
|
||||
}
|
||||
|
@ -54,6 +56,9 @@ struct UserAvatar: View {
|
|||
}
|
||||
}
|
||||
.priority(.high)
|
||||
.task {
|
||||
await CacheImageService.shared.downloadImage(url: accountAvatar)
|
||||
}
|
||||
.frame(width: size.size.width, height: size.size.height)
|
||||
}
|
||||
} else {
|
||||
|
|
Loading…
Reference in New Issue