Restructure model

This commit is contained in:
Arkadiusz Fal
2021-10-21 11:30:33 +02:00
parent c3326a56af
commit bb8a8dee05
15 changed files with 38 additions and 16 deletions

View File

@@ -0,0 +1,79 @@
import Defaults
import Foundation
struct Account: Defaults.Serializable, Hashable, Identifiable {
struct AccountsBridge: Defaults.Bridge {
typealias Value = Account
typealias Serializable = [String: String]
func serialize(_ value: Value?) -> Serializable? {
guard let value = value else {
return nil
}
return [
"id": value.id,
"instanceID": value.instanceID,
"name": value.name ?? "",
"url": value.url,
"sid": value.sid
]
}
func deserialize(_ object: Serializable?) -> Value? {
guard
let object = object,
let id = object["id"],
let instanceID = object["instanceID"],
let url = object["url"],
let sid = object["sid"]
else {
return nil
}
let name = object["name"] ?? ""
return Account(id: id, instanceID: instanceID, name: name, url: url, sid: sid)
}
}
static var bridge = AccountsBridge()
let id: String
let instanceID: String
var name: String?
let url: String
let sid: String
let anonymous: Bool
init(id: String? = nil, instanceID: String? = nil, name: String? = nil, url: String? = nil, sid: String? = nil, anonymous: Bool = false) {
self.anonymous = anonymous
self.id = id ?? (anonymous ? "anonymous-\(instanceID!)" : UUID().uuidString)
self.instanceID = instanceID ?? UUID().uuidString
self.name = name
self.url = url ?? ""
self.sid = sid ?? ""
}
var instance: Instance {
Defaults[.instances].first { $0.id == instanceID }!
}
var anonymizedSID: String {
guard sid.count > 3 else {
return ""
}
let index = sid.index(sid.startIndex, offsetBy: 4)
return String(sid[..<index])
}
var description: String {
(name != nil && name!.isEmpty) ? "Unnamed (\(anonymizedSID))" : name!
}
func hash(into hasher: inout Hasher) {
hasher.combine(sid)
}
}

View File

@@ -0,0 +1,143 @@
import Foundation
import Siesta
import SwiftUI
final class AccountValidator: Service {
let app: Binding<VideosApp>
let url: String
let account: Account?
var formObjectID: Binding<String>
var isValid: Binding<Bool>
var isValidated: Binding<Bool>
var isValidating: Binding<Bool>
var error: Binding<String?>?
init(
app: Binding<VideosApp>,
url: String,
account: Account? = nil,
id: Binding<String>,
isValid: Binding<Bool>,
isValidated: Binding<Bool>,
isValidating: Binding<Bool>,
error: Binding<String?>? = nil
) {
self.app = app
self.url = url
self.account = account
formObjectID = id
self.isValid = isValid
self.isValidated = isValidated
self.isValidating = isValidating
self.error = error
super.init(baseURL: url)
configure()
}
func configure() {
configure {
$0.pipeline[.parsing].add(SwiftyJSONTransformer, contentTypes: ["*/json"])
}
configure("/api/v1/auth/feed", requestMethods: [.get]) {
guard self.account != nil else {
return
}
$0.headers["Cookie"] = self.cookieHeader
}
}
func validateInstance() {
reset()
// TODO: validation for Piped instances
guard app.wrappedValue == .invidious else {
isValid.wrappedValue = true
error?.wrappedValue = nil
isValidated.wrappedValue = true
isValidating.wrappedValue = false
return
}
stats
.load()
.onSuccess { response in
guard self.url == self.formObjectID.wrappedValue else {
return
}
if response
.json
.dictionaryValue["software"]?
.dictionaryValue["name"]?
.stringValue == "invidious"
{
self.isValid.wrappedValue = true
self.error?.wrappedValue = nil
} else {
self.isValid.wrappedValue = false
self.error?.wrappedValue = "Not an Invidious Instance"
}
}
.onFailure { error in
guard self.url == self.formObjectID.wrappedValue else {
return
}
self.isValid.wrappedValue = false
self.error?.wrappedValue = error.userMessage
}
.onCompletion { _ in
self.isValidated.wrappedValue = true
self.isValidating.wrappedValue = false
}
}
func validateInvidiousAccount() {
reset()
feed
.load()
.onSuccess { _ in
guard self.account!.sid == self.formObjectID.wrappedValue else {
return
}
self.isValid.wrappedValue = true
}
.onFailure { _ in
guard self.account!.sid == self.formObjectID.wrappedValue else {
return
}
self.isValid.wrappedValue = false
}
.onCompletion { _ in
self.isValidated.wrappedValue = true
self.isValidating.wrappedValue = false
}
}
func reset() {
isValid.wrappedValue = false
isValidated.wrappedValue = false
isValidating.wrappedValue = false
error?.wrappedValue = nil
}
var cookieHeader: String {
"SID=\(account!.sid)"
}
var stats: Resource {
resource("/api/v1/stats")
}
var feed: Resource {
resource("/api/v1/auth/feed")
}
}

