Sora/Sora/Views/SettingsView/SettingsSubViews/SettingsViewTrackers.swift
cranci1 3661e601e5
Some checks are pending
Build and Release IPA / Build IPA (push) Waiting to run
Update SettingsViewTrackers.swift
2025-03-23 18:01:01 +01:00

203 lines
7.1 KiB
Swift

//
// 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 = .accentColor
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"))
.placeholder {
RoundedRectangle(cornerRadius: 10)
.fill(Color.gray.opacity(0.3))
.frame(width: 80, height: 80)
.shimmering()
}
.resizable()
.frame(width: 80, height: 80)
.clipShape(Rectangle())
.cornerRadius(10)
Text("AniList.co")
.font(.title2)
}
if isLoading {
ProgressView()
} else {
if isLoggedIn {
HStack(spacing: 0) {
Text("Logged in as ")
Text(username)
.foregroundColor(profileColor)
.font(.body)
.fontWeight(.semibold)
}
} else {
Text(status)
.multilineTextAlignment(.center)
}
}
Button(isLoggedIn ? "Log Out from AniList.co" : "Log In with AniList.co") {
if isLoggedIn {
logout()
} else {
login()
}
}
.font(.body)
}
}
.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)
}
}