damn this looks nice ong

This commit is contained in:
cranci1 2025-02-09 10:02:19 +01:00
parent c7a35b11a5
commit 051a710c70
5 changed files with 474 additions and 5 deletions

View file

@ -8,6 +8,9 @@
/* Begin PBXBuildFile section */
130C6BFA2D53AB1F00DC1432 /* SettingsViewData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 130C6BF92D53AB1F00DC1432 /* SettingsViewData.swift */; };
13103E842D589D8B000F0673 /* AniList-Seasonal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13103E832D589D8B000F0673 /* AniList-Seasonal.swift */; };
13103E862D58A328000F0673 /* AniList-Trending.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13103E852D58A328000F0673 /* AniList-Trending.swift */; };
13103E892D58A39A000F0673 /* AniListItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13103E882D58A39A000F0673 /* AniListItem.swift */; };
131845F92D47C62D00CA7A54 /* SettingsViewGeneral.swift in Sources */ = {isa = PBXBuildFile; fileRef = 131845F82D47C62D00CA7A54 /* SettingsViewGeneral.swift */; };
133D7C6E2D2BE2500075467E /* SoraApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 133D7C6D2D2BE2500075467E /* SoraApp.swift */; };
133D7C702D2BE2500075467E /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 133D7C6F2D2BE2500075467E /* ContentView.swift */; };
@ -45,6 +48,9 @@
/* Begin PBXFileReference section */
130C6BF82D53A4C200DC1432 /* Sora.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Sora.entitlements; sourceTree = "<group>"; };
130C6BF92D53AB1F00DC1432 /* SettingsViewData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewData.swift; sourceTree = "<group>"; };
13103E832D589D8B000F0673 /* AniList-Seasonal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AniList-Seasonal.swift"; sourceTree = "<group>"; };
13103E852D58A328000F0673 /* AniList-Trending.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AniList-Trending.swift"; sourceTree = "<group>"; };
13103E882D58A39A000F0673 /* AniListItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AniListItem.swift; sourceTree = "<group>"; };
131845F82D47C62D00CA7A54 /* SettingsViewGeneral.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewGeneral.swift; sourceTree = "<group>"; };
133D7C6A2D2BE2500075467E /* Sora.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Sora.app; sourceTree = BUILT_PRODUCTS_DIR; };
133D7C6D2D2BE2500075467E /* SoraApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoraApp.swift; sourceTree = "<group>"; };
@ -92,6 +98,40 @@
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
13103E802D589D6C000F0673 /* Tracking Services */ = {
isa = PBXGroup;
children = (
13103E812D589D77000F0673 /* AniList */,
);
path = "Tracking Services";
sourceTree = "<group>";
};
13103E812D589D77000F0673 /* AniList */ = {
isa = PBXGroup;
children = (
13103E872D58A392000F0673 /* Struct */,
13103E822D589D7D000F0673 /* HomePage */,
);
path = AniList;
sourceTree = "<group>";
};
13103E822D589D7D000F0673 /* HomePage */ = {
isa = PBXGroup;
children = (
13103E832D589D8B000F0673 /* AniList-Seasonal.swift */,
13103E852D58A328000F0673 /* AniList-Trending.swift */,
);
path = HomePage;
sourceTree = "<group>";
};
13103E872D58A392000F0673 /* Struct */ = {
isa = PBXGroup;
children = (
13103E882D58A39A000F0673 /* AniListItem.swift */,
);
path = Struct;
sourceTree = "<group>";
};
133D7C612D2BE2500075467E = {
isa = PBXGroup;
children = (
@ -113,6 +153,7 @@
children = (
130C6BF82D53A4C200DC1432 /* Sora.entitlements */,
13DC0C412D2EC9BA00D0F966 /* Info.plist */,
13103E802D589D6C000F0673 /* Tracking Services */,
133D7C852D2BE2640075467E /* Utils */,
133D7C7B2D2BE2630075467E /* Views */,
133D7C6D2D2BE2500075467E /* SoraApp.swift */,
@ -373,8 +414,10 @@
133D7C932D2BE2640075467E /* Modules.swift in Sources */,
133D7C702D2BE2500075467E /* ContentView.swift in Sources */,
13D99CF72D4E73C300250A86 /* ModuleAdditionSettingsView.swift in Sources */,
13103E862D58A328000F0673 /* AniList-Trending.swift in Sources */,
13EA2BD62D32D97400C1EBD7 /* Double+Extension.swift in Sources */,
133D7C8F2D2BE2640075467E /* MediaInfoView.swift in Sources */,
13103E892D58A39A000F0673 /* AniListItem.swift in Sources */,
131845F92D47C62D00CA7A54 /* SettingsViewGeneral.swift in Sources */,
133D7C8D2D2BE2640075467E /* HomeView.swift in Sources */,
13EA2BDC2D32D9FF00C1EBD7 /* MiruDataStruct.swift in Sources */,
@ -385,6 +428,7 @@
133D7C922D2BE2640075467E /* URLSession.swift in Sources */,
133D7C912D2BE2640075467E /* SettingsViewModule.swift in Sources */,
133F55BB2D33B55100E08EEA /* LibraryManager.swift in Sources */,
13103E842D589D8B000F0673 /* AniList-Seasonal.swift in Sources */,
133D7C8E2D2BE2640075467E /* LibraryView.swift in Sources */,
133D7C6E2D2BE2500075467E /* SoraApp.swift in Sources */,
138AA1B92D2D66FD0021F9DF /* CircularProgressBar.swift in Sources */,

View file

@ -0,0 +1,117 @@
//
// AniList-Seasonal.swift
// Sora
//
// Created by Francesco on 09/02/25.
//
import Foundation
class AnilistServiceSeasonalAnime {
func fetchSeasonalAnime(completion: @escaping ([AniListItem]?) -> Void) {
let currentDate = Date()
let calendar = Calendar.current
let year = calendar.component(.year, from: currentDate)
let month = calendar.component(.month, from: currentDate)
let season: String
switch month {
case 1...3:
season = "WINTER"
case 4...6:
season = "SPRING"
case 7...9:
season = "SUMMER"
default:
season = "FALL"
}
let query = """
query {
Page(page: 1, perPage: 100) {
media(season: \(season), seasonYear: \(year), type: ANIME, isAdult: false) {
id
title {
romaji
english
native
}
coverImage {
large
}
}
}
}
"""
guard let url = URL(string: "https://graphql.anilist.co") else {
print("Invalid URL")
completion(nil)
return
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
let parameters: [String: Any] = ["query": query]
do {
request.httpBody = try JSONSerialization.data(withJSONObject: parameters, options: [])
} catch {
print("Error encoding JSON: \(error.localizedDescription)")
completion(nil)
return
}
let task = URLSession.custom.dataTask(with: request) { data, response, error in
DispatchQueue.main.async {
if let error = error {
print("Error fetching seasonal anime: \(error.localizedDescription)")
completion(nil)
return
}
guard let data = data else {
print("No data returned")
completion(nil)
return
}
do {
if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any],
let dataObject = json["data"] as? [String: Any],
let page = dataObject["Page"] as? [String: Any],
let media = page["media"] as? [[String: Any]] {
let seasonalAnime: [AniListItem] = media.compactMap { item -> AniListItem? in
guard let id = item["id"] as? Int,
let titleData = item["title"] as? [String: Any],
let romaji = titleData["romaji"] as? String,
let english = titleData["english"] as? String?,
let native = titleData["native"] as? String?,
let coverImageData = item["coverImage"] as? [String: Any],
let largeImageUrl = coverImageData["large"] as? String,
URL(string: largeImageUrl) != nil else {
return nil
}
return AniListItem(
id: id,
title: AniListTitle(romaji: romaji, english: english, native: native),
coverImage: AniListCoverImage(large: largeImageUrl)
)
}
completion(seasonalAnime)
} else {
print("Error parsing JSON or missing expected fields")
completion(nil)
}
} catch {
print("Error decoding JSON: \(error.localizedDescription)")
completion(nil)
}
}
}
task.resume()
}
}

View file

@ -0,0 +1,93 @@
//
// AniList-Trending.swift
// Sora
//
// Created by Francesco on 09/02/25.
//
import Foundation
class AnilistServiceTrendingAnime {
func fetchTrendingAnime(completion: @escaping ([AniListItem]?) -> Void) {
let query = """
query {
Page(page: 1, perPage: 100) {
media(sort: TRENDING_DESC, type: ANIME, isAdult: false) {
id
title {
romaji
english
native
}
coverImage {
large
}
}
}
}
"""
guard let url = URL(string: "https://graphql.anilist.co") else {
print("Invalid URL")
completion(nil)
return
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
let parameters: [String: Any] = ["query": query]
do {
request.httpBody = try JSONSerialization.data(withJSONObject: parameters, options: [])
} catch {
print("Error encoding JSON: \(error.localizedDescription)")
completion(nil)
return
}
let task = URLSession.custom.dataTask(with: request) { data, response, error in
DispatchQueue.main.async {
if let error = error {
print("Error fetching trending anime: \(error.localizedDescription)")
completion(nil)
return
}
guard let data = data else {
print("No data returned")
completion(nil)
return
}
do {
if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any],
let dataObject = json["data"] as? [String: Any],
let page = dataObject["Page"] as? [String: Any],
let media = page["media"] as? [[String: Any]] {
let trendingAnime: [AniListItem] = media.compactMap { item in
guard let id = item["id"] as? Int,
let titleData = item["title"] as? [String: Any],
let romaji = titleData["romaji"] as? String,
let coverImageData = item["coverImage"] as? [String: Any],
let largeImageUrl = coverImageData["large"] as? String else {
return nil
}
return AniListItem(
id: id,
title: AniListTitle(romaji: romaji, english: titleData["english"] as? String, native: titleData["native"] as? String),
coverImage: AniListCoverImage(large: largeImageUrl)
)
}
completion(trendingAnime)
} else {
print("Error parsing JSON or missing expected fields")
completion(nil)
}
} catch {
print("Error decoding JSON: \(error.localizedDescription)")
completion(nil)
}
}
}
task.resume()
}
}

View file

@ -0,0 +1,24 @@
//
// AniListItem.swift
// Sora
//
// Created by Francesco on 09/02/25.
//
import Foundation
struct AniListItem: Codable {
let id: Int
let title: AniListTitle
let coverImage: AniListCoverImage
}
struct AniListTitle: Codable {
let romaji: String
let english: String?
let native: String?
}
struct AniListCoverImage: Codable {
let large: String
}

View file

@ -6,11 +6,202 @@
//
import SwiftUI
import Kingfisher
struct HomeView: View {
var body: some View {
Text("Home View")
.font(.largeTitle)
.padding()
struct Shimmer: ViewModifier {
@State private var phase: CGFloat = -1
func body(content: Content) -> some View {
content
.overlay(
Rectangle()
.fill(
LinearGradient(
gradient: Gradient(stops: [
.init(color: Color.white.opacity(0.0), location: 0.3),
.init(color: Color.white.opacity(0.6), location: 0.5),
.init(color: Color.white.opacity(0.0), location: 0.7)
]),
startPoint: .leading,
endPoint: .trailing
)
)
.rotationEffect(.degrees(20))
.offset(x: self.phase * 300)
.blendMode(.plusLighter)
)
.mask(content)
.onAppear {
withAnimation(Animation.linear(duration: 1.5).repeatForever(autoreverses: false)) {
self.phase = 1
}
}
}
}
extension View {
func shimmering() -> some View {
self.modifier(Shimmer())
}
}
struct SkeletonCell: View {
var body: some View {
VStack {
RoundedRectangle(cornerRadius: 10)
.fill(Color.gray.opacity(0.3))
.frame(width: 130, height: 195)
.cornerRadius(10)
.shimmering()
RoundedRectangle(cornerRadius: 5)
.fill(Color.gray.opacity(0.3))
.frame(width: 130, height: 20)
.padding(.top, 4)
.shimmering()
}
}
}
struct HomeView: View {
@State private var aniListItems: [AniListItem] = []
@State private var trendingItems: [AniListItem] = []
private var currentDeviceSeasonAndYear: (season: String, year: Int) {
let currentDate = Date()
let calendar = Calendar.current
let year = calendar.component(.year, from: currentDate)
let month = calendar.component(.month, from: currentDate)
let season: String
switch month {
case 1...3:
season = "Winter"
case 4...6:
season = "Spring"
case 7...9:
season = "Summer"
default:
season = "Fall"
}
return (season, year)
}
private var trendingDateString: String {
let formatter = DateFormatter()
formatter.dateFormat = "EEEE, dd MMMM yyyy"
return formatter.string(from: Date())
}
var body: some View {
NavigationView {
ScrollView {
VStack(alignment: .leading, spacing: 16) {
HStack(alignment: .bottom, spacing: 5) {
Text("Seasonal")
.font(.headline)
Text("of \(currentDeviceSeasonAndYear.season) \(String(format: "%d", currentDeviceSeasonAndYear.year))")
.font(.subheadline)
.foregroundColor(.gray)
}
.padding(.horizontal, 8)
.padding(.top, 8)
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 8) {
if aniListItems.isEmpty {
ForEach(0..<5, id: \.self) { _ in
SkeletonCell()
}
} else {
ForEach(aniListItems, id: \.id) { item in
VStack {
KFImage(URL(string: item.coverImage.large))
.placeholder {
RoundedRectangle(cornerRadius: 10)
.fill(Color.gray.opacity(0.3))
.frame(width: 130, height: 195)
.shimmering()
}
.setProcessor(RoundCornerImageProcessor(cornerRadius: 10))
.resizable()
.scaledToFill()
.frame(width: 130, height: 195)
.cornerRadius(10)
.clipped()
Text(item.title.romaji)
.font(.caption)
.frame(width: 130)
.lineLimit(1)
.multilineTextAlignment(.center)
}
}
}
}
.padding(.horizontal, 8)
}
HStack(alignment: .bottom, spacing: 5) {
Text("Trending")
.font(.headline)
Text("on \(trendingDateString)")
.font(.subheadline)
.foregroundColor(.gray)
}
.padding(.horizontal, 8)
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 8) {
if trendingItems.isEmpty {
ForEach(0..<5, id: \.self) { _ in
SkeletonCell()
}
} else {
ForEach(trendingItems, id: \.id) { item in
VStack {
KFImage(URL(string: item.coverImage.large))
.placeholder {
RoundedRectangle(cornerRadius: 10)
.fill(Color.gray.opacity(0.3))
.frame(width: 130, height: 195)
.shimmering()
}
.setProcessor(RoundCornerImageProcessor(cornerRadius: 10))
.resizable()
.scaledToFill()
.frame(width: 130, height: 195)
.cornerRadius(10)
.clipped()
Text(item.title.romaji)
.font(.caption)
.frame(width: 130)
.lineLimit(1)
.multilineTextAlignment(.center)
}
}
}
}
.padding(.horizontal, 8)
}
}
.padding(.bottom, 16)
.padding(.leading, 7)
}
.navigationTitle("Home")
}
.onAppear {
AnilistServiceSeasonalAnime().fetchSeasonalAnime { items in
if let items = items {
aniListItems = items
}
}
AnilistServiceTrendingAnime().fetchTrendingAnime { items in
if let items = items {
trendingItems = items
}
}
}
}
}