mirror of
https://github.com/yattee/yattee.git
synced 2025-08-06 18:54:11 +00:00
Restructure model
This commit is contained in:
79
Model/Accounts/Account.swift
Normal file
79
Model/Accounts/Account.swift
Normal 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)
|
||||
}
|
||||
}
|
143
Model/Accounts/AccountValidator.swift
Normal file
143
Model/Accounts/AccountValidator.swift
Normal 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")
|
||||
}
|
||||
}
|
89
Model/Accounts/AccountsModel.swift
Normal file
89
Model/Accounts/AccountsModel.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
80
Model/Accounts/Instance.swift
Normal file
80
Model/Accounts/Instance.swift
Normal 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)
|
||||
}
|
||||
}
|
47
Model/Accounts/InstancesModel.swift
Normal file
47
Model/Accounts/InstancesModel.swift
Normal 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
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user