From fa758159dc2fba4165a8c497602fe496103d694a Mon Sep 17 00:00:00 2001 From: Francesco <100066266+cranci1@users.noreply.github.com> Date: Sun, 8 Jun 2025 21:28:02 +0200 Subject: [PATCH] migrated to NukeUI from KingFisher --- .../Modules/ModuleAdditionSettingsView.swift | 54 +++-- Sora/Views/DownloadView.swift | 53 +++-- Sora/Views/LibraryView/AllBookmarks.swift | 64 ++++-- Sora/Views/LibraryView/AllWatching.swift | 127 ++++++----- Sora/Views/LibraryView/LibraryView.swift | 212 ++++++++++-------- .../MediaInfoView/AnilistMatchPopupView.swift | 22 +- .../EpisodeCell/EpisodeCell.swift | 30 ++- Sora/Views/MediaInfoView/MediaInfoView.swift | 49 +--- Sora/Views/SearchView/SearchResultsGrid.swift | 24 +- .../SearchView/SearchViewComponents.swift | 70 +++--- .../SettingsSubViews/SettingsViewAbout.swift | 46 ++-- .../SettingsSubViews/SettingsViewModule.swift | 20 +- .../SettingsViewTrackers.swift | 36 +-- Sulfur.xcodeproj/project.pbxproj | 34 +-- .../xcshareddata/swiftpm/Package.resolved | 26 ++- 15 files changed, 502 insertions(+), 365 deletions(-) diff --git a/Sora/Utils/Modules/ModuleAdditionSettingsView.swift b/Sora/Utils/Modules/ModuleAdditionSettingsView.swift index f901d93..758824c 100644 --- a/Sora/Utils/Modules/ModuleAdditionSettingsView.swift +++ b/Sora/Utils/Modules/ModuleAdditionSettingsView.swift @@ -5,8 +5,8 @@ // Created by Francesco on 01/02/25. // +import NukeUI import SwiftUI -import Kingfisher struct ModuleAdditionSettingsView: View { @Environment(\.presentationMode) var presentationMode @@ -22,7 +22,7 @@ struct ModuleAdditionSettingsView: View { ZStack { LinearGradient( gradient: Gradient(colors: [ - colorScheme == .dark ? Color.black : Color.white, + colorScheme == .light ? Color.black : Color.white, Color.accentColor.opacity(0.08) ]), startPoint: .top, @@ -45,17 +45,24 @@ struct ModuleAdditionSettingsView: View { VStack(spacing: 24) { if let metadata = moduleMetadata { VStack(spacing: 0) { - KFImage(URL(string: metadata.iconUrl)) - .resizable() - .aspectRatio(contentMode: .fill) - .frame(width: 90, height: 90) - .clipShape(RoundedRectangle(cornerRadius: 22, style: .continuous)) - .shadow(color: Color.accentColor.opacity(0.18), radius: 10, x: 0, y: 6) - .overlay( - RoundedRectangle(cornerRadius: 22) - .stroke(Color.accentColor, lineWidth: 2) - ) - .padding(.top, 10) + LazyImage(source: URL(string: metadata.iconUrl)) { state in + if let uiImage = state.imageContainer?.image { + Image(uiImage: uiImage) + .resizable() + .aspectRatio(contentMode: .fill) + } else { + Rectangle() + .fill(Color(.systemGray5)) + } + } + .frame(width: 90, height: 90) + .clipShape(RoundedRectangle(cornerRadius: 22, style: .continuous)) + .shadow(color: Color.accentColor.opacity(0.18), radius: 10, x: 0, y: 6) + .overlay( + RoundedRectangle(cornerRadius: 22) + .stroke(Color.accentColor, lineWidth: 2) + ) + .padding(.top, 10) VStack(spacing: 6) { Text(metadata.sourceName) @@ -65,12 +72,19 @@ struct ModuleAdditionSettingsView: View { .padding(.top, 6) HStack(spacing: 10) { - KFImage(URL(string: metadata.author.icon)) - .resizable() - .aspectRatio(contentMode: .fill) - .frame(width: 32, height: 32) - .clipShape(Circle()) - .shadow(radius: 2) + LazyImage(source: URL(string: metadata.author.icon)) { state in + if let uiImage = state.imageContainer?.image { + Image(uiImage: uiImage) + .resizable() + .aspectRatio(contentMode: .fill) + } else { + Circle() + .fill(Color(.systemGray5)) + } + } + .frame(width: 32, height: 32) + .clipShape(Circle()) + .shadow(radius: 2) VStack(alignment: .leading, spacing: 0) { Text(metadata.author.name) .font(.headline) @@ -166,7 +180,7 @@ struct ModuleAdditionSettingsView: View { Text("Add Module") } .font(.headline) - .foregroundColor(colorScheme == .dark ? .black : .white) + .foregroundColor(colorScheme == .light ? .black : .white) .frame(maxWidth: .infinity) .padding(.vertical, 14) .background( diff --git a/Sora/Views/DownloadView.swift b/Sora/Views/DownloadView.swift index bfc7d4c..2c4d300 100644 --- a/Sora/Views/DownloadView.swift +++ b/Sora/Views/DownloadView.swift @@ -5,9 +5,9 @@ // Created by doomsboygaming on 5/22/25 // -import SwiftUI import AVKit -import Kingfisher +import NukeUI +import SwiftUI struct DownloadView: View { @EnvironmentObject var jsController: JSController @@ -741,13 +741,16 @@ struct EnhancedActiveDownloadCard: View { HStack(spacing: 16) { Group { if let imageURL = download.imageURL { - KFImage(imageURL) - .placeholder { + LazyImage(source: imageURL) { state in + if let uiImage = state.imageContainer?.image { + Image(uiImage: uiImage) + .resizable() + .aspectRatio(contentMode: .fill) + } else { Rectangle() .fill(.tertiary) } - .resizable() - .aspectRatio(contentMode: .fill) + } } else { Rectangle() .fill(.tertiary) @@ -899,16 +902,18 @@ struct EnhancedDownloadGroupCard: View { NavigationLink(destination: EnhancedShowEpisodesView(group: group, onDelete: onDelete, onPlay: onPlay)) { VStack(spacing: 0) { HStack(spacing: 16) { - // Poster Group { if let posterURL = group.posterURL { - KFImage(posterURL) - .placeholder { + LazyImage(source: posterURL) { state in + if let uiImage = state.imageContainer?.image { + Image(uiImage: uiImage) + .resizable() + .aspectRatio(contentMode: .fill) + } else { Rectangle() .fill(.tertiary) } - .resizable() - .aspectRatio(contentMode: .fill) + } } else { Rectangle() .fill(.tertiary) @@ -921,7 +926,6 @@ struct EnhancedDownloadGroupCard: View { .frame(width: 56, height: 84) .clipShape(RoundedRectangle(cornerRadius: 8)) - // Content VStack(alignment: .leading, spacing: 8) { Text(group.title) .font(.headline) @@ -1000,18 +1004,20 @@ struct EnhancedShowEpisodesView: View { var body: some View { ScrollView { VStack(spacing: 24) { - // Header Section VStack(spacing: 20) { HStack(alignment: .top, spacing: 20) { Group { if let posterURL = group.posterURL { - KFImage(posterURL) - .placeholder { + LazyImage(source: posterURL) { state in + if let uiImage = state.imageContainer?.image { + Image(uiImage: uiImage) + .resizable() + .aspectRatio(contentMode: .fill) + } else { Rectangle() .fill(.tertiary) } - .resizable() - .aspectRatio(contentMode: .fill) + } } else { Rectangle() .fill(.tertiary) @@ -1192,16 +1198,18 @@ struct EnhancedEpisodeRow: View { var body: some View { VStack(spacing: 0) { HStack(spacing: 16) { - // Thumbnail Group { if let backdropURL = asset.metadata?.backdropURL ?? asset.metadata?.posterURL { - KFImage(backdropURL) - .placeholder { + LazyImage(source: backdropURL) { state in + if let uiImage = state.imageContainer?.image { + Image(uiImage: uiImage) + .resizable() + .aspectRatio(contentMode: .fill) + } else { Rectangle() .fill(.tertiary) } - .resizable() - .aspectRatio(contentMode: .fill) + } } else { Rectangle() .fill(.tertiary) @@ -1214,7 +1222,6 @@ struct EnhancedEpisodeRow: View { .frame(width: 100, height: 60) .clipShape(RoundedRectangle(cornerRadius: 8)) - // Content VStack(alignment: .leading, spacing: 4) { Text(asset.episodeDisplayName) .font(.headline) diff --git a/Sora/Views/LibraryView/AllBookmarks.swift b/Sora/Views/LibraryView/AllBookmarks.swift index b06ec95..d24fc94 100644 --- a/Sora/Views/LibraryView/AllBookmarks.swift +++ b/Sora/Views/LibraryView/AllBookmarks.swift @@ -5,9 +5,9 @@ // Created by paul on 29/04/2025. // -import SwiftUI -import Kingfisher import UIKit +import NukeUI +import SwiftUI extension View { func circularGradientOutlineTwo() -> some View { @@ -59,28 +59,44 @@ struct BookmarkCell: View { var body: some View { if let module = moduleManager.modules.first(where: { $0.id.uuidString == bookmark.moduleId }) { ZStack { - KFImage(URL(string: bookmark.imageUrl)) - .resizable() - .aspectRatio(0.72, contentMode: .fill) - .frame(width: 162, height: 243) - .cornerRadius(12) - .clipped() - .overlay( - ZStack { - Circle() - .fill(Color.black.opacity(0.5)) - .frame(width: 28, height: 28) - .overlay( - KFImage(URL(string: module.metadata.iconUrl)) - .resizable() - .scaledToFill() - .frame(width: 32, height: 32) - .clipShape(Circle()) - ) - } - .padding(8), - alignment: .topLeading - ) + LazyImage(source: URL(string: bookmark.imageUrl)) { state in + if let uiImage = state.imageContainer?.image { + Image(uiImage: uiImage) + .resizable() + .aspectRatio(0.72, contentMode: .fill) + .frame(width: 162, height: 243) + .cornerRadius(12) + .clipped() + } else { + RoundedRectangle(cornerRadius: 12) + .fill(Color.gray.opacity(0.3)) + .frame(width: 162, height: 243) + } + } + .overlay( + ZStack { + Circle() + .fill(Color.black.opacity(0.5)) + .frame(width: 28, height: 28) + .overlay( + LazyImage(source: URL(string: module.metadata.iconUrl)) { state in + if let uiImage = state.imageContainer?.image { + Image(uiImage: uiImage) + .resizable() + .scaledToFill() + .frame(width: 32, height: 32) + .clipShape(Circle()) + } else { + Circle() + .fill(Color.gray.opacity(0.3)) + .frame(width: 32, height: 32) + } + } + ) + } + .padding(8), + alignment: .topLeading + ) VStack { Spacer() diff --git a/Sora/Views/LibraryView/AllWatching.swift b/Sora/Views/LibraryView/AllWatching.swift index 8c0fefa..95a5940 100644 --- a/Sora/Views/LibraryView/AllWatching.swift +++ b/Sora/Views/LibraryView/AllWatching.swift @@ -5,9 +5,9 @@ // Created by paul on 24/05/2025. // -import SwiftUI -import Kingfisher import UIKit +import NukeUI +import SwiftUI extension View { func circularGradientOutline() -> some View { @@ -206,75 +206,86 @@ struct FullWidthContinueWatchingCell: View { }) { GeometryReader { geometry in ZStack(alignment: .bottomLeading) { - KFImage(URL(string: item.imageUrl.isEmpty ? "https://raw.githubusercontent.com/cranci1/Sora/refs/heads/main/assets/banner2.png" : item.imageUrl)) - .placeholder { + LazyImage(source: URL(string: item.imageUrl.isEmpty ? "https://raw.githubusercontent.com/cranci1/Sora/refs/heads/main/assets/banner2.png" : item.imageUrl)) { state in + if let uiImage = state.imageContainer?.image { + Image(uiImage: uiImage) + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width: geometry.size.width, height: 157.03) + .cornerRadius(10) + .clipped() + } else { RoundedRectangle(cornerRadius: 10) .fill(Color.gray.opacity(0.3)) .frame(height: 157.03) .shimmering() } - .resizable() - .aspectRatio(contentMode: .fill) - .frame(width: geometry.size.width, height: 157.03) - .cornerRadius(10) - .clipped() - .overlay( - ZStack { - ProgressiveBlurView() - .cornerRadius(10, corners: [.bottomLeft, .bottomRight]) + } + .overlay( + ZStack { + ProgressiveBlurView() + .cornerRadius(10, corners: [.bottomLeft, .bottomRight]) + + VStack(alignment: .leading, spacing: 4) { + Spacer() + Text(item.mediaTitle) + .font(.headline) + .foregroundColor(.white) + .lineLimit(1) - VStack(alignment: .leading, spacing: 4) { - Spacer() - Text(item.mediaTitle) - .font(.headline) - .foregroundColor(.white) - .lineLimit(1) + HStack { + Text("Episode \(item.episodeNumber)") + .font(.subheadline) + .foregroundColor(.white.opacity(0.9)) - HStack { - Text("Episode \(item.episodeNumber)") - .font(.subheadline) - .foregroundColor(.white.opacity(0.9)) - - Spacer() - - Text("\(Int(item.progress * 100))% seen") - .font(.caption) - .foregroundColor(.white.opacity(0.9)) - } + Spacer() + + Text("\(Int(item.progress * 100))% seen") + .font(.caption) + .foregroundColor(.white.opacity(0.9)) } - .padding(10) - .background( - LinearGradient( - colors: [ - .black.opacity(0.7), - .black.opacity(0.0) - ], - startPoint: .bottom, - endPoint: .top - ) + } + .padding(10) + .background( + LinearGradient( + colors: [ + .black.opacity(0.7), + .black.opacity(0.0) + ], + startPoint: .bottom, + endPoint: .top + ) .clipped() .cornerRadius(10, corners: [.bottomLeft, .bottomRight]) .shadow(color: .black.opacity(0.3), radius: 3, x: 0, y: 1) + ) + }, + alignment: .bottom + ) + .overlay( + ZStack { + Circle() + .fill(Color.black.opacity(0.5)) + .frame(width: 28, height: 28) + .overlay( + LazyImage(source: URL(string: item.module.metadata.iconUrl)) { state in + if let uiImage = state.imageContainer?.image { + Image(uiImage: uiImage) + .resizable() + .scaledToFill() + .frame(width: 32, height: 32) + .clipShape(Circle()) + } else { + Circle() + .fill(Color.gray.opacity(0.3)) + .frame(width: 32, height: 32) + } + } ) - }, - alignment: .bottom - ) - .overlay( - ZStack { - Circle() - .fill(Color.black.opacity(0.5)) - .frame(width: 28, height: 28) - .overlay( - KFImage(URL(string: item.module.metadata.iconUrl)) - .resizable() - .scaledToFill() - .frame(width: 32, height: 32) - .clipShape(Circle()) - ) - } + } .padding(8), - alignment: .topLeading - ) + alignment: .topLeading + ) } } .frame(height: 157.03) diff --git a/Sora/Views/LibraryView/LibraryView.swift b/Sora/Views/LibraryView/LibraryView.swift index 6a97b7e..3858110 100644 --- a/Sora/Views/LibraryView/LibraryView.swift +++ b/Sora/Views/LibraryView/LibraryView.swift @@ -5,9 +5,9 @@ // Created by Francesco on 05/01/25. // -import SwiftUI -import Kingfisher import UIKit +import NukeUI +import SwiftUI struct LibraryView: View { @EnvironmentObject private var libraryManager: LibraryManager @@ -280,85 +280,96 @@ struct ContinueWatchingCell: View { } }) { ZStack(alignment: .bottomLeading) { - KFImage(URL(string: item.imageUrl.isEmpty ? "https://raw.githubusercontent.com/cranci1/Sora/refs/heads/main/assets/banner2.png" : item.imageUrl)) - .placeholder { + LazyImage(source: URL(string: item.imageUrl.isEmpty ? "https://raw.githubusercontent.com/cranci1/Sora/refs/heads/main/assets/banner2.png" : item.imageUrl)) { state in + if let uiImage = state.imageContainer?.image { + Image(uiImage: uiImage) + .resizable() + .aspectRatio(16/9, contentMode: .fill) + .frame(width: 280, height: 157.03) + .cornerRadius(10) + .clipped() + } else { RoundedRectangle(cornerRadius: 10) .fill(Color.gray.opacity(0.3)) .frame(width: 280, height: 157.03) .shimmering() } - .resizable() - .aspectRatio(16/9, contentMode: .fill) - .frame(width: 280, height: 157.03) - .cornerRadius(10) - .clipped() - .overlay( - ZStack { - ProgressiveBlurView() - .cornerRadius(10, corners: [.bottomLeft, .bottomRight]) + } + .overlay( + ZStack { + ProgressiveBlurView() + .cornerRadius(10, corners: [.bottomLeft, .bottomRight]) + + VStack(alignment: .leading, spacing: 4) { + Spacer() + Text(item.mediaTitle) + .font(.headline) + .foregroundColor(.white) + .lineLimit(1) - VStack(alignment: .leading, spacing: 4) { - Spacer() - Text(item.mediaTitle) - .font(.headline) - .foregroundColor(.white) - .lineLimit(1) + HStack { + Text("Episode \(item.episodeNumber)") + .font(.subheadline) + .foregroundColor(.white.opacity(0.9)) - HStack { - Text("Episode \(item.episodeNumber)") - .font(.subheadline) - .foregroundColor(.white.opacity(0.9)) - - Spacer() - - Text("\(Int(item.progress * 100))% seen") - .font(.caption) - .foregroundColor(.white.opacity(0.9)) - } + Spacer() + + Text("\(Int(item.progress * 100))% seen") + .font(.caption) + .foregroundColor(.white.opacity(0.9)) } - .padding(10) - .background( - LinearGradient( - colors: [ - .black.opacity(0.7), - .black.opacity(0.0) - ], - startPoint: .bottom, - endPoint: .top - ) - .clipped() - .cornerRadius(10, corners: [.bottomLeft, .bottomRight]) - .shadow(color: .black.opacity(0.3), radius: 3, x: 0, y: 1) + } + .padding(10) + .background( + LinearGradient( + colors: [ + .black.opacity(0.7), + .black.opacity(0.0) + ], + startPoint: .bottom, + endPoint: .top ) - }, - alignment: .bottom - ) - .overlay( - ZStack { - if item.streamUrl.hasPrefix("file://") { - Image(systemName: "arrow.down.app.fill") - .resizable() - .scaledToFit() - .frame(width: 24, height: 24) - .foregroundColor(.white) - .background(Color.black.cornerRadius(6)) - .padding(8) - } else { - Circle() - .fill(Color.black.opacity(0.5)) - .frame(width: 28, height: 28) - .overlay( - KFImage(URL(string: item.module.metadata.iconUrl)) - .resizable() - .scaledToFill() - .frame(width: 32, height: 32) - .clipShape(Circle()) - ) - .padding(8) - } - }, - alignment: .topLeading - ) + .clipped() + .cornerRadius(10, corners: [.bottomLeft, .bottomRight]) + .shadow(color: .black.opacity(0.3), radius: 3, x: 0, y: 1) + ) + }, + alignment: .bottom + ) + .overlay( + ZStack { + if item.streamUrl.hasPrefix("file://") { + Image(systemName: "arrow.down.app.fill") + .resizable() + .scaledToFit() + .frame(width: 24, height: 24) + .foregroundColor(.white) + .background(Color.black.cornerRadius(6)) + .padding(8) + } else { + Circle() + .fill(Color.black.opacity(0.5)) + .frame(width: 28, height: 28) + .overlay( + LazyImage(source: URL(string: item.module.metadata.iconUrl)) { state in + if let uiImage = state.imageContainer?.image { + Image(uiImage: uiImage) + .resizable() + .scaledToFill() + .frame(width: 32, height: 32) + .clipShape(Circle()) + } else { + Circle() + .fill(Color.gray.opacity(0.3)) + .frame(width: 32, height: 32) + } + } + ) + .padding(8) + } + }, + alignment: .topLeading + ) } .frame(width: 280, height: 157.03) } @@ -525,34 +536,45 @@ struct BookmarkItemView: View { isDetailActive = true }) { ZStack { - KFImage(URL(string: item.imageUrl)) - .placeholder { + LazyImage(source: URL(string: item.imageUrl)) { state in + if let uiImage = state.imageContainer?.image { + Image(uiImage: uiImage) + .resizable() + .aspectRatio(0.72, contentMode: .fill) + .frame(width: 162, height: 243) + .cornerRadius(12) + .clipped() + } else { RoundedRectangle(cornerRadius: 12) .fill(Color.gray.opacity(0.3)) .aspectRatio(2 / 3, contentMode: .fit) .shimmering() } - .resizable() - .aspectRatio(0.72, contentMode: .fill) - .frame(width: 162, height: 243) - .cornerRadius(12) - .clipped() - .overlay( - ZStack { - Circle() - .fill(Color.black.opacity(0.5)) - .frame(width: 28, height: 28) - .overlay( - KFImage(URL(string: module.metadata.iconUrl)) - .resizable() - .scaledToFill() - .frame(width: 32, height: 32) - .clipShape(Circle()) - ) - } - .padding(8), - alignment: .topLeading - ) + } + .overlay( + ZStack { + Circle() + .fill(Color.black.opacity(0.5)) + .frame(width: 28, height: 28) + .overlay( + LazyImage(source: URL(string: module.metadata.iconUrl)) { state in + if let uiImage = state.imageContainer?.image { + Image(uiImage: uiImage) + .resizable() + .scaledToFill() + .frame(width: 32, height: 32) + .clipShape(Circle()) + } else { + Circle() + .fill(Color.gray.opacity(0.3)) + .frame(width: 32, height: 32) + } + } + ) + } + .padding(8), + alignment: .topLeading + ) VStack { Spacer() diff --git a/Sora/Views/MediaInfoView/AnilistMatchPopupView.swift b/Sora/Views/MediaInfoView/AnilistMatchPopupView.swift index 57f72ae..61aa2a2 100644 --- a/Sora/Views/MediaInfoView/AnilistMatchPopupView.swift +++ b/Sora/Views/MediaInfoView/AnilistMatchPopupView.swift @@ -5,8 +5,8 @@ // Created by seiike on 01/06/2025. // +import NukeUI import SwiftUI -import Kingfisher struct AnilistMatchPopupView: View { let seriesTitle: String @@ -32,7 +32,6 @@ struct AnilistMatchPopupView: View { NavigationView { ScrollView { VStack(alignment: .leading, spacing: 4) { - // (Optional) A hidden header; can be omitted if empty Text("".uppercased()) .font(.footnote) .foregroundStyle(.gray) @@ -62,11 +61,20 @@ struct AnilistMatchPopupView: View { HStack(spacing: 12) { if let cover = result["cover"] as? String, let url = URL(string: cover) { - KFImage(url) - .resizable() - .aspectRatio(contentMode: .fill) - .frame(width: 50, height: 70) - .cornerRadius(6) + LazyImage(source: url) { state in + if let uiImage = state.imageContainer?.image { + Image(uiImage: uiImage) + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width: 50, height: 70) + .cornerRadius(6) + } else { + Rectangle() + .fill(.tertiary) + .frame(width: 50, height: 70) + .cornerRadius(6) + } + } } VStack(alignment: .leading, spacing: 2) { diff --git a/Sora/Views/MediaInfoView/EpisodeCell/EpisodeCell.swift b/Sora/Views/MediaInfoView/EpisodeCell/EpisodeCell.swift index dd8c20b..4681521 100644 --- a/Sora/Views/MediaInfoView/EpisodeCell/EpisodeCell.swift +++ b/Sora/Views/MediaInfoView/EpisodeCell/EpisodeCell.swift @@ -5,8 +5,8 @@ // Created by Francesco on 18/12/24. // +import NukeUI import SwiftUI -import Kingfisher import AVFoundation struct EpisodeCell: View { @@ -264,14 +264,28 @@ struct EpisodeCell: View { private var episodeThumbnail: some View { ZStack { if let url = URL(string: episodeImageUrl.isEmpty ? defaultBannerImage : episodeImageUrl) { - KFImage(url) - .onFailure { error in - Logger.shared.log("Failed to load episode image: \(error)", type: "Error") + LazyImage(source: url) { state in + if let image = state.imageContainer?.image { + Image(uiImage: image) + .resizable() + .aspectRatio(16/9, contentMode: .fill) + .frame(width: 100, height: 56) + .cornerRadius(8) + } else if state.error != nil { + Rectangle() + .fill(.tertiary) + .frame(width: 100, height: 56) + .cornerRadius(8) + .onAppear { + Logger.shared.log("Failed to load episode image: \(state.error?.localizedDescription ?? "Unknown error")", type: "Error") + } + } else { + Rectangle() + .fill(.tertiary) + .frame(width: 100, height: 56) + .cornerRadius(8) } - .resizable() - .aspectRatio(16/9, contentMode: .fill) - .frame(width: 100, height: 56) - .cornerRadius(8) + } } else { Rectangle() .fill(Color.gray.opacity(0.3)) diff --git a/Sora/Views/MediaInfoView/MediaInfoView.swift b/Sora/Views/MediaInfoView/MediaInfoView.swift index 449e965..39ec350 100644 --- a/Sora/Views/MediaInfoView/MediaInfoView.swift +++ b/Sora/Views/MediaInfoView/MediaInfoView.swift @@ -5,8 +5,8 @@ // Created by Francesco on 05/01/25. // +import NukeUI import SwiftUI -import Kingfisher import SafariServices private let tmdbFetcher = TMDBFetcher() @@ -212,46 +212,21 @@ struct MediaInfoView: View { private var mainScrollView: some View { ScrollView { ZStack(alignment: .top) { - KFImage(URL(string: imageUrl)) - .placeholder { + LazyImage(source: URL(string: imageUrl)) { state in + if let uiImage = state.imageContainer?.image { + Image(uiImage: uiImage) + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width: UIScreen.main.bounds.width, height: 700) + .clipped() + } else { Rectangle() .fill(Color.gray.opacity(0.3)) .shimmering() + .frame(width: UIScreen.main.bounds.width, height: 700) + .clipped() } - .resizable() - .aspectRatio(contentMode: .fill) - .frame(width: UIScreen.main.bounds.width, height: 700) - .clipped() - KFImage(URL(string: imageUrl)) - .placeholder { EmptyView() } - .resizable() - .aspectRatio(contentMode: .fill) - .frame(width: UIScreen.main.bounds.width, height: 700) - .clipped() - .blur(radius: 30) - .mask( - LinearGradient( - gradient: Gradient(stops: [ - .init(color: .clear, location: 0.0), - .init(color: .clear, location: 0.6), - .init(color: .black, location: 0.8), - .init(color: .black, location: 1.0) - ]), - startPoint: .top, - endPoint: .bottom - ) - ) - .overlay( - LinearGradient( - gradient: Gradient(stops: [ - .init(color: .clear, location: 0.0), - .init(color: .clear, location: 0.7), - .init(color: (colorScheme == .dark ? Color.black : Color.white).opacity(0.9), location: 1.0) - ]), - startPoint: .top, - endPoint: .bottom - ) - ) + } VStack(spacing: 0) { Rectangle() .fill(Color.clear) diff --git a/Sora/Views/SearchView/SearchResultsGrid.swift b/Sora/Views/SearchView/SearchResultsGrid.swift index 0a0fde1..2d5db62 100644 --- a/Sora/Views/SearchView/SearchResultsGrid.swift +++ b/Sora/Views/SearchView/SearchResultsGrid.swift @@ -5,8 +5,8 @@ // Created by paul on 28/05/25. // +import NukeUI import SwiftUI -import Kingfisher struct SearchResultsGrid: View { @AppStorage("mediaColumnsPortrait") private var mediaColumnsPortrait: Int = 2 @@ -32,12 +32,22 @@ struct SearchResultsGrid: View { ForEach(items) { item in NavigationLink(destination: MediaInfoView(title: item.title, imageUrl: item.imageUrl, href: item.href, module: selectedModule)) { ZStack { - KFImage(URL(string: item.imageUrl)) - .resizable() - .aspectRatio(0.72, contentMode: .fill) - .frame(width: cellWidth, height: cellWidth * 1.5) - .cornerRadius(12) - .clipped() + LazyImage(source: URL(string: item.imageUrl)) { state in + if let uiImage = state.imageContainer?.image { + Image(uiImage: uiImage) + .resizable() + .aspectRatio(0.72, contentMode: .fill) + .frame(width: cellWidth, height: cellWidth * 1.5) + .cornerRadius(12) + .clipped() + } else { + Rectangle() + .fill(.tertiary) + .frame(width: cellWidth, height: cellWidth * 1.5) + .cornerRadius(12) + .clipped() + } + } VStack { Spacer() diff --git a/Sora/Views/SearchView/SearchViewComponents.swift b/Sora/Views/SearchView/SearchViewComponents.swift index 50c557d..efce137 100644 --- a/Sora/Views/SearchView/SearchViewComponents.swift +++ b/Sora/Views/SearchView/SearchViewComponents.swift @@ -5,8 +5,8 @@ // Created by Francesco on 27/01/25. // +import NukeUI import SwiftUI -import Kingfisher struct ModuleSelectorMenu: View { let selectedModule: ScrapingModule? @@ -27,11 +27,19 @@ struct ModuleSelectorMenu: View { onModuleSelected(module.id.uuidString) } label: { HStack { - KFImage(URL(string: module.metadata.iconUrl)) - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 20, height: 20) - .cornerRadius(4) + LazyImage(source: URL(string: module.metadata.iconUrl)) { state in + if let uiImage = state.imageContainer?.image { + Image(uiImage: uiImage) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 20, height: 20) + .cornerRadius(4) + } else { + Circle() + .fill(Color(.systemGray5)) + } + } + Text(module.metadata.sourceName) if module.id.uuidString == selectedModuleId { Image(systemName: "checkmark") @@ -48,29 +56,37 @@ struct ModuleSelectorMenu: View { Text(selectedModule.metadata.sourceName) .font(.headline) .foregroundColor(.primary) - KFImage(URL(string: selectedModule.metadata.iconUrl)) - .resizable() - .frame(width: 36, height: 36) - .clipShape(Circle()) - .background( + LazyImage(source: URL(string: selectedModule.metadata.iconUrl)) { state in + if let uiImage = state.imageContainer?.image { + Image(uiImage: uiImage) + .resizable() + .frame(width: 36, height: 36) + .clipShape(Circle()) + } else { Circle() .fill(.ultraThinMaterial) - .overlay( - Circle() - .stroke( - LinearGradient( - gradient: Gradient(stops: [ - .init(color: Color.accentColor.opacity(gradientOpacity), location: 0), - .init(color: Color.accentColor.opacity(0), location: 1) - ]), - startPoint: .top, - endPoint: .bottom - ), - lineWidth: 0.5 - ) - ) - .matchedGeometryEffect(id: "background_circle", in: animation) - ) + .frame(width: 36, height: 36) + } + } + .background( + Circle() + .fill(.ultraThinMaterial) + .overlay( + Circle() + .stroke( + LinearGradient( + gradient: Gradient(stops: [ + .init(color: Color.accentColor.opacity(gradientOpacity), location: 0), + .init(color: Color.accentColor.opacity(0), location: 1) + ]), + startPoint: .top, + endPoint: .bottom + ), + lineWidth: 0.5 + ) + ) + .matchedGeometryEffect(id: "background_circle", in: animation) + ) } else { Text("Select Module") .font(.headline) diff --git a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewAbout.swift b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewAbout.swift index 966e8ec..7f6ef7a 100644 --- a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewAbout.swift +++ b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewAbout.swift @@ -5,8 +5,8 @@ // Created by Francesco on 26/05/25. // +import NukeUI import SwiftUI -import Kingfisher fileprivate struct SettingsSection: View { let title: String @@ -66,14 +66,18 @@ struct SettingsViewAbout: View { VStack(spacing: 24) { SettingsSection(title: "App Info", footer: "Sora/Sulfur will always remain free with no ADs!") { HStack(alignment: .center, spacing: 16) { - KFImage(URL(string: "https://raw.githubusercontent.com/cranci1/Sora/refs/heads/dev/Sora/Assets.xcassets/AppIcons/AppIcon_Default.appiconset/darkmode.png")) - .placeholder { + LazyImage(source: URL(string: "https://raw.githubusercontent.com/cranci1/Sora/refs/heads/dev/Sora/Assets.xcassets/AppIcons/AppIcon_Default.appiconset/darkmode.png")) { state in + if let uiImage = state.imageContainer?.image { + Image(uiImage: uiImage) + .resizable() + .frame(width: 100, height: 100) + .cornerRadius(20) + .shadow(radius: 5) + } else { ProgressView() + .frame(width: 40, height: 40) } - .resizable() - .frame(width: 100, height: 100) - .cornerRadius(20) - .shadow(radius: 5) + } VStack(alignment: .leading, spacing: 8) { Text("Sora") @@ -96,13 +100,17 @@ struct SettingsViewAbout: View { } }) { HStack { - KFImage(URL(string: "https://avatars.githubusercontent.com/u/100066266?v=4")) - .placeholder { + LazyImage(source: URL(string: "https://avatars.githubusercontent.com/u/100066266?v=4")) { state in + if let uiImage = state.imageContainer?.image { + Image(uiImage: uiImage) + .resizable() + .frame(width: 40, height: 40) + .clipShape(Circle()) + } else { ProgressView() + .frame(width: 40, height: 40) } - .resizable() - .frame(width: 40, height: 40) - .clipShape(Circle()) + } VStack(alignment: .leading) { Text("cranci1") @@ -205,13 +213,17 @@ struct ContributorView: View { } }) { HStack { - KFImage(URL(string: contributor.avatarUrl)) - .placeholder { + LazyImage(source: URL(string: contributor.avatarUrl)) { state in + if let uiImage = state.imageContainer?.image { + Image(uiImage: uiImage) + .resizable() + .frame(width: 40, height: 40) + .clipShape(Circle()) + } else { ProgressView() + .frame(width: 40, height: 40) } - .resizable() - .frame(width: 40, height: 40) - .clipShape(Circle()) + } Text(contributor.login) .font(.headline) diff --git a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewModule.swift b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewModule.swift index 6aeafd2..82787fb 100644 --- a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewModule.swift +++ b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewModule.swift @@ -5,8 +5,8 @@ // Created by Francesco on 05/01/25. // +import NukeUI import SwiftUI -import Kingfisher fileprivate struct SettingsSection: View { let title: String @@ -67,11 +67,19 @@ fileprivate struct ModuleListItemView: View { var body: some View { VStack(spacing: 0) { HStack { - KFImage(URL(string: module.metadata.iconUrl)) - .resizable() - .frame(width: 40, height: 40) - .clipShape(Circle()) - .padding(.trailing, 10) + LazyImage(source: URL(string: module.metadata.iconUrl)) { state in + if let uiImage = state.imageContainer?.image { + Image(uiImage: uiImage) + .resizable() + .frame(width: 40, height: 40) + .clipShape(Circle()) + .padding(.trailing, 10) + } else { + Circle() + .frame(width: 40, height: 40) + .padding(.trailing, 10) + } + } VStack(alignment: .leading, spacing: 2) { HStack(alignment: .bottom, spacing: 4) { diff --git a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewTrackers.swift b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewTrackers.swift index d9cd150..4299148 100644 --- a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewTrackers.swift +++ b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewTrackers.swift @@ -5,9 +5,9 @@ // Created by Francesco on 23/03/25. // +import NukeUI import SwiftUI import Security -import Kingfisher fileprivate struct SettingsSection: View { let title: String @@ -120,18 +120,21 @@ struct SettingsViewTrackers: View { SettingsSection(title: "AniList") { VStack(spacing: 0) { HStack(alignment: .center, spacing: 10) { - KFImage(URL(string: "https://raw.githubusercontent.com/cranci1/Ryu/2f10226aa087154974a70c1ec78aa83a47daced9/Ryu/Assets.xcassets/Listing/Anilist.imageset/anilist.png")) - .placeholder { + LazyImage(source: URL(string: "https://raw.githubusercontent.com/cranci1/Ryu/2f10226aa087154974a70c1ec78aa83a47daced9/Ryu/Assets.xcassets/Listing/Anilist.imageset/anilist.png")) { state in + if let uiImage = state.imageContainer?.image { + Image(uiImage: uiImage) + .resizable() + .frame(width: 60, height: 60) + .clipShape(Rectangle()) + .cornerRadius(10) + .padding(.trailing, 10) + } else { RoundedRectangle(cornerRadius: 10) .fill(Color.gray.opacity(0.3)) .frame(width: 60, height: 60) .shimmering() } - .resizable() - .frame(width: 60, height: 60) - .clipShape(Rectangle()) - .cornerRadius(10) - .padding(.trailing, 10) + } VStack(alignment: .leading, spacing: 4) { Text("AniList.co") @@ -212,18 +215,21 @@ struct SettingsViewTrackers: View { SettingsSection(title: "Trakt") { VStack(spacing: 0) { HStack(alignment: .center, spacing: 10) { - KFImage(URL(string: "https://static-00.iconduck.com/assets.00/trakt-icon-2048x2048-2633ksxg.png")) - .placeholder { + LazyImage(source: URL(string: "https://static-00.iconduck.com/assets.00/trakt-icon-2048x2048-2633ksxg.png")) { state in + if let uiImage = state.imageContainer?.image { + Image(uiImage: uiImage) + .resizable() + .frame(width: 60, height: 60) + .clipShape(Rectangle()) + .cornerRadius(10) + .padding(.trailing, 10) + } else { RoundedRectangle(cornerRadius: 10) .fill(Color.gray.opacity(0.3)) .frame(width: 60, height: 60) .shimmering() } - .resizable() - .frame(width: 60, height: 60) - .clipShape(Rectangle()) - .cornerRadius(10) - .padding(.trailing, 10) + } VStack(alignment: .leading, spacing: 4) { Text("Trakt.tv") diff --git a/Sulfur.xcodeproj/project.pbxproj b/Sulfur.xcodeproj/project.pbxproj index 16bbe89..c6ecca4 100644 --- a/Sulfur.xcodeproj/project.pbxproj +++ b/Sulfur.xcodeproj/project.pbxproj @@ -48,7 +48,6 @@ 1359ED142D76F49900C13034 /* finTopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1359ED132D76F49900C13034 /* finTopView.swift */; }; 135CCBE22D4D1138008B9C0E /* SettingsViewPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 135CCBE12D4D1138008B9C0E /* SettingsViewPlayer.swift */; }; 13637B8A2DE0EA1100BDA2FC /* UserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13637B892DE0EA1100BDA2FC /* UserDefaults.swift */; }; - 13637B8D2DE0ECCC00BDA2FC /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 13637B8C2DE0ECCC00BDA2FC /* Kingfisher */; }; 13637B902DE0ECD200BDA2FC /* Drops in Frameworks */ = {isa = PBXBuildFile; productRef = 13637B8F2DE0ECD200BDA2FC /* Drops */; }; 13637B932DE0ECDB00BDA2FC /* MarqueeLabel in Frameworks */ = {isa = PBXBuildFile; productRef = 13637B922DE0ECDB00BDA2FC /* MarqueeLabel */; }; 136BBE802DB1038000906B5E /* Notification+Name.swift in Sources */ = {isa = PBXBuildFile; fileRef = 136BBE7F2DB1038000906B5E /* Notification+Name.swift */; }; @@ -61,6 +60,7 @@ 1399FAD62D3AB3DB00E97C31 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1399FAD52D3AB3DB00E97C31 /* Logger.swift */; }; 13B77E202DA457AA00126FDF /* AniListPushUpdates.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13B77E1F2DA457AA00126FDF /* AniListPushUpdates.swift */; }; 13B7F4C12D58FFDD0045714A /* Shimmer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13B7F4C02D58FFDD0045714A /* Shimmer.swift */; }; + 13BC689F2DF61327009A0651 /* NukeUI in Frameworks */ = {isa = PBXBuildFile; productRef = 13BC689E2DF61327009A0651 /* NukeUI */; }; 13C0E5EA2D5F85EA00E7F619 /* ContinueWatchingManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13C0E5E92D5F85EA00E7F619 /* ContinueWatchingManager.swift */; }; 13C0E5EC2D5F85F800E7F619 /* ContinueWatchingItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13C0E5EB2D5F85F800E7F619 /* ContinueWatchingItem.swift */; }; 13CBA0882D60F19C00EFE70A /* VTTSubtitlesLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13CBA0872D60F19C00EFE70A /* VTTSubtitlesLoader.swift */; }; @@ -191,9 +191,9 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 13637B8D2DE0ECCC00BDA2FC /* Kingfisher in Frameworks */, 13637B902DE0ECD200BDA2FC /* Drops in Frameworks */, 13637B932DE0ECDB00BDA2FC /* MarqueeLabel in Frameworks */, + 13BC689F2DF61327009A0651 /* NukeUI in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -614,9 +614,9 @@ ); name = Sulfur; packageProductDependencies = ( - 13637B8C2DE0ECCC00BDA2FC /* Kingfisher */, 13637B8F2DE0ECD200BDA2FC /* Drops */, 13637B922DE0ECDB00BDA2FC /* MarqueeLabel */, + 13BC689E2DF61327009A0651 /* NukeUI */, ); productName = Sora; productReference = 133D7C6A2D2BE2500075467E /* Sulfur.app */; @@ -646,9 +646,9 @@ ); mainGroup = 133D7C612D2BE2500075467E; packageReferences = ( - 13637B8B2DE0ECCC00BDA2FC /* XCRemoteSwiftPackageReference "Kingfisher" */, 13637B8E2DE0ECD200BDA2FC /* XCRemoteSwiftPackageReference "Drops" */, 13637B912DE0ECDB00BDA2FC /* XCRemoteSwiftPackageReference "MarqueeLabel" */, + 13BC689D2DF61327009A0651 /* XCRemoteSwiftPackageReference "NukeUI" */, ); productRefGroup = 133D7C6B2D2BE2500075467E /* Products */; projectDirPath = ""; @@ -994,14 +994,6 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ - 13637B8B2DE0ECCC00BDA2FC /* XCRemoteSwiftPackageReference "Kingfisher" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/onevcat/Kingfisher.git"; - requirement = { - kind = exactVersion; - version = 7.9.1; - }; - }; 13637B8E2DE0ECD200BDA2FC /* XCRemoteSwiftPackageReference "Drops" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/omaralbeik/Drops.git"; @@ -1018,14 +1010,17 @@ version = 4.2.1; }; }; + 13BC689D2DF61327009A0651 /* XCRemoteSwiftPackageReference "NukeUI" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/kean/NukeUI"; + requirement = { + branch = main; + kind = branch; + }; + }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ - 13637B8C2DE0ECCC00BDA2FC /* Kingfisher */ = { - isa = XCSwiftPackageProductDependency; - package = 13637B8B2DE0ECCC00BDA2FC /* XCRemoteSwiftPackageReference "Kingfisher" */; - productName = Kingfisher; - }; 13637B8F2DE0ECD200BDA2FC /* Drops */ = { isa = XCSwiftPackageProductDependency; package = 13637B8E2DE0ECD200BDA2FC /* XCRemoteSwiftPackageReference "Drops" */; @@ -1036,6 +1031,11 @@ package = 13637B912DE0ECDB00BDA2FC /* XCRemoteSwiftPackageReference "MarqueeLabel" */; productName = MarqueeLabel; }; + 13BC689E2DF61327009A0651 /* NukeUI */ = { + isa = XCSwiftPackageProductDependency; + package = 13BC689D2DF61327009A0651 /* XCRemoteSwiftPackageReference "NukeUI" */; + productName = NukeUI; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 133D7C622D2BE2500075467E /* Project object */; diff --git a/Sulfur.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Sulfur.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index a843cd1..9c7f2bf 100644 --- a/Sulfur.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Sulfur.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -11,12 +11,12 @@ } }, { - "package": "Kingfisher", - "repositoryURL": "https://github.com/onevcat/Kingfisher.git", + "package": "Gifu", + "repositoryURL": "https://github.com/kaishin/Gifu", "state": { "branch": null, - "revision": "b6f62758f21a8c03cd64f4009c037cfa580a256e", - "version": "7.9.1" + "revision": "82da0086dea14ca9afc9801234ad8dc4cd9e2738", + "version": "3.4.1" } }, { @@ -27,6 +27,24 @@ "revision": "cffb6938940d3242882e6a2f9170b7890a4729ea", "version": "4.2.1" } + }, + { + "package": "Nuke", + "repositoryURL": "https://github.com/kean/Nuke.git", + "state": { + "branch": null, + "revision": "a002b7fd786f2df2ed4333fe73a9727499fd9d97", + "version": "10.11.2" + } + }, + { + "package": "NukeUI", + "repositoryURL": "https://github.com/kean/NukeUI", + "state": { + "branch": "main", + "revision": "7338ed8ea76de18598bfafbca0cbdc74300a6b10", + "version": null + } } ] },