mirror of
https://github.com/yattee/yattee.git
synced 2025-08-06 10:44:06 +00:00
Locations manifest, reorganized instances settings
This commit is contained in:
@@ -5,37 +5,50 @@ struct Account: Defaults.Serializable, Hashable, Identifiable {
|
||||
static var bridge = AccountsBridge()
|
||||
|
||||
let id: String
|
||||
let instanceID: String
|
||||
let app: VideosApp
|
||||
let instanceID: String?
|
||||
var name: String?
|
||||
let url: String
|
||||
let username: String
|
||||
let password: String?
|
||||
var token: String?
|
||||
let anonymous: Bool
|
||||
let country: String?
|
||||
let region: String?
|
||||
|
||||
init(
|
||||
id: String? = nil,
|
||||
app: VideosApp? = nil,
|
||||
instanceID: String? = nil,
|
||||
name: String? = nil,
|
||||
url: String? = nil,
|
||||
username: String? = nil,
|
||||
password: String? = nil,
|
||||
token: String? = nil,
|
||||
anonymous: Bool = false
|
||||
anonymous: Bool = false,
|
||||
country: String? = nil,
|
||||
region: String? = nil
|
||||
) {
|
||||
self.anonymous = anonymous
|
||||
|
||||
self.id = id ?? (anonymous ? "anonymous-\(instanceID!)" : UUID().uuidString)
|
||||
self.instanceID = instanceID ?? UUID().uuidString
|
||||
self.id = id ?? (anonymous ? "anonymous-\(instanceID ?? url ?? UUID().uuidString)" : UUID().uuidString)
|
||||
self.app = app ?? .invidious
|
||||
self.instanceID = instanceID
|
||||
self.name = name
|
||||
self.url = url ?? ""
|
||||
self.username = username ?? ""
|
||||
self.token = token
|
||||
self.password = password ?? ""
|
||||
self.country = country
|
||||
self.region = region
|
||||
}
|
||||
|
||||
var instance: Instance! {
|
||||
Defaults[.instances].first { $0.id == instanceID }
|
||||
Defaults[.instances].first { $0.id == instanceID } ?? Instance(app: app, name: url, apiURL: url)
|
||||
}
|
||||
|
||||
var isPublic: Bool {
|
||||
instanceID.isNil
|
||||
}
|
||||
|
||||
var shortUsername: String {
|
||||
|
@@ -3,7 +3,7 @@ import Siesta
|
||||
import SwiftUI
|
||||
|
||||
final class AccountValidator: Service {
|
||||
let app: Binding<VideosApp>
|
||||
let app: Binding<VideosApp?>
|
||||
let url: String
|
||||
let account: Account!
|
||||
|
||||
@@ -13,8 +13,10 @@ final class AccountValidator: Service {
|
||||
var isValidating: Binding<Bool>
|
||||
var error: Binding<String?>?
|
||||
|
||||
private var appsToValidateInstance = VideosApp.allCases
|
||||
|
||||
init(
|
||||
app: Binding<VideosApp>,
|
||||
app: Binding<VideosApp?>,
|
||||
url: String,
|
||||
account: Account? = nil,
|
||||
id: Binding<String>,
|
||||
@@ -54,82 +56,128 @@ final class AccountValidator: Service {
|
||||
}
|
||||
}
|
||||
|
||||
func instanceValidationResource(_ app: VideosApp) -> Resource {
|
||||
switch app {
|
||||
case .invidious:
|
||||
return resource("/api/v1/videos/dQw4w9WgXcQ")
|
||||
|
||||
case .piped:
|
||||
return resource("/streams/dQw4w9WgXcQ")
|
||||
}
|
||||
}
|
||||
|
||||
func validateInstance() {
|
||||
reset()
|
||||
|
||||
neverGonnaGiveYouUp
|
||||
guard let app = appsToValidateInstance.popLast() else { return }
|
||||
tryValidatingUsing(app)
|
||||
}
|
||||
|
||||
func tryValidatingUsing(_ app: VideosApp) {
|
||||
instanceValidationResource(app)
|
||||
.load()
|
||||
.onSuccess { response in
|
||||
guard self.url == self.formObjectID.wrappedValue else {
|
||||
return
|
||||
}
|
||||
|
||||
guard !response.json.isEmpty else {
|
||||
guard let app = self.appsToValidateInstance.popLast() else {
|
||||
self.isValid.wrappedValue = false
|
||||
self.isValidated.wrappedValue = true
|
||||
self.isValidating.wrappedValue = false
|
||||
return
|
||||
}
|
||||
|
||||
self.tryValidatingUsing(app)
|
||||
return
|
||||
}
|
||||
|
||||
let json = response.json.dictionaryValue
|
||||
let author = self.app.wrappedValue == .invidious ? json["author"] : json["uploader"]
|
||||
let author = app == .invidious ? json["author"] : json["uploader"]
|
||||
|
||||
if author == "Rick Astley" {
|
||||
self.app.wrappedValue = app
|
||||
self.isValid.wrappedValue = true
|
||||
self.error?.wrappedValue = nil
|
||||
} else {
|
||||
self.isValid.wrappedValue = false
|
||||
}
|
||||
self.isValidated.wrappedValue = true
|
||||
self.isValidating.wrappedValue = false
|
||||
}
|
||||
.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
|
||||
if self.appsToValidateInstance.isEmpty {
|
||||
self.isValidating.wrappedValue = false
|
||||
self.isValidated.wrappedValue = true
|
||||
self.isValid.wrappedValue = false
|
||||
self.error?.wrappedValue = error.userMessage
|
||||
} else {
|
||||
guard let app = self.appsToValidateInstance.popLast() else { return }
|
||||
self.tryValidatingUsing(app)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func validateAccount() {
|
||||
reset()
|
||||
|
||||
accountRequest
|
||||
.onSuccess { response in
|
||||
guard self.account!.username == self.formObjectID.wrappedValue else {
|
||||
return
|
||||
}
|
||||
guard let request = accountRequest else {
|
||||
isValid.wrappedValue = false
|
||||
isValidated.wrappedValue = true
|
||||
isValidating.wrappedValue = false
|
||||
|
||||
switch self.app.wrappedValue {
|
||||
case .invidious:
|
||||
self.isValid.wrappedValue = true
|
||||
case .piped:
|
||||
let error = response.json.dictionaryValue["error"]?.string
|
||||
let token = response.json.dictionaryValue["token"]?.string
|
||||
self.isValid.wrappedValue = error?.isEmpty ?? !(token?.isEmpty ?? true)
|
||||
self.error!.wrappedValue = error
|
||||
}
|
||||
}
|
||||
.onFailure { _ in
|
||||
guard self.account!.username == self.formObjectID.wrappedValue else {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
self.isValid.wrappedValue = false
|
||||
request.onSuccess { response in
|
||||
guard self.account!.username == self.formObjectID.wrappedValue else {
|
||||
return
|
||||
}
|
||||
.onCompletion { _ in
|
||||
self.isValidated.wrappedValue = true
|
||||
self.isValidating.wrappedValue = false
|
||||
|
||||
switch self.app.wrappedValue {
|
||||
case .invidious:
|
||||
self.isValid.wrappedValue = true
|
||||
case .piped:
|
||||
let error = response.json.dictionaryValue["error"]?.string
|
||||
let token = response.json.dictionaryValue["token"]?.string
|
||||
self.isValid.wrappedValue = error?.isEmpty ?? !(token?.isEmpty ?? true)
|
||||
self.error!.wrappedValue = error
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
.onFailure { _ in
|
||||
guard self.account!.username == self.formObjectID.wrappedValue else {
|
||||
return
|
||||
}
|
||||
|
||||
self.isValid.wrappedValue = false
|
||||
}
|
||||
.onCompletion { _ in
|
||||
self.isValidated.wrappedValue = true
|
||||
self.isValidating.wrappedValue = false
|
||||
}
|
||||
}
|
||||
|
||||
var accountRequest: Request {
|
||||
var accountRequest: Request? {
|
||||
switch app.wrappedValue {
|
||||
case .invidious:
|
||||
return feed.load()
|
||||
case .piped:
|
||||
return login.request(.post, json: ["username": account.username, "password": account.password])
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func reset() {
|
||||
appsToValidateInstance = VideosApp.allCases
|
||||
app.wrappedValue = nil
|
||||
isValid.wrappedValue = false
|
||||
isValidated.wrappedValue = false
|
||||
isValidating.wrappedValue = false
|
||||
|
@@ -12,7 +12,7 @@ struct AccountsBridge: Defaults.Bridge {
|
||||
|
||||
return [
|
||||
"id": value.id,
|
||||
"instanceID": value.instanceID,
|
||||
"instanceID": value.instanceID ?? "",
|
||||
"name": value.name ?? "",
|
||||
"apiURL": value.url,
|
||||
"username": value.username,
|
||||
|
@@ -8,6 +8,8 @@ final class AccountsModel: ObservableObject {
|
||||
@Published private var invidious = InvidiousAPI()
|
||||
@Published private var piped = PipedAPI()
|
||||
|
||||
@Published var publicAccount: Account?
|
||||
|
||||
private var cancellables = [AnyCancellable]()
|
||||
|
||||
var all: [Account] {
|
||||
@@ -70,7 +72,7 @@ final class AccountsModel: ObservableObject {
|
||||
piped.setAccount(account)
|
||||
}
|
||||
|
||||
Defaults[.lastAccountID] = account.anonymous ? nil : account.id
|
||||
Defaults[.lastAccountID] = account.anonymous ? (account.isPublic ? "public" : nil) : account.id
|
||||
Defaults[.lastInstanceID] = account.instanceID
|
||||
}
|
||||
|
||||
|
104
Model/InstancesManifest.swift
Normal file
104
Model/InstancesManifest.swift
Normal file
@@ -0,0 +1,104 @@
|
||||
import Defaults
|
||||
import Foundation
|
||||
import Siesta
|
||||
import SwiftyJSON
|
||||
|
||||
final class InstancesManifest: Service, ObservableObject {
|
||||
static let builtinManifestUrl = "https://r.yattee.stream/manifest.json"
|
||||
static let shared = InstancesManifest()
|
||||
|
||||
@Published var instances = [ManifestedInstance]()
|
||||
|
||||
init() {
|
||||
super.init()
|
||||
|
||||
configure {
|
||||
$0.pipeline[.parsing].add(SwiftyJSONTransformer, contentTypes: ["*/json"])
|
||||
}
|
||||
|
||||
configureTransformer(
|
||||
manifestURL,
|
||||
requestMethods: [.get]
|
||||
) { (content: Entity<JSON>
|
||||
) -> [ManifestedInstance] in
|
||||
guard let instances = content.json.dictionaryValue["instances"] else { return [] }
|
||||
|
||||
return instances.arrayValue.compactMap(self.extractInstance)
|
||||
}
|
||||
}
|
||||
|
||||
func setPublicAccount(_ country: String?, accounts: AccountsModel, asCurrent: Bool = true) {
|
||||
guard let country = country else {
|
||||
accounts.publicAccount = nil
|
||||
if asCurrent {
|
||||
accounts.setCurrent(nil)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
instancesList.load().onSuccess { response in
|
||||
if let instances: [ManifestedInstance] = response.typedContent() {
|
||||
guard let instance = instances.filter { $0.country == country }.randomElement() else { return }
|
||||
let account = instance.anonymousAccount
|
||||
accounts.publicAccount = account
|
||||
if asCurrent {
|
||||
accounts.setCurrent(account)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func changePublicAccount(_ accounts: AccountsModel, settings: SettingsModel) {
|
||||
instancesList.load().onSuccess { response in
|
||||
if let instances: [ManifestedInstance] = response.typedContent() {
|
||||
let countryInstances = instances.filter { $0.country == Defaults[.countryOfPublicInstances] }
|
||||
let region = countryInstances.first?.region ?? "Europe"
|
||||
var regionInstances = instances.filter { $0.region == region }
|
||||
|
||||
if let publicAccountUrl = accounts.publicAccount?.url {
|
||||
regionInstances = regionInstances.filter { $0.url.absoluteString != publicAccountUrl }
|
||||
}
|
||||
|
||||
guard let instance = regionInstances.randomElement() else {
|
||||
settings.presentAlert(title: "Could not change location", message: "No locations available at the moment")
|
||||
return
|
||||
}
|
||||
|
||||
let account = instance.anonymousAccount
|
||||
accounts.publicAccount = account
|
||||
accounts.setCurrent(account)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func extractInstance(from json: JSON) -> ManifestedInstance? {
|
||||
guard let app = json["app"].string,
|
||||
let videosApp = VideosApp(rawValue: app.lowercased()),
|
||||
let region = json["region"].string,
|
||||
let country = json["country"].string,
|
||||
let flag = json["flag"].string,
|
||||
let url = json["url"].url else { return nil }
|
||||
|
||||
return ManifestedInstance(
|
||||
app: videosApp,
|
||||
country: country,
|
||||
region: region,
|
||||
flag: flag,
|
||||
url: url
|
||||
)
|
||||
}
|
||||
|
||||
var manifestURL: String {
|
||||
var url = Defaults[.instancesManifest]
|
||||
|
||||
if url.isEmpty {
|
||||
url = Self.builtinManifestUrl
|
||||
}
|
||||
|
||||
return url
|
||||
}
|
||||
|
||||
var instancesList: Resource {
|
||||
resource(absoluteURL: manifestURL)
|
||||
}
|
||||
}
|
30
Model/ManifestedInstance.swift
Normal file
30
Model/ManifestedInstance.swift
Normal file
@@ -0,0 +1,30 @@
|
||||
import Foundation
|
||||
|
||||
struct ManifestedInstance: Identifiable, Hashable {
|
||||
let id = UUID().uuidString
|
||||
let app: VideosApp
|
||||
let country: String
|
||||
let region: String
|
||||
let flag: String
|
||||
let url: URL
|
||||
|
||||
var instance: Instance {
|
||||
.init(app: app, name: "Public - \(country)", apiURL: url.absoluteString)
|
||||
}
|
||||
|
||||
var location: String {
|
||||
"\(flag) \(country)"
|
||||
}
|
||||
|
||||
var anonymousAccount: Account {
|
||||
.init(
|
||||
id: UUID().uuidString,
|
||||
app: app,
|
||||
name: location,
|
||||
url: url.absoluteString,
|
||||
anonymous: true,
|
||||
country: country,
|
||||
region: region
|
||||
)
|
||||
}
|
||||
}
|
@@ -221,8 +221,9 @@ final class NavigationModel: ObservableObject {
|
||||
#endif
|
||||
}
|
||||
|
||||
func presentAlert(title: String, message: String) {
|
||||
alert = Alert(title: Text(title), message: Text(message))
|
||||
func presentAlert(title: String, message: String? = nil) {
|
||||
let message = message.isNil ? nil : Text(message!)
|
||||
alert = Alert(title: Text(title), message: message)
|
||||
presentingAlert = true
|
||||
}
|
||||
|
||||
|
18
Model/SettingsModel.swift
Normal file
18
Model/SettingsModel.swift
Normal file
@@ -0,0 +1,18 @@
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
class SettingsModel: ObservableObject {
|
||||
@Published var presentingAlert = false
|
||||
@Published var alert = Alert(title: Text("Error"))
|
||||
|
||||
func presentAlert(title: String, message: String? = nil) {
|
||||
let message = message.isNil ? nil : Text(message!)
|
||||
alert = Alert(title: Text(title), message: message)
|
||||
presentingAlert = true
|
||||
}
|
||||
|
||||
func presentAlert(_ alert: Alert) {
|
||||
self.alert = alert
|
||||
presentingAlert = true
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user