mirror of
https://github.com/cranci1/Sora.git
synced 2026-03-11 17:45:37 +00:00
damn this looks nice ong
This commit is contained in:
parent
c7a35b11a5
commit
051a710c70
5 changed files with 474 additions and 5 deletions
|
|
@ -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 */,
|
||||
|
|
|
|||
117
Sora/Tracking Services/AniList/HomePage/AniList-Seasonal.swift
Normal file
117
Sora/Tracking Services/AniList/HomePage/AniList-Seasonal.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
24
Sora/Tracking Services/AniList/Struct/AniListItem.swift
Normal file
24
Sora/Tracking Services/AniList/Struct/AniListItem.swift
Normal 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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue