From ed8fa69d9a4ac69706520346bbe711ce3fe98f79 Mon Sep 17 00:00:00 2001 From: Marcin Czachursk Date: Fri, 30 Dec 2022 18:20:54 +0100 Subject: [PATCH] Storing account in CoreData. --- Vernissage.xcodeproj/project.pbxproj | 44 +++ .../CoreData/AccountData+CoreDataClass.swift | 15 + .../AccountData+CoreDataProperties.swift | 39 +++ Vernissage/CoreData/Persistence.swift | 4 +- Vernissage/Models/ApplicationState.swift | 24 ++ Vernissage/Services/RemoteFileService.swift | 24 ++ .../Vernissage.xcdatamodel/contents | 23 +- Vernissage/VernissageApp.swift | 3 +- Vernissage/Views/DetailsView.swift | 1 + Vernissage/Views/FederatedFeedView.swift | 20 ++ Vernissage/Views/HomeFeedView.swift | 128 ++++++++ Vernissage/Views/LocalFeedView.swift | 20 ++ Vernissage/Views/MainView.swift | 298 +++++++++--------- Vernissage/Views/NotificationsView.swift | 20 ++ 14 files changed, 507 insertions(+), 156 deletions(-) create mode 100644 Vernissage/CoreData/AccountData+CoreDataClass.swift create mode 100644 Vernissage/CoreData/AccountData+CoreDataProperties.swift create mode 100644 Vernissage/Models/ApplicationState.swift create mode 100644 Vernissage/Services/RemoteFileService.swift create mode 100644 Vernissage/Views/FederatedFeedView.swift create mode 100644 Vernissage/Views/HomeFeedView.swift create mode 100644 Vernissage/Views/LocalFeedView.swift create mode 100644 Vernissage/Views/NotificationsView.swift diff --git a/Vernissage.xcodeproj/project.pbxproj b/Vernissage.xcodeproj/project.pbxproj index 3a2cb47..93473ab 100644 --- a/Vernissage.xcodeproj/project.pbxproj +++ b/Vernissage.xcodeproj/project.pbxproj @@ -20,6 +20,15 @@ F88C2480295C38400006098B /* MastodonSwift in Frameworks */ = {isa = PBXBuildFile; productRef = F88C247F295C38400006098B /* MastodonSwift */; }; F88C2482295C3A4F0006098B /* DetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88C2481295C3A4F0006098B /* DetailsView.swift */; }; F88C2486295C48030006098B /* HTMLFotmattedText.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88C2485295C48030006098B /* HTMLFotmattedText.swift */; }; + F88FAD21295F3944009B20C9 /* HomeFeedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88FAD20295F3944009B20C9 /* HomeFeedView.swift */; }; + F88FAD23295F3FC4009B20C9 /* LocalFeedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88FAD22295F3FC4009B20C9 /* LocalFeedView.swift */; }; + F88FAD25295F3FF7009B20C9 /* FederatedFeedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88FAD24295F3FF7009B20C9 /* FederatedFeedView.swift */; }; + F88FAD27295F400E009B20C9 /* NotificationsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88FAD26295F400E009B20C9 /* NotificationsView.swift */; }; + F88FAD2A295F43B8009B20C9 /* AccountData+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88FAD28295F43B8009B20C9 /* AccountData+CoreDataClass.swift */; }; + F88FAD2B295F43B8009B20C9 /* AccountData+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88FAD29295F43B8009B20C9 /* AccountData+CoreDataProperties.swift */; }; + F88FAD2D295F4AD7009B20C9 /* ApplicationState.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88FAD2C295F4AD7009B20C9 /* ApplicationState.swift */; }; + F88FAD2F295F4D3C009B20C9 /* AccountData+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88FAD2E295F4D3C009B20C9 /* AccountData+CoreDataProperties.swift */; }; + F88FAD32295F5029009B20C9 /* RemoteFileService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88FAD31295F5029009B20C9 /* RemoteFileService.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -36,6 +45,15 @@ F88C2477295C37BB0006098B /* Vernissage.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Vernissage.xcdatamodel; sourceTree = ""; }; F88C2481295C3A4F0006098B /* DetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailsView.swift; sourceTree = ""; }; F88C2485295C48030006098B /* HTMLFotmattedText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTMLFotmattedText.swift; sourceTree = ""; }; + F88FAD20295F3944009B20C9 /* HomeFeedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeFeedView.swift; sourceTree = ""; }; + F88FAD22295F3FC4009B20C9 /* LocalFeedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalFeedView.swift; sourceTree = ""; }; + F88FAD24295F3FF7009B20C9 /* FederatedFeedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FederatedFeedView.swift; sourceTree = ""; }; + F88FAD26295F400E009B20C9 /* NotificationsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsView.swift; sourceTree = ""; }; + F88FAD28295F43B8009B20C9 /* AccountData+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AccountData+CoreDataClass.swift"; sourceTree = ""; }; + F88FAD29295F43B8009B20C9 /* AccountData+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AccountData+CoreDataProperties.swift"; sourceTree = ""; }; + F88FAD2C295F4AD7009B20C9 /* ApplicationState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationState.swift; sourceTree = ""; }; + F88FAD2E295F4D3C009B20C9 /* AccountData+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = "AccountData+CoreDataProperties.swift"; path = "Vernissage/CoreData/AccountData+CoreDataProperties.swift"; sourceTree = ""; }; + F88FAD31295F5029009B20C9 /* RemoteFileService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteFileService.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -55,6 +73,10 @@ children = ( F88C2481295C3A4F0006098B /* DetailsView.swift */, F88C246D295C37B80006098B /* MainView.swift */, + F88FAD20295F3944009B20C9 /* HomeFeedView.swift */, + F88FAD22295F3FC4009B20C9 /* LocalFeedView.swift */, + F88FAD24295F3FF7009B20C9 /* FederatedFeedView.swift */, + F88FAD26295F400E009B20C9 /* NotificationsView.swift */, ); path = Views; sourceTree = ""; @@ -71,6 +93,7 @@ isa = PBXGroup; children = ( F8341F91295C63BB009C8EE6 /* ImageStatus.swift */, + F88FAD2C295F4AD7009B20C9 /* ApplicationState.swift */, ); path = Models; sourceTree = ""; @@ -78,6 +101,8 @@ F8341F96295C6427009C8EE6 /* CoreData */ = { isa = PBXGroup; children = ( + F88FAD28295F43B8009B20C9 /* AccountData+CoreDataClass.swift */, + F88FAD29295F43B8009B20C9 /* AccountData+CoreDataProperties.swift */, F88C2474295C37BB0006098B /* Persistence.swift */, ); path = CoreData; @@ -103,6 +128,7 @@ F88C245F295C37B80006098B = { isa = PBXGroup; children = ( + F88FAD2E295F4D3C009B20C9 /* AccountData+CoreDataProperties.swift */, F88C246A295C37B80006098B /* Vernissage */, F88C2469295C37B80006098B /* Products */, ); @@ -119,6 +145,7 @@ F88C246A295C37B80006098B /* Vernissage */ = { isa = PBXGroup; children = ( + F88FAD30295F5010009B20C9 /* Services */, F83901A2295D863B00456AE2 /* Widgets */, F8341F97295C6434009C8EE6 /* Formatters */, F8341F96295C6427009C8EE6 /* CoreData */, @@ -141,6 +168,14 @@ path = "Preview Content"; sourceTree = ""; }; + F88FAD30295F5010009B20C9 /* Services */ = { + isa = PBXGroup; + children = ( + F88FAD31295F5029009B20C9 /* RemoteFileService.swift */, + ); + path = Services; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -217,7 +252,12 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + F88FAD23295F3FC4009B20C9 /* LocalFeedView.swift in Sources */, + F88FAD2B295F43B8009B20C9 /* AccountData+CoreDataProperties.swift in Sources */, + F88FAD21295F3944009B20C9 /* HomeFeedView.swift in Sources */, + F88FAD2F295F4D3C009B20C9 /* AccountData+CoreDataProperties.swift in Sources */, F88C2475295C37BB0006098B /* Persistence.swift in Sources */, + F88FAD2A295F43B8009B20C9 /* AccountData+CoreDataClass.swift in Sources */, F8341F92295C63BB009C8EE6 /* ImageStatus.swift in Sources */, F83901A6295D8EC000456AE2 /* LabelIconView.swift in Sources */, F8341F90295C636C009C8EE6 /* UIImage.swift in Sources */, @@ -227,6 +267,10 @@ F88C2486295C48030006098B /* HTMLFotmattedText.swift in Sources */, F88C246C295C37B80006098B /* VernissageApp.swift in Sources */, F83901A4295D864D00456AE2 /* TagView.swift in Sources */, + F88FAD25295F3FF7009B20C9 /* FederatedFeedView.swift in Sources */, + F88FAD32295F5029009B20C9 /* RemoteFileService.swift in Sources */, + F88FAD27295F400E009B20C9 /* NotificationsView.swift in Sources */, + F88FAD2D295F4AD7009B20C9 /* ApplicationState.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Vernissage/CoreData/AccountData+CoreDataClass.swift b/Vernissage/CoreData/AccountData+CoreDataClass.swift new file mode 100644 index 0000000..33ffd92 --- /dev/null +++ b/Vernissage/CoreData/AccountData+CoreDataClass.swift @@ -0,0 +1,15 @@ +// +// https://mczachurski.dev +// Copyright © 2022 Marcin Czachurski and the repository contributors. +// Licensed under the MIT License. +// + +// + +import Foundation +import CoreData + +@objc(AccountData) +public class AccountData: NSManagedObject { + +} diff --git a/Vernissage/CoreData/AccountData+CoreDataProperties.swift b/Vernissage/CoreData/AccountData+CoreDataProperties.swift new file mode 100644 index 0000000..b9709df --- /dev/null +++ b/Vernissage/CoreData/AccountData+CoreDataProperties.swift @@ -0,0 +1,39 @@ +// +// https://mczachurski.dev +// Copyright © 2022 Marcin Czachurski and the repository contributors. +// Licensed under the MIT License. +// + +// + +import Foundation +import CoreData + + +extension AccountData { + + @nonobjc public class func fetchRequest() -> NSFetchRequest { + return NSFetchRequest(entityName: "AccountData") + } + + @NSManaged public var id: String? + @NSManaged public var username: String? + @NSManaged public var acct: String? + @NSManaged public var displayName: String? + @NSManaged public var note: String? + @NSManaged public var url: URL? + @NSManaged public var avatar: URL? + @NSManaged public var header: URL? + @NSManaged public var locked: Bool + @NSManaged public var createdAt: String? + @NSManaged public var followersCount: Int32 + @NSManaged public var followingCount: Int32 + @NSManaged public var statusesCount: Int32 + @NSManaged public var accessToken: String? + @NSManaged public var avatarData: Data? + +} + +extension AccountData : Identifiable { + +} diff --git a/Vernissage/CoreData/Persistence.swift b/Vernissage/CoreData/Persistence.swift index e5cb734..731b077 100644 --- a/Vernissage/CoreData/Persistence.swift +++ b/Vernissage/CoreData/Persistence.swift @@ -14,8 +14,8 @@ struct PersistenceController { let result = PersistenceController(inMemory: true) let viewContext = result.container.viewContext for _ in 0..<10 { - let newItem = Item(context: viewContext) - newItem.timestamp = Date() + let newItem = AccountData(context: viewContext) + newItem.id = "123" } do { try viewContext.save() diff --git a/Vernissage/Models/ApplicationState.swift b/Vernissage/Models/ApplicationState.swift new file mode 100644 index 0000000..462541f --- /dev/null +++ b/Vernissage/Models/ApplicationState.swift @@ -0,0 +1,24 @@ +// +// https://mczachurski.dev +// Copyright © 2022 Marcin Czachurski and the repository contributors. +// Licensed under the MIT License. +// + + +import Foundation + +public class ApplicationState: ObservableObject { + public static let shared = ApplicationState() + + @Published var accountData: AccountData? +} + +extension ApplicationState { + public static var preview: ApplicationState = { + let applicationState = ApplicationState() + + applicationState.accountData = AccountData() + + return applicationState + }() +} diff --git a/Vernissage/Services/RemoteFileService.swift b/Vernissage/Services/RemoteFileService.swift new file mode 100644 index 0000000..b239260 --- /dev/null +++ b/Vernissage/Services/RemoteFileService.swift @@ -0,0 +1,24 @@ +// +// https://mczachurski.dev +// Copyright © 2022 Marcin Czachurski and the repository contributors. +// Licensed under the MIT License. +// + + +import Foundation + +public class RemoteFileService { + public static let shared = RemoteFileService() + + public func fetchData(url: URL) async throws -> Data? { + let urlRequest = URLRequest(url: url) + + // Fetching data. + let (data, response) = try await URLSession.shared.data(for: urlRequest) + guard (response as? HTTPURLResponse)?.statusCode == 200 else { + return nil + } + + return data + } +} diff --git a/Vernissage/Vernissage.xcdatamodeld/Vernissage.xcdatamodel/contents b/Vernissage/Vernissage.xcdatamodeld/Vernissage.xcdatamodel/contents index 9ed2921..b557e8f 100644 --- a/Vernissage/Vernissage.xcdatamodeld/Vernissage.xcdatamodel/contents +++ b/Vernissage/Vernissage.xcdatamodeld/Vernissage.xcdatamodel/contents @@ -1,9 +1,20 @@ - - - + + + + + + + + + + + + + + + + + - - - \ No newline at end of file diff --git a/Vernissage/VernissageApp.swift b/Vernissage/VernissageApp.swift index 5803d3c..d231d38 100644 --- a/Vernissage/VernissageApp.swift +++ b/Vernissage/VernissageApp.swift @@ -9,12 +9,13 @@ import SwiftUI @main struct VernissageApp: App { let persistenceController = PersistenceController.shared - + var body: some Scene { WindowGroup { NavigationStack { MainView() .environment(\.managedObjectContext, persistenceController.container.viewContext) + .environmentObject(ApplicationState.shared) } .navigationViewStyle(.stack) } diff --git a/Vernissage/Views/DetailsView.swift b/Vernissage/Views/DetailsView.swift index 078c755..7d08f62 100644 --- a/Vernissage/Views/DetailsView.swift +++ b/Vernissage/Views/DetailsView.swift @@ -86,6 +86,7 @@ struct DetailsView: View { .padding(8) } } + .navigationBarTitle("Details") } } diff --git a/Vernissage/Views/FederatedFeedView.swift b/Vernissage/Views/FederatedFeedView.swift new file mode 100644 index 0000000..a3227ef --- /dev/null +++ b/Vernissage/Views/FederatedFeedView.swift @@ -0,0 +1,20 @@ +// +// https://mczachurski.dev +// Copyright © 2022 Marcin Czachurski and the repository contributors. +// Licensed under the MIT License. +// + + +import SwiftUI + +struct FederatedFeedView: View { + var body: some View { + Text("Federated feed") + } +} + +struct FederatedFeedView_Previews: PreviewProvider { + static var previews: some View { + FederatedFeedView() + } +} diff --git a/Vernissage/Views/HomeFeedView.swift b/Vernissage/Views/HomeFeedView.swift new file mode 100644 index 0000000..a370628 --- /dev/null +++ b/Vernissage/Views/HomeFeedView.swift @@ -0,0 +1,128 @@ +// +// https://mczachurski.dev +// Copyright © 2022 Marcin Czachurski and the repository contributors. +// Licensed under the MIT License. +// + + +import SwiftUI +import MastodonSwift +import UIKit + +struct HomeFeedView: View { + + @State private var statuses: [Status] = [] + @State private var images: [ImageStatus] = [] + @State private var showLoading = false + + private static let initialColumns = 1 + @State private var gridColumns = Array(repeating: GridItem(.flexible()), count: initialColumns) + + var body: some View { + ZStack { + ScrollView { + LazyVGrid(columns: gridColumns) { + ForEach(images) { item in + NavigationLink(destination: DetailsView(current: item)) { + Image(uiImage: item.image) + .resizable().aspectRatio(contentMode: .fit) + } + } + } + } + + VStack(alignment:.trailing) { + Spacer() + HStack { + Spacer() + Button { + + } label: { + Image(systemName: "plus") + .font(.body) + .padding(16) + .foregroundColor(.white) + .background(.ultraThinMaterial, in: Circle()) + } + } + }.padding() + + if showLoading { + ProgressView() + .progressViewStyle(CircularProgressViewStyle()) + } + } + .refreshable { + do { + try await loadData() + } catch { + print("Error", error) + } + } + .task { + do { + defer { + self.showLoading = false + } + + self.showLoading = true + try await loadData() + } catch { + print("Error", error) + } + } + } + + private func loadData() async throws { + let accessToken = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJhdWQiOiI2MTQwOCIsImp0aSI6IjZjMDg4N2ZlZThjZjBmZjQ1N2RjZDQ5MTU2YjE2NzYyYmQ2MDQ1NDQ1MmEwYzEyMzVmNDA3YjY2YjFhYjU3ZjBjMTY2YjFmZmIyNjJlZDg2IiwiaWF0IjoxNjcyMDU5MDYwLjI3NDgyMSwibmJmIjoxNjcyMDU5MDYwLjI3NDgyNCwiZXhwIjoxNzAzNTk1MDYwLjI1MzM1Nywic3ViIjoiNjc4MjMiLCJzY29wZXMiOlsicmVhZCIsIndyaXRlIiwiZm9sbG93Il19.kGvg3lW8lF1X1mOTdgGgoXNyzwUIJz5hz5RJKK_WiSoBWDQNadhZDty7XMNF0IAPjxOSi6UaIx2av7_eH_65aNlKFw89bkm8bT_zFQW2V0KbADJ-NmE6X0B_NgU2CNoF5IPn6bhCFHCKMtV6MWAQ_db6DT-LXaGemMY3QimcJzCqQuXI_1ouiZ235T297uEPNTrLwtLq-x_UoO-wx254LStBalDIGDVHAa4by9IT-mvu-QXz7k8pH2NHKoX-9Ql_Y3G9RJJNqoOmWMU45Dyo2HaJKKEb1tkeJ9tA3LIYgbwnEbG2PJ7CE8CXxtakiCIflJZpzzOmq1jXLAsCJ1mHnc77o7NfMaB_hY-f8PEI6d2ttOdH8bNlreF2avznNAIVHg_bf-yv_4wKUCUe0QZMG_yWqOwOk6lyruvboSGKuI5RnYsJbXBoJTGMLON6jVmtiKPbHy-9jNcfFgShAc3D5kTO-8Avj9_RquqEh1TQF_S4ljmganxKzMihyMDLK1OVcXzCFO6FKlCw7YKvbfJk1Qrn9kPBrVDM5jzIyXAmqRd1ivcE9nAdYb2l7KnxW_pi31uT0IdJMpTkZrUQSDMyEnj0HgV6Yd5BDlLG6Cnk8GXATTcU-a1pgE13OtWsCpD2cZQm-tOsFHWBDvY-BA0RtTvQAyEUxRIP9NjHe8rSR90" + + let client = MastodonClient(baseURL: URL(string: "https://pixelfed.social")!) + .getAuthenticated(token: accessToken) + + self.statuses = try await client.getHomeTimeline(limit: 40) + + var imagesCache: [ImageStatus] = [] + for item in self.statuses { + let imageStatus = try await self.fetchImage(status: item) + + if let imageStatus { + imagesCache.append(imageStatus) + } + } + + self.images = imagesCache + } + + public func fetchImage(status: Status) async throws -> ImageStatus? { + guard let url = status.mediaAttachments.first?.url, let id = status.mediaAttachments.first?.id else { + return nil + } + + guard let data = try await RemoteFileService.shared.fetchData(url: url) else { + return nil + } + + let image = UIImage(data: data) + guard let image = image else { + return nil + } + + /* + var exif = image.getExifData() + if let dict = exif as? [String: AnyObject] { + dict.keys.map { key in + print(key) + print(dict[key]) + } + } + */ + + return ImageStatus(id: id,image: image, status: status) + } +} + +struct HomeFeedView_Previews: PreviewProvider { + static var previews: some View { + HomeFeedView() + } +} diff --git a/Vernissage/Views/LocalFeedView.swift b/Vernissage/Views/LocalFeedView.swift new file mode 100644 index 0000000..6623c2d --- /dev/null +++ b/Vernissage/Views/LocalFeedView.swift @@ -0,0 +1,20 @@ +// +// https://mczachurski.dev +// Copyright © 2022 Marcin Czachurski and the repository contributors. +// Licensed under the MIT License. +// + + +import SwiftUI + +struct LocalFeedView: View { + var body: some View { + Text("Local feed") + } +} + +struct LocalFeedView_Previews: PreviewProvider { + static var previews: some View { + LocalFeedView() + } +} diff --git a/Vernissage/Views/MainView.swift b/Vernissage/Views/MainView.swift index 4ae387e..6e3b4c7 100644 --- a/Vernissage/Views/MainView.swift +++ b/Vernissage/Views/MainView.swift @@ -12,122 +12,35 @@ import MastodonSwift struct MainView: View { @Environment(\.managedObjectContext) private var viewContext - @FetchRequest( - sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)], - animation: .default) - private var items: FetchedResults + @EnvironmentObject var applicationState: ApplicationState - @State private var statuses: [Status] = [] - @State private var account: Account? - @State private var images: [ImageStatus] = [] - @State private var current: ImageStatus? = nil - @State private var navBarTitle: String = "Home feed" - - private static let initialColumns = 1 - @State private var gridColumns = Array(repeating: GridItem(.flexible()), count: initialColumns) - - var body: some View { - ZStack { - ScrollView { - LazyVGrid(columns: gridColumns) { - ForEach(images) { item in - NavigationLink(destination: DetailsView(current: item)) { - Image(uiImage: item.image) - .resizable().aspectRatio(contentMode: .fit) - } - } - } - } - - VStack(alignment:.trailing) { - Spacer() - HStack { - Spacer() - Button { - - } label: { - Image(systemName: "plus") - .font(.body) - .padding(16) - .foregroundColor(.white) - .background(.ultraThinMaterial, in: Circle()) - } - } - }.padding() - - }.refreshable { - do { - try await loadData() - } catch { - print("Error", error) + @State private var navBarTitle: String = "Home" + @State private var viewMode: ViewMode = .home { + didSet { + switch viewMode { + case .home: + self.navBarTitle = "Home" + case .local: + self.navBarTitle = "Local" + case .federated: + self.navBarTitle = "Federated" + case .notifications: + self.navBarTitle = "Notifications" } } + } + + private enum ViewMode { + case home, local, federated, notifications + } + + var body: some View { + self.getMainView() .navigationBarTitle(navBarTitle) .navigationBarTitleDisplayMode(.inline) .toolbar { - ToolbarItem(placement: .navigationBarTrailing) { - Menu { - Button { - navBarTitle = "Home feed" - } label: { - HStack { - Text("Home feed") - Image(systemName: "house") - } - } - - Button { - navBarTitle = "Local feed" - } label: { - HStack { - Text("Local feed") - Image(systemName: "text.redaction") - } - } - - Button { - navBarTitle = "Global feed" - } label: { - HStack { - Text("Global feed") - Image(systemName: "globe.europe.africa") - } - } - - Button { - navBarTitle = "Notifications" - } label: { - HStack { - Text("Notifications") - Image(systemName: "bell.badge") - } - } - } label: { - Image(systemName: "ellipsis.circle") - } - } - - ToolbarItem(placement: .navigationBarLeading) { - Button { - // Open settings view. - } label: { - if let avatarUrl = self.account?.avatar { - AsyncImage(url: avatarUrl) { image in - image - .resizable() - .clipShape(Circle()) - .shadow(radius: 10) - .aspectRatio(contentMode: .fit) - } placeholder: { - Image(systemName: "person.circle") - } - .frame(height: 25) - .frame(width: 25) - } else { - Image(systemName: "person.circle") - } - } - } + self.getLeadingToolbar() + self.getPrincipalToolbar() } .task { do { @@ -138,57 +51,148 @@ struct MainView: View { } } + @ViewBuilder + private func getMainView() -> some View { + switch self.viewMode { + case .home: + HomeFeedView() + case .local: + LocalFeedView() + case .federated: + FederatedFeedView() + case .notifications: + NotificationsView() + } + } + + @ToolbarContentBuilder + private func getPrincipalToolbar() -> some ToolbarContent { + ToolbarItem(placement: .principal) { + Menu { + Button { + viewMode = .home + } label: { + HStack { + Text("Home") + Image(systemName: "house") + } + } + + Button { + viewMode = .local + } label: { + HStack { + Text("Local") + Image(systemName: "text.redaction") + } + } + + Button { + viewMode = .federated + } label: { + HStack { + Text("Global") + Image(systemName: "globe.europe.africa") + } + } + + Button { + viewMode = .notifications + } label: { + HStack { + Text("Notifications") + Image(systemName: "bell.badge") + } + } + } label: { + HStack { + Text(navBarTitle) + .font(.headline) + Image(systemName: "chevron.down") + .font(.subheadline) + } + .frame(width: 150) + .foregroundColor(Color.white) + } + } + } + + @ToolbarContentBuilder + private func getLeadingToolbar() -> some ToolbarContent { + ToolbarItem(placement: .navigationBarLeading) { + Button { + // Open settings view. + } label: { + if let avatarData = self.applicationState.accountData?.avatarData, let uiImage = UIImage(data: avatarData) { + Image(uiImage: uiImage) + .resizable() + .clipShape(Circle()) + .shadow(radius: 10) + .aspectRatio(contentMode: .fit) + .frame(height: 32) + .frame(width: 32) + } else { + Image(systemName: "person.circle") + } + } + } + } + private func loadData() async throws { + + // Set account data from database. + let accountDataFromDb = self.getAccountData() + if let accountDataFromDb { + self.applicationState.accountData = accountDataFromDb + return + } + + // Retrieve account data from API. let accessToken = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJhdWQiOiI2MTQwOCIsImp0aSI6IjZjMDg4N2ZlZThjZjBmZjQ1N2RjZDQ5MTU2YjE2NzYyYmQ2MDQ1NDQ1MmEwYzEyMzVmNDA3YjY2YjFhYjU3ZjBjMTY2YjFmZmIyNjJlZDg2IiwiaWF0IjoxNjcyMDU5MDYwLjI3NDgyMSwibmJmIjoxNjcyMDU5MDYwLjI3NDgyNCwiZXhwIjoxNzAzNTk1MDYwLjI1MzM1Nywic3ViIjoiNjc4MjMiLCJzY29wZXMiOlsicmVhZCIsIndyaXRlIiwiZm9sbG93Il19.kGvg3lW8lF1X1mOTdgGgoXNyzwUIJz5hz5RJKK_WiSoBWDQNadhZDty7XMNF0IAPjxOSi6UaIx2av7_eH_65aNlKFw89bkm8bT_zFQW2V0KbADJ-NmE6X0B_NgU2CNoF5IPn6bhCFHCKMtV6MWAQ_db6DT-LXaGemMY3QimcJzCqQuXI_1ouiZ235T297uEPNTrLwtLq-x_UoO-wx254LStBalDIGDVHAa4by9IT-mvu-QXz7k8pH2NHKoX-9Ql_Y3G9RJJNqoOmWMU45Dyo2HaJKKEb1tkeJ9tA3LIYgbwnEbG2PJ7CE8CXxtakiCIflJZpzzOmq1jXLAsCJ1mHnc77o7NfMaB_hY-f8PEI6d2ttOdH8bNlreF2avznNAIVHg_bf-yv_4wKUCUe0QZMG_yWqOwOk6lyruvboSGKuI5RnYsJbXBoJTGMLON6jVmtiKPbHy-9jNcfFgShAc3D5kTO-8Avj9_RquqEh1TQF_S4ljmganxKzMihyMDLK1OVcXzCFO6FKlCw7YKvbfJk1Qrn9kPBrVDM5jzIyXAmqRd1ivcE9nAdYb2l7KnxW_pi31uT0IdJMpTkZrUQSDMyEnj0HgV6Yd5BDlLG6Cnk8GXATTcU-a1pgE13OtWsCpD2cZQm-tOsFHWBDvY-BA0RtTvQAyEUxRIP9NjHe8rSR90" let client = MastodonClient(baseURL: URL(string: "https://pixelfed.social")!) .getAuthenticated(token: accessToken) - self.statuses = try await client.getHomeTimeline() - self.account = try await client.verifyCredentials() + // Get account information from server. + let account = try await client.verifyCredentials() - var imagesCache: [ImageStatus] = [] - for item in self.statuses { - let imageStatus = try await self.fetchImage(status: item, accessToken: accessToken) - - if let imageStatus { - imagesCache.append(imageStatus) - } + // Create account object in database. + let accountData = AccountData(context: viewContext) + 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.accessToken = accessToken + + // Download avatar image. + if let avatarUrl = account.avatar { + let avatarData = try await RemoteFileService.shared.fetchData(url: avatarUrl) + accountData.avatarData = avatarData } - self.images = imagesCache + // Save account data in database and in application state. + try self.viewContext.save() + self.applicationState.accountData = accountData } - public func fetchImage(status: Status, accessToken: String) async throws -> ImageStatus? { - guard let url = status.mediaAttachments.first?.url, let id = status.mediaAttachments.first?.id else { - return nil - } - - let urlRequest = URLRequest(url: url) - - // Fetching data. - let (data, response) = try await URLSession.shared.data(for: urlRequest) - guard (response as? HTTPURLResponse)?.statusCode == 200 else { - return nil - } + private func getAccountData() -> AccountData? { + let fetchRequest: NSFetchRequest = AccountData.fetchRequest() - // Decoding JSON. - let image = UIImage(data: data) - guard let image = image else { + do { + return try self.viewContext.fetch(fetchRequest).first + } + catch { return nil } - - /* - var exif = image.getExifData() - if let dict = exif as? [String: AnyObject] { - dict.keys.map { key in - print(key) - print(dict[key]) - } - } - */ - - return ImageStatus(id: id,image: image, status: status) } } diff --git a/Vernissage/Views/NotificationsView.swift b/Vernissage/Views/NotificationsView.swift new file mode 100644 index 0000000..3ad2fa3 --- /dev/null +++ b/Vernissage/Views/NotificationsView.swift @@ -0,0 +1,20 @@ +// +// https://mczachurski.dev +// Copyright © 2022 Marcin Czachurski and the repository contributors. +// Licensed under the MIT License. +// + + +import SwiftUI + +struct NotificationsView: View { + var body: some View { + Text("Notifications") + } +} + +struct NotificationsView_Previews: PreviewProvider { + static var previews: some View { + NotificationsView() + } +}