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:
Seiike 2025-06-14 15:50:53 +02:00 committed by GitHub
parent c42d53f8f5
commit ffeddb37e6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 67 additions and 82 deletions

View file

@ -1,33 +1,32 @@
// //
// AnilistMatchPopupView.swift // AnilistMatchPopupView.swift
// Sulfur // Sulfur
//
// Created by seiike on 01/06/2025.
// //
// Created by seiike on 01/06/2025.
import NukeUI import NukeUI
import SwiftUI import SwiftUI
struct AnilistMatchPopupView: View { struct AnilistMatchPopupView: View {
let seriesTitle: String let seriesTitle: String
let onSelect: (Int) -> Void let onSelect: (Int, String) -> Void
@State private var results: [[String: Any]] = [] @State private var results: [[String: Any]] = []
@State private var isLoading = true @State private var isLoading = true
@AppStorage("selectedAppearance") private var selectedAppearance: Appearance = .system @AppStorage("selectedAppearance") private var selectedAppearance: Appearance = .system
@Environment(\.colorScheme) private var colorScheme @Environment(\.colorScheme) private var colorScheme
private var isLightMode: Bool { private var isLightMode: Bool {
selectedAppearance == .light selectedAppearance == .light
|| (selectedAppearance == .system && colorScheme == .light) || (selectedAppearance == .system && colorScheme == .light)
} }
@State private var manualIDText: String = "" @State private var manualIDText: String = ""
@State private var showingManualIDAlert = false @State private var showingManualIDAlert = false
@Environment(\.dismiss) private var dismiss @Environment(\.dismiss) private var dismiss
var body: some View { var body: some View {
NavigationView { NavigationView {
ScrollView { ScrollView {
@ -36,7 +35,7 @@ struct AnilistMatchPopupView: View {
.font(.footnote) .font(.footnote)
.foregroundStyle(.gray) .foregroundStyle(.gray)
.padding(.horizontal, 10) .padding(.horizontal, 10)
VStack(spacing: 0) { VStack(spacing: 0) {
if isLoading { if isLoading {
ProgressView() ProgressView()
@ -52,10 +51,11 @@ struct AnilistMatchPopupView: View {
LazyVStack(spacing: 15) { LazyVStack(spacing: 15) {
ForEach(results.indices, id: \.self) { index in ForEach(results.indices, id: \.self) { index in
let result = results[index] let result = results[index]
Button(action: { Button(action: {
if let id = result["id"] as? Int { if let id = result["id"] as? Int {
onSelect(id) let title = result["title"] as? String ?? seriesTitle
onSelect(id, title)
dismiss()
} }
}) { }) {
HStack(spacing: 12) { HStack(spacing: 12) {
@ -76,19 +76,18 @@ struct AnilistMatchPopupView: View {
} }
} }
} }
VStack(alignment: .leading, spacing: 2) { VStack(alignment: .leading, spacing: 2) {
Text(result["title"] as? String ?? "Unknown") Text(result["title"] as? String ?? "Unknown")
.font(.body) .font(.body)
.foregroundStyle(.primary) .foregroundStyle(.primary)
if let english = result["title_english"] as? String { if let english = result["title_english"] as? String {
Text(english) Text(english)
.font(.caption) .font(.caption)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
} }
} }
Spacer() Spacer()
} }
.padding(11) .padding(11)
@ -120,7 +119,7 @@ struct AnilistMatchPopupView: View {
.padding(.top, 16) .padding(.top, 16)
} }
} }
if !results.isEmpty { if !results.isEmpty {
Text("Tap a title to override the current match.") Text("Tap a title to override the current match.")
.font(.footnote) .font(.footnote)
@ -135,38 +134,36 @@ struct AnilistMatchPopupView: View {
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.toolbar { .toolbar {
ToolbarItem(placement: .cancellationAction) { ToolbarItem(placement: .cancellationAction) {
Button("Cancel") { Button("Cancel") { dismiss() }
dismiss() .foregroundColor(isLightMode ? .black : .white)
}
.foregroundColor(isLightMode ? .black : .white)
} }
ToolbarItem(placement: .navigationBarTrailing) { ToolbarItem(placement: .navigationBarTrailing) {
Button(action: { Button {
manualIDText = "" manualIDText = ""
showingManualIDAlert = true showingManualIDAlert = true
}) { } label: {
Image(systemName: "number") Image(systemName: "number")
.foregroundColor(isLightMode ? .black : .white) .foregroundColor(isLightMode ? .black : .white)
} }
} }
} }
.alert("Set Custom AniList ID", isPresented: $showingManualIDAlert, actions: { .alert("Set Custom AniList ID", isPresented: $showingManualIDAlert) {
TextField("AniList ID", text: $manualIDText) TextField("AniList ID", text: $manualIDText)
.keyboardType(.numberPad) .keyboardType(.numberPad)
Button("Cancel", role: .cancel) { } Button("Cancel", role: .cancel) { }
Button("Save", action: { Button("Save") {
if let idInt = Int(manualIDText.trimmingCharacters(in: .whitespaces)) { if let idInt = Int(manualIDText.trimmingCharacters(in: .whitespaces)) {
onSelect(idInt) onSelect(idInt, seriesTitle)
dismiss() dismiss()
} }
}) }
}, message: { } message: {
Text("Enter the AniList ID for this series") Text("Enter the AniList ID for this series")
}) }
} }
.onAppear(perform: fetchMatches) .onAppear(perform: fetchMatches)
} }
private func fetchMatches() { private func fetchMatches() {
let query = """ let query = """
query { query {
@ -184,35 +181,32 @@ struct AnilistMatchPopupView: View {
} }
} }
""" """
guard let url = URL(string: "https://graphql.anilist.co") else { return } guard let url = URL(string: "https://graphql.anilist.co") else { return }
var request = URLRequest(url: url) var request = URLRequest(url: url)
request.httpMethod = "POST" request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type") request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = try? JSONSerialization.data(withJSONObject: ["query": query]) request.httpBody = try? JSONSerialization.data(withJSONObject: ["query": query])
URLSession.shared.dataTask(with: request) { data, _, _ in URLSession.shared.dataTask(with: request) { data, _, _ in
DispatchQueue.main.async { DispatchQueue.main.async {
self.isLoading = false isLoading = false
guard
guard let data = data, let data = data,
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any], let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let dataDict = json["data"] as? [String: Any], let dataDict = json["data"] as? [String: Any],
let page = dataDict["Page"] as? [String: Any], let page = dataDict["Page"] as? [String: Any],
let mediaList = page["media"] as? [[String: Any]] else { let mediaList = page["media"] as? [[String: Any]]
return else { return }
}
results = mediaList.map { media in
self.results = mediaList.map { media in
let titleInfo = media["title"] as? [String: Any] let titleInfo = media["title"] as? [String: Any]
let cover = (media["coverImage"] as? [String: Any])?["large"] as? String let cover = (media["coverImage"] as? [String: Any])?["large"] as? String
return [ return [
"id": media["id"] ?? 0, "id": media["id"] ?? 0,
"title": titleInfo?["romaji"] ?? "Unknown", "title": titleInfo?["romaji"] ?? "Unknown",
"title_english": titleInfo?["english"], "title_english": titleInfo?["english"] as Any,
"cover": cover "cover": cover as Any
] ]
} }
} }

View file

@ -1,16 +1,15 @@
// //
// TMDBMatchPopupView.swift // TMDBMatchPopupView.swift
// Sulfur // Sulfur
//
// Created by seiike on 12/06/2025.
// //
// Created by seiike on 12/06/2025.
import SwiftUI import SwiftUI
import NukeUI import NukeUI
struct TMDBMatchPopupView: View { struct TMDBMatchPopupView: View {
let seriesTitle: String let seriesTitle: String
let onSelect: (Int, TMDBFetcher.MediaType) -> Void let onSelect: (Int, TMDBFetcher.MediaType, String) -> Void
@State private var results: [ResultItem] = [] @State private var results: [ResultItem] = []
@State private var isLoading = true @State private var isLoading = true
@ -54,10 +53,10 @@ struct TMDBMatchPopupView: View {
} else { } else {
LazyVStack(spacing: 15) { LazyVStack(spacing: 15) {
ForEach(results) { item in ForEach(results) { item in
Button(action: { Button {
onSelect(item.id, item.mediaType) onSelect(item.id, item.mediaType, item.title)
dismiss() dismiss()
}) { } label: {
HStack(spacing: 12) { HStack(spacing: 12) {
if let poster = item.posterURL, let url = URL(string: poster) { if let poster = item.posterURL, let url = URL(string: poster) {
LazyImage(url: url) { state in LazyImage(url: url) { state in
@ -112,9 +111,7 @@ struct TMDBMatchPopupView: View {
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.toolbar { .toolbar {
ToolbarItem(placement: .cancellationAction) { ToolbarItem(placement: .cancellationAction) {
Button("Cancel") { Button("Cancel") { dismiss() }
dismiss()
}
} }
} }
.alert("Error Fetching Results", isPresented: $showingError) { .alert("Error Fetching Results", isPresented: $showingError) {
@ -129,7 +126,6 @@ struct TMDBMatchPopupView: View {
private func fetchMatches() { private func fetchMatches() {
isLoading = true isLoading = true
results = [] results = []
let fetcher = TMDBFetcher() let fetcher = TMDBFetcher()
let apiKey = fetcher.apiKey let apiKey = fetcher.apiKey
let dispatchGroup = DispatchGroup() let dispatchGroup = DispatchGroup()
@ -148,9 +144,10 @@ struct TMDBMatchPopupView: View {
URLSession.shared.dataTask(with: url) { data, _, error in URLSession.shared.dataTask(with: url) { data, _, error in
defer { dispatchGroup.leave() } defer { dispatchGroup.leave() }
guard error == nil,
guard error == nil, let data = data, let data = data,
let response = try? JSONDecoder().decode(TMDBSearchResponse.self, from: data) else { let response = try? JSONDecoder().decode(TMDBSearchResponse.self, from: data)
else {
encounteredError = true encounteredError = true
return return
} }
@ -165,10 +162,7 @@ struct TMDBMatchPopupView: View {
} }
dispatchGroup.notify(queue: .main) { dispatchGroup.notify(queue: .main) {
if encounteredError { if encounteredError { showingError = true }
showingError = true
}
// Keep API order (by popularity), limit to top 6 overall
results = Array(temp.prefix(6)) results = Array(temp.prefix(6))
isLoading = false isLoading = false
} }

View file

@ -655,14 +655,17 @@ struct MediaInfoView: View {
.circularGradientOutline() .circularGradientOutline()
} }
.sheet(isPresented: $isMatchingPresented) { .sheet(isPresented: $isMatchingPresented) {
AnilistMatchPopupView(seriesTitle: title) { selectedID in AnilistMatchPopupView(seriesTitle: title) { id, matched in
handleAniListMatch(selectedID: selectedID) handleAniListMatch(selectedID: id)
matchedTitle = matched // now in scope
fetchMetadataIDIfNeeded() fetchMetadataIDIfNeeded()
} }
} }
.sheet(isPresented: $isTMDBMatchingPresented) { .sheet(isPresented: $isTMDBMatchingPresented) {
TMDBMatchPopupView(seriesTitle: title) { id, type in TMDBMatchPopupView(seriesTitle: title) { id, type, matched in
tmdbID = id; tmdbType = type tmdbID = id
tmdbType = type
matchedTitle = matched // now in scope
fetchMetadataIDIfNeeded() fetchMetadataIDIfNeeded()
} }
} }
@ -671,18 +674,12 @@ struct MediaInfoView: View {
@ViewBuilder @ViewBuilder
private var menuContent: some View { private var menuContent: some View {
Group { Group {
if let active = activeProvider { if let provider = activeProvider {
Text("Provider: \(active)") Text("Matched \(provider): \(matchedTitle ?? title)")
.font(.caption) .font(.caption2)
.foregroundColor(.gray) .foregroundColor(.secondary)
.padding(.vertical, 4)
Divider()
} }
Text("Matched ID: \(itemID ?? 0)")
.font(.caption2)
.foregroundColor(.secondary)
if activeProvider == "AniList" { if activeProvider == "AniList" {
Button("Match with AniList") { Button("Match with AniList") {
isMatchingPresented = true isMatchingPresented = true