trackers page
Some checks are pending
Build and Release IPA / Build IPA (push) Waiting to run

This commit is contained in:
cranci1 2025-03-23 10:58:32 +01:00
parent 748cc4e999
commit 59b5f84077
3 changed files with 201 additions and 0 deletions

View file

@ -0,0 +1,194 @@
//
// SettingsViewTrackers.swift
// Sora
//
// Created by Francesco on 23/03/25.
//
import SwiftUI
import Security
import Kingfisher
struct SettingsViewTrackers: View {
@State private var status: String = "You are not logged in"
@State private var isLoggedIn: Bool = false
@State private var username: String = ""
@State private var isLoading: Bool = false
@State private var profileColor: Color = .primary
var body: some View {
Form {
Section(header: Text("AniList"), footer: Text("Sora and cranci1 are not affiliated with AniList in any way.")) {
HStack() {
KFImage(URL(string: "https://raw.githubusercontent.com/cranci1/Ryu/2f10226aa087154974a70c1ec78aa83a47daced9/Ryu/Assets.xcassets/Listing/Anilist.imageset/anilist.png"))
.resizable()
.frame(width: 80, height: 80)
.clipShape(Rectangle())
Text("AniList.co")
.font(.title2)
}
if isLoading {
ProgressView()
} else {
if isLoggedIn {
HStack(spacing: 0) {
Text("Logged in as ")
Text(username)
.foregroundColor(profileColor)
.fontWeight(.semibold)
}
} else {
Text(status)
.multilineTextAlignment(.center)
}
}
Button(isLoggedIn ? "Log Out from AniList.co" : "Log In with AniList.co") {
if isLoggedIn {
logout()
} else {
login()
}
}
}
}
.navigationTitle("Trackers")
.onAppear {
updateStatus()
}
}
func login() {
status = "Starting authentication..."
AniListLogin.authenticate()
}
func logout() {
removeTokenFromKeychain()
status = "You are not logged in"
isLoggedIn = false
username = ""
profileColor = .primary
}
func updateStatus() {
if let token = getTokenFromKeychain() {
isLoggedIn = true
fetchUserInfo(token: token)
} else {
isLoggedIn = false
status = "You are not logged in"
}
}
func fetchUserInfo(token: String) {
isLoading = true
let userInfoURL = URL(string: "https://graphql.anilist.co")!
var request = URLRequest(url: userInfoURL)
request.httpMethod = "POST"
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let query = """
{
Viewer {
id
name
options {
profileColor
}
}
}
"""
let body: [String: Any] = ["query": query]
do {
request.httpBody = try JSONSerialization.data(withJSONObject: body, options: [])
} catch {
status = "Failed to serialize request"
Logger.shared.log("Failed to serialize request", type: "Error")
isLoading = false
return
}
URLSession.shared.dataTask(with: request) { data, response, error in
DispatchQueue.main.async {
isLoading = false
if let error = error {
status = "Error: \(error.localizedDescription)"
Logger.shared.log("Error: \(error.localizedDescription)", type: "Error")
return
}
guard let data = data else {
status = "No data received"
Logger.shared.log("No data received", type: "Error")
return
}
do {
if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any],
let dataDict = json["data"] as? [String: Any],
let viewer = dataDict["Viewer"] as? [String: Any],
let name = viewer["name"] as? String,
let options = viewer["options"] as? [String: Any],
let colorName = options["profileColor"] as? String {
username = name
profileColor = colorFromName(colorName)
status = "Logged in as \(name)"
} else {
status = "Unexpected response format!"
Logger.shared.log("Unexpected response format!", type: "Error")
}
} catch {
status = "Failed to parse response: \(error.localizedDescription)"
Logger.shared.log("Failed to parse response: \(error.localizedDescription)", type: "Error")
}
}
}.resume()
}
func colorFromName(_ name: String) -> Color {
switch name.lowercased() {
case "blue":
return .blue
case "purple":
return .purple
case "green":
return .green
case "orange":
return .orange
case "red":
return .red
case "pink":
return .pink
case "gray":
return .gray
default:
return .accentColor
}
}
func getTokenFromKeychain() -> String? {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: "me.cranci.sora.AniListToken",
kSecAttrAccount as String: "AniListAccessToken",
kSecReturnData as String: kCFBooleanTrue!,
kSecMatchLimit as String: kSecMatchLimitOne
]
var item: CFTypeRef?
let status = SecItemCopyMatching(query as CFDictionary, &item)
guard status == errSecSuccess, let tokenData = item as? Data else {
return nil
}
return String(data: tokenData, encoding: .utf8)
}
func removeTokenFromKeychain() {
let deleteQuery: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: "me.cranci.sora.AniListToken",
kSecAttrAccount as String: "AniListAccessToken"
]
SecItemDelete(deleteQuery as CFDictionary)
}
}

