mirror of
https://github.com/cranci1/Sora.git
synced 2026-03-28 13:28:45 +00:00
This commit is contained in:
parent
842df29bfe
commit
ae75fd80ce
4 changed files with 454 additions and 40 deletions
|
|
@ -30,6 +30,8 @@
|
|||
133D7C972D2BE2AF0075467E /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 133D7C962D2BE2AF0075467E /* Kingfisher */; };
|
||||
133F55BB2D33B55100E08EEA /* LibraryManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 133F55BA2D33B55100E08EEA /* LibraryManager.swift */; };
|
||||
135CCBE22D4D1138008B9C0E /* SettingsViewPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 135CCBE12D4D1138008B9C0E /* SettingsViewPlayer.swift */; };
|
||||
136F21B92D5B8DD8006409AC /* AniList-MediaInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 136F21B82D5B8DD8006409AC /* AniList-MediaInfo.swift */; };
|
||||
136F21BC2D5B8F29006409AC /* AniList-DetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 136F21BB2D5B8F29006409AC /* AniList-DetailsView.swift */; };
|
||||
138AA1B82D2D66FD0021F9DF /* EpisodeCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 138AA1B62D2D66FD0021F9DF /* EpisodeCell.swift */; };
|
||||
138AA1B92D2D66FD0021F9DF /* CircularProgressBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 138AA1B72D2D66FD0021F9DF /* CircularProgressBar.swift */; };
|
||||
139935662D468C450065CEFF /* ModuleManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 139935652D468C450065CEFF /* ModuleManager.swift */; };
|
||||
|
|
@ -73,6 +75,8 @@
|
|||
133D7C8B2D2BE2640075467E /* JSController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JSController.swift; sourceTree = "<group>"; };
|
||||
133F55BA2D33B55100E08EEA /* LibraryManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryManager.swift; sourceTree = "<group>"; };
|
||||
135CCBE12D4D1138008B9C0E /* SettingsViewPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewPlayer.swift; sourceTree = "<group>"; };
|
||||
136F21B82D5B8DD8006409AC /* AniList-MediaInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AniList-MediaInfo.swift"; sourceTree = "<group>"; };
|
||||
136F21BB2D5B8F29006409AC /* AniList-DetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AniList-DetailsView.swift"; sourceTree = "<group>"; };
|
||||
138AA1B62D2D66FD0021F9DF /* EpisodeCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EpisodeCell.swift; sourceTree = "<group>"; };
|
||||
138AA1B72D2D66FD0021F9DF /* CircularProgressBar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CircularProgressBar.swift; sourceTree = "<group>"; };
|
||||
139935652D468C450065CEFF /* ModuleManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModuleManager.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -115,6 +119,7 @@
|
|||
13103E812D589D77000F0673 /* AniList */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
136F21B72D5B8DAC006409AC /* MediaInfo */,
|
||||
13103E872D58A392000F0673 /* Struct */,
|
||||
13103E822D589D7D000F0673 /* HomePage */,
|
||||
);
|
||||
|
|
@ -124,6 +129,7 @@
|
|||
13103E822D589D7D000F0673 /* HomePage */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
136F21BA2D5B8F17006409AC /* DetailsView */,
|
||||
13103E832D589D8B000F0673 /* AniList-Seasonal.swift */,
|
||||
13103E852D58A328000F0673 /* AniList-Trending.swift */,
|
||||
);
|
||||
|
|
@ -272,6 +278,22 @@
|
|||
path = LibraryView;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
136F21B72D5B8DAC006409AC /* MediaInfo */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
136F21B82D5B8DD8006409AC /* AniList-MediaInfo.swift */,
|
||||
);
|
||||
path = MediaInfo;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
136F21BA2D5B8F17006409AC /* DetailsView */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
136F21BB2D5B8F29006409AC /* AniList-DetailsView.swift */,
|
||||
);
|
||||
path = DetailsView;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
138AA1B52D2D66EC0021F9DF /* EpisodeCell */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
|
@ -430,6 +452,7 @@
|
|||
1E9FF1D32D403E49008AC100 /* SettingsViewLoggerFilter.swift in Sources */,
|
||||
13EA2BD92D32D98400C1EBD7 /* NormalPlayer.swift in Sources */,
|
||||
133D7C932D2BE2640075467E /* Modules.swift in Sources */,
|
||||
136F21B92D5B8DD8006409AC /* AniList-MediaInfo.swift in Sources */,
|
||||
133D7C702D2BE2500075467E /* ContentView.swift in Sources */,
|
||||
13D99CF72D4E73C300250A86 /* ModuleAdditionSettingsView.swift in Sources */,
|
||||
13103E862D58A328000F0673 /* AniList-Trending.swift in Sources */,
|
||||
|
|
@ -447,6 +470,7 @@
|
|||
133D7C942D2BE2640075467E /* JSController.swift in Sources */,
|
||||
133D7C922D2BE2640075467E /* URLSession.swift in Sources */,
|
||||
133D7C912D2BE2640075467E /* SettingsViewModule.swift in Sources */,
|
||||
136F21BC2D5B8F29006409AC /* AniList-DetailsView.swift in Sources */,
|
||||
133F55BB2D33B55100E08EEA /* LibraryManager.swift in Sources */,
|
||||
13103E842D589D8B000F0673 /* AniList-Seasonal.swift in Sources */,
|
||||
133D7C8E2D2BE2640075467E /* LibraryView.swift in Sources */,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,249 @@
|
|||
//
|
||||
// AniList-DetailsView.swift
|
||||
// Sora
|
||||
//
|
||||
// Created by Francesco on 11/02/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Kingfisher
|
||||
|
||||
struct MediaDetailItem: View {
|
||||
var title: String
|
||||
var value: String
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
Text(value)
|
||||
.font(.headline)
|
||||
Text(title)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.padding(.horizontal)
|
||||
}
|
||||
}
|
||||
|
||||
struct AniListDetailsView: View {
|
||||
let animeID: Int
|
||||
@State private var mediaInfo: [String: Any]?
|
||||
@State private var isLoading: Bool = true
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
VStack(spacing: 16) {
|
||||
if isLoading {
|
||||
ProgressView()
|
||||
.padding()
|
||||
} else if let media = mediaInfo {
|
||||
if let coverDict = media["coverImage"] as? [String: Any],
|
||||
let posterURLString = coverDict["extraLarge"] as? String,
|
||||
let posterURL = URL(string: posterURLString) {
|
||||
KFImage(posterURL)
|
||||
.placeholder {
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.fill(Color.gray.opacity(0.3))
|
||||
.frame(width: 200, height: 300)
|
||||
.shimmering()
|
||||
}
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 200, height: 300)
|
||||
.cornerRadius(10)
|
||||
.shadow(radius: 5)
|
||||
}
|
||||
|
||||
if let titleDict = media["title"] as? [String: Any],
|
||||
let userPreferred = titleDict["userPreferred"] as? String {
|
||||
Text(userPreferred)
|
||||
.font(.title2)
|
||||
.fontWeight(.bold)
|
||||
.padding(.top, 8)
|
||||
}
|
||||
|
||||
if let trailer = media["trailer"] as? [String: Any],
|
||||
let trailerID = trailer["id"] as? String,
|
||||
let site = trailer["site"] as? String {
|
||||
if site.lowercased() == "youtube",
|
||||
let url = URL(string: "https://www.youtube.com/watch?v=\(trailerID)") {
|
||||
Link("Watch Trailer on YouTube", destination: url)
|
||||
.padding(.top, 4)
|
||||
} else {
|
||||
Text("Trailer available on \(site)")
|
||||
.padding(.top, 4)
|
||||
}
|
||||
}
|
||||
|
||||
if let synopsis = media["description"] as? String {
|
||||
Text(synopsis)
|
||||
.padding(.horizontal)
|
||||
.foregroundColor(.secondary)
|
||||
.font(.system(size: 14))
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
if let format = media["format"] as? String {
|
||||
Text("Format: \(format)")
|
||||
}
|
||||
if let status = media["status"] as? String {
|
||||
Text("Status: \(status)")
|
||||
}
|
||||
if let season = media["season"] as? String {
|
||||
Text("Season: \(season)")
|
||||
}
|
||||
if let startDate = media["startDate"] as? [String: Any],
|
||||
let year = startDate["year"] as? Int,
|
||||
let month = startDate["month"] as? Int,
|
||||
let day = startDate["day"] as? Int {
|
||||
Text("Start Date: \(year)-\(month)-\(day)")
|
||||
}
|
||||
if let endDate = media["endDate"] as? [String: Any],
|
||||
let year = endDate["year"] as? Int,
|
||||
let month = endDate["month"] as? Int,
|
||||
let day = endDate["day"] as? Int {
|
||||
Text("End Date: \(year)-\(month)-\(day)")
|
||||
}
|
||||
if let country = media["countryOfOrigin"] as? String {
|
||||
Text("Country: \(country)")
|
||||
}
|
||||
if let source = media["source"] as? String {
|
||||
Text("Source: \(source)")
|
||||
}
|
||||
}
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.padding(.horizontal)
|
||||
.padding(.top, 4)
|
||||
|
||||
HStack(spacing: 24) {
|
||||
if let type = media["type"] as? String {
|
||||
MediaDetailItem(title: "Type", value: type)
|
||||
}
|
||||
if let episodes = media["episodes"] as? Int {
|
||||
MediaDetailItem(title: "Episodes", value: "\(episodes)")
|
||||
}
|
||||
if let duration = media["duration"] as? Int {
|
||||
MediaDetailItem(title: "Length", value: "\(duration) mins")
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical)
|
||||
|
||||
if let charactersDict = media["characters"] as? [String: Any],
|
||||
let edges = charactersDict["edges"] as? [[String: Any]] {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("Characters")
|
||||
.font(.headline)
|
||||
.padding(.horizontal)
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(spacing: 16) {
|
||||
ForEach(Array(edges.enumerated()), id: \.offset) { _, edge in
|
||||
if let node = edge["node"] as? [String: Any],
|
||||
let nameDict = node["name"] as? [String: Any],
|
||||
let fullName = nameDict["full"] as? String,
|
||||
let imageDict = node["image"] as? [String: Any],
|
||||
let imageUrlStr = imageDict["large"] as? String,
|
||||
let imageUrl = URL(string: imageUrlStr) {
|
||||
VStack {
|
||||
KFImage(imageUrl)
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.frame(width: 120, height: 120)
|
||||
.clipShape(Circle())
|
||||
Text(fullName)
|
||||
.font(.caption)
|
||||
}
|
||||
.frame(width: 140, height: 140)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let stats = media["stats"] as? [String: Any],
|
||||
let scoreDistribution = stats["scoreDistribution"] as? [[String: Any]] {
|
||||
VStack(alignment: .center) {
|
||||
Text("Score Distribution")
|
||||
.font(.headline)
|
||||
HStack(alignment: .bottom, spacing: 8) {
|
||||
let maxValue = scoreDistribution.compactMap { $0["amount"] as? Int }.max() ?? 1
|
||||
ForEach(Array(scoreDistribution.enumerated()), id: \.offset) { _, dataPoint in
|
||||
if let score = dataPoint["score"] as? Int,
|
||||
let amount = dataPoint["amount"] as? Int {
|
||||
VStack {
|
||||
Rectangle()
|
||||
.fill(Color.accentColor)
|
||||
.frame(width: 20, height: CGFloat(amount) / CGFloat(maxValue) * 100)
|
||||
Text("\(score)")
|
||||
.font(.caption)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.horizontal)
|
||||
}
|
||||
|
||||
if let relations = media["relations"] as? [String: Any],
|
||||
let nodes = relations["nodes"] as? [[String: Any]] {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("Correlation")
|
||||
.font(.headline)
|
||||
.padding(.horizontal)
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(spacing: 10) {
|
||||
ForEach(Array(nodes.enumerated()), id: \.offset) { _, node in
|
||||
if let titleDict = node["title"] as? [String: Any],
|
||||
let title = titleDict["userPreferred"] as? String,
|
||||
let coverImageDict = node["coverImage"] as? [String: Any],
|
||||
let imageUrlStr = coverImageDict["extraLarge"] as? String,
|
||||
let imageUrl = URL(string: imageUrlStr) {
|
||||
VStack {
|
||||
KFImage(imageUrl)
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.frame(width: 100, height: 150)
|
||||
.cornerRadius(10)
|
||||
Text(title)
|
||||
.font(.caption)
|
||||
}
|
||||
.frame(width: 130, height: 195)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
Text("Failed to load media details.")
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationBarTitle("")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.navigationViewStyle(StackNavigationViewStyle())
|
||||
.onAppear {
|
||||
fetchDetails()
|
||||
}
|
||||
}
|
||||
|
||||
private func fetchDetails() {
|
||||
AnilistServiceMediaInfo.fetchAnimeDetails(animeID: animeID) { result in
|
||||
DispatchQueue.main.async {
|
||||
switch result {
|
||||
case .success(let media):
|
||||
self.mediaInfo = media
|
||||
case .failure(let error):
|
||||
print("Error: \(error)")
|
||||
}
|
||||
self.isLoading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
135
Sora/Tracking Services/AniList/MediaInfo/AniList-MediaInfo.swift
Normal file
135
Sora/Tracking Services/AniList/MediaInfo/AniList-MediaInfo.swift
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
//
|
||||
// AniList-MediaInfo.swift
|
||||
// Sora
|
||||
//
|
||||
// Created by Francesco on 11/02/25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class AnilistServiceMediaInfo {
|
||||
static func fetchAnimeDetails(animeID: Int, completion: @escaping (Result<[String: Any], Error>) -> Void) {
|
||||
let query = """
|
||||
query {
|
||||
Media(id: \(animeID), type: ANIME) {
|
||||
id
|
||||
idMal
|
||||
title {
|
||||
romaji
|
||||
english
|
||||
native
|
||||
userPreferred
|
||||
}
|
||||
type
|
||||
format
|
||||
status
|
||||
description
|
||||
startDate {
|
||||
year
|
||||
month
|
||||
day
|
||||
}
|
||||
endDate {
|
||||
year
|
||||
month
|
||||
day
|
||||
}
|
||||
season
|
||||
episodes
|
||||
duration
|
||||
countryOfOrigin
|
||||
isLicensed
|
||||
source
|
||||
hashtag
|
||||
trailer {
|
||||
id
|
||||
site
|
||||
}
|
||||
updatedAt
|
||||
coverImage {
|
||||
extraLarge
|
||||
}
|
||||
bannerImage
|
||||
genres
|
||||
popularity
|
||||
tags {
|
||||
id
|
||||
name
|
||||
}
|
||||
relations {
|
||||
nodes {
|
||||
id
|
||||
coverImage { extraLarge }
|
||||
title { userPreferred },
|
||||
mediaListEntry { status }
|
||||
}
|
||||
}
|
||||
characters {
|
||||
edges {
|
||||
node {
|
||||
name {
|
||||
full
|
||||
}
|
||||
image {
|
||||
large
|
||||
}
|
||||
}
|
||||
role
|
||||
voiceActors {
|
||||
name {
|
||||
first
|
||||
last
|
||||
native
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
siteUrl
|
||||
stats {
|
||||
scoreDistribution {
|
||||
score
|
||||
amount
|
||||
}
|
||||
}
|
||||
airingSchedule(notYetAired: true) {
|
||||
nodes {
|
||||
airingAt
|
||||
episode
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
let apiUrl = URL(string: "https://graphql.anilist.co")!
|
||||
|
||||
var request = URLRequest(url: apiUrl)
|
||||
request.httpMethod = "POST"
|
||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
request.httpBody = try? JSONSerialization.data(withJSONObject: ["query": query], options: [])
|
||||
|
||||
URLSession.custom.dataTask(with: request) { data, response, error in
|
||||
if let error = error {
|
||||
completion(.failure(error))
|
||||
return
|
||||
}
|
||||
|
||||
guard let data = data else {
|
||||
completion(.failure(NSError(domain: "AnimeService", code: 0, userInfo: [NSLocalizedDescriptionKey: "No data received"])))
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any],
|
||||
let data = json["data"] as? [String: Any],
|
||||
let media = data["Media"] as? [String: Any] {
|
||||
completion(.success(media))
|
||||
} else {
|
||||
completion(.failure(NSError(domain: "AnimeService", code: 0, userInfo: [NSLocalizedDescriptionKey: "Invalid response format"])))
|
||||
}
|
||||
} catch {
|
||||
completion(.failure(error))
|
||||
}
|
||||
}.resume()
|
||||
}
|
||||
}
|
||||
|
|
@ -60,26 +60,29 @@ struct HomeView: View {
|
|||
}
|
||||
} 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)
|
||||
NavigationLink(destination: AniListDetailsView(animeID: item.id)) {
|
||||
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)
|
||||
.foregroundColor(.primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -104,26 +107,29 @@ struct HomeView: View {
|
|||
}
|
||||
} 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)
|
||||
NavigationLink(destination: AniListDetailsView(animeID: item.id)) {
|
||||
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)
|
||||
.foregroundColor(.primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue