View refactoring

This commit is contained in:
Marcin Czachursk 2023-01-03 14:09:22 +01:00
parent cc73acedd0
commit 271b4f4e0f
25 changed files with 868 additions and 432 deletions

View File

@ -15,8 +15,20 @@
F800480A2961EA1900E6868A /* AttachmentDataHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F80048092961EA1900E6868A /* AttachmentDataHandler.swift */; };
F8341F90295C636C009C8EE6 /* UIImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8341F8F295C636C009C8EE6 /* UIImage.swift */; };
F8341F92295C63BB009C8EE6 /* ImageStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8341F91295C63BB009C8EE6 /* ImageStatus.swift */; };
F83901A4295D864D00456AE2 /* TagView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F83901A3295D864D00456AE2 /* TagView.swift */; };
F83901A6295D8EC000456AE2 /* LabelIconView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F83901A5295D8EC000456AE2 /* LabelIconView.swift */; };
F83901A4295D864D00456AE2 /* Tag.swift in Sources */ = {isa = PBXBuildFile; fileRef = F83901A3295D864D00456AE2 /* Tag.swift */; };
F83901A6295D8EC000456AE2 /* LabelIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = F83901A5295D8EC000456AE2 /* LabelIcon.swift */; };
F85D4971296402DC00751DF7 /* AuthorizationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85D4970296402DC00751DF7 /* AuthorizationService.swift */; };
F85D4973296406E700751DF7 /* BottomRight.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85D4972296406E700751DF7 /* BottomRight.swift */; };
F85D4975296407F100751DF7 /* TimelineService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85D4974296407F100751DF7 /* TimelineService.swift */; };
F85D497729640A5200751DF7 /* ImageRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85D497629640A5200751DF7 /* ImageRow.swift */; };
F85D497929640B9D00751DF7 /* ImagesCarousel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85D497829640B9D00751DF7 /* ImagesCarousel.swift */; };
F85D497B29640C8200751DF7 /* UsernameRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85D497A29640C8200751DF7 /* UsernameRow.swift */; };
F85D497D29640D5900751DF7 /* InteractionRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85D497C29640D5900751DF7 /* InteractionRow.swift */; };
F85D497F296416C800751DF7 /* CommentsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85D497E296416C800751DF7 /* CommentsSection.swift */; };
F85D4981296417F700751DF7 /* MastodonClientAuthenticated+Context.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85D4980296417F700751DF7 /* MastodonClientAuthenticated+Context.swift */; };
F85D498329642FAC00751DF7 /* AttachmentData+Comperable.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85D498229642FAC00751DF7 /* AttachmentData+Comperable.swift */; };
F85D49852964301800751DF7 /* StatusData+Attachments.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85D49842964301800751DF7 /* StatusData+Attachments.swift */; };
F85D49872964334100751DF7 /* String+Date.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85D49862964334100751DF7 /* String+Date.swift */; };
F866F6A0296040A8002E8F88 /* ApplicationSettings+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = F866F69E296040A8002E8F88 /* ApplicationSettings+CoreDataClass.swift */; };
F866F6A1296040A8002E8F88 /* ApplicationSettings+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = F866F69F296040A8002E8F88 /* ApplicationSettings+CoreDataProperties.swift */; };
F866F6A329604161002E8F88 /* AccountDataHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F866F6A229604161002E8F88 /* AccountDataHandler.swift */; };
@ -52,8 +64,20 @@
F80048092961EA1900E6868A /* AttachmentDataHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentDataHandler.swift; sourceTree = "<group>"; };
F8341F8F295C636C009C8EE6 /* UIImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIImage.swift; sourceTree = "<group>"; };
F8341F91295C63BB009C8EE6 /* ImageStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageStatus.swift; sourceTree = "<group>"; };
F83901A3295D864D00456AE2 /* TagView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagView.swift; sourceTree = "<group>"; };
F83901A5295D8EC000456AE2 /* LabelIconView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabelIconView.swift; sourceTree = "<group>"; };
F83901A3295D864D00456AE2 /* Tag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tag.swift; sourceTree = "<group>"; };
F83901A5295D8EC000456AE2 /* LabelIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabelIcon.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>"; };
F85D4974296407F100751DF7 /* TimelineService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineService.swift; sourceTree = "<group>"; };
F85D497629640A5200751DF7 /* ImageRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageRow.swift; sourceTree = "<group>"; };
F85D497829640B9D00751DF7 /* ImagesCarousel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagesCarousel.swift; sourceTree = "<group>"; };
F85D497A29640C8200751DF7 /* UsernameRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UsernameRow.swift; sourceTree = "<group>"; };
F85D497C29640D5900751DF7 /* InteractionRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractionRow.swift; sourceTree = "<group>"; };
F85D497E296416C800751DF7 /* CommentsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentsSection.swift; sourceTree = "<group>"; };
F85D4980296417F700751DF7 /* MastodonClientAuthenticated+Context.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MastodonClientAuthenticated+Context.swift"; sourceTree = "<group>"; };
F85D498229642FAC00751DF7 /* AttachmentData+Comperable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AttachmentData+Comperable.swift"; sourceTree = "<group>"; };
F85D49842964301800751DF7 /* StatusData+Attachments.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StatusData+Attachments.swift"; sourceTree = "<group>"; };
F85D49862964334100751DF7 /* String+Date.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Date.swift"; sourceTree = "<group>"; };
F866F69E296040A8002E8F88 /* ApplicationSettings+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ApplicationSettings+CoreDataClass.swift"; sourceTree = "<group>"; };
F866F69F296040A8002E8F88 /* ApplicationSettings+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ApplicationSettings+CoreDataProperties.swift"; sourceTree = "<group>"; };
F866F6A229604161002E8F88 /* AccountDataHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountDataHandler.swift; sourceTree = "<group>"; };
@ -111,6 +135,8 @@
isa = PBXGroup;
children = (
F8341F8F295C636C009C8EE6 /* UIImage.swift */,
F85D4980296417F700751DF7 /* MastodonClientAuthenticated+Context.swift */,
F85D49862964334100751DF7 /* String+Date.swift */,
);
path = Extensions;
sourceTree = "<group>";
@ -130,8 +156,10 @@
children = (
F80047FF2961850500E6868A /* AttachmentData+CoreDataClass.swift */,
F80048002961850500E6868A /* AttachmentData+CoreDataProperties.swift */,
F85D498229642FAC00751DF7 /* AttachmentData+Comperable.swift */,
F80048012961850500E6868A /* StatusData+CoreDataClass.swift */,
F80048022961850500E6868A /* StatusData+CoreDataProperties.swift */,
F85D49842964301800751DF7 /* StatusData+Attachments.swift */,
F866F69E296040A8002E8F88 /* ApplicationSettings+CoreDataClass.swift */,
F866F69F296040A8002E8F88 /* ApplicationSettings+CoreDataProperties.swift */,
F88FAD28295F43B8009B20C9 /* AccountData+CoreDataClass.swift */,
@ -156,8 +184,14 @@
F83901A2295D863B00456AE2 /* Widgets */ = {
isa = PBXGroup;
children = (
F83901A3295D864D00456AE2 /* TagView.swift */,
F83901A5295D8EC000456AE2 /* LabelIconView.swift */,
F83901A3295D864D00456AE2 /* Tag.swift */,
F83901A5295D8EC000456AE2 /* LabelIcon.swift */,
F85D4972296406E700751DF7 /* BottomRight.swift */,
F85D497629640A5200751DF7 /* ImageRow.swift */,
F85D497829640B9D00751DF7 /* ImagesCarousel.swift */,
F85D497A29640C8200751DF7 /* UsernameRow.swift */,
F85D497C29640D5900751DF7 /* InteractionRow.swift */,
F85D497E296416C800751DF7 /* CommentsSection.swift */,
);
path = Widgets;
sourceTree = "<group>";
@ -210,6 +244,8 @@
isa = PBXGroup;
children = (
F88FAD31295F5029009B20C9 /* RemoteFileService.swift */,
F85D4970296402DC00751DF7 /* AuthorizationService.swift */,
F85D4974296407F100751DF7 /* TimelineService.swift */,
);
path = Services;
sourceTree = "<group>";
@ -290,30 +326,41 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
F85D497729640A5200751DF7 /* ImageRow.swift in Sources */,
F80048082961E6DE00E6868A /* StatusDataHandler.swift in Sources */,
F866F6A0296040A8002E8F88 /* ApplicationSettings+CoreDataClass.swift in Sources */,
F88FAD23295F3FC4009B20C9 /* LocalFeedView.swift in Sources */,
F88FAD2B295F43B8009B20C9 /* AccountData+CoreDataProperties.swift in Sources */,
F85D4975296407F100751DF7 /* TimelineService.swift in Sources */,
F80048062961850500E6868A /* StatusData+CoreDataProperties.swift in Sources */,
F88FAD21295F3944009B20C9 /* HomeFeedView.swift in Sources */,
F88C2475295C37BB0006098B /* CoreDataHandler.swift in Sources */,
F88FAD2A295F43B8009B20C9 /* AccountData+CoreDataClass.swift in Sources */,
F85D49872964334100751DF7 /* String+Date.swift in Sources */,
F8341F92295C63BB009C8EE6 /* ImageStatus.swift in Sources */,
F85D498329642FAC00751DF7 /* AttachmentData+Comperable.swift in Sources */,
F85D497B29640C8200751DF7 /* UsernameRow.swift in Sources */,
F85D497929640B9D00751DF7 /* ImagesCarousel.swift in Sources */,
F80048052961850500E6868A /* StatusData+CoreDataClass.swift in Sources */,
F80048042961850500E6868A /* AttachmentData+CoreDataProperties.swift in Sources */,
F83901A6295D8EC000456AE2 /* LabelIconView.swift in Sources */,
F83901A6295D8EC000456AE2 /* LabelIcon.swift in Sources */,
F800480A2961EA1900E6868A /* AttachmentDataHandler.swift in Sources */,
F80048032961850500E6868A /* AttachmentData+CoreDataClass.swift in Sources */,
F8341F90295C636C009C8EE6 /* UIImage.swift in Sources */,
F85D4981296417F700751DF7 /* MastodonClientAuthenticated+Context.swift in Sources */,
F88C246E295C37B80006098B /* MainView.swift in Sources */,
F88C2478295C37BB0006098B /* Vernissage.xcdatamodeld in Sources */,
F88C2482295C3A4F0006098B /* DetailsView.swift in Sources */,
F866F6A329604161002E8F88 /* AccountDataHandler.swift in Sources */,
F85D497F296416C800751DF7 /* CommentsSection.swift in Sources */,
F88C2486295C48030006098B /* HTMLFotmattedText.swift in Sources */,
F866F6A529604194002E8F88 /* ApplicationSettingsHandler.swift in Sources */,
F85D49852964301800751DF7 /* StatusData+Attachments.swift in Sources */,
F85D497D29640D5900751DF7 /* InteractionRow.swift in Sources */,
F866F6A729604629002E8F88 /* SignInView.swift in Sources */,
F88C246C295C37B80006098B /* VernissageApp.swift in Sources */,
F83901A4295D864D00456AE2 /* TagView.swift in Sources */,
F85D4971296402DC00751DF7 /* AuthorizationService.swift in Sources */,
F83901A4295D864D00456AE2 /* Tag.swift in Sources */,
F88FAD25295F3FF7009B20C9 /* FederatedFeedView.swift in Sources */,
F88FAD32295F5029009B20C9 /* RemoteFileService.swift in Sources */,
F88FAD27295F400E009B20C9 /* NotificationsView.swift in Sources */,
@ -321,6 +368,7 @@
F866F6A1296040A8002E8F88 /* ApplicationSettings+CoreDataProperties.swift in Sources */,
F866F6AE29606367002E8F88 /* ApplicationViewMode.swift in Sources */,
F866F6AA29605AFA002E8F88 /* SceneDelegate.swift in Sources */,
F85D4973296406E700751DF7 /* BottomRight.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};

View File

@ -0,0 +1,14 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the MIT License.
//
import Foundation
extension AttachmentData : Comparable {
public static func < (lhs: AttachmentData, rhs: AttachmentData) -> Bool {
lhs.id < rhs.id
}
}

View File

@ -0,0 +1,13 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the MIT License.
//
import Foundation
extension StatusData {
func attachments() -> [AttachmentData] {
return self.attachmentRelation?.sorted(by: <) ?? []
}
}

View File

@ -62,9 +62,3 @@ extension StatusData {
extension StatusData : Identifiable {
}
extension StatusData {
func attachments() -> [AttachmentData] {
return Array(self.attachmentRelation ?? [])
}
}

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 MastodonClientAuthenticated {
func getContext(for statusId: String) async throws -> Context {
let request = try Self.request(
for: baseURL,
target: Mastodon.Statuses.context(statusId),
withBearerToken: token
)
let (data, _) = try await urlSession.data(for: request)
return try JSONDecoder().decode(Context.self, from: data)
}
}

View File

@ -0,0 +1,104 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the MIT License.
//
import Foundation
extension String {
public enum DateFormatType {
/// The ISO8601 formatted year "yyyy" i.e. 1997
case isoYear
/// The ISO8601 formatted year and month "yyyy-MM" i.e. 1997-07
case isoYearMonth
/// The ISO8601 formatted date "yyyy-MM-dd" i.e. 1997-07-16
case isoDate
/// The ISO8601 formatted date and time "yyyy-MM-dd'T'HH:mmZ" i.e. 1997-07-16T19:20+01:00
case isoDateTime
/// The ISO8601 formatted date, time and sec "yyyy-MM-dd'T'HH:mm:ssZ" i.e. 1997-07-16T19:20:30+01:00
case isoDateTimeSec
/// The ISO8601 formatted date, time and millisec "yyyy-MM-dd'T'HH:mm:ss.SSSZ" i.e. 1997-07-16T19:20:30.45+01:00
case isoDateTimeMilliSec
/// The dotNet formatted date "/Date(%d%d)/" i.e. "/Date(1268123281843)/"
case dotNet
/// The RSS formatted date "EEE, d MMM yyyy HH:mm:ss ZZZ" i.e. "Fri, 09 Sep 2011 15:26:08 +0200"
case rss
/// The Alternative RSS formatted date "d MMM yyyy HH:mm:ss ZZZ" i.e. "09 Sep 2011 15:26:08 +0200"
case altRSS
/// The http header formatted date "EEE, dd MM yyyy HH:mm:ss ZZZ" i.e. "Tue, 15 Nov 1994 12:45:26 GMT"
case httpHeader
/// A generic standard format date i.e. "EEE MMM dd HH:mm:ss Z yyyy"
case standard
/// A custom date format string
case custom(String)
/// The local formatted date and time "yyyy-MM-dd HH:mm:ss" i.e. 1997-07-16 19:20:00
case localDateTimeSec
/// The local formatted date "yyyy-MM-dd" i.e. 1997-07-16
case localDate
/// The local formatted time "hh:mm a" i.e. 07:20 am
case localTimeWithNoon
/// The local formatted date and time "yyyyMMddHHmmss" i.e. 19970716192000
case localPhotoSave
case birthDateFormatOne
case birthDateFormatTwo
///
case messageRTetriveFormat
///
case emailTimePreview
var stringFormat:String {
switch self {
//handle iso Time
case .birthDateFormatOne: return "dd/MM/YYYY"
case .birthDateFormatTwo: return "dd-MM-YYYY"
case .isoYear: return "yyyy"
case .isoYearMonth: return "yyyy-MM"
case .isoDate: return "yyyy-MM-dd"
case .isoDateTime: return "yyyy-MM-dd'T'HH:mmZ"
case .isoDateTimeSec: return "yyyy-MM-dd'T'HH:mm:ssZ"
case .isoDateTimeMilliSec: return "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
case .dotNet: return "/Date(%d%f)/"
case .rss: return "EEE, d MMM yyyy HH:mm:ss ZZZ"
case .altRSS: return "d MMM yyyy HH:mm:ss ZZZ"
case .httpHeader: return "EEE, dd MM yyyy HH:mm:ss ZZZ"
case .standard: return "EEE MMM dd HH:mm:ss Z yyyy"
case .custom(let customFormat): return customFormat
//handle local Time
case .localDateTimeSec: return "yyyy-MM-dd HH:mm:ss"
case .localTimeWithNoon: return "hh:mm a"
case .localDate: return "yyyy-MM-dd"
case .localPhotoSave: return "yyyyMMddHHmmss"
case .messageRTetriveFormat: return "yyyy-MM-dd'T'HH:mm:ssZZZZZ"
case .emailTimePreview: return "dd MMM yyyy, h:mm a"
}
}
}
func toDate(_ format: DateFormatType = .isoDate) -> Date?{
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = format.stringFormat
let date = dateFormatter.date(from: self)
return date
}
}

View File

@ -11,13 +11,17 @@ struct HTMLFormattedText: UIViewRepresentable {
let text: String
private let textView = UITextView()
private let fontSize: Int
private let width: Int
init(_ content: String) {
init(_ content: String, withFontSize fontSize: Int = 16, andWidth width: Int? = nil) {
self.text = content
self.fontSize = fontSize
self.width = width ?? Int(UIScreen.main.bounds.width) - 16
}
func makeUIView(context: UIViewRepresentableContext<Self>) -> UITextView {
textView.widthAnchor.constraint(equalToConstant:UIScreen.main.bounds.width - 16).isActive = true
textView.widthAnchor.constraint(equalToConstant: CGFloat(self.width)).isActive = true
textView.isSelectable = false
textView.isUserInteractionEnabled = false
textView.translatesAutoresizingMaskIntoConstraints = false
@ -42,12 +46,12 @@ struct HTMLFormattedText: UIViewRepresentable {
}
let largeAttributes = [
NSAttributedString.Key.font: UIFont.systemFont(ofSize: 16),
NSAttributedString.Key.font: UIFont.systemFont(ofSize: CGFloat(self.fontSize)),
NSAttributedString.Key.foregroundColor: UIColor(Color("mainTextColor"))
]
let linkAttributes = [
NSAttributedString.Key.font: UIFont.systemFont(ofSize: 16),
NSAttributedString.Key.font: UIFont.systemFont(ofSize: CGFloat(self.fontSize)),
NSAttributedString.Key.foregroundColor: UIColor(Color("AccentColor"))
]

View File

@ -3,7 +3,6 @@
// Copyright © 2022 Marcin Czachurski and the repository contributors.
// Licensed under the MIT License.
//
import SwiftUI
import MastodonSwift

View File

@ -0,0 +1,168 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the MIT License.
//
import Foundation
import MastodonSwift
public class AuthorizationService {
public static let shared = AuthorizationService()
public func verifyAccount(_ result: @escaping (AccountData?) -> Void) async {
let accountDataHandler = AccountDataHandler()
let currentAccount = accountDataHandler.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 {
result(nil)
return
}
// When we have at least one account then we have to verify access token.
let client = MastodonClient(baseURL: accountData.serverUrl).getAuthenticated(token: accessToken)
do {
let account = try await client.verifyCredentials()
try await self.updateAccount(accountData: accountData, account: account)
result(accountData)
} catch {
do {
try await self.refreshCredentials(accountData: accountData)
result(accountData)
} catch {
// TODO: show information to the user.
print("Cannot refresh credentials!!!")
}
}
}
public func signIn(serverAddress: String, _ result: @escaping (AccountData?) -> Void) async throws {
let baseUrl = URL(string: serverAddress)!
let client = MastodonClient(baseURL: baseUrl)
// Verify address.
let instanceInformation = try await client.readInstanceInformation()
print(instanceInformation)
// Create application (we will get clientId amd clientSecret).
let oAuthApp = try await client.createApp(
named: "Photofed",
redirectUri: "oauth-vernissage://oauth-callback/mastodon",
scopes: Scopes(["read", "write", "follow", "push"]),
website: baseUrl)
// Authorize a user (browser, we will get clientCode).
let oAuthSwiftCredential = try await client.authenticate(
app: oAuthApp,
scope: Scopes(["read", "write", "follow", "push"]))
// Get authenticated client.
let authenticatedClient = client.getAuthenticated(token: oAuthSwiftCredential.oauthToken)
// Get account information from server.
let account = try await authenticatedClient.verifyCredentials()
// Create account object in database.
let accountDataHandler = AccountDataHandler()
let accountData = accountDataHandler.createAccountDataEntity()
accountData.id = account.id
accountData.username = account.username
accountData.acct = account.acct
accountData.displayName = account.displayName
accountData.note = account.note
accountData.url = account.url
accountData.avatar = account.avatar
accountData.header = account.header
accountData.locked = account.locked
accountData.createdAt = account.createdAt
accountData.followersCount = Int32(account.followersCount)
accountData.followingCount = Int32(account.followingCount)
accountData.statusesCount = Int32(account.statusesCount)
accountData.serverUrl = baseUrl
accountData.clientId = oAuthApp.clientId
accountData.clientSecret = oAuthApp.clientSecret
accountData.clientVapidKey = oAuthApp.vapidKey ?? ""
accountData.accessToken = oAuthSwiftCredential.oauthToken
// Download avatar image.
if let avatarUrl = account.avatar {
do {
let avatarData = try await RemoteFileService.shared.fetchData(url: avatarUrl)
accountData.avatarData = avatarData
}
catch {
print("Avatar has not been downloaded")
}
}
// Set newly created account as current.
let applicationSettingsHandler = ApplicationSettingsHandler()
let defaultSettings = applicationSettingsHandler.getDefaultSettings()
defaultSettings.currentAccount = accountData.id
// Save account data in database and in application state.
CoreDataHandler.shared.save()
// Return account data.
result(accountData)
}
private func refreshCredentials(accountData: AccountData) async throws {
let client = MastodonClient(baseURL: accountData.serverUrl)
// Create application (we will get clientId amd clientSecret).
let oAuthApp = App(clientId: accountData.clientId, clientSecret: accountData.clientSecret)
// Authorize a user (browser, we will get clientCode).
let oAuthSwiftCredential = try await client.authenticate(app: oAuthApp, scope: Scopes(["read", "write", "follow", "push"]))
// Get authenticated client.
let authenticatedClient = client.getAuthenticated(token: oAuthSwiftCredential.oauthToken)
// Get account information from server.
let account = try await authenticatedClient.verifyCredentials()
try await self.updateAccount(accountData: accountData, account: account, accessToken: oAuthSwiftCredential.oauthToken)
}
private func updateAccount(accountData: AccountData, account: Account, accessToken: String? = nil) async throws {
accountData.username = account.username
accountData.acct = account.acct
accountData.displayName = account.displayName
accountData.note = account.note
accountData.url = account.url
accountData.avatar = account.avatar
accountData.header = account.header
accountData.locked = account.locked
accountData.createdAt = account.createdAt
accountData.followersCount = Int32(account.followersCount)
accountData.followingCount = Int32(account.followingCount)
accountData.statusesCount = Int32(account.statusesCount)
if accessToken != nil {
accountData.accessToken = accessToken
}
// Download avatar image.
if let avatarUrl = account.avatar {
do {
let avatarData = try await RemoteFileService.shared.fetchData(url: avatarUrl)
accountData.avatarData = avatarData
}
catch {
print("Avatar has not been downloaded")
}
}
// We have to be sure that account id is saved as default account.
let applicationSettingsHandler = ApplicationSettingsHandler()
let defaultSettings = applicationSettingsHandler.getDefaultSettings()
defaultSettings.currentAccount = accountData.id
// Save account data in database and in application state.
CoreDataHandler.shared.save()
}
}

View File

@ -0,0 +1,136 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the MIT License.
//
import Foundation
import CoreData
import MastodonSwift
public class TimelineService {
public static let shared = TimelineService()
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)
guard let oldestStatus = oldestStatus else {
return
}
try await self.loadData(for: accountData, on: backgroundContext, maxId: oldestStatus.id)
}
public func onTopOfList(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 newestStatus = statusDataHandler.getMaximumStatus(viewContext: backgroundContext)
guard let newestStatus = newestStatus else {
return
}
try await self.loadData(for: accountData, on: backgroundContext, minId: newestStatus.id)
}
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)
}
private func loadData(for accountData: AccountData, on backgroundContext: NSManagedObjectContext, minId: String? = nil, maxId: String? = nil) async throws {
guard let accessToken = accountData.accessToken else {
return
}
// Get maximimum downloaded stauts id.
let attachmentDataHandler = AttachmentDataHandler()
let statusDataHandler = StatusDataHandler()
// 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)
// Download status images and save it into database.
for status in statuses {
// Save status data in database.
let statusDataEntity = statusDataHandler.createStatusDataEntity(viewContext: backgroundContext)
statusDataEntity.accountAvatar = status.account?.avatar
statusDataEntity.accountDisplayName = status.account?.displayName
statusDataEntity.accountId = status.account!.id
statusDataEntity.accountUsername = status.account!.username
statusDataEntity.applicationName = status.application?.name
statusDataEntity.applicationWebsite = status.application?.website
statusDataEntity.bookmarked = status.bookmarked
statusDataEntity.content = status.content
statusDataEntity.createdAt = status.createdAt
statusDataEntity.favourited = status.favourited
statusDataEntity.favouritesCount = Int32(status.favouritesCount)
statusDataEntity.id = status.id
statusDataEntity.inReplyToAccount = status.inReplyToAccount
statusDataEntity.inReplyToId = status.inReplyToId
statusDataEntity.muted = status.muted
statusDataEntity.pinned = status.pinned
statusDataEntity.reblogged = status.reblogged
statusDataEntity.reblogsCount = Int32(status.reblogsCount)
statusDataEntity.sensitive = status.sensitive
statusDataEntity.spoilerText = status.spoilerText
statusDataEntity.uri = status.uri
statusDataEntity.url = status.url
statusDataEntity.visibility = status.visibility.rawValue
for attachment in status.mediaAttachments {
let imageData = try await self.fetchImage(attachment: attachment)
guard let imageData = imageData else {
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 = 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
attachmentData.statusId = statusDataEntity.id
attachmentData.data = imageData
attachmentData.statusRelation = statusDataEntity
statusDataEntity.addToAttachmentRelation(attachmentData)
}
}
try backgroundContext.save()
}
private func fetchImage(attachment: Attachment) async throws -> Data? {
guard let data = try await RemoteFileService.shared.fetchData(url: attachment.url) else {
return nil
}
return data
}
}

View File

@ -5,10 +5,9 @@
//
import SwiftUI
import MastodonSwift
@main
struct VernissageApp: SwiftUI.App {
struct VernissageApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
let coreDataHandler = CoreDataHandler.shared
@ -35,97 +34,19 @@ struct VernissageApp: SwiftUI.App {
}
}
.task {
let accountDataHandler = AccountDataHandler()
let currentAccount = accountDataHandler.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 {
self.applicationViewMode = .signIn
return
}
// When we have at least one account then we have to verify access token.
let client = MastodonClient(baseURL: accountData.serverUrl).getAuthenticated(token: accessToken)
do {
let account = try await client.verifyCredentials()
try await self.updateAccount(accountData: accountData, account: account)
self.applicationViewMode = .mainView
self.applicationState.accountData = accountData
} catch {
do {
try await self.refreshCredentials(accountData: accountData)
self.applicationViewMode = .mainView
self.applicationState.accountData = accountData
} catch {
// TODO: show information to the user.
print("Cannot refresh credentials!!!")
await AuthorizationService.shared.verifyAccount({ accountData in
guard let accountData = accountData else {
self.applicationViewMode = .signIn
return
}
}
self.applicationState.accountData = accountData
self.applicationViewMode = .mainView
})
}
.navigationViewStyle(.stack)
}
}
private func refreshCredentials(accountData: AccountData) async throws {
let client = MastodonClient(baseURL: accountData.serverUrl)
// Create application (we will get clientId amd clientSecret).
let oAuthApp = App(clientId: accountData.clientId, clientSecret: accountData.clientSecret)
// Authorize a user (browser, we will get clientCode).
let oAuthSwiftCredential = try await client.authenticate(app: oAuthApp, scope: Scopes(["read", "write", "follow", "push"]))
// Get authenticated client.
let authenticatedClient = client.getAuthenticated(token: oAuthSwiftCredential.oauthToken)
// Get account information from server.
let account = try await authenticatedClient.verifyCredentials()
try await self.updateAccount(accountData: accountData, account: account, accessToken: oAuthSwiftCredential.oauthToken)
self.applicationState.accountData = accountData
self.applicationViewMode = .mainView
}
private func updateAccount(accountData: AccountData, account: Account, accessToken: String? = nil) async throws {
accountData.username = account.username
accountData.acct = account.acct
accountData.displayName = account.displayName
accountData.note = account.note
accountData.url = account.url
accountData.avatar = account.avatar
accountData.header = account.header
accountData.locked = account.locked
accountData.createdAt = account.createdAt
accountData.followersCount = Int32(account.followersCount)
accountData.followingCount = Int32(account.followingCount)
accountData.statusesCount = Int32(account.statusesCount)
if accessToken != nil {
accountData.accessToken = accessToken
}
// Download avatar image.
if let avatarUrl = account.avatar {
do {
let avatarData = try await RemoteFileService.shared.fetchData(url: avatarUrl)
accountData.avatarData = avatarData
}
catch {
print("Avatar has not been downloaded")
}
}
// We have to be sure that account id is saved as default account.
let applicationSettingsHandler = ApplicationSettingsHandler()
let defaultSettings = applicationSettingsHandler.getDefaultSettings()
defaultSettings.currentAccount = accountData.id
// Save account data in database and in application state.
try self.coreDataHandler.container.viewContext.save()
}
}
class AppDelegate: NSObject, UIApplicationDelegate {

View File

@ -9,120 +9,50 @@ import MastodonSwift
import AVFoundation
struct DetailsView: View {
@Environment(\.dismiss) private var dismiss
@State public var statusData: StatusData
@State private var height: Double = 0.0
var body: some View {
ScrollView {
VStack (alignment: .leading) {
TabView {
ForEach(statusData.attachments(), id: \.self) { attachment in
if let image = UIImage(data: attachment.data) {
Image(uiImage: image)
.resizable()
.aspectRatio(contentMode: .fit)
}
}
}
.frame(height: CGFloat(self.height))
.tabViewStyle(PageTabViewStyle())
ImagesCarousel(attachments: statusData.attachments())
VStack(alignment: .leading) {
HStack (alignment: .center) {
AsyncImage(url: statusData.accountAvatar) { image in
image
.resizable()
.clipShape(Circle())
.aspectRatio(contentMode: .fit)
} placeholder: {
Image(systemName: "person.circle")
.resizable()
.foregroundColor(Color("mainTextColor"))
}
.frame(width: 48.0, height: 48.0)
VStack (alignment: .leading) {
Text(statusData.accountDisplayName ?? statusData.accountUsername)
.foregroundColor(Color("displayNameColor"))
Text("@\(statusData.accountUsername)")
.foregroundColor(Color("lightGrayColor"))
.font(.footnote)
}
.padding(.leading, 8)
}
UsernameRow(statusData: statusData)
HTMLFormattedText(statusData.content)
.padding(.leading, -4)
VStack (alignment: .leading) {
LabelIconView(iconName: "camera", value: "SONY ILCE-7M3")
LabelIconView(iconName: "camera.aperture", value: "Viltrox 24mm F1.8 E")
LabelIconView(iconName: "timelapse", value: "24.0 mm, f/1.8, 1/640s, ISO 100")
LabelIconView(iconName: "calendar", value: "2 Oct 2022")
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 (alignment: .top) {
TagView {
// Favorite
} content: {
HStack {
Image(systemName: statusData.favourited ? "heart.fill" : "heart")
Text("\(statusData.favouritesCount) likes")
}
}
TagView {
// Reboost
} content: {
HStack {
Image(systemName: statusData.reblogged ? "arrowshape.turn.up.forward.fill" : "arrowshape.turn.up.forward")
Text("\(statusData.reblogsCount) boosts")
}
}
Spacer()
TagView {
// Bookmark
} content: {
Image(systemName: statusData.bookmarked ? "bookmark.fill" : "bookmark")
}
HStack {
Text("Uploaded")
Text(statusData.createdAt.toDate(.isoDateTimeMilliSec) ?? Date(), style: .relative)
.padding(.horizontal, -4)
Text("ago")
}
.font(.subheadline)
.foregroundColor(Color("mainTextColor"))
.foregroundColor(Color("lightGrayColor"))
.font(.footnote)
InteractionRow(statusData: statusData)
}
.padding(8)
CommentsSection(statusId: statusData.id)
}
}
.navigationBarTitle("Details")
.onAppear {
self.calculateImageHeight()
}
}
private func calculateImageHeight() {
var imageHeight = 0.0
var imageWidth = 0.0
for item in statusData.attachments() {
if let image = UIImage(data: item.data) {
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 DetailsView_Previews: PreviewProvider {
static var previews: some View {
Text("")
// DetailsView(current: ImageStatus(id: "123", image: UIImage(), status: Status(from: <#T##Decoder#>)))
DetailsView(statusData: StatusData())
}
}

View File

@ -3,7 +3,6 @@
// Copyright © 2022 Marcin Czachurski and the repository contributors.
// Licensed under the MIT License.
//
import SwiftUI

View File

@ -5,9 +5,6 @@
//
import SwiftUI
import MastodonSwift
import UIKit
import CoreData
struct HomeFeedView: View {
@Environment(\.managedObjectContext) private var viewContext
@ -25,33 +22,10 @@ struct HomeFeedView: View {
ScrollView {
LazyVGrid(columns: gridColumns) {
ForEach(dbStatuses, id: \.self) { item in
NavigationLink(destination: DetailsView(statusData: item)) {
if let attachmenData = item.attachmentRelation?.first,
let uiImage = UIImage(data: attachmenData.data) {
ZStack {
Image(uiImage: uiImage)
.resizable()
.aspectRatio(contentMode: .fit)
if let count = item.attachmentRelation?.count, count > 1 {
VStack(alignment:.trailing) {
Spacer()
HStack {
Spacer()
Text("1 / \(count)")
.padding(.horizontal, 6)
.padding(.vertical, 3)
.font(.caption2)
.foregroundColor(.black)
.background(.ultraThinMaterial, in: Capsule())
}
}.padding()
}
}
} else {
Text("Error")
}
NavigationLink(destination:
DetailsView(statusData: item)
.environmentObject(applicationState)) {
ImageRow(attachments: item.attachments())
}
}
@ -60,7 +34,9 @@ struct HomeFeedView: View {
.onAppear {
Task {
do {
try await onBottomOfList()
if let accountData = self.applicationState.accountData {
try await TimelineService.shared.onBottomOfList(for: accountData)
}
} catch {
print("Error", error)
}
@ -76,7 +52,9 @@ struct HomeFeedView: View {
}
.refreshable {
do {
try await onTopOfList()
if let accountData = self.applicationState.accountData {
try await TimelineService.shared.onTopOfList(for: accountData)
}
} catch {
print("Error", error)
}
@ -85,7 +63,9 @@ struct HomeFeedView: View {
do {
if self.dbStatuses.isEmpty {
self.showLoading = true
try await onTopOfList()
if let accountData = self.applicationState.accountData {
try await TimelineService.shared.onTopOfList(for: accountData)
}
self.showLoading = false
}
} catch {
@ -94,124 +74,6 @@ struct HomeFeedView: View {
}
}
}
private func onBottomOfList() 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)
guard let oldestStatus = oldestStatus else {
return
}
try await self.loadData(on: backgroundContext, maxId: oldestStatus.id)
}
private func onTopOfList() 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 newestStatus = statusDataHandler.getMaximumStatus(viewContext: backgroundContext)
guard let newestStatus = newestStatus else {
return
}
try await self.loadData(on: backgroundContext, minId: newestStatus.id)
}
private func loadData(on backgroundContext: NSManagedObjectContext, minId: String? = nil, maxId: String? = nil) async throws {
guard let accessData = self.applicationState.accountData, let accessToken = accessData.accessToken else {
return
}
// Get maximimum downloaded stauts id.
let attachmentDataHandler = AttachmentDataHandler()
let statusDataHandler = StatusDataHandler()
// Retrieve statuses from API.
let client = MastodonClient(baseURL: accessData.serverUrl).getAuthenticated(token: accessToken)
let statuses = try await client.getHomeTimeline(maxId: maxId, minId: minId, limit: 40)
// Download status images and save it into database.
for status in statuses {
// Save status data in database.
let statusDataEntity = statusDataHandler.createStatusDataEntity(viewContext: backgroundContext)
statusDataEntity.accountAvatar = status.account?.avatar
statusDataEntity.accountDisplayName = status.account?.displayName
statusDataEntity.accountId = status.account!.id
statusDataEntity.accountUsername = status.account!.username
statusDataEntity.applicationName = status.application?.name
statusDataEntity.applicationWebsite = status.application?.website
statusDataEntity.bookmarked = status.bookmarked
statusDataEntity.content = status.content
statusDataEntity.createdAt = status.createdAt
statusDataEntity.favourited = status.favourited
statusDataEntity.favouritesCount = Int32(status.favouritesCount)
statusDataEntity.id = status.id
statusDataEntity.inReplyToAccount = status.inReplyToAccount
statusDataEntity.inReplyToId = status.inReplyToId
statusDataEntity.muted = status.muted
statusDataEntity.pinned = status.pinned
statusDataEntity.reblogged = status.reblogged
statusDataEntity.reblogsCount = Int32(status.reblogsCount)
statusDataEntity.sensitive = status.sensitive
statusDataEntity.spoilerText = status.spoilerText
statusDataEntity.uri = status.uri
statusDataEntity.url = status.url
statusDataEntity.visibility = status.visibility.rawValue
for attachment in status.mediaAttachments {
let imageData = try await self.fetchImage(attachment: attachment)
guard let imageData = imageData else {
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 = 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
attachmentData.statusId = statusDataEntity.id
attachmentData.data = imageData
attachmentData.statusRelation = statusDataEntity
statusDataEntity.addToAttachmentRelation(attachmentData)
}
}
try backgroundContext.save()
}
public func fetchImage(attachment: Attachment) async throws -> Data? {
guard let data = try await RemoteFileService.shared.fetchData(url: attachment.url) else {
return nil
}
return data
}
}
struct HomeFeedView_Previews: PreviewProvider {

View File

@ -3,7 +3,6 @@
// Copyright © 2022 Marcin Czachurski and the repository contributors.
// Licensed under the MIT License.
//
import SwiftUI

View File

@ -3,7 +3,6 @@
// Copyright © 2022 Marcin Czachurski and the repository contributors.
// Licensed under the MIT License.
//
import SwiftUI

View File

@ -5,7 +5,6 @@
//
import SwiftUI
import MastodonSwift
struct SignInView: View {
@Environment(\.managedObjectContext) private var viewContext
@ -29,7 +28,10 @@ struct SignInView: View {
Button("Go") {
Task {
try await self.signIn()
try await AuthorizationService.shared.signIn(serverAddress: serverAddress, { accountData in
self.applicationState.accountData = accountData
onSignInStateChenge(.mainView)
})
}
}
}
@ -38,79 +40,6 @@ struct SignInView: View {
.navigationBarTitle("Sign in to Pixelfed")
.navigationBarTitleDisplayMode(.inline)
}
private func signIn() async throws {
let baseUrl = URL(string: serverAddress)!
let client = MastodonClient(baseURL: baseUrl)
// Verify address.
let instanceInformation = try await client.readInstanceInformation()
print(instanceInformation)
// Create application (we will get clientId amd clientSecret).
let oAuthApp = try await client.createApp(
named: "Photofed",
redirectUri: "oauth-vernissage://oauth-callback/mastodon",
scopes: Scopes(["read", "write", "follow", "push"]),
website: baseUrl)
// Authorize a user (browser, we will get clientCode).
let oAuthSwiftCredential = try await client.authenticate(
app: oAuthApp,
scope: Scopes(["read", "write", "follow", "push"]))
// Get authenticated client.
let authenticatedClient = client.getAuthenticated(token: oAuthSwiftCredential.oauthToken)
// Get account information from server.
let account = try await authenticatedClient.verifyCredentials()
// Create account object in database.
let accountDataHandler = AccountDataHandler()
let accountData = accountDataHandler.createAccountDataEntity()
accountData.id = account.id
accountData.username = account.username
accountData.acct = account.acct
accountData.displayName = account.displayName
accountData.note = account.note
accountData.url = account.url
accountData.avatar = account.avatar
accountData.header = account.header
accountData.locked = account.locked
accountData.createdAt = account.createdAt
accountData.followersCount = Int32(account.followersCount)
accountData.followingCount = Int32(account.followingCount)
accountData.statusesCount = Int32(account.statusesCount)
accountData.serverUrl = baseUrl
accountData.clientId = oAuthApp.clientId
accountData.clientSecret = oAuthApp.clientSecret
accountData.clientVapidKey = oAuthApp.vapidKey ?? ""
accountData.accessToken = oAuthSwiftCredential.oauthToken
// Download avatar image.
if let avatarUrl = account.avatar {
do {
let avatarData = try await RemoteFileService.shared.fetchData(url: avatarUrl)
accountData.avatarData = avatarData
}
catch {
print("Avatar has not been downloaded")
}
}
// Set newly created account as current.
let applicationSettingsHandler = ApplicationSettingsHandler()
let defaultSettings = applicationSettingsHandler.getDefaultSettings()
defaultSettings.currentAccount = accountData.id
// Save account data in database and in application state.
try self.viewContext.save()
self.applicationState.accountData = accountData
self.onSignInStateChenge(.mainView)
}
}
struct SignInView_Previews: PreviewProvider {

View File

@ -0,0 +1,33 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the MIT License.
//
import SwiftUI
struct BottomRight<Content: View>: View {
let content: Content
init(@ViewBuilder content: () -> Content) {
self.content = content()
}
var body: some View {
VStack(alignment:.trailing) {
Spacer()
HStack {
Spacer()
content
}
}
}
}
struct BottomRight_Previews: PreviewProvider {
static var previews: some View {
BottomRight {
Text("1/2")
}
}
}

View File

@ -0,0 +1,80 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the MIT License.
//
import SwiftUI
import MastodonSwift
struct CommentsSection: View {
@EnvironmentObject var applicationState: ApplicationState
@State public var statusId: String
@State public var withDivider = true
@State private var context: Context?
private let contentWidth = Int(UIScreen.main.bounds.width) - 50
var body: some View {
VStack {
if let context = context {
ForEach(context.descendants, id: \.id) { status in
HStack (alignment: .top) {
AsyncImage(url: status.account?.avatar) { image in
image
.resizable()
.clipShape(Circle())
.aspectRatio(contentMode: .fit)
} placeholder: {
Image(systemName: "person.circle")
.resizable()
.foregroundColor(Color("mainTextColor"))
}
.frame(width: 32.0, height: 32.0)
VStack (alignment: .leading) {
HStack (alignment: .top) {
Text(status.account?.displayName ?? status.account?.username ?? "")
.foregroundColor(Color("displayNameColor"))
.font(.footnote)
.fontWeight(.bold)
Text("@\(status.account?.username ?? "")")
.foregroundColor(Color("lightGrayColor"))
.font(.footnote)
}
.padding(.bottom, -10)
HTMLFormattedText(status.content, withFontSize: 14, andWidth: contentWidth)
.padding(.leading, -4)
}
}
.padding(.horizontal, 8)
CommentsSection(statusId: status.id, withDivider: false)
if withDivider {
Rectangle()
.size(width: UIScreen.main.bounds.width, height: 4)
.fill(Color("mainTextColor"))
.opacity(0.05)
}
}
}
}.task {
do {
if let accountData = applicationState.accountData {
self.context = try await TimelineService.shared.getComments(for: statusId, and: accountData)
}
} catch {
print("Error \(error.localizedDescription)")
}
}
}
}
struct CommentsSection_Previews: PreviewProvider {
static var previews: some View {
CommentsSection(statusId: "", withDivider: true)
}
}

View File

@ -0,0 +1,42 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the MIT License.
//
import SwiftUI
struct ImageRow: View {
@State public var attachments: [AttachmentData]
var body: some View {
if let attachmenData = attachments.first,
let uiImage = UIImage(data: attachmenData.data) {
ZStack {
Image(uiImage: uiImage)
.resizable()
.aspectRatio(contentMode: .fit)
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()
}
}
} else {
Text("Error")
}
}
}
struct ImageRow_Previews: PreviewProvider {
static var previews: some View {
ImageRow(attachments: [])
}
}

View File

@ -0,0 +1,52 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the MIT License.
//
import SwiftUI
struct ImagesCarousel: View {
@State public var attachments: [AttachmentData]
@State private var height: Double = 0.0
var body: some View {
TabView {
ForEach(attachments, id: \.self) { attachment in
if let image = UIImage(data: attachment.data) {
Image(uiImage: image)
.resizable()
.aspectRatio(contentMode: .fit)
}
}
}
.frame(height: CGFloat(self.height))
.tabViewStyle(PageTabViewStyle())
.onAppear {
self.calculateImageHeight()
}
}
private func calculateImageHeight() {
var imageHeight = 0.0
var imageWidth = 0.0
for item in attachments {
if let image = UIImage(data: item.data) {
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 {
ImagesCarousel(attachments: [])
}
}

View File

@ -0,0 +1,49 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the MIT License.
//
import SwiftUI
struct InteractionRow: View {
@State public var statusData: StatusData
var body: some View {
HStack (alignment: .top) {
Tag {
// Favorite
} content: {
HStack {
Image(systemName: statusData.favourited ? "heart.fill" : "heart")
Text("\(statusData.favouritesCount) likes")
}
}
Tag {
// Reboost
} content: {
HStack {
Image(systemName: statusData.reblogged ? "arrowshape.turn.up.forward.fill" : "arrowshape.turn.up.forward")
Text("\(statusData.reblogsCount) boosts")
}
}
Spacer()
Tag {
// Bookmark
} content: {
Image(systemName: statusData.bookmarked ? "bookmark.fill" : "bookmark")
}
}
.font(.subheadline)
.foregroundColor(Color("mainTextColor"))
}
}
struct InteractionRow_Previews: PreviewProvider {
static var previews: some View {
InteractionRow(statusData: StatusData())
}
}

View File

@ -3,11 +3,10 @@
// Copyright © 2022 Marcin Czachurski and the repository contributors.
// Licensed under the MIT License.
//
import SwiftUI
struct LabelIconView: View {
struct LabelIcon: View {
let iconName: String
let value: String
@ -24,6 +23,6 @@ struct LabelIconView: View {
struct LabelIconView_Previews: PreviewProvider {
static var previews: some View {
LabelIconView(iconName: "camera", value: "Sony A7")
LabelIcon(iconName: "camera", value: "Sony A7")
}
}

View File

@ -3,11 +3,10 @@
// Copyright © 2022 Marcin Czachurski and the repository contributors.
// Licensed under the MIT License.
//
import SwiftUI
struct TagView<Content: View>: View {
struct Tag<Content: View>: View {
let content: Content
let action: () -> Void
@ -31,7 +30,7 @@ struct TagView<Content: View>: View {
struct TagView_Previews: PreviewProvider {
static var previews: some View {
TagView {
Tag {
} content: {
HStack {

View File

@ -0,0 +1,42 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the MIT License.
//
import SwiftUI
struct UsernameRow: View {
@State public var statusData: StatusData
var body: some View {
HStack (alignment: .center) {
AsyncImage(url: statusData.accountAvatar) { image in
image
.resizable()
.clipShape(Circle())
.aspectRatio(contentMode: .fit)
} placeholder: {
Image(systemName: "person.circle")
.resizable()
.foregroundColor(Color("mainTextColor"))
}
.frame(width: 48.0, height: 48.0)
VStack (alignment: .leading) {
Text(statusData.accountDisplayName ?? statusData.accountUsername)
.foregroundColor(Color("displayNameColor"))
Text("@\(statusData.accountUsername)")
.foregroundColor(Color("lightGrayColor"))
.font(.footnote)
}
.padding(.leading, 8)
}
}
}
struct UsernameRow_Previews: PreviewProvider {
static var previews: some View {
UsernameRow(statusData: StatusData())
}
}