View file

@ -21,6 +21,9 @@ struct SettingsView: View {
NavigationLink(destination: SettingsViewModule()) {
Text("Modules")
}
NavigationLink(destination: SettingsViewTrackers()) {
Text("Trackers")
}
}
Section(header: Text("Info")) {

View file

@ -51,6 +51,7 @@
13DB468D2D90093A008CBC03 /* Anilist-Login.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13DB468B2D900939008CBC03 /* Anilist-Login.swift */; };
13DB468E2D90093A008CBC03 /* Anilist-Token.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13DB468C2D90093A008CBC03 /* Anilist-Token.swift */; };
13DB46902D900A38008CBC03 /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13DB468F2D900A38008CBC03 /* URL.swift */; };
13DB46922D900BCE008CBC03 /* SettingsViewTrackers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13DB46912D900BCE008CBC03 /* SettingsViewTrackers.swift */; };
13DB7CC32D7D99C0004371D3 /* SubtitleSettingsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13DB7CC22D7D99C0004371D3 /* SubtitleSettingsManager.swift */; };
13DB7CC62D7DC7D2004371D3 /* FFmpeg-iOS-Lame in Frameworks */ = {isa = PBXBuildFile; productRef = 13DB7CC52D7DC7D2004371D3 /* FFmpeg-iOS-Lame */; };
13DB7CEC2D7DED5D004371D3 /* DownloadManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13DB7CEB2D7DED5D004371D3 /* DownloadManager.swift */; };
@ -108,6 +109,7 @@
13DB468B2D900939008CBC03 /* Anilist-Login.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Anilist-Login.swift"; sourceTree = "<group>"; };
13DB468C2D90093A008CBC03 /* Anilist-Token.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Anilist-Token.swift"; sourceTree = "<group>"; };
13DB468F2D900A38008CBC03 /* URL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URL.swift; sourceTree = "<group>"; };
13DB46912D900BCE008CBC03 /* SettingsViewTrackers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewTrackers.swift; sourceTree = "<group>"; };
13DB7CC22D7D99C0004371D3 /* SubtitleSettingsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubtitleSettingsManager.swift; sourceTree = "<group>"; };
13DB7CEB2D7DED5D004371D3 /* DownloadManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadManager.swift; sourceTree = "<group>"; };
13DC0C412D2EC9BA00D0F966 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
@ -274,6 +276,7 @@
131845F82D47C62D00CA7A54 /* SettingsViewGeneral.swift */,
135CCBE12D4D1138008B9C0E /* SettingsViewPlayer.swift */,
130C6BF92D53AB1F00DC1432 /* SettingsViewData.swift */,
13DB46912D900BCE008CBC03 /* SettingsViewTrackers.swift */,
);
path = SettingsSubViews;
sourceTree = "<group>";
@ -561,6 +564,7 @@
73D164D52D8B5B470011A360 /* JavaScriptCore+Extensions.swift in Sources */,
13EA2BD52D32D97400C1EBD7 /* CustomPlayer.swift in Sources */,
1399FAD42D3AB38C00E97C31 /* SettingsViewLogger.swift in Sources */,
13DB46922D900BCE008CBC03 /* SettingsViewTrackers.swift in Sources */,
13C0E5EA2D5F85EA00E7F619 /* ContinueWatchingManager.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;