Add sign-in to server

This commit is contained in:
Marcin Czachursk 2022-12-31 16:31:05 +01:00
parent ed8fa69d9a
commit 37c6dc0699
17 changed files with 511 additions and 148 deletions

View File

@ -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 = "<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>"; };
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>"; };
F866F6A429604194002E8F88 /* ApplicationSettingsHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationSettingsHandler.swift; sourceTree = "<group>"; };
F866F6A629604629002E8F88 /* SignInView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInView.swift; sourceTree = "<group>"; };
F866F6A829604FFF002E8F88 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
F866F6A929605AFA002E8F88 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
F866F6AD29606367002E8F88 /* ApplicationViewMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationViewMode.swift; sourceTree = "<group>"; };
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 = "<group>"; };
F88C246D295C37B80006098B /* MainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainView.swift; sourceTree = "<group>"; };
F88C246F295C37BB0006098B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
F88C2472295C37BB0006098B /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
F88C2474295C37BB0006098B /* Persistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Persistence.swift; sourceTree = "<group>"; };
F88C2474295C37BB0006098B /* CoreDataHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataHandler.swift; sourceTree = "<group>"; };
F88C2477295C37BB0006098B /* Vernissage.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Vernissage.xcdatamodel; sourceTree = "<group>"; };
F88C2481295C3A4F0006098B /* DetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailsView.swift; sourceTree = "<group>"; };
F88C2485295C48030006098B /* HTMLFotmattedText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTMLFotmattedText.swift; sourceTree = "<group>"; };
@ -52,7 +66,6 @@
F88FAD28295F43B8009B20C9 /* AccountData+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AccountData+CoreDataClass.swift"; sourceTree = "<group>"; };
F88FAD29295F43B8009B20C9 /* AccountData+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AccountData+CoreDataProperties.swift"; sourceTree = "<group>"; };
F88FAD2C295F4AD7009B20C9 /* ApplicationState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationState.swift; sourceTree = "<group>"; };
F88FAD2E295F4D3C009B20C9 /* AccountData+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = "AccountData+CoreDataProperties.swift"; path = "Vernissage/CoreData/AccountData+CoreDataProperties.swift"; sourceTree = "<group>"; };
F88FAD31295F5029009B20C9 /* RemoteFileService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteFileService.swift; sourceTree = "<group>"; };
/* 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 = "<group>";
@ -94,6 +108,7 @@
children = (
F8341F91295C63BB009C8EE6 /* ImageStatus.swift */,
F88FAD2C295F4AD7009B20C9 /* ApplicationState.swift */,
F866F6AD29606367002E8F88 /* ApplicationViewMode.swift */,
);
path = Models;
sourceTree = "<group>";
@ -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 = "<group>";
@ -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 */

View File

@ -16,21 +16,25 @@ extension AccountData {
return NSFetchRequest<AccountData>(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
}

View File

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

View File

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

View File

@ -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<ApplicationSettings> {
return NSFetchRequest<ApplicationSettings>(entityName: "ApplicationSettings")
}
@NSManaged public var currentAccount: String?
}
extension ApplicationSettings : Identifiable {
}

View File

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

View File

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

View File

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

17
Vernissage/Info.plist Normal file
View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLSchemes</key>
<array>
<string>oauth-vernissage</string>
</array>
</dict>
</array>
</dict>
</plist>

View File

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

View File

@ -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<UIOpenURLContext>) {
guard let url = URLContexts.first?.url else {
return
}
if url.host == "oauth-callback" {
OAuthSwift.handle(url: url)
}
}
}

View File

@ -5,6 +5,9 @@
<attribute name="acct" attributeType="String"/>
<attribute name="avatar" optional="YES" attributeType="URI"/>
<attribute name="avatarData" optional="YES" attributeType="Binary"/>
<attribute name="clientId" attributeType="String"/>
<attribute name="clientSecret" attributeType="String"/>
<attribute name="clientVapidKey" attributeType="String"/>
<attribute name="createdAt" attributeType="String"/>
<attribute name="displayName" optional="YES" attributeType="String"/>
<attribute name="followersCount" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
@ -13,8 +16,12 @@
<attribute name="id" attributeType="String"/>
<attribute name="locked" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="note" optional="YES" attributeType="String"/>
<attribute name="serverUrl" attributeType="URI"/>
<attribute name="statusesCount" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="url" optional="YES" attributeType="URI"/>
<attribute name="username" attributeType="String"/>
</entity>
<entity name="ApplicationSettings" representedClassName="ApplicationSettings" syncable="YES">
<attribute name="currentAccount" optional="YES" attributeType="String"/>
</entity>
</model>

View File

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

View File

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

View File

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

View File

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

View File

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