680 lines
26 KiB
Swift
680 lines
26 KiB
Swift
import SwiftUI
|
|
|
|
struct LeagueCenterView: View {
|
|
@Environment(GamesViewModel.self) private var gamesViewModel
|
|
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
|
@State private var viewModel = LeagueCenterViewModel()
|
|
@State private var selectedGame: Game?
|
|
|
|
private var rosterColumns: [GridItem] {
|
|
let columnCount: Int
|
|
#if os(iOS)
|
|
columnCount = horizontalSizeClass == .compact ? 1 : 2
|
|
#else
|
|
columnCount = 3
|
|
#endif
|
|
|
|
return Array(repeating: GridItem(.flexible(), spacing: 14), count: columnCount)
|
|
}
|
|
|
|
private var horizontalPadding: CGFloat {
|
|
#if os(iOS)
|
|
horizontalSizeClass == .compact ? 20 : 32
|
|
#else
|
|
56
|
|
#endif
|
|
}
|
|
|
|
var body: some View {
|
|
ScrollView {
|
|
VStack(alignment: .leading, spacing: 34) {
|
|
header
|
|
|
|
if let overviewErrorMessage = viewModel.overviewErrorMessage {
|
|
messagePanel(overviewErrorMessage, tint: .orange)
|
|
}
|
|
|
|
scheduleSection
|
|
standingsSection
|
|
teamsSection
|
|
|
|
if let selectedTeam = viewModel.selectedTeam {
|
|
teamProfileSection(team: selectedTeam)
|
|
} else if let teamErrorMessage = viewModel.teamErrorMessage {
|
|
messagePanel(teamErrorMessage, tint: .orange)
|
|
}
|
|
|
|
if !viewModel.roster.isEmpty {
|
|
rosterSection
|
|
}
|
|
|
|
if let player = viewModel.selectedPlayer {
|
|
playerSection(player: player)
|
|
} else if let playerErrorMessage = viewModel.playerErrorMessage {
|
|
messagePanel(playerErrorMessage, tint: .orange)
|
|
}
|
|
}
|
|
.padding(.horizontal, horizontalPadding)
|
|
.padding(.vertical, 40)
|
|
}
|
|
.background(screenBackground.ignoresSafeArea())
|
|
.task {
|
|
await viewModel.loadInitial()
|
|
}
|
|
.sheet(item: $selectedGame) { game in
|
|
StreamOptionsSheet(game: game)
|
|
}
|
|
}
|
|
|
|
private var header: some View {
|
|
HStack(alignment: .top, spacing: 24) {
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
Text("Around MLB")
|
|
.font(.system(size: 42, weight: .bold, design: .rounded))
|
|
.foregroundStyle(.white)
|
|
|
|
Text("Schedules, standings, team context, roster access, and player snapshots in one control room.")
|
|
.font(.system(size: 16, weight: .medium))
|
|
.foregroundStyle(.white.opacity(0.58))
|
|
}
|
|
|
|
Spacer()
|
|
|
|
HStack(spacing: 12) {
|
|
infoPill(title: "\(viewModel.scheduleGames.count)", label: "Games", color: .blue)
|
|
infoPill(title: "\(viewModel.teams.count)", label: "Teams", color: .green)
|
|
infoPill(title: "\(viewModel.standings.count)", label: "Divisions", color: .orange)
|
|
}
|
|
}
|
|
}
|
|
|
|
private var scheduleSection: some View {
|
|
VStack(alignment: .leading, spacing: 18) {
|
|
HStack {
|
|
VStack(alignment: .leading, spacing: 6) {
|
|
Text("Schedule")
|
|
.font(.system(size: 30, weight: .bold, design: .rounded))
|
|
.foregroundStyle(.white)
|
|
|
|
Text(viewModel.displayDateString)
|
|
.font(.system(size: 16, weight: .medium))
|
|
.foregroundStyle(.white.opacity(0.55))
|
|
}
|
|
|
|
Spacer()
|
|
|
|
HStack(spacing: 12) {
|
|
dateButton(title: "Previous", systemImage: "chevron.left") {
|
|
Task { await viewModel.goToPreviousDay() }
|
|
}
|
|
|
|
dateButton(title: "Today", systemImage: "calendar") {
|
|
Task { await viewModel.goToToday() }
|
|
}
|
|
|
|
dateButton(title: "Next", systemImage: "chevron.right") {
|
|
Task { await viewModel.goToNextDay() }
|
|
}
|
|
}
|
|
}
|
|
|
|
if viewModel.scheduleGames.isEmpty && viewModel.isLoadingOverview {
|
|
loadingPanel(title: "Loading schedule...")
|
|
} else {
|
|
VStack(spacing: 14) {
|
|
ForEach(viewModel.scheduleGames.prefix(10)) { game in
|
|
scheduleRow(game)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func scheduleRow(_ game: StatsGame) -> some View {
|
|
let linkedGame = gamesViewModel.games.first { $0.gamePk == String(game.gamePk) }
|
|
|
|
return Button {
|
|
if let linkedGame {
|
|
selectedGame = linkedGame
|
|
}
|
|
} label: {
|
|
HStack(spacing: 18) {
|
|
teamMiniColumn(team: game.teams.away)
|
|
VStack(spacing: 6) {
|
|
Text(scoreText(for: game))
|
|
.font(.system(size: 28, weight: .black, design: .rounded))
|
|
.foregroundStyle(.white)
|
|
.monospacedDigit()
|
|
|
|
Text(statusText(for: game))
|
|
.font(.system(size: 14, weight: .semibold))
|
|
.foregroundStyle(statusColor(for: game))
|
|
}
|
|
.frame(width: 160)
|
|
|
|
teamMiniColumn(team: game.teams.home, alignTrailing: true)
|
|
|
|
Spacer()
|
|
|
|
VStack(alignment: .trailing, spacing: 6) {
|
|
if let venue = game.venue?.name {
|
|
Text(venue)
|
|
.font(.system(size: 14, weight: .medium))
|
|
.foregroundStyle(.white.opacity(0.56))
|
|
.lineLimit(1)
|
|
}
|
|
|
|
Text(linkedGame != nil ? "Open game sheet" : "Info only")
|
|
.font(.system(size: 13, weight: .bold, design: .rounded))
|
|
.foregroundStyle(linkedGame != nil ? .blue.opacity(0.95) : .white.opacity(0.34))
|
|
}
|
|
}
|
|
.padding(22)
|
|
.background(sectionPanel)
|
|
}
|
|
.platformCardStyle()
|
|
.disabled(linkedGame == nil)
|
|
}
|
|
|
|
private func teamMiniColumn(team: StatsTeamGameInfo, alignTrailing: Bool = false) -> some View {
|
|
let info = TeamInfo(
|
|
code: team.team.abbreviation ?? "MLB",
|
|
name: team.team.name ?? "MLB",
|
|
score: team.score,
|
|
teamId: team.team.id,
|
|
record: team.leagueRecord.map { "\($0.wins)-\($0.losses)" }
|
|
)
|
|
|
|
return HStack(spacing: 12) {
|
|
if !alignTrailing {
|
|
TeamLogoView(team: info, size: 56)
|
|
}
|
|
|
|
VStack(alignment: alignTrailing ? .trailing : .leading, spacing: 6) {
|
|
Text(info.code)
|
|
.font(.system(size: 22, weight: .black, design: .rounded))
|
|
.foregroundStyle(.white)
|
|
|
|
Text(info.displayName)
|
|
.font(.system(size: 16, weight: .semibold))
|
|
.foregroundStyle(.white.opacity(0.7))
|
|
.lineLimit(1)
|
|
|
|
if let record = info.record {
|
|
Text(record)
|
|
.font(.system(size: 13, weight: .bold, design: .monospaced))
|
|
.foregroundStyle(.white.opacity(0.46))
|
|
}
|
|
}
|
|
|
|
if alignTrailing {
|
|
TeamLogoView(team: info, size: 56)
|
|
}
|
|
}
|
|
.frame(maxWidth: .infinity, alignment: alignTrailing ? .trailing : .leading)
|
|
}
|
|
|
|
private var standingsSection: some View {
|
|
VStack(alignment: .leading, spacing: 18) {
|
|
Text("Standings")
|
|
.font(.system(size: 30, weight: .bold, design: .rounded))
|
|
.foregroundStyle(.white)
|
|
|
|
if viewModel.standings.isEmpty && viewModel.isLoadingOverview {
|
|
loadingPanel(title: "Loading standings...")
|
|
} else {
|
|
ScrollView(.horizontal) {
|
|
HStack(spacing: 18) {
|
|
ForEach(viewModel.standings, id: \.division?.id) { record in
|
|
standingsCard(record)
|
|
.frame(width: 360)
|
|
}
|
|
}
|
|
.padding(.vertical, 4)
|
|
}
|
|
.platformFocusSection()
|
|
.scrollClipDisabled()
|
|
}
|
|
}
|
|
}
|
|
|
|
private func standingsCard(_ record: StandingsDivisionRecord) -> some View {
|
|
VStack(alignment: .leading, spacing: 14) {
|
|
Text(record.division?.name ?? "Division")
|
|
.font(.system(size: 22, weight: .bold, design: .rounded))
|
|
.foregroundStyle(.white)
|
|
|
|
VStack(spacing: 10) {
|
|
ForEach(record.teamRecords.sorted(by: standingsSort), id: \.team.id) { team in
|
|
HStack(spacing: 10) {
|
|
Text(team.divisionRank ?? "-")
|
|
.font(.system(size: 12, weight: .black, design: .rounded))
|
|
.foregroundStyle(.white.opacity(0.52))
|
|
.frame(width: 22)
|
|
|
|
Text(team.team.abbreviation ?? team.team.name ?? "MLB")
|
|
.font(.system(size: 16, weight: .bold, design: .rounded))
|
|
.foregroundStyle(.white)
|
|
|
|
Spacer()
|
|
|
|
if let wins = team.wins, let losses = team.losses {
|
|
Text("\(wins)-\(losses)")
|
|
.font(.system(size: 15, weight: .bold, design: .monospaced))
|
|
.foregroundStyle(.white.opacity(0.86))
|
|
}
|
|
|
|
Text(team.gamesBack ?? "-")
|
|
.font(.system(size: 14, weight: .semibold))
|
|
.foregroundStyle(.white.opacity(0.45))
|
|
.frame(width: 44, alignment: .trailing)
|
|
}
|
|
.padding(.vertical, 4)
|
|
}
|
|
}
|
|
}
|
|
.padding(22)
|
|
.background(sectionPanel)
|
|
}
|
|
|
|
private var teamsSection: some View {
|
|
VStack(alignment: .leading, spacing: 18) {
|
|
Text("Teams")
|
|
.font(.system(size: 30, weight: .bold, design: .rounded))
|
|
.foregroundStyle(.white)
|
|
|
|
ScrollView(.horizontal) {
|
|
HStack(spacing: 16) {
|
|
ForEach(viewModel.teams) { team in
|
|
Button {
|
|
Task { await viewModel.selectTeam(team.id) }
|
|
} label: {
|
|
VStack(alignment: .leading, spacing: 14) {
|
|
TeamLogoView(team: team.teamInfo, size: 72)
|
|
|
|
VStack(alignment: .leading, spacing: 6) {
|
|
Text(team.abbreviation)
|
|
.font(.system(size: 20, weight: .black, design: .rounded))
|
|
.foregroundStyle(.white)
|
|
|
|
Text(team.name)
|
|
.font(.system(size: 16, weight: .semibold))
|
|
.foregroundStyle(.white.opacity(0.76))
|
|
.lineLimit(2)
|
|
}
|
|
|
|
Spacer()
|
|
|
|
Text(team.recordText ?? "Season")
|
|
.font(.system(size: 13, weight: .bold, design: .monospaced))
|
|
.foregroundStyle(.white.opacity(0.5))
|
|
}
|
|
.frame(width: 210, height: 220, alignment: .leading)
|
|
.padding(18)
|
|
.background(
|
|
RoundedRectangle(cornerRadius: 24, style: .continuous)
|
|
.fill(TeamAssets.color(for: team.abbreviation).opacity(0.16))
|
|
)
|
|
.overlay(
|
|
RoundedRectangle(cornerRadius: 24, style: .continuous)
|
|
.stroke(.white.opacity(0.08), lineWidth: 1)
|
|
)
|
|
}
|
|
.platformCardStyle()
|
|
}
|
|
}
|
|
.padding(.vertical, 4)
|
|
}
|
|
.platformFocusSection()
|
|
.scrollClipDisabled()
|
|
}
|
|
}
|
|
|
|
private func teamProfileSection(team: TeamProfile) -> some View {
|
|
VStack(alignment: .leading, spacing: 18) {
|
|
Text("Team Profile")
|
|
.font(.system(size: 30, weight: .bold, design: .rounded))
|
|
.foregroundStyle(.white)
|
|
|
|
if viewModel.isLoadingTeam {
|
|
loadingPanel(title: "Loading team profile...")
|
|
} else {
|
|
HStack(alignment: .top, spacing: 24) {
|
|
TeamLogoView(team: team.teamInfo, size: 96)
|
|
|
|
VStack(alignment: .leading, spacing: 12) {
|
|
Text(team.name)
|
|
.font(.system(size: 34, weight: .bold, design: .rounded))
|
|
.foregroundStyle(.white)
|
|
|
|
HStack(spacing: 10) {
|
|
detailChip(team.recordText ?? "Season", color: .blue)
|
|
if let gamesBack = team.gamesBackText {
|
|
detailChip(gamesBack, color: .orange)
|
|
}
|
|
if let division = team.divisionName {
|
|
detailChip(division, color: .green)
|
|
}
|
|
}
|
|
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
profileLine(label: "Venue", value: team.venueName)
|
|
profileLine(label: "League", value: team.leagueName)
|
|
profileLine(label: "Franchise", value: team.franchiseName)
|
|
profileLine(label: "First Year", value: team.firstYearOfPlay)
|
|
}
|
|
}
|
|
|
|
Spacer()
|
|
}
|
|
.padding(24)
|
|
.background(sectionPanel)
|
|
.contentShape(RoundedRectangle(cornerRadius: 24, style: .continuous))
|
|
.platformFocusable()
|
|
}
|
|
}
|
|
}
|
|
|
|
private var rosterSection: some View {
|
|
VStack(alignment: .leading, spacing: 18) {
|
|
Text("Roster")
|
|
.font(.system(size: 30, weight: .bold, design: .rounded))
|
|
.foregroundStyle(.white)
|
|
|
|
LazyVGrid(columns: rosterColumns, spacing: 14) {
|
|
ForEach(viewModel.roster) { player in
|
|
Button {
|
|
Task { await viewModel.selectPlayer(player.id) }
|
|
} label: {
|
|
HStack(spacing: 14) {
|
|
playerHeadshot(url: player.headshotURL, size: 58)
|
|
|
|
VStack(alignment: .leading, spacing: 6) {
|
|
Text(player.fullName)
|
|
.font(.system(size: 16, weight: .bold))
|
|
.foregroundStyle(.white)
|
|
.lineLimit(2)
|
|
|
|
Text("\(player.positionAbbreviation ?? "P") · #\(player.jerseyNumber ?? "--")")
|
|
.font(.system(size: 13, weight: .semibold))
|
|
.foregroundStyle(.white.opacity(0.5))
|
|
}
|
|
|
|
Spacer()
|
|
}
|
|
.padding(16)
|
|
.background(sectionPanel)
|
|
}
|
|
.platformCardStyle()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func playerSection(player: PlayerProfile) -> some View {
|
|
VStack(alignment: .leading, spacing: 18) {
|
|
Text("Player Profile")
|
|
.font(.system(size: 30, weight: .bold, design: .rounded))
|
|
.foregroundStyle(.white)
|
|
|
|
if viewModel.isLoadingPlayer {
|
|
loadingPanel(title: "Loading player profile...")
|
|
} else {
|
|
VStack(alignment: .leading, spacing: 20) {
|
|
HStack(alignment: .top, spacing: 24) {
|
|
playerHeadshot(url: player.headshotURL, size: 112)
|
|
|
|
VStack(alignment: .leading, spacing: 12) {
|
|
HStack(spacing: 10) {
|
|
if let primaryNumber = player.primaryNumber {
|
|
Text("#\(primaryNumber)")
|
|
.font(.system(size: 15, weight: .black, design: .rounded))
|
|
.foregroundStyle(.white.opacity(0.7))
|
|
}
|
|
|
|
if let position = player.primaryPosition {
|
|
detailChip(position, color: .blue)
|
|
}
|
|
}
|
|
|
|
Text(player.fullName)
|
|
.font(.system(size: 34, weight: .bold, design: .rounded))
|
|
.foregroundStyle(.white)
|
|
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
profileLine(label: "Age", value: player.currentAge.map(String.init))
|
|
profileLine(label: "Bats / Throws", value: [player.bats, player.throwsHand].compactMap { $0 }.joined(separator: " / "))
|
|
profileLine(label: "Height / Weight", value: [player.height, player.weight.map { "\($0) lbs" }].compactMap { $0 }.joined(separator: " / "))
|
|
profileLine(label: "Born", value: player.birthPlace)
|
|
profileLine(label: "Debut", value: player.debutDate)
|
|
}
|
|
}
|
|
|
|
Spacer()
|
|
}
|
|
|
|
if !player.statGroups.isEmpty {
|
|
HStack(alignment: .top, spacing: 16) {
|
|
ForEach(player.statGroups) { group in
|
|
VStack(alignment: .leading, spacing: 14) {
|
|
Text("\(group.title) \(player.seasonLabel)")
|
|
.font(.system(size: 18, weight: .bold, design: .rounded))
|
|
.foregroundStyle(.white)
|
|
|
|
VStack(alignment: .leading, spacing: 10) {
|
|
ForEach(group.items, id: \.label) { item in
|
|
HStack {
|
|
Text(item.label)
|
|
.font(.system(size: 13, weight: .bold, design: .rounded))
|
|
.foregroundStyle(.white.opacity(0.5))
|
|
|
|
Spacer()
|
|
|
|
Text(item.value)
|
|
.font(.system(size: 18, weight: .black, design: .rounded))
|
|
.foregroundStyle(.white)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
.padding(18)
|
|
.background(
|
|
RoundedRectangle(cornerRadius: 20, style: .continuous)
|
|
.fill(.white.opacity(0.05))
|
|
)
|
|
}
|
|
}
|
|
} else {
|
|
Text("No regular-season MLB stats available for \(player.seasonLabel).")
|
|
.font(.system(size: 18, weight: .semibold, design: .rounded))
|
|
.foregroundStyle(.white.opacity(0.7))
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
.padding(.top, 4)
|
|
}
|
|
}
|
|
.padding(24)
|
|
.background(sectionPanel)
|
|
.contentShape(RoundedRectangle(cornerRadius: 24, style: .continuous))
|
|
.platformFocusable()
|
|
}
|
|
}
|
|
}
|
|
|
|
private func playerHeadshot(url: URL, size: CGFloat) -> some View {
|
|
AsyncImage(url: url) { phase in
|
|
switch phase {
|
|
case .success(let image):
|
|
image
|
|
.resizable()
|
|
.scaledToFill()
|
|
default:
|
|
ZStack {
|
|
Circle()
|
|
.fill(.white.opacity(0.08))
|
|
Image(systemName: "person.fill")
|
|
.font(.system(size: size * 0.34, weight: .bold))
|
|
.foregroundStyle(.white.opacity(0.32))
|
|
}
|
|
}
|
|
}
|
|
.frame(width: size, height: size)
|
|
.clipShape(Circle())
|
|
.overlay(
|
|
Circle()
|
|
.stroke(.white.opacity(0.08), lineWidth: 1)
|
|
)
|
|
}
|
|
|
|
private func profileLine(label: String, value: String?) -> some View {
|
|
let displayValue: String
|
|
if let value, !value.isEmpty {
|
|
displayValue = value
|
|
} else {
|
|
displayValue = "Unavailable"
|
|
}
|
|
|
|
return HStack(spacing: 10) {
|
|
Text(label)
|
|
.font(.system(size: 13, weight: .bold, design: .rounded))
|
|
.foregroundStyle(.white.opacity(0.45))
|
|
.frame(width: 92, alignment: .leading)
|
|
|
|
Text(displayValue)
|
|
.font(.system(size: 15, weight: .semibold))
|
|
.foregroundStyle(.white.opacity(0.82))
|
|
}
|
|
}
|
|
|
|
private func messagePanel(_ text: String, tint: Color) -> some View {
|
|
Text(text)
|
|
.font(.system(size: 15, weight: .semibold))
|
|
.foregroundStyle(tint.opacity(0.95))
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
.padding(18)
|
|
.background(sectionPanel)
|
|
}
|
|
|
|
private func detailChip(_ title: String, color: Color) -> some View {
|
|
Text(title)
|
|
.font(.system(size: 13, weight: .bold, design: .rounded))
|
|
.foregroundStyle(color.opacity(0.95))
|
|
.padding(.horizontal, 12)
|
|
.padding(.vertical, 8)
|
|
.background(color.opacity(0.12))
|
|
.clipShape(Capsule())
|
|
}
|
|
|
|
private func infoPill(title: String, label: String, color: Color) -> some View {
|
|
VStack(spacing: 4) {
|
|
Text(title)
|
|
.font(.system(size: 22, weight: .black, design: .rounded))
|
|
.foregroundStyle(color)
|
|
.monospacedDigit()
|
|
Text(label)
|
|
.font(.system(size: 12, weight: .semibold))
|
|
.foregroundStyle(.white.opacity(0.5))
|
|
}
|
|
.padding(.horizontal, 18)
|
|
.padding(.vertical, 12)
|
|
.background(sectionPanel)
|
|
}
|
|
|
|
private func dateButton(title: String, systemImage: String, action: @escaping () -> Void) -> some View {
|
|
Button(action: action) {
|
|
HStack(spacing: 8) {
|
|
Image(systemName: systemImage)
|
|
.font(.system(size: 12, weight: .bold))
|
|
Text(title)
|
|
.font(.system(size: 14, weight: .semibold))
|
|
}
|
|
.foregroundStyle(.white.opacity(0.84))
|
|
.padding(.horizontal, 14)
|
|
.padding(.vertical, 10)
|
|
.background(.white.opacity(0.08))
|
|
.clipShape(Capsule())
|
|
}
|
|
.platformCardStyle()
|
|
}
|
|
|
|
private func loadingPanel(title: String) -> some View {
|
|
HStack {
|
|
Spacer()
|
|
ProgressView(title)
|
|
.font(.system(size: 16, weight: .semibold))
|
|
.foregroundStyle(.white.opacity(0.75))
|
|
Spacer()
|
|
}
|
|
.padding(.vertical, 34)
|
|
.background(sectionPanel)
|
|
}
|
|
|
|
private func scoreText(for game: StatsGame) -> String {
|
|
if game.isScheduled {
|
|
return game.startTime ?? "TBD"
|
|
}
|
|
let away = game.teams.away.score ?? 0
|
|
let home = game.teams.home.score ?? 0
|
|
return "\(away) - \(home)"
|
|
}
|
|
|
|
private func statusText(for game: StatsGame) -> String {
|
|
if game.isLive {
|
|
return game.linescore?.currentInningDisplay ?? "Live"
|
|
}
|
|
if game.isFinal {
|
|
return "Final"
|
|
}
|
|
return game.status.detailedState ?? "Scheduled"
|
|
}
|
|
|
|
private func statusColor(for game: StatsGame) -> Color {
|
|
if game.isLive {
|
|
return .red.opacity(0.9)
|
|
}
|
|
if game.isFinal {
|
|
return .white.opacity(0.72)
|
|
}
|
|
return .blue.opacity(0.9)
|
|
}
|
|
|
|
private func standingsSort(lhs: StandingsTeamRecord, rhs: StandingsTeamRecord) -> Bool {
|
|
Int(lhs.divisionRank ?? "99") ?? 99 < Int(rhs.divisionRank ?? "99") ?? 99
|
|
}
|
|
|
|
private var sectionPanel: some View {
|
|
RoundedRectangle(cornerRadius: 24, style: .continuous)
|
|
.fill(.black.opacity(0.22))
|
|
.overlay {
|
|
RoundedRectangle(cornerRadius: 24, style: .continuous)
|
|
.strokeBorder(.white.opacity(0.08), lineWidth: 1)
|
|
}
|
|
}
|
|
|
|
private var screenBackground: some View {
|
|
ZStack {
|
|
LinearGradient(
|
|
colors: [
|
|
Color(red: 0.04, green: 0.05, blue: 0.09),
|
|
Color(red: 0.03, green: 0.06, blue: 0.1),
|
|
Color(red: 0.02, green: 0.03, blue: 0.06),
|
|
],
|
|
startPoint: .topLeading,
|
|
endPoint: .bottomTrailing
|
|
)
|
|
|
|
Circle()
|
|
.fill(.blue.opacity(0.18))
|
|
.frame(width: 520, height: 520)
|
|
.blur(radius: 110)
|
|
.offset(x: -360, y: -260)
|
|
|
|
Circle()
|
|
.fill(.orange.opacity(0.16))
|
|
.frame(width: 560, height: 560)
|
|
.blur(radius: 120)
|
|
.offset(x: 420, y: -80)
|
|
}
|
|
}
|
|
}
|