View File

@@ -0,0 +1,89 @@
import Combine
import Defaults
import Foundation
final class AccountsModel: ObservableObject {
@Published private(set) var current: Account!
@Published private var invidious = InvidiousAPI()
@Published private var piped = PipedAPI()
private var cancellables = [AnyCancellable]()
var all: [Account] {
Defaults[.accounts]
}
var lastUsed: Account? {
guard let id = Defaults[.lastAccountID] else {
return nil
}
return AccountsModel.find(id)
}
var app: VideosApp {
current?.instance.app ?? .invidious
}
var api: VideosAPI {
app == .piped ? piped : invidious
}
var isEmpty: Bool {
current.isNil
}
var signedIn: Bool {
!isEmpty && !current.anonymous
}
init() {
cancellables.append(
invidious.objectWillChange.sink { [weak self] _ in self?.objectWillChange.send() }
)
cancellables.append(
piped.objectWillChange.sink { [weak self] _ in self?.objectWillChange.send() }
)
}
func setCurrent(_ account: Account! = nil) {
guard account != current else {
return
}
current = account
guard !account.isNil else {
return
}
switch account.instance.app {
case .invidious:
invidious.setAccount(account)
case .piped:
piped.setAccount(account)
}
Defaults[.lastAccountID] = account.anonymous ? nil : account.id
Defaults[.lastInstanceID] = account.instanceID
}
static func find(_ id: Account.ID) -> Account? {
Defaults[.accounts].first { $0.id == id }
}
static func add(instance: Instance, name: String, sid: String) -> Account {
let account = Account(instanceID: instance.id, name: name, url: instance.url, sid: sid)
Defaults[.accounts].append(account)
return account
}
static func remove(_ account: Account) {
if let accountIndex = Defaults[.accounts].firstIndex(where: { $0.id == account.id }) {
Defaults[.accounts].remove(at: accountIndex)
}
}
}

View File

@@ -0,0 +1,80 @@
import Defaults
import Foundation
struct Instance: Defaults.Serializable, Hashable, Identifiable {
struct InstancesBridge: Defaults.Bridge {
typealias Value = Instance
typealias Serializable = [String: String]
func serialize(_ value: Value?) -> Serializable? {
guard let value = value else {
return nil
}
return [
"app": value.app.rawValue,
"id": value.id,
"name": value.name,
"url": value.url
]
}
func deserialize(_ object: Serializable?) -> Value? {
guard
let object = object,
let app = VideosApp(rawValue: object["app"] ?? ""),
let id = object["id"],
let url = object["url"]
else {
return nil
}
let name = object["name"] ?? ""
return Instance(app: app, id: id, name: name, url: url)
}
}
static var bridge = InstancesBridge()
let app: VideosApp
let id: String
let name: String
let url: String
init(app: VideosApp, id: String? = nil, name: String, url: String) {
self.app = app
self.id = id ?? UUID().uuidString
self.name = name
self.url = url
}
var anonymous: VideosAPI {
switch app {
case .invidious:
return InvidiousAPI(account: anonymousAccount)
case .piped:
return PipedAPI(account: anonymousAccount)
}
}
var description: String {
"\(app.name) - \(shortDescription)"
}
var longDescription: String {
name.isEmpty ? "\(app.name) - \(url)" : "\(app.name) - \(name) (\(url))"
}
var shortDescription: String {
name.isEmpty ? url : name
}
var anonymousAccount: Account {
Account(instanceID: id, name: "Anonymous", url: url, anonymous: true)
}
func hash(into hasher: inout Hasher) {
hasher.combine(url)
}
}

View File

@@ -0,0 +1,47 @@
import Defaults
import Foundation
final class InstancesModel: ObservableObject {
var all: [Instance] {
Defaults[.instances]
}
var lastUsed: Instance? {
guard let id = Defaults[.lastInstanceID] else {
return nil
}
return InstancesModel.find(id)
}
static func find(_ id: Instance.ID?) -> Instance? {
guard id != nil else {
return nil
}
return Defaults[.instances].first { $0.id == id }
}
static func accounts(_ id: Instance.ID?) -> [Account] {
Defaults[.accounts].filter { $0.instanceID == id }
}
static func add(app: VideosApp, name: String, url: String) -> Instance {
let instance = Instance(app: app, id: UUID().uuidString, name: name, url: url)
Defaults[.instances].append(instance)
return instance
}
static func remove(_ instance: Instance) {
let accounts = InstancesModel.accounts(instance.id)
if let index = Defaults[.instances].firstIndex(where: { $0.id == instance.id }) {
Defaults[.instances].remove(at: index)
accounts.forEach { AccountsModel.remove($0) }
}
}
static func setLastAccount(_ account: Account?) {
Defaults[.lastAccountID] = account?.id
}
}