mirror of
https://github.com/cranci1/Sora.git
synced 2026-01-11 20:10:24 +00:00
instead of matched id being an int now its the actual name of the series (#190)
* removed double bs for id telling
* improved anilist logic, single episode anilist sync, anilist sync also avaiaiable with tmdb as provider, tmdb posters avaiabale with anilist as provider
* instead of telling the id of the match now it tells the name
* gotta release a testflight 🙏
This commit is contained in:
parent
c42d53f8f5
commit
ffeddb37e6
3 changed files with 67 additions and 82 deletions
|
|
@ -1,33 +1,32 @@
|
|||
//
|
||||
// AnilistMatchPopupView.swift
|
||||
// Sulfur
|
||||
//
|
||||
// Created by seiike on 01/06/2025.
|
||||
// AnilistMatchPopupView.swift
|
||||
// Sulfur
|
||||
//
|
||||
// Created by seiike on 01/06/2025.
|
||||
|
||||
import NukeUI
|
||||
import SwiftUI
|
||||
|
||||
struct AnilistMatchPopupView: View {
|
||||
let seriesTitle: String
|
||||
let onSelect: (Int) -> Void
|
||||
|
||||
let onSelect: (Int, String) -> Void
|
||||
|
||||
@State private var results: [[String: Any]] = []
|
||||
@State private var isLoading = true
|
||||
|
||||
|
||||
@AppStorage("selectedAppearance") private var selectedAppearance: Appearance = .system
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
|
||||
|
||||
private var isLightMode: Bool {
|
||||
selectedAppearance == .light
|
||||
|| (selectedAppearance == .system && colorScheme == .light)
|
||||
|| (selectedAppearance == .system && colorScheme == .light)
|
||||
}
|
||||
|
||||
|
||||
@State private var manualIDText: String = ""
|
||||
@State private var showingManualIDAlert = false
|
||||
|
||||
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
ScrollView {
|
||||
|
|
@ -36,7 +35,7 @@ struct AnilistMatchPopupView: View {
|
|||
.font(.footnote)
|
||||
.foregroundStyle(.gray)
|
||||
.padding(.horizontal, 10)
|
||||
|
||||
|
||||
VStack(spacing: 0) {
|
||||
if isLoading {
|
||||
ProgressView()
|
||||
|
|
@ -52,10 +51,11 @@ struct AnilistMatchPopupView: View {
|
|||
LazyVStack(spacing: 15) {
|
||||
ForEach(results.indices, id: \.self) { index in
|
||||
let result = results[index]
|
||||
|
||||
Button(action: {
|
||||
if let id = result["id"] as? Int {
|
||||
onSelect(id)
|
||||
let title = result["title"] as? String ?? seriesTitle
|
||||
onSelect(id, title)
|
||||
dismiss()
|
||||
}
|
||||
}) {
|
||||
HStack(spacing: 12) {
|
||||
|
|
@ -76,19 +76,18 @@ struct AnilistMatchPopupView: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(result["title"] as? String ?? "Unknown")
|
||||
.font(.body)
|
||||
.foregroundStyle(.primary)
|
||||
|
||||
if let english = result["title_english"] as? String {
|
||||
Text(english)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding(11)
|
||||
|
|
@ -120,7 +119,7 @@ struct AnilistMatchPopupView: View {
|
|||
.padding(.top, 16)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if !results.isEmpty {
|
||||
Text("Tap a title to override the current match.")
|
||||
.font(.footnote)
|
||||
|
|
@ -135,38 +134,36 @@ struct AnilistMatchPopupView: View {
|
|||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Button("Cancel") {
|
||||
dismiss()
|
||||
}
|
||||
.foregroundColor(isLightMode ? .black : .white)
|
||||
Button("Cancel") { dismiss() }
|
||||
.foregroundColor(isLightMode ? .black : .white)
|
||||
}
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button(action: {
|
||||
Button {
|
||||
manualIDText = ""
|
||||
showingManualIDAlert = true
|
||||
}) {
|
||||
} label: {
|
||||
Image(systemName: "number")
|
||||
.foregroundColor(isLightMode ? .black : .white)
|
||||
}
|
||||
}
|
||||
}
|
||||
.alert("Set Custom AniList ID", isPresented: $showingManualIDAlert, actions: {
|
||||
.alert("Set Custom AniList ID", isPresented: $showingManualIDAlert) {
|
||||
TextField("AniList ID", text: $manualIDText)
|
||||
.keyboardType(.numberPad)
|
||||
Button("Cancel", role: .cancel) { }
|
||||
Button("Save", action: {
|
||||
Button("Save") {
|
||||
if let idInt = Int(manualIDText.trimmingCharacters(in: .whitespaces)) {
|
||||
onSelect(idInt)
|
||||
onSelect(idInt, seriesTitle)
|
||||
dismiss()
|
||||
}
|
||||
})
|
||||
}, message: {
|
||||
}
|
||||
} message: {
|
||||
Text("Enter the AniList ID for this series")
|
||||
})
|
||||
}
|
||||
}
|
||||
.onAppear(perform: fetchMatches)
|
||||
}
|
||||
|
||||
|
||||
private func fetchMatches() {
|
||||
let query = """
|
||||
query {
|
||||
|
|
@ -184,35 +181,32 @@ struct AnilistMatchPopupView: View {
|
|||
}
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
guard let url = URL(string: "https://graphql.anilist.co") else { return }
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "POST"
|
||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
request.httpBody = try? JSONSerialization.data(withJSONObject: ["query": query])
|
||||
|
||||
|
||||
URLSession.shared.dataTask(with: request) { data, _, _ in
|
||||
DispatchQueue.main.async {
|
||||
self.isLoading = false
|
||||
|
||||
guard let data = data,
|
||||
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
|
||||
let dataDict = json["data"] as? [String: Any],
|
||||
let page = dataDict["Page"] as? [String: Any],
|
||||
let mediaList = page["media"] as? [[String: Any]] else {
|
||||
return
|
||||
}
|
||||
|
||||
self.results = mediaList.map { media in
|
||||
isLoading = false
|
||||
guard
|
||||
let data = data,
|
||||
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
|
||||
let dataDict = json["data"] as? [String: Any],
|
||||
let page = dataDict["Page"] as? [String: Any],
|
||||
let mediaList = page["media"] as? [[String: Any]]
|
||||
else { return }
|
||||
|
||||
results = mediaList.map { media in
|
||||
let titleInfo = media["title"] as? [String: Any]
|
||||
let cover = (media["coverImage"] as? [String: Any])?["large"] as? String
|
||||
|
||||
return [
|
||||
"id": media["id"] ?? 0,
|
||||
"title": titleInfo?["romaji"] ?? "Unknown",
|
||||
"title_english": titleInfo?["english"],
|
||||
"cover": cover
|
||||
"title_english": titleInfo?["english"] as Any,
|
||||
"cover": cover as Any
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,15 @@
|
|||
//
|
||||
// TMDBMatchPopupView.swift
|
||||
// Sulfur
|
||||
//
|
||||
// Created by seiike on 12/06/2025.
|
||||
// TMDBMatchPopupView.swift
|
||||
// Sulfur
|
||||
//
|
||||
// Created by seiike on 12/06/2025.
|
||||
|
||||
import SwiftUI
|
||||
import NukeUI
|
||||
|
||||
struct TMDBMatchPopupView: View {
|
||||
let seriesTitle: String
|
||||
let onSelect: (Int, TMDBFetcher.MediaType) -> Void
|
||||
let onSelect: (Int, TMDBFetcher.MediaType, String) -> Void
|
||||
|
||||
@State private var results: [ResultItem] = []
|
||||
@State private var isLoading = true
|
||||
|
|
@ -54,10 +53,10 @@ struct TMDBMatchPopupView: View {
|
|||
} else {
|
||||
LazyVStack(spacing: 15) {
|
||||
ForEach(results) { item in
|
||||
Button(action: {
|
||||
onSelect(item.id, item.mediaType)
|
||||
Button {
|
||||
onSelect(item.id, item.mediaType, item.title)
|
||||
dismiss()
|
||||
}) {
|
||||
} label: {
|
||||
HStack(spacing: 12) {
|
||||
if let poster = item.posterURL, let url = URL(string: poster) {
|
||||
LazyImage(url: url) { state in
|
||||
|
|
@ -112,9 +111,7 @@ struct TMDBMatchPopupView: View {
|
|||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Button("Cancel") {
|
||||
dismiss()
|
||||
}
|
||||
Button("Cancel") { dismiss() }
|
||||
}
|
||||
}
|
||||
.alert("Error Fetching Results", isPresented: $showingError) {
|
||||
|
|
@ -129,7 +126,6 @@ struct TMDBMatchPopupView: View {
|
|||
private func fetchMatches() {
|
||||
isLoading = true
|
||||
results = []
|
||||
|
||||
let fetcher = TMDBFetcher()
|
||||
let apiKey = fetcher.apiKey
|
||||
let dispatchGroup = DispatchGroup()
|
||||
|
|
@ -148,9 +144,10 @@ struct TMDBMatchPopupView: View {
|
|||
|
||||
URLSession.shared.dataTask(with: url) { data, _, error in
|
||||
defer { dispatchGroup.leave() }
|
||||
|
||||
guard error == nil, let data = data,
|
||||
let response = try? JSONDecoder().decode(TMDBSearchResponse.self, from: data) else {
|
||||
guard error == nil,
|
||||
let data = data,
|
||||
let response = try? JSONDecoder().decode(TMDBSearchResponse.self, from: data)
|
||||
else {
|
||||
encounteredError = true
|
||||
return
|
||||
}
|
||||
|
|
@ -165,10 +162,7 @@ struct TMDBMatchPopupView: View {
|
|||
}
|
||||
|
||||
dispatchGroup.notify(queue: .main) {
|
||||
if encounteredError {
|
||||
showingError = true
|
||||
}
|
||||
// Keep API order (by popularity), limit to top 6 overall
|
||||
if encounteredError { showingError = true }
|
||||
results = Array(temp.prefix(6))
|
||||
isLoading = false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -655,14 +655,17 @@ struct MediaInfoView: View {
|
|||
.circularGradientOutline()
|
||||
}
|
||||
.sheet(isPresented: $isMatchingPresented) {
|
||||
AnilistMatchPopupView(seriesTitle: title) { selectedID in
|
||||
handleAniListMatch(selectedID: selectedID)
|
||||
AnilistMatchPopupView(seriesTitle: title) { id, matched in
|
||||
handleAniListMatch(selectedID: id)
|
||||
matchedTitle = matched // ← now in scope
|
||||
fetchMetadataIDIfNeeded()
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $isTMDBMatchingPresented) {
|
||||
TMDBMatchPopupView(seriesTitle: title) { id, type in
|
||||
tmdbID = id; tmdbType = type
|
||||
TMDBMatchPopupView(seriesTitle: title) { id, type, matched in
|
||||
tmdbID = id
|
||||
tmdbType = type
|
||||
matchedTitle = matched // ← now in scope
|
||||
fetchMetadataIDIfNeeded()
|
||||
}
|
||||
}
|
||||
|
|
@ -671,18 +674,12 @@ struct MediaInfoView: View {
|
|||
@ViewBuilder
|
||||
private var menuContent: some View {
|
||||
Group {
|
||||
if let active = activeProvider {
|
||||
Text("Provider: \(active)")
|
||||
.font(.caption)
|
||||
.foregroundColor(.gray)
|
||||
.padding(.vertical, 4)
|
||||
Divider()
|
||||
if let provider = activeProvider {
|
||||
Text("Matched \(provider): \(matchedTitle ?? title)")
|
||||
.font(.caption2)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
Text("Matched ID: \(itemID ?? 0)")
|
||||
.font(.caption2)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
if activeProvider == "AniList" {
|
||||
Button("Match with AniList") {
|
||||
isMatchingPresented = true
|
||||
|
|
|
|||
Loading…
Reference in a new issue