diff --git a/Vernissage.xcodeproj/project.pbxproj b/Vernissage.xcodeproj/project.pbxproj index 93473ab..b7a4c98 100644 --- a/Vernissage.xcodeproj/project.pbxproj +++ b/Vernissage.xcodeproj/project.pbxproj @@ -11,13 +11,20 @@ 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 */; }; + 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 */; }; + F866F6A529604194002E8F88 /* ApplicationSettingsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F866F6A429604194002E8F88 /* ApplicationSettingsHandler.swift */; }; + F866F6A729604629002E8F88 /* SignInView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F866F6A629604629002E8F88 /* SignInView.swift */; }; + F866F6AA29605AFA002E8F88 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F866F6A929605AFA002E8F88 /* SceneDelegate.swift */; }; + F866F6AE29606367002E8F88 /* ApplicationViewMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = F866F6AD29606367002E8F88 /* ApplicationViewMode.swift */; }; + F866F6B729608467002E8F88 /* MastodonSwift in Frameworks */ = {isa = PBXBuildFile; productRef = F866F6B629608467002E8F88 /* MastodonSwift */; }; F88C246C295C37B80006098B /* VernissageApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88C246B295C37B80006098B /* VernissageApp.swift */; }; F88C246E295C37B80006098B /* MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88C246D295C37B80006098B /* MainView.swift */; }; F88C2470295C37BB0006098B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F88C246F295C37BB0006098B /* Assets.xcassets */; }; F88C2473295C37BB0006098B /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F88C2472295C37BB0006098B /* Preview Assets.xcassets */; }; - F88C2475295C37BB0006098B /* Persistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88C2474295C37BB0006098B /* Persistence.swift */; }; + F88C2475295C37BB0006098B /* CoreDataHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88C2474295C37BB0006098B /* CoreDataHandler.swift */; }; F88C2478295C37BB0006098B /* Vernissage.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = F88C2476295C37BB0006098B /* Vernissage.xcdatamodeld */; }; - 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 */; }; @@ -27,7 +34,6 @@ 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 */ @@ -36,12 +42,20 @@ F8341F91295C63BB009C8EE6 /* ImageStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageStatus.swift; sourceTree = ""; }; F83901A3295D864D00456AE2 /* TagView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagView.swift; sourceTree = ""; }; F83901A5295D8EC000456AE2 /* LabelIconView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabelIconView.swift; sourceTree = ""; }; + F866F69E296040A8002E8F88 /* ApplicationSettings+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ApplicationSettings+CoreDataClass.swift"; sourceTree = ""; }; + F866F69F296040A8002E8F88 /* ApplicationSettings+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ApplicationSettings+CoreDataProperties.swift"; sourceTree = ""; }; + F866F6A229604161002E8F88 /* AccountDataHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountDataHandler.swift; sourceTree = ""; }; + F866F6A429604194002E8F88 /* ApplicationSettingsHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationSettingsHandler.swift; sourceTree = ""; }; + F866F6A629604629002E8F88 /* SignInView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInView.swift; sourceTree = ""; }; + F866F6A829604FFF002E8F88 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; + F866F6A929605AFA002E8F88 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; + F866F6AD29606367002E8F88 /* ApplicationViewMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationViewMode.swift; sourceTree = ""; }; F88C2468295C37B80006098B /* Vernissage.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Vernissage.app; sourceTree = BUILT_PRODUCTS_DIR; }; F88C246B295C37B80006098B /* VernissageApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VernissageApp.swift; sourceTree = ""; }; F88C246D295C37B80006098B /* MainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainView.swift; sourceTree = ""; }; F88C246F295C37BB0006098B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; F88C2472295C37BB0006098B /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; - F88C2474295C37BB0006098B /* Persistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Persistence.swift; sourceTree = ""; }; + F88C2474295C37BB0006098B /* CoreDataHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataHandler.swift; sourceTree = ""; }; 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 = ""; }; @@ -52,7 +66,6 @@ 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 */ @@ -61,7 +74,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - F88C2480295C38400006098B /* MastodonSwift in Frameworks */, + F866F6B729608467002E8F88 /* MastodonSwift in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -77,6 +90,7 @@ F88FAD22295F3FC4009B20C9 /* LocalFeedView.swift */, F88FAD24295F3FF7009B20C9 /* FederatedFeedView.swift */, F88FAD26295F400E009B20C9 /* NotificationsView.swift */, + F866F6A629604629002E8F88 /* SignInView.swift */, ); path = Views; sourceTree = ""; @@ -94,6 +108,7 @@ children = ( F8341F91295C63BB009C8EE6 /* ImageStatus.swift */, F88FAD2C295F4AD7009B20C9 /* ApplicationState.swift */, + F866F6AD29606367002E8F88 /* ApplicationViewMode.swift */, ); path = Models; sourceTree = ""; @@ -101,9 +116,13 @@ F8341F96295C6427009C8EE6 /* CoreData */ = { isa = PBXGroup; children = ( + F866F69E296040A8002E8F88 /* ApplicationSettings+CoreDataClass.swift */, + F866F69F296040A8002E8F88 /* ApplicationSettings+CoreDataProperties.swift */, F88FAD28295F43B8009B20C9 /* AccountData+CoreDataClass.swift */, F88FAD29295F43B8009B20C9 /* AccountData+CoreDataProperties.swift */, - F88C2474295C37BB0006098B /* Persistence.swift */, + F88C2474295C37BB0006098B /* CoreDataHandler.swift */, + F866F6A229604161002E8F88 /* AccountDataHandler.swift */, + F866F6A429604194002E8F88 /* ApplicationSettingsHandler.swift */, ); path = CoreData; sourceTree = ""; @@ -128,7 +147,6 @@ F88C245F295C37B80006098B = { isa = PBXGroup; children = ( - F88FAD2E295F4D3C009B20C9 /* AccountData+CoreDataProperties.swift */, F88C246A295C37B80006098B /* Vernissage */, F88C2469295C37B80006098B /* Products */, ); @@ -145,6 +163,7 @@ F88C246A295C37B80006098B /* Vernissage */ = { isa = PBXGroup; children = ( + F866F6A829604FFF002E8F88 /* Info.plist */, F88FAD30295F5010009B20C9 /* Services */, F83901A2295D863B00456AE2 /* Widgets */, F8341F97295C6434009C8EE6 /* Formatters */, @@ -153,6 +172,7 @@ F8341F94295C63FE009C8EE6 /* Extensions */, F8341F93295C63E2009C8EE6 /* Views */, F88C246B295C37B80006098B /* VernissageApp.swift */, + F866F6A929605AFA002E8F88 /* SceneDelegate.swift */, F88C246F295C37BB0006098B /* Assets.xcassets */, F88C2476295C37BB0006098B /* Vernissage.xcdatamodeld */, F88C2471295C37BB0006098B /* Preview Content */, @@ -193,7 +213,7 @@ ); name = Vernissage; packageProductDependencies = ( - F88C247F295C38400006098B /* MastodonSwift */, + F866F6B629608467002E8F88 /* MastodonSwift */, ); productName = Vernissage; productReference = F88C2468295C37B80006098B /* Vernissage.app */; @@ -224,7 +244,7 @@ ); mainGroup = F88C245F295C37B80006098B; packageReferences = ( - F88C247E295C38400006098B /* XCRemoteSwiftPackageReference "Mastodon" */, + F866F6B529608467002E8F88 /* XCRemoteSwiftPackageReference "Mastodon" */, ); productRefGroup = F88C2469295C37B80006098B /* Products */; projectDirPath = ""; @@ -252,11 +272,11 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + F866F6A0296040A8002E8F88 /* ApplicationSettings+CoreDataClass.swift in Sources */, 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 */, + F88C2475295C37BB0006098B /* CoreDataHandler.swift in Sources */, F88FAD2A295F43B8009B20C9 /* AccountData+CoreDataClass.swift in Sources */, F8341F92295C63BB009C8EE6 /* ImageStatus.swift in Sources */, F83901A6295D8EC000456AE2 /* LabelIconView.swift in Sources */, @@ -264,13 +284,19 @@ F88C246E295C37B80006098B /* MainView.swift in Sources */, F88C2478295C37BB0006098B /* Vernissage.xcdatamodeld in Sources */, F88C2482295C3A4F0006098B /* DetailsView.swift in Sources */, + F866F6A329604161002E8F88 /* AccountDataHandler.swift in Sources */, F88C2486295C48030006098B /* HTMLFotmattedText.swift in Sources */, + F866F6A529604194002E8F88 /* ApplicationSettingsHandler.swift in Sources */, + F866F6A729604629002E8F88 /* SignInView.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 */, + F866F6A1296040A8002E8F88 /* ApplicationSettings+CoreDataProperties.swift in Sources */, + F866F6AE29606367002E8F88 /* ApplicationViewMode.swift in Sources */, + F866F6AA29605AFA002E8F88 /* SceneDelegate.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -402,6 +428,7 @@ DEVELOPMENT_TEAM = B2U9FEKYP8; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = Vernissage/Info.plist; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; @@ -432,6 +459,7 @@ DEVELOPMENT_TEAM = B2U9FEKYP8; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = Vernissage/Info.plist; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; @@ -475,9 +503,9 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ - F88C247E295C38400006098B /* XCRemoteSwiftPackageReference "Mastodon" */ = { + F866F6B529608467002E8F88 /* XCRemoteSwiftPackageReference "Mastodon" */ = { isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/Swiftodon/Mastodon.swift"; + repositoryURL = "https://github.com/mczachurski/Mastodon.swift"; requirement = { branch = main; kind = branch; @@ -486,9 +514,9 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ - F88C247F295C38400006098B /* MastodonSwift */ = { + F866F6B629608467002E8F88 /* MastodonSwift */ = { isa = XCSwiftPackageProductDependency; - package = F88C247E295C38400006098B /* XCRemoteSwiftPackageReference "Mastodon" */; + package = F866F6B529608467002E8F88 /* XCRemoteSwiftPackageReference "Mastodon" */; productName = MastodonSwift; }; /* End XCSwiftPackageProductDependency section */ diff --git a/Vernissage/CoreData/AccountData+CoreDataProperties.swift b/Vernissage/CoreData/AccountData+CoreDataProperties.swift index b9709df..732cd82 100644 --- a/Vernissage/CoreData/AccountData+CoreDataProperties.swift +++ b/Vernissage/CoreData/AccountData+CoreDataProperties.swift @@ -16,21 +16,25 @@ extension AccountData { return NSFetchRequest(entityName: "AccountData") } - @NSManaged public var id: String? - @NSManaged public var username: String? + @NSManaged public var accessToken: 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 avatarData: Data? @NSManaged public var createdAt: String? + @NSManaged public var displayName: String? @NSManaged public var followersCount: Int32 @NSManaged public var followingCount: Int32 + @NSManaged public var header: URL? + @NSManaged public var id: String? + @NSManaged public var locked: Bool + @NSManaged public var note: String? @NSManaged public var statusesCount: Int32 - @NSManaged public var accessToken: String? - @NSManaged public var avatarData: Data? + @NSManaged public var url: URL? + @NSManaged public var username: String? + @NSManaged public var clientId: String + @NSManaged public var clientSecret: String + @NSManaged public var clientVapidKey: String + @NSManaged public var serverUrl: URL } diff --git a/Vernissage/CoreData/AccountDataHandler.swift b/Vernissage/CoreData/AccountDataHandler.swift new file mode 100644 index 0000000..34fe0d4 --- /dev/null +++ b/Vernissage/CoreData/AccountDataHandler.swift @@ -0,0 +1,26 @@ +// +// https://mczachurski.dev +// Copyright © 2022 Marcin Czachurski and the repository contributors. +// Licensed under the MIT License. +// + + +import Foundation + +class AccountDataHandler { + func getAccountsData() -> [AccountData] { + let context = CoreDataHandler.shared.container.viewContext + let fetchRequest = AccountData.fetchRequest() + do { + return try context.fetch(fetchRequest) + } catch { + print("Error during fetching accounts") + return [] + } + } + + func createAccountDataEntity() -> AccountData { + let context = CoreDataHandler.shared.container.viewContext + return AccountData(context: context) + } +} diff --git a/Vernissage/CoreData/ApplicationSettings+CoreDataClass.swift b/Vernissage/CoreData/ApplicationSettings+CoreDataClass.swift new file mode 100644 index 0000000..d20ad7f --- /dev/null +++ b/Vernissage/CoreData/ApplicationSettings+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(ApplicationSettings) +public class ApplicationSettings: NSManagedObject { + +} diff --git a/Vernissage/CoreData/ApplicationSettings+CoreDataProperties.swift b/Vernissage/CoreData/ApplicationSettings+CoreDataProperties.swift new file mode 100644 index 0000000..c4e33f3 --- /dev/null +++ b/Vernissage/CoreData/ApplicationSettings+CoreDataProperties.swift @@ -0,0 +1,25 @@ +// +// https://mczachurski.dev +// Copyright © 2022 Marcin Czachurski and the repository contributors. +// Licensed under the MIT License. +// + +// + +import Foundation +import CoreData + + +extension ApplicationSettings { + + @nonobjc public class func fetchRequest() -> NSFetchRequest { + return NSFetchRequest(entityName: "ApplicationSettings") + } + + @NSManaged public var currentAccount: String? + +} + +extension ApplicationSettings : Identifiable { + +} diff --git a/Vernissage/CoreData/ApplicationSettingsHandler.swift b/Vernissage/CoreData/ApplicationSettingsHandler.swift new file mode 100644 index 0000000..07a0932 --- /dev/null +++ b/Vernissage/CoreData/ApplicationSettingsHandler.swift @@ -0,0 +1,36 @@ +// +// https://mczachurski.dev +// Copyright © 2022 Marcin Czachurski and the repository contributors. +// Licensed under the MIT License. +// + + +import Foundation + +class ApplicationSettingsHandler { + func getDefaultSettings() -> ApplicationSettings { + var settingsList: [ApplicationSettings] = [] + + let context = CoreDataHandler.shared.container.viewContext + let fetchRequest = ApplicationSettings.fetchRequest() + do { + settingsList = try context.fetch(fetchRequest) + } catch { + print("Error during fetching application settings") + } + + if let settings = settingsList.first { + return settings + } else { + let settings = self.createApplicationSettingsEntity() + CoreDataHandler.shared.save() + + return settings + } + } + + private func createApplicationSettingsEntity() -> ApplicationSettings { + let context = CoreDataHandler.shared.container.viewContext + return ApplicationSettings(context: context) + } +} diff --git a/Vernissage/CoreData/Persistence.swift b/Vernissage/CoreData/CoreDataHandler.swift similarity index 70% rename from Vernissage/CoreData/Persistence.swift rename to Vernissage/CoreData/CoreDataHandler.swift index 731b077..9fb782a 100644 --- a/Vernissage/CoreData/Persistence.swift +++ b/Vernissage/CoreData/CoreDataHandler.swift @@ -7,28 +7,9 @@ import CoreData -struct PersistenceController { - static let shared = PersistenceController() - - static var preview: PersistenceController = { - let result = PersistenceController(inMemory: true) - let viewContext = result.container.viewContext - for _ in 0..<10 { - let newItem = AccountData(context: viewContext) - newItem.id = "123" - } - do { - try viewContext.save() - } catch { - // Replace this implementation with code to handle the error appropriately. - // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. - let nsError = error as NSError - fatalError("Unresolved error \(nsError), \(nsError.userInfo)") - } - return result - }() - - let container: NSPersistentContainer +public class CoreDataHandler { + public static let shared = CoreDataHandler() + public let container: NSPersistentContainer init(inMemory: Bool = false) { container = NSPersistentContainer(name: "Vernissage") @@ -53,4 +34,40 @@ struct PersistenceController { }) container.viewContext.automaticallyMergesChangesFromParent = true } + + public func save() { + let context = self.container.viewContext + if context.hasChanges { + do { + try context.save() + } catch { + // Replace this implementation with code to handle the error appropriately. + // fatalError() causes the application to generate a crash log and terminate. + // You should not use this function in a shipping application, although it may be useful during development. + + let nserror = error as NSError + fatalError("Unresolved error \(nserror), \(nserror.userInfo)") + } + } + } +} + +extension CoreDataHandler { + public static var preview: CoreDataHandler = { + let result = CoreDataHandler(inMemory: true) + let viewContext = result.container.viewContext + for _ in 0..<10 { + let newItem = AccountData(context: viewContext) + newItem.id = "123" + } + do { + try viewContext.save() + } catch { + // Replace this implementation with code to handle the error appropriately. + // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. + let nsError = error as NSError + fatalError("Unresolved error \(nsError), \(nsError.userInfo)") + } + return result + }() } diff --git a/Vernissage/Formatters/HTMLFotmattedText.swift b/Vernissage/Formatters/HTMLFotmattedText.swift index bda498e..9a10790 100644 --- a/Vernissage/Formatters/HTMLFotmattedText.swift +++ b/Vernissage/Formatters/HTMLFotmattedText.swift @@ -3,7 +3,6 @@ // Copyright © 2022 Marcin Czachurski and the repository contributors. // Licensed under the MIT License. // - import UIKit import SwiftUI @@ -38,7 +37,7 @@ struct HTMLFormattedText: UIViewRepresentable { } private func converHTML(text: String) -> NSAttributedString?{ - guard let data = text.data(using: .utf8) else { + guard let data = text.data(using: .utf16) else { return nil } diff --git a/Vernissage/Info.plist b/Vernissage/Info.plist new file mode 100644 index 0000000..a30a15a --- /dev/null +++ b/Vernissage/Info.plist @@ -0,0 +1,17 @@ + + + + + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLSchemes + + oauth-vernissage + + + + + diff --git a/Vernissage/Models/ApplicationViewMode.swift b/Vernissage/Models/ApplicationViewMode.swift new file mode 100644 index 0000000..9b3b153 --- /dev/null +++ b/Vernissage/Models/ApplicationViewMode.swift @@ -0,0 +1,12 @@ +// +// https://mczachurski.dev +// Copyright © 2022 Marcin Czachurski and the repository contributors. +// Licensed under the MIT License. +// + + +import Foundation + +enum ApplicationViewMode { + case loading, signIn, mainView +} diff --git a/Vernissage/SceneDelegate.swift b/Vernissage/SceneDelegate.swift new file mode 100644 index 0000000..c9d2dea --- /dev/null +++ b/Vernissage/SceneDelegate.swift @@ -0,0 +1,21 @@ +// +// https://mczachurski.dev +// Copyright © 2022 Marcin Czachurski and the repository contributors. +// Licensed under the MIT License. +// + + +import SwiftUI +import MastodonSwift +import OAuthSwift + +class SceneDelegate: NSObject, UISceneDelegate { + func scene(_ scene: UIScene, openURLContexts URLContexts: Set) { + guard let url = URLContexts.first?.url else { + return + } + if url.host == "oauth-callback" { + OAuthSwift.handle(url: url) + } + } +} diff --git a/Vernissage/Vernissage.xcdatamodeld/Vernissage.xcdatamodel/contents b/Vernissage/Vernissage.xcdatamodeld/Vernissage.xcdatamodel/contents index b557e8f..71dd21f 100644 --- a/Vernissage/Vernissage.xcdatamodeld/Vernissage.xcdatamodel/contents +++ b/Vernissage/Vernissage.xcdatamodeld/Vernissage.xcdatamodel/contents @@ -5,6 +5,9 @@ + + + @@ -13,8 +16,12 @@ + + + + \ No newline at end of file diff --git a/Vernissage/VernissageApp.swift b/Vernissage/VernissageApp.swift index d231d38..0d827b8 100644 --- a/Vernissage/VernissageApp.swift +++ b/Vernissage/VernissageApp.swift @@ -5,19 +5,130 @@ // import SwiftUI +import MastodonSwift @main -struct VernissageApp: App { - let persistenceController = PersistenceController.shared +struct VernissageApp: SwiftUI.App { + @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate + + let coreDataHandler = CoreDataHandler.shared + let applicationState = ApplicationState.shared + + @State var applicationViewMode: ApplicationViewMode = .loading var body: some Scene { WindowGroup { NavigationStack { - MainView() - .environment(\.managedObjectContext, persistenceController.container.viewContext) - .environmentObject(ApplicationState.shared) + switch applicationViewMode { + case .loading: + Text("Loading") + case .signIn: + SignInView { viewMode in + applicationViewMode = viewMode + } + .environment(\.managedObjectContext, coreDataHandler.container.viewContext) + .environmentObject(applicationState) + case .mainView: + MainView() + .environment(\.managedObjectContext, coreDataHandler.container.viewContext) + .environmentObject(applicationState) + } + } + .task { + let accountDataHandler = AccountDataHandler() + let accounts = accountDataHandler.getAccountsData() + + // 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 = accounts.first, 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!!!") + } + } } .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") + } + } + + // Save account data in database and in application state. + try self.coreDataHandler.container.viewContext.save() + } +} + +class AppDelegate: NSObject, UIApplicationDelegate { + func application(_ application: UIApplication, + configurationForConnecting connectingSceneSession: UISceneSession, + options: UIScene.ConnectionOptions + ) -> UISceneConfiguration { + let sceneConfig: UISceneConfiguration = UISceneConfiguration(name: nil, sessionRole: connectingSceneSession.role) + sceneConfig.delegateClass = SceneDelegate.self + return sceneConfig + } } diff --git a/Vernissage/Views/DetailsView.swift b/Vernissage/Views/DetailsView.swift index 7d08f62..7af6084 100644 --- a/Vernissage/Views/DetailsView.swift +++ b/Vernissage/Views/DetailsView.swift @@ -24,14 +24,13 @@ struct DetailsView: View { image .resizable() .clipShape(Circle()) - .shadow(radius: 10) .aspectRatio(contentMode: .fit) } placeholder: { Image(systemName: "person.circle") .resizable() + .foregroundColor(Color("mainTextColor")) } - .frame(height: 48) - .frame(width: 48) + .frame(width: 48.0, height: 48.0) VStack (alignment: .leading) { Text(current.status.account?.displayName ?? current.status.account?.username ?? "") diff --git a/Vernissage/Views/HomeFeedView.swift b/Vernissage/Views/HomeFeedView.swift index a370628..741d076 100644 --- a/Vernissage/Views/HomeFeedView.swift +++ b/Vernissage/Views/HomeFeedView.swift @@ -10,6 +10,7 @@ import MastodonSwift import UIKit struct HomeFeedView: View { + @EnvironmentObject var applicationState: ApplicationState @State private var statuses: [Status] = [] @State private var images: [ImageStatus] = [] @@ -61,24 +62,22 @@ struct HomeFeedView: View { } .task { do { - defer { - self.showLoading = false - } - self.showLoading = true try await loadData() + self.showLoading = false } catch { + self.showLoading = false 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) - + guard let accessData = self.applicationState.accountData, let accessToken = accessData.accessToken else { + return + } + + let client = MastodonClient(baseURL: accessData.serverUrl).getAuthenticated(token: accessToken) self.statuses = try await client.getHomeTimeline(limit: 40) var imagesCache: [ImageStatus] = [] diff --git a/Vernissage/Views/MainView.swift b/Vernissage/Views/MainView.swift index 6e3b4c7..7c885c1 100644 --- a/Vernissage/Views/MainView.swift +++ b/Vernissage/Views/MainView.swift @@ -11,22 +11,12 @@ import MastodonSwift struct MainView: View { @Environment(\.managedObjectContext) private var viewContext - @EnvironmentObject var applicationState: ApplicationState @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" - } + self.navBarTitle = self.getViewTitle(viewMode: viewMode) } } @@ -42,13 +32,6 @@ struct MainView: View { self.getLeadingToolbar() self.getPrincipalToolbar() } - .task { - do { - try await loadData() - } catch { - print("Error", error) - } - } } @ViewBuilder @@ -73,7 +56,7 @@ struct MainView: View { viewMode = .home } label: { HStack { - Text("Home") + Text(self.getViewTitle(viewMode: .home)) Image(systemName: "house") } } @@ -82,7 +65,7 @@ struct MainView: View { viewMode = .local } label: { HStack { - Text("Local") + Text(self.getViewTitle(viewMode: .local)) Image(systemName: "text.redaction") } } @@ -91,7 +74,7 @@ struct MainView: View { viewMode = .federated } label: { HStack { - Text("Global") + Text(self.getViewTitle(viewMode: .federated)) Image(systemName: "globe.europe.africa") } } @@ -100,7 +83,7 @@ struct MainView: View { viewMode = .notifications } label: { HStack { - Text("Notifications") + Text(self.getViewTitle(viewMode: .notifications)) Image(systemName: "bell.badge") } } @@ -112,7 +95,7 @@ struct MainView: View { .font(.subheadline) } .frame(width: 150) - .foregroundColor(Color.white) + .foregroundColor(Color("mainTextColor")) } } } @@ -127,71 +110,27 @@ struct MainView: View { Image(uiImage: uiImage) .resizable() .clipShape(Circle()) - .shadow(radius: 10) - .aspectRatio(contentMode: .fit) - .frame(height: 32) - .frame(width: 32) + .frame(width: 32.0, height: 32.0) } else { Image(systemName: "person.circle") + .resizable() + .frame(width: 32.0, height: 32.0) + .foregroundColor(Color("mainTextColor")) } } } } - 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) - - // Get account information from server. - let account = try await client.verifyCredentials() - - // 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 - } - - // Save account data in database and in application state. - try self.viewContext.save() - self.applicationState.accountData = accountData - } - - private func getAccountData() -> AccountData? { - let fetchRequest: NSFetchRequest = AccountData.fetchRequest() - - do { - return try self.viewContext.fetch(fetchRequest).first - } - catch { - return nil + private func getViewTitle(viewMode: ViewMode) -> String { + switch viewMode { + case .home: + return "Home" + case .local: + return "Local" + case .federated: + return "Federated" + case .notifications: + return "Notifications" } } } @@ -199,6 +138,6 @@ struct MainView: View { struct MainView_Previews: PreviewProvider { static var previews: some View { - MainView().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext) + MainView().environment(\.managedObjectContext, CoreDataHandler.preview.container.viewContext) } } diff --git a/Vernissage/Views/SignInView.swift b/Vernissage/Views/SignInView.swift new file mode 100644 index 0000000..2db3e98 --- /dev/null +++ b/Vernissage/Views/SignInView.swift @@ -0,0 +1,108 @@ +// +// https://mczachurski.dev +// Copyright © 2022 Marcin Czachurski and the repository contributors. +// Licensed under the MIT License. +// + +import SwiftUI +import MastodonSwift + +struct SignInView: View { + @Environment(\.managedObjectContext) private var viewContext + @EnvironmentObject var applicationState: ApplicationState + + @State private var serverAddress: String = "" + + var onSignInStateChenge: (_ applicationViewMode: ApplicationViewMode) -> Void? + + var body: some View { + VStack { + HStack { + TextField( + "Server address", + text: $serverAddress + ) + .onSubmit { + } + .textInputAutocapitalization(.never) + .disableAutocorrection(true) + + Button("Go") { + Task { + 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") + } + } + + // Save account data in database and in application state. + try self.viewContext.save() + self.applicationState.accountData = accountData + self.onSignInStateChenge(.mainView) + } + } + } + } + .padding() + .navigationBarTitle("Sign in to Pixelfed") + .navigationBarTitleDisplayMode(.inline) + } +} + +struct SignInView_Previews: PreviewProvider { + static var previews: some View { + SignInView { applicationViewMode in + } + } +}