mirror of
https://github.com/cranci1/Sora.git
synced 2026-01-11 20:10:24 +00:00
Merge branch 'pr/183' into dev
This commit is contained in:
commit
35c4f35f5e
6 changed files with 365 additions and 127 deletions
|
|
@ -210,6 +210,9 @@
|
|||
},
|
||||
"Error" : {
|
||||
|
||||
},
|
||||
"Error Fetching Results" : {
|
||||
|
||||
},
|
||||
"Failed to load contributors" : {
|
||||
"localizations" : {
|
||||
|
|
@ -351,7 +354,10 @@
|
|||
"Match with AniList" : {
|
||||
|
||||
},
|
||||
"Matched with: %@" : {
|
||||
"Match with TMDB" : {
|
||||
|
||||
},
|
||||
"Matched ID: %lld" : {
|
||||
|
||||
},
|
||||
"Max Concurrent Downloads" : {
|
||||
|
|
@ -430,6 +436,9 @@
|
|||
},
|
||||
"Please select a module from settings" : {
|
||||
|
||||
},
|
||||
"Provider: %@" : {
|
||||
|
||||
},
|
||||
"Queued" : {
|
||||
|
||||
|
|
@ -552,6 +561,9 @@
|
|||
},
|
||||
"The module provided only a single episode, this is most likely a movie, so we decided to make separate screens for these cases." : {
|
||||
|
||||
},
|
||||
"TMDB Match" : {
|
||||
|
||||
},
|
||||
"Trackers" : {
|
||||
|
||||
|
|
@ -561,6 +573,9 @@
|
|||
},
|
||||
"Try different search terms" : {
|
||||
|
||||
},
|
||||
"Unable to fetch matches. Please try again later." : {
|
||||
|
||||
},
|
||||
"Use TMDB Poster Image" : {
|
||||
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ class TMDBFetcher {
|
|||
let results: [TMDBResult]
|
||||
}
|
||||
|
||||
private let apiKey = "738b4edd0a156cc126dc4a4b8aea4aca"
|
||||
let apiKey = "738b4edd0a156cc126dc4a4b8aea4aca"
|
||||
private let session = URLSession.custom
|
||||
|
||||
func fetchBestMatchID(for title: String, completion: @escaping (Int?, MediaType?) -> Void) {
|
||||
|
|
|
|||
|
|
@ -65,6 +65,8 @@ struct MediaInfoView: View {
|
|||
@State private var showStreamLoadingView: Bool = false
|
||||
@State private var currentStreamTitle: String = ""
|
||||
@State private var activeFetchID: UUID? = nil
|
||||
@State private var activeProvider: String?
|
||||
@State private var isTMDBMatchingPresented = false
|
||||
|
||||
@State private var refreshTrigger: Bool = false
|
||||
@State private var buttonRefreshTrigger: Bool = false
|
||||
|
|
@ -85,6 +87,15 @@ struct MediaInfoView: View {
|
|||
@Environment(\.colorScheme) private var colorScheme
|
||||
@Environment(\.verticalSizeClass) private var verticalSizeClass
|
||||
|
||||
@AppStorage("metadataProvidersOrder") private var metadataProvidersOrderData: Data = {
|
||||
try! JSONEncoder().encode(["AniList","TMDB"])
|
||||
}()
|
||||
|
||||
private var metadataProvidersOrder: [String] {
|
||||
get { (try? JSONDecoder().decode([String].self, from: metadataProvidersOrderData)) ?? ["AniList","TMDB"] }
|
||||
set { metadataProvidersOrderData = try! JSONEncoder().encode(newValue) }
|
||||
}
|
||||
|
||||
private var isGroupedBySeasons: Bool {
|
||||
return groupedEpisodes().count > 1
|
||||
}
|
||||
|
|
@ -252,10 +263,7 @@ struct MediaInfoView: View {
|
|||
ZStack(alignment: .top) {
|
||||
gradientOverlay
|
||||
|
||||
VStack(alignment: .leading, spacing: 24) {
|
||||
Spacer()
|
||||
.frame(height: 100)
|
||||
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
headerSection
|
||||
if !episodeLinks.isEmpty {
|
||||
episodesSection
|
||||
|
|
@ -263,34 +271,31 @@ struct MediaInfoView: View {
|
|||
noEpisodesSection
|
||||
}
|
||||
}
|
||||
.padding(.horizontal)
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var gradientOverlay: some View {
|
||||
ZStack {
|
||||
ProgressiveBlurView()
|
||||
.opacity(0.8)
|
||||
|
||||
LinearGradient(
|
||||
gradient: Gradient(stops: [
|
||||
.init(color: (colorScheme == .dark ? Color.black : Color.white).opacity(0.0), location: 0.0),
|
||||
.init(color: (colorScheme == .dark ? Color.black : Color.white).opacity(0.5), location: 0.5),
|
||||
.init(color: (colorScheme == .dark ? Color.black : Color.white), location: 1.0)
|
||||
]),
|
||||
startPoint: .top,
|
||||
endPoint: .bottom
|
||||
)
|
||||
}
|
||||
LinearGradient(
|
||||
gradient: Gradient(stops: [
|
||||
.init(color: (colorScheme == .dark ? Color.black : Color.white).opacity(0.0), location: 0.0),
|
||||
.init(color: (colorScheme == .dark ? Color.black : Color.white).opacity(0.5), location: 0.2),
|
||||
.init(color: (colorScheme == .dark ? Color.black : Color.white).opacity(0.8), location: 0.5),
|
||||
.init(color: (colorScheme == .dark ? Color.black : Color.white), location: 1.0)
|
||||
]),
|
||||
startPoint: .top,
|
||||
endPoint: .bottom
|
||||
)
|
||||
.frame(height: 300)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 0))
|
||||
.shadow(color: (colorScheme == .dark ? Color.black : Color.white).opacity(1), radius: 10, x: 0, y: 10)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var headerSection: some View {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
if !airdate.isEmpty && airdate != "N/A" && airdate != "No Data" {
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: "calendar")
|
||||
|
|
@ -303,7 +308,7 @@ struct MediaInfoView: View {
|
|||
}
|
||||
|
||||
Text(title)
|
||||
.font(.system(size: 32, weight: .bold))
|
||||
.font(.system(size: 28, weight: .bold))
|
||||
.foregroundColor(.primary)
|
||||
.lineLimit(3)
|
||||
.onLongPressGesture {
|
||||
|
|
@ -653,6 +658,13 @@ struct MediaInfoView: View {
|
|||
.sheet(isPresented: $isMatchingPresented) {
|
||||
AnilistMatchPopupView(seriesTitle: title) { selectedID in
|
||||
handleAniListMatch(selectedID: selectedID)
|
||||
fetchMetadataIDIfNeeded() // ← use your new async re-try loop
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $isTMDBMatchingPresented) {
|
||||
TMDBMatchPopupView(seriesTitle: title) { id, type in
|
||||
tmdbID = id; tmdbType = type
|
||||
fetchMetadataIDIfNeeded()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -660,34 +672,44 @@ struct MediaInfoView: View {
|
|||
@ViewBuilder
|
||||
private var menuContent: some View {
|
||||
Group {
|
||||
if let id = itemID ?? customAniListID {
|
||||
let labelText = (matchedTitle?.isEmpty == false ? matchedTitle! : "\(id)")
|
||||
Text("Matched with: \(labelText)")
|
||||
// Show which provider “won”
|
||||
if let active = activeProvider {
|
||||
Text("Provider: \(active)")
|
||||
.font(.caption)
|
||||
.foregroundColor(.gray)
|
||||
.padding(.vertical, 4)
|
||||
Divider()
|
||||
}
|
||||
|
||||
Divider()
|
||||
|
||||
Text("Matched ID: \(itemID ?? 0)")
|
||||
.font(.caption2)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
if let _ = customAniListID {
|
||||
if activeProvider == "AniList" {
|
||||
Button("Match with AniList") {
|
||||
isMatchingPresented = true
|
||||
}
|
||||
Text("Matched ID: \(itemID ?? 0)")
|
||||
.font(.caption2)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
Button(action: { resetAniListID() }) {
|
||||
Label("Reset AniList ID", systemImage: "arrow.clockwise")
|
||||
}
|
||||
}
|
||||
|
||||
if let id = itemID ?? customAniListID {
|
||||
Button(action: { openAniListPage(id: id) }) {
|
||||
|
||||
Button(action: { openAniListPage(id: itemID ?? 0) }) {
|
||||
Label("Open in AniList", systemImage: "link")
|
||||
}
|
||||
}
|
||||
|
||||
if UserDefaults.standard.string(forKey: "metadataProviders") ?? "TMDB" == "AniList" {
|
||||
Button(action: { isMatchingPresented = true }) {
|
||||
Label("Match with AniList", systemImage: "magnifyingglass")
|
||||
// TMDB branch: only match
|
||||
else if activeProvider == "TMDB" {
|
||||
Button("Match with TMDB") {
|
||||
isTMDBMatchingPresented = true
|
||||
}
|
||||
}
|
||||
|
||||
// Keep all of your existing poster & debug options
|
||||
posterMenuOptions
|
||||
|
||||
Divider()
|
||||
|
|
@ -697,6 +719,7 @@ struct MediaInfoView: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ViewBuilder
|
||||
private var posterMenuOptions: some View {
|
||||
|
|
@ -1169,30 +1192,60 @@ struct MediaInfoView: View {
|
|||
}
|
||||
|
||||
private func fetchMetadataIDIfNeeded() {
|
||||
let provider = UserDefaults.standard.string(forKey: "metadataProviders") ?? "TMDB"
|
||||
let cleaned = cleanTitle(title)
|
||||
let order = metadataProvidersOrder
|
||||
let cleanedTitle = cleanTitle(title)
|
||||
|
||||
itemID = nil
|
||||
tmdbID = nil
|
||||
|
||||
tmdbFetcher.fetchBestMatchID(for: cleaned) { id, type in
|
||||
DispatchQueue.main.async {
|
||||
self.tmdbID = id
|
||||
self.tmdbType = type
|
||||
Logger.shared.log("Fetched TMDB ID: \(id ?? -1) (\(type?.rawValue ?? "unknown")) for title: \(cleaned)", type: "Debug")
|
||||
}
|
||||
}
|
||||
|
||||
fetchItemID(byTitle: cleaned) { result in
|
||||
activeProvider = nil
|
||||
isError = false
|
||||
|
||||
fetchItemID(byTitle: cleanedTitle) { result in
|
||||
switch result {
|
||||
case .success(let id):
|
||||
DispatchQueue.main.async {
|
||||
self.itemID = id
|
||||
Logger.shared.log("Fetched AniList ID: \(id) for title: \(cleaned)", type: "Debug")
|
||||
}
|
||||
DispatchQueue.main.async { self.itemID = id }
|
||||
case .failure(let error):
|
||||
Logger.shared.log("Failed to fetch AniList ID: \(error)", type: "Error")
|
||||
Logger.shared.log("Failed to fetch AniList ID for tracking: \(error)", type: "Error")
|
||||
}
|
||||
}
|
||||
|
||||
func tryNext(_ index: Int) {
|
||||
guard index < order.count else {
|
||||
isError = true
|
||||
return
|
||||
}
|
||||
let provider = order[index]
|
||||
if provider == "TMDB" {
|
||||
tmdbFetcher.fetchBestMatchID(for: cleanedTitle) { id, type in
|
||||
DispatchQueue.main.async {
|
||||
if let id = id, let type = type {
|
||||
self.tmdbID = id
|
||||
self.tmdbType = type
|
||||
self.activeProvider = "TMDB"
|
||||
UserDefaults.standard.set("TMDB", forKey: "metadataProviders")
|
||||
} else {
|
||||
tryNext(index + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if provider == "AniList" {
|
||||
fetchItemID(byTitle: cleanedTitle) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
DispatchQueue.main.async {
|
||||
self.activeProvider = "AniList"
|
||||
UserDefaults.standard.set("AniList", forKey: "metadataProviders")
|
||||
}
|
||||
case .failure:
|
||||
tryNext(index + 1)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
tryNext(index + 1)
|
||||
}
|
||||
}
|
||||
|
||||
tryNext(0)
|
||||
}
|
||||
|
||||
private func fetchItemID(byTitle title: String, completion: @escaping (Result<Int, Error>) -> Void) {
|
||||
|
|
|
|||
176
Sora/Views/MediaInfoView/TMDBMatchPopupView.swift
Normal file
176
Sora/Views/MediaInfoView/TMDBMatchPopupView.swift
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
//
|
||||
// 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
|
||||
|
||||
@State private var results: [ResultItem] = []
|
||||
@State private var isLoading = true
|
||||
@State private var showingError = false
|
||||
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
struct ResultItem: Identifiable {
|
||||
let id: Int
|
||||
let title: String
|
||||
let mediaType: TMDBFetcher.MediaType
|
||||
let posterURL: String?
|
||||
}
|
||||
|
||||
private struct TMDBSearchResult: Decodable {
|
||||
let id: Int
|
||||
let name: String?
|
||||
let title: String?
|
||||
let poster_path: String?
|
||||
let popularity: Double
|
||||
}
|
||||
|
||||
private struct TMDBSearchResponse: Decodable {
|
||||
let results: [TMDBSearchResult]
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
ScrollView {
|
||||
VStack(spacing: 0) {
|
||||
if isLoading {
|
||||
ProgressView()
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding()
|
||||
} else if results.isEmpty {
|
||||
Text("No matches found")
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.gray)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding()
|
||||
} else {
|
||||
LazyVStack(spacing: 15) {
|
||||
ForEach(results) { item in
|
||||
Button(action: {
|
||||
onSelect(item.id, item.mediaType)
|
||||
dismiss()
|
||||
}) {
|
||||
HStack(spacing: 12) {
|
||||
if let poster = item.posterURL, let url = URL(string: poster) {
|
||||
LazyImage(url: url) { state in
|
||||
if let image = state.imageContainer?.image {
|
||||
Image(uiImage: image)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(width: 50, height: 75)
|
||||
.cornerRadius(6)
|
||||
} else {
|
||||
Rectangle()
|
||||
.fill(.tertiary)
|
||||
.frame(width: 50, height: 75)
|
||||
.cornerRadius(6)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(item.title)
|
||||
.font(.body)
|
||||
.foregroundStyle(.primary)
|
||||
Text(item.mediaType.rawValue.capitalized)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
.padding(11)
|
||||
.frame(maxWidth: .infinity)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 15)
|
||||
.fill(.ultraThinMaterial)
|
||||
)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 15)
|
||||
.stroke(
|
||||
Color.accentColor.opacity(0.2),
|
||||
lineWidth: 0.5
|
||||
)
|
||||
)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.vertical, 16)
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("TMDB Match")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Button("Cancel") {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
.alert("Error Fetching Results", isPresented: $showingError) {
|
||||
Button("OK", role: .cancel) { }
|
||||
} message: {
|
||||
Text("Unable to fetch matches. Please try again later.")
|
||||
}
|
||||
}
|
||||
.onAppear(perform: fetchMatches)
|
||||
}
|
||||
|
||||
private func fetchMatches() {
|
||||
isLoading = true
|
||||
results = []
|
||||
|
||||
let fetcher = TMDBFetcher()
|
||||
let apiKey = fetcher.apiKey
|
||||
let dispatchGroup = DispatchGroup()
|
||||
var temp: [ResultItem] = []
|
||||
var encounteredError = false
|
||||
|
||||
for type in TMDBFetcher.MediaType.allCases {
|
||||
dispatchGroup.enter()
|
||||
let query = seriesTitle.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? ""
|
||||
let urlString = "https://api.themoviedb.org/3/search/\(type.rawValue)?api_key=\(apiKey)&query=\(query)"
|
||||
guard let url = URL(string: urlString) else {
|
||||
encounteredError = true
|
||||
dispatchGroup.leave()
|
||||
continue
|
||||
}
|
||||
|
||||
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 {
|
||||
encounteredError = true
|
||||
return
|
||||
}
|
||||
|
||||
let items = response.results.prefix(6).map { res -> ResultItem in
|
||||
let title = (type == .tv ? res.name : res.title) ?? "Unknown"
|
||||
let poster = res.poster_path.map { "https://image.tmdb.org/t/p/w500\($0)" }
|
||||
return ResultItem(id: res.id, title: title, mediaType: type, posterURL: poster)
|
||||
}
|
||||
temp.append(contentsOf: items)
|
||||
}.resume()
|
||||
}
|
||||
|
||||
dispatchGroup.notify(queue: .main) {
|
||||
if encounteredError {
|
||||
showingError = true
|
||||
}
|
||||
// Keep API order (by popularity), limit to top 6 overall
|
||||
results = Array(temp.prefix(6))
|
||||
isLoading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -154,12 +154,17 @@ struct SettingsViewGeneral: View {
|
|||
@AppStorage("fetchEpisodeMetadata") private var fetchEpisodeMetadata: Bool = true
|
||||
@AppStorage("analyticsEnabled") private var analyticsEnabled: Bool = false
|
||||
@AppStorage("hideSplashScreen") private var hideSplashScreenEnable: Bool = false
|
||||
@AppStorage("metadataProviders") private var metadataProviders: String = "TMDB"
|
||||
@AppStorage("metadataProvidersOrder") private var metadataProvidersOrderData: Data = {
|
||||
try! JSONEncoder().encode(["TMDB","AniList"])
|
||||
}()
|
||||
@AppStorage("tmdbImageWidth") private var TMDBimageWidht: String = "original"
|
||||
@AppStorage("mediaColumnsPortrait") private var mediaColumnsPortrait: Int = 2
|
||||
@AppStorage("mediaColumnsLandscape") private var mediaColumnsLandscape: Int = 4
|
||||
|
||||
private let metadataProvidersList = ["AniList", "TMDB"]
|
||||
private var metadataProvidersOrder: [String] {
|
||||
get { (try? JSONDecoder().decode([String].self, from: metadataProvidersOrderData)) ?? ["AniList","TMDB"] }
|
||||
set { metadataProvidersOrderData = try! JSONEncoder().encode(newValue) }
|
||||
}
|
||||
private let TMDBimageWidhtList = ["300", "500", "780", "1280", "original"]
|
||||
private let sortOrderOptions = ["Ascending", "Descending"]
|
||||
@EnvironmentObject var settings: Settings
|
||||
|
|
@ -208,85 +213,70 @@ struct SettingsViewGeneral: View {
|
|||
isOn: $fetchEpisodeMetadata
|
||||
)
|
||||
|
||||
if metadataProviders == "TMDB" {
|
||||
List {
|
||||
ForEach(metadataProvidersOrder, id: \.self) { prov in
|
||||
Text(prov)
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
.onMove { idx, dest in
|
||||
var arr = metadataProvidersOrder
|
||||
arr.move(fromOffsets: idx, toOffset: dest)
|
||||
metadataProvidersOrderData = try! JSONEncoder().encode(arr)
|
||||
}
|
||||
}
|
||||
.environment(\.editMode, .constant(.active))
|
||||
.frame(height: 140)
|
||||
|
||||
SettingsSection(
|
||||
title: "Media Grid Layout",
|
||||
footer: "Adjust the number of media items per row in portrait and landscape modes."
|
||||
) {
|
||||
SettingsPickerRow(
|
||||
icon: "server.rack",
|
||||
title: "Metadata Provider",
|
||||
options: metadataProvidersList,
|
||||
optionToString: { $0 },
|
||||
selection: $metadataProviders,
|
||||
showDivider: true
|
||||
icon: "rectangle.portrait",
|
||||
title: "Portrait Columns",
|
||||
options: UIDevice.current.userInterfaceIdiom == .pad ? Array(1...5) : Array(1...4),
|
||||
optionToString: { "\($0)" },
|
||||
selection: $mediaColumnsPortrait
|
||||
)
|
||||
|
||||
SettingsPickerRow(
|
||||
icon: "square.stack.3d.down.right",
|
||||
title: "Thumbnails Width",
|
||||
options: TMDBimageWidhtList,
|
||||
optionToString: { $0 },
|
||||
selection: $TMDBimageWidht,
|
||||
icon: "rectangle",
|
||||
title: "Landscape Columns",
|
||||
options: UIDevice.current.userInterfaceIdiom == .pad ? Array(2...8) : Array(2...5),
|
||||
optionToString: { "\($0)" },
|
||||
selection: $mediaColumnsLandscape,
|
||||
showDivider: false
|
||||
)
|
||||
} else {
|
||||
SettingsPickerRow(
|
||||
icon: "server.rack",
|
||||
title: "Metadata Provider",
|
||||
options: metadataProvidersList,
|
||||
optionToString: { $0 },
|
||||
selection: $metadataProviders,
|
||||
}
|
||||
|
||||
SettingsSection(
|
||||
title: "Modules",
|
||||
footer: "Note that the modules will be replaced only if there is a different version string inside the JSON file."
|
||||
) {
|
||||
SettingsToggleRow(
|
||||
icon: "arrow.clockwise",
|
||||
title: "Refresh Modules on Launch",
|
||||
isOn: $refreshModulesOnLaunch,
|
||||
showDivider: false
|
||||
)
|
||||
}
|
||||
|
||||
SettingsSection(
|
||||
title: "Advanced",
|
||||
footer: "Anonymous data is collected to improve the app. No personal information is collected. This can be disabled at any time."
|
||||
) {
|
||||
SettingsToggleRow(
|
||||
icon: "chart.bar",
|
||||
title: "Enable Analytics",
|
||||
isOn: $analyticsEnabled,
|
||||
showDivider: false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
SettingsSection(
|
||||
title: "Media Grid Layout",
|
||||
footer: "Adjust the number of media items per row in portrait and landscape modes."
|
||||
) {
|
||||
SettingsPickerRow(
|
||||
icon: "rectangle.portrait",
|
||||
title: "Portrait Columns",
|
||||
options: UIDevice.current.userInterfaceIdiom == .pad ? Array(1...5) : Array(1...4),
|
||||
optionToString: { "\($0)" },
|
||||
selection: $mediaColumnsPortrait
|
||||
)
|
||||
|
||||
SettingsPickerRow(
|
||||
icon: "rectangle",
|
||||
title: "Landscape Columns",
|
||||
options: UIDevice.current.userInterfaceIdiom == .pad ? Array(2...8) : Array(2...5),
|
||||
optionToString: { "\($0)" },
|
||||
selection: $mediaColumnsLandscape,
|
||||
showDivider: false
|
||||
)
|
||||
}
|
||||
|
||||
SettingsSection(
|
||||
title: "Modules",
|
||||
footer: "Note that the modules will be replaced only if there is a different version string inside the JSON file."
|
||||
) {
|
||||
SettingsToggleRow(
|
||||
icon: "arrow.clockwise",
|
||||
title: "Refresh Modules on Launch",
|
||||
isOn: $refreshModulesOnLaunch,
|
||||
showDivider: false
|
||||
)
|
||||
}
|
||||
|
||||
SettingsSection(
|
||||
title: "Advanced",
|
||||
footer: "Anonymous data is collected to improve the app. No personal information is collected. This can be disabled at any time."
|
||||
) {
|
||||
SettingsToggleRow(
|
||||
icon: "chart.bar",
|
||||
title: "Enable Analytics",
|
||||
isOn: $analyticsEnabled,
|
||||
showDivider: false
|
||||
)
|
||||
}
|
||||
.padding(.vertical, 20)
|
||||
}
|
||||
.padding(.vertical, 20)
|
||||
.navigationTitle("General")
|
||||
.scrollViewBottomPadding()
|
||||
}
|
||||
.navigationTitle("General")
|
||||
.scrollViewBottomPadding()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -88,6 +88,7 @@
|
|||
1E47859B2DEBC5960095BF2F /* AnilistMatchPopupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E47859A2DEBC5960095BF2F /* AnilistMatchPopupView.swift */; };
|
||||
1E9FF1D32D403E49008AC100 /* SettingsViewLoggerFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E9FF1D22D403E42008AC100 /* SettingsViewLoggerFilter.swift */; };
|
||||
1EAC7A322D888BC50083984D /* MusicProgressSlider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EAC7A312D888BC50083984D /* MusicProgressSlider.swift */; };
|
||||
1EDA48F42DFAC374002A4EC3 /* TMDBMatchPopupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EDA48F32DFAC374002A4EC3 /* TMDBMatchPopupView.swift */; };
|
||||
1EF5C3A92DB988E40032BF07 /* CommunityLib.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EF5C3A82DB988D70032BF07 /* CommunityLib.swift */; };
|
||||
7222485F2DCBAA2C00CABE2D /* DownloadModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7222485D2DCBAA2C00CABE2D /* DownloadModels.swift */; };
|
||||
722248602DCBAA2C00CABE2D /* M3U8StreamExtractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7222485E2DCBAA2C00CABE2D /* M3U8StreamExtractor.swift */; };
|
||||
|
|
@ -181,6 +182,7 @@
|
|||
1E47859A2DEBC5960095BF2F /* AnilistMatchPopupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnilistMatchPopupView.swift; sourceTree = "<group>"; };
|
||||
1E9FF1D22D403E42008AC100 /* SettingsViewLoggerFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewLoggerFilter.swift; sourceTree = "<group>"; };
|
||||
1EAC7A312D888BC50083984D /* MusicProgressSlider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MusicProgressSlider.swift; sourceTree = "<group>"; };
|
||||
1EDA48F32DFAC374002A4EC3 /* TMDBMatchPopupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TMDBMatchPopupView.swift; sourceTree = "<group>"; };
|
||||
1EF5C3A82DB988D70032BF07 /* CommunityLib.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommunityLib.swift; sourceTree = "<group>"; };
|
||||
7222485D2DCBAA2C00CABE2D /* DownloadModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadModels.swift; sourceTree = "<group>"; };
|
||||
7222485E2DCBAA2C00CABE2D /* M3U8StreamExtractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = M3U8StreamExtractor.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -368,6 +370,7 @@
|
|||
133D7C7F2D2BE2630075467E /* MediaInfoView */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
1EDA48F32DFAC374002A4EC3 /* TMDBMatchPopupView.swift */,
|
||||
138AA1B52D2D66EC0021F9DF /* EpisodeCell */,
|
||||
133D7C802D2BE2630075467E /* MediaInfoView.swift */,
|
||||
1E47859A2DEBC5960095BF2F /* AnilistMatchPopupView.swift */,
|
||||
|
|
@ -705,6 +708,7 @@
|
|||
files = (
|
||||
135CCBE22D4D1138008B9C0E /* SettingsViewPlayer.swift in Sources */,
|
||||
131270172DC13A010093AA9C /* DownloadManager.swift in Sources */,
|
||||
1EDA48F42DFAC374002A4EC3 /* TMDBMatchPopupView.swift in Sources */,
|
||||
1327FBA72D758CEA00FC6689 /* Analytics.swift in Sources */,
|
||||
13DC0C462D302C7500D0F966 /* VideoPlayer.swift in Sources */,
|
||||
1359ED142D76F49900C13034 /* finTopView.swift in Sources */,
|
||||
|
|
@ -940,7 +944,7 @@
|
|||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"Sora/Preview Content\"";
|
||||
DEVELOPMENT_TEAM = 399LMK6Q2Y;
|
||||
DEVELOPMENT_TEAM = 385Y24WAN5;
|
||||
"ENABLE_HARDENED_RUNTIME[sdk=macosx*]" = NO;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
|
|
@ -958,7 +962,7 @@
|
|||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = me.cranci.sulfur;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = me.cranci.sulfur1;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||
|
|
@ -982,7 +986,7 @@
|
|||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"Sora/Preview Content\"";
|
||||
DEVELOPMENT_TEAM = 399LMK6Q2Y;
|
||||
DEVELOPMENT_TEAM = 385Y24WAN5;
|
||||
"ENABLE_HARDENED_RUNTIME[sdk=macosx*]" = NO;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
|
|
@ -1000,7 +1004,7 @@
|
|||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = me.cranci.sulfur;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = me.cranci.sulfur1;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||
|
|
|
|||
Loading…
Reference in a new issue