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
// 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
]
}
}

View file

@ -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
}

View file

@ -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