iOS widget enhancements
This commit is contained in:
@@ -1,28 +1,29 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
/// Marianum-M peeking out of the bottom-right corner. Sized to the longer
|
/// Marianum-M peeking out of the bottom-right corner. Sized to the longer
|
||||||
/// widget edge so it scales with resize; offset nudges a sliver behind the
|
/// widget edge so it scales with the picked WidgetFamily; offset nudges a
|
||||||
/// edge.
|
/// sliver behind the edge. Opacity comes from the widget palette so the
|
||||||
|
/// watermark matches Android's `watermarkAlpha` at the same brightness.
|
||||||
struct MarianumWatermark: View {
|
struct MarianumWatermark: View {
|
||||||
@Environment(\.colorScheme) private var colorScheme
|
@Environment(\.widgetPalette) private var palette
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
GeometryReader { geo in
|
GeometryReader { geo in
|
||||||
let markSize = min(400, max(160, max(geo.size.width, geo.size.height) * 0.8))
|
let markSize = min(400, max(160, max(geo.size.width, geo.size.height) * 0.8))
|
||||||
let offsetX = markSize * 0.18
|
let offsetX = markSize * 0.18
|
||||||
let offsetY = markSize * 0.18
|
let offsetY = markSize * 0.18
|
||||||
ZStack(alignment: .bottomTrailing) {
|
ZStack(alignment: .bottomTrailing) {
|
||||||
Color.clear
|
Color.clear
|
||||||
Image("marianum_m")
|
Image("marianum_m")
|
||||||
.resizable()
|
.resizable()
|
||||||
.renderingMode(.template)
|
.renderingMode(.template)
|
||||||
.aspectRatio(contentMode: .fit)
|
.aspectRatio(contentMode: .fit)
|
||||||
.foregroundStyle(.primary)
|
.foregroundStyle(palette.textPrimary)
|
||||||
.frame(width: markSize, height: markSize)
|
.frame(width: markSize, height: markSize)
|
||||||
.opacity(colorScheme == .dark ? 0.025 : 0.014)
|
.opacity(palette.watermarkOpacity)
|
||||||
.offset(x: offsetX, y: offsetY)
|
.offset(x: offsetX, y: offsetY)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
.clipped()
|
|
||||||
}
|
}
|
||||||
|
.clipped()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ func subjectFont(forHourHeight hourHeight: CGFloat) -> CGFloat {
|
|||||||
|
|
||||||
struct TimetableDayView: View {
|
struct TimetableDayView: View {
|
||||||
let entry: TimetableEntry
|
let entry: TimetableEntry
|
||||||
|
@Environment(\.widgetPalette) private var palette
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
@@ -65,7 +66,6 @@ struct TimetableDayView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.background(MarianumWatermark())
|
.background(MarianumWatermark())
|
||||||
.widgetThemeOverride(entry.themeMode)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
@@ -99,11 +99,11 @@ struct TimetableDayView: View {
|
|||||||
HStack {
|
HStack {
|
||||||
Text(dayLabel(for: data.anchorDate))
|
Text(dayLabel(for: data.anchorDate))
|
||||||
.font(.system(size: 14, weight: .semibold))
|
.font(.system(size: 14, weight: .semibold))
|
||||||
.foregroundStyle(.primary)
|
.foregroundStyle(palette.textPrimary)
|
||||||
Spacer()
|
Spacer()
|
||||||
Text("Stand: \(freshnessLabel(for: data.fetchedAt))")
|
Text("Stand: \(freshnessLabel(for: data.fetchedAt))")
|
||||||
.font(.system(size: 10))
|
.font(.system(size: 10))
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(palette.textSecondary)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,7 +112,7 @@ struct TimetableDayView: View {
|
|||||||
Spacer()
|
Spacer()
|
||||||
Text(text)
|
Text(text)
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(palette.textSecondary)
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
@@ -122,9 +122,10 @@ struct TimetableDayView: View {
|
|||||||
VStack(spacing: 4) {
|
VStack(spacing: 4) {
|
||||||
Text("Marianum Stundenplan")
|
Text("Marianum Stundenplan")
|
||||||
.font(.system(size: 14, weight: .semibold))
|
.font(.system(size: 14, weight: .semibold))
|
||||||
|
.foregroundStyle(palette.textPrimary)
|
||||||
Text(message)
|
Text(message)
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(palette.textSecondary)
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
@@ -141,6 +142,8 @@ struct TimeGridView: View {
|
|||||||
/// Week-widget passes 3 for narrow columns; day-widget keeps 7.
|
/// Week-widget passes 3 for narrow columns; day-widget keeps 7.
|
||||||
var horizontalPadding: CGFloat = 7
|
var horizontalPadding: CGFloat = 7
|
||||||
|
|
||||||
|
@Environment(\.widgetPalette) private var palette
|
||||||
|
|
||||||
private var totalVirtualMinutes: Int {
|
private var totalVirtualMinutes: Int {
|
||||||
periods.last?.virtualEndMinutes ?? FALLBACK_VIRTUAL_MINUTES
|
periods.last?.virtualEndMinutes ?? FALLBACK_VIRTUAL_MINUTES
|
||||||
}
|
}
|
||||||
@@ -156,10 +159,12 @@ struct TimeGridView: View {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
HStack(alignment: .top, spacing: 0) {
|
HStack(alignment: .top, spacing: 0) {
|
||||||
if showTimeLabels {
|
if showTimeLabels {
|
||||||
|
// 28pt matches the Week widget's time-label column so the
|
||||||
|
// visual rhythm is identical across both widgets.
|
||||||
timeLabelsColumn
|
timeLabelsColumn
|
||||||
.frame(width: 32, alignment: .topTrailing)
|
.frame(width: 28, alignment: .topTrailing)
|
||||||
Rectangle()
|
Rectangle()
|
||||||
.fill(Color.primary.opacity(0.13))
|
.fill(palette.divider)
|
||||||
.frame(width: 1)
|
.frame(width: 1)
|
||||||
}
|
}
|
||||||
ZStack(alignment: .top) {
|
ZStack(alignment: .top) {
|
||||||
@@ -175,13 +180,9 @@ struct TimeGridView: View {
|
|||||||
|
|
||||||
private var timeLabelsColumn: some View {
|
private var timeLabelsColumn: some View {
|
||||||
ZStack(alignment: .topTrailing) {
|
ZStack(alignment: .topTrailing) {
|
||||||
// Hour rules continue through the time-label column so it reads
|
|
||||||
// as a real table column rather than a free-floating tick list.
|
|
||||||
// Hour rules extend through the time-label column so it reads
|
|
||||||
// as a table column rather than a free-floating tick list.
|
|
||||||
ForEach(periodBoundaries(periods), id: \.self) { virtualMin in
|
ForEach(periodBoundaries(periods), id: \.self) { virtualMin in
|
||||||
Rectangle()
|
Rectangle()
|
||||||
.fill(Color.primary.opacity(0.08))
|
.fill(palette.divider)
|
||||||
.frame(height: 1)
|
.frame(height: 1)
|
||||||
.offset(y: CGFloat(virtualMin) * hourHeight / 60.0)
|
.offset(y: CGFloat(virtualMin) * hourHeight / 60.0)
|
||||||
}
|
}
|
||||||
@@ -190,16 +191,16 @@ struct TimeGridView: View {
|
|||||||
if compactLabels {
|
if compactLabels {
|
||||||
Text("\(period.name).")
|
Text("\(period.name).")
|
||||||
.font(.system(size: 9, weight: .bold))
|
.font(.system(size: 9, weight: .bold))
|
||||||
.foregroundStyle(.primary)
|
.foregroundStyle(palette.textPrimary)
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
} else {
|
} else {
|
||||||
Text(formatHm(period.startMinutes))
|
Text(formatHm(period.startMinutes))
|
||||||
.font(.system(size: 9))
|
.font(.system(size: 9))
|
||||||
.foregroundStyle(.primary)
|
.foregroundStyle(palette.textPrimary)
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
Text("\(period.name).")
|
Text("\(period.name).")
|
||||||
.font(.system(size: 7, weight: .bold))
|
.font(.system(size: 7, weight: .bold))
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(palette.textSecondary)
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -212,11 +213,9 @@ struct TimeGridView: View {
|
|||||||
|
|
||||||
private var gridLines: some View {
|
private var gridLines: some View {
|
||||||
ZStack(alignment: .top) {
|
ZStack(alignment: .top) {
|
||||||
// Hour rules continue through the time-label column so it reads
|
|
||||||
// as a real table column rather than a free-floating tick list.
|
|
||||||
ForEach(periodBoundaries(periods), id: \.self) { virtualMin in
|
ForEach(periodBoundaries(periods), id: \.self) { virtualMin in
|
||||||
Rectangle()
|
Rectangle()
|
||||||
.fill(Color.primary.opacity(0.08))
|
.fill(palette.divider)
|
||||||
.frame(height: 1)
|
.frame(height: 1)
|
||||||
.offset(y: CGFloat(virtualMin) * hourHeight / 60.0)
|
.offset(y: CGFloat(virtualMin) * hourHeight / 60.0)
|
||||||
}
|
}
|
||||||
@@ -232,7 +231,7 @@ struct TimeGridView: View {
|
|||||||
let virtualGap = next.virtualStartMinutes - curr.virtualEndMinutes
|
let virtualGap = next.virtualStartMinutes - curr.virtualEndMinutes
|
||||||
if virtualGap > 0 {
|
if virtualGap > 0 {
|
||||||
Rectangle()
|
Rectangle()
|
||||||
.fill(Color.primary.opacity(0.03))
|
.fill(palette.breakBlock)
|
||||||
.frame(height: CGFloat(virtualGap) * hourHeight / 60.0)
|
.frame(height: CGFloat(virtualGap) * hourHeight / 60.0)
|
||||||
.padding(.horizontal, 1)
|
.padding(.horizontal, 1)
|
||||||
.offset(y: CGFloat(curr.virtualEndMinutes) * hourHeight / 60.0)
|
.offset(y: CGFloat(curr.virtualEndMinutes) * hourHeight / 60.0)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import WidgetKit
|
|||||||
|
|
||||||
struct TimetableWeekView: View {
|
struct TimetableWeekView: View {
|
||||||
let entry: TimetableEntry
|
let entry: TimetableEntry
|
||||||
|
@Environment(\.widgetPalette) private var palette
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
@@ -15,7 +16,6 @@ struct TimetableWeekView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.background(MarianumWatermark())
|
.background(MarianumWatermark())
|
||||||
.widgetThemeOverride(entry.themeMode)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
@@ -52,7 +52,7 @@ struct TimetableWeekView: View {
|
|||||||
|
|
||||||
private var columnDivider: some View {
|
private var columnDivider: some View {
|
||||||
Rectangle()
|
Rectangle()
|
||||||
.fill(Color.primary.opacity(0.13))
|
.fill(palette.divider)
|
||||||
.frame(width: 1)
|
.frame(width: 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,16 +63,23 @@ struct TimetableWeekView: View {
|
|||||||
return HStack {
|
return HStack {
|
||||||
Text("KW \(week) · \(shortDate(data.anchorDate))–\(shortDate(endDate))")
|
Text("KW \(week) · \(shortDate(data.anchorDate))–\(shortDate(endDate))")
|
||||||
.font(.system(size: 13, weight: .semibold))
|
.font(.system(size: 13, weight: .semibold))
|
||||||
.foregroundStyle(.primary)
|
.foregroundStyle(palette.textPrimary)
|
||||||
Spacer()
|
Spacer()
|
||||||
Text("Stand: \(freshnessLabel(for: data.fetchedAt))")
|
Text("Stand: \(freshnessLabel(for: data.fetchedAt))")
|
||||||
.font(.system(size: 10))
|
.font(.system(size: 10))
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(palette.textSecondary)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func dayHeaderRow(data: WidgetTimetableData) -> some View {
|
private func dayHeaderRow(data: WidgetTimetableData) -> some View {
|
||||||
let cal = Calendar.current
|
let cal = Calendar.current
|
||||||
|
// `.frame(maxWidth: .infinity)` is critical: without an explicit
|
||||||
|
// greedy width (or a true `Spacer()` inside), this HStack collapses
|
||||||
|
// to the sum of its fixed-width children in a `.leading` VStack
|
||||||
|
// and the 5 day-columns end up sharing ~5pt of width — the labels
|
||||||
|
// crunch to the left and stop aligning with the grid columns below.
|
||||||
|
// The fixed height keeps `columnDivider`'s vertically-flexible
|
||||||
|
// Rectangle from stealing space from the GeometryReader.
|
||||||
return HStack(spacing: 0) {
|
return HStack(spacing: 0) {
|
||||||
Spacer().frame(width: 28)
|
Spacer().frame(width: 28)
|
||||||
columnDivider
|
columnDivider
|
||||||
@@ -81,15 +88,16 @@ struct TimetableWeekView: View {
|
|||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
Text(weekday(for: day))
|
Text(weekday(for: day))
|
||||||
.font(.system(size: 11, weight: .bold))
|
.font(.system(size: 11, weight: .bold))
|
||||||
.foregroundStyle(.primary)
|
.foregroundStyle(palette.textPrimary)
|
||||||
Text(shortDate(day))
|
Text(shortDate(day))
|
||||||
.font(.system(size: 9))
|
.font(.system(size: 9))
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(palette.textSecondary)
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
if offset < 4 { columnDivider }
|
if offset < 4 { columnDivider }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.frame(maxWidth: .infinity, minHeight: 26, maxHeight: 26)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func timeLabelsColumn(hourHeight: CGFloat, periods: [WidgetPeriod]) -> some View {
|
private func timeLabelsColumn(hourHeight: CGFloat, periods: [WidgetPeriod]) -> some View {
|
||||||
@@ -98,7 +106,7 @@ struct TimetableWeekView: View {
|
|||||||
return ZStack(alignment: .topTrailing) {
|
return ZStack(alignment: .topTrailing) {
|
||||||
ForEach(periodBoundaries(periods), id: \.self) { virtualMin in
|
ForEach(periodBoundaries(periods), id: \.self) { virtualMin in
|
||||||
Rectangle()
|
Rectangle()
|
||||||
.fill(Color.primary.opacity(0.08))
|
.fill(palette.divider)
|
||||||
.frame(height: 1)
|
.frame(height: 1)
|
||||||
.offset(y: CGFloat(virtualMin) * hourHeight / 60.0)
|
.offset(y: CGFloat(virtualMin) * hourHeight / 60.0)
|
||||||
}
|
}
|
||||||
@@ -106,11 +114,11 @@ struct TimetableWeekView: View {
|
|||||||
VStack(alignment: .trailing, spacing: -2) {
|
VStack(alignment: .trailing, spacing: -2) {
|
||||||
Text(String(format: "%02d:%02d", period.startMinutes / 60, period.startMinutes % 60))
|
Text(String(format: "%02d:%02d", period.startMinutes / 60, period.startMinutes % 60))
|
||||||
.font(.system(size: 8))
|
.font(.system(size: 8))
|
||||||
.foregroundStyle(.primary)
|
.foregroundStyle(palette.textPrimary)
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
Text("\(period.name).")
|
Text("\(period.name).")
|
||||||
.font(.system(size: 6, weight: .bold))
|
.font(.system(size: 6, weight: .bold))
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(palette.textSecondary)
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
}
|
}
|
||||||
.offset(y: CGFloat(period.virtualStartMinutes) * hourHeight / 60.0)
|
.offset(y: CGFloat(period.virtualStartMinutes) * hourHeight / 60.0)
|
||||||
@@ -150,9 +158,10 @@ struct TimetableWeekView: View {
|
|||||||
VStack(spacing: 4) {
|
VStack(spacing: 4) {
|
||||||
Text("Marianum Stundenplan")
|
Text("Marianum Stundenplan")
|
||||||
.font(.system(size: 14, weight: .semibold))
|
.font(.system(size: 14, weight: .semibold))
|
||||||
|
.foregroundStyle(palette.textPrimary)
|
||||||
Text(message)
|
Text(message)
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(palette.textSecondary)
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ struct TimetableDayWidget: Widget {
|
|||||||
|
|
||||||
var body: some WidgetConfiguration {
|
var body: some WidgetConfiguration {
|
||||||
StaticConfiguration(kind: kind, provider: TimetableDayProvider()) { entry in
|
StaticConfiguration(kind: kind, provider: TimetableDayProvider()) { entry in
|
||||||
TimetableDayView(entry: entry).widgetContainerBackground()
|
TimetableDayView(entry: entry).widgetSurface(entry: entry)
|
||||||
}
|
}
|
||||||
.configurationDisplayName("Marianum · Heute")
|
.configurationDisplayName("Marianum · Heute")
|
||||||
.description("Stundenplan und Vertretungen für den anstehenden Schultag.")
|
.description("Stundenplan und Vertretungen für den anstehenden Schultag.")
|
||||||
@@ -52,7 +52,7 @@ struct TimetableWeekWidget: Widget {
|
|||||||
|
|
||||||
var body: some WidgetConfiguration {
|
var body: some WidgetConfiguration {
|
||||||
StaticConfiguration(kind: kind, provider: TimetableWeekProvider()) { entry in
|
StaticConfiguration(kind: kind, provider: TimetableWeekProvider()) { entry in
|
||||||
TimetableWeekView(entry: entry).widgetContainerBackground()
|
TimetableWeekView(entry: entry).widgetSurface(entry: entry)
|
||||||
}
|
}
|
||||||
.configurationDisplayName("Marianum · Woche")
|
.configurationDisplayName("Marianum · Woche")
|
||||||
.description("Stundenplan und Vertretungen für die ganze Schulwoche.")
|
.description("Stundenplan und Vertretungen für die ganze Schulwoche.")
|
||||||
@@ -116,6 +116,8 @@ struct TimetableEntry: TimelineEntry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
extension View {
|
extension View {
|
||||||
|
/// Applies the user's chosen light/dark override on top of the system
|
||||||
|
/// scheme so the widget honours the in-app theme setting.
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
func widgetThemeOverride(_ mode: String) -> some View {
|
func widgetThemeOverride(_ mode: String) -> some View {
|
||||||
switch mode {
|
switch mode {
|
||||||
@@ -125,14 +127,38 @@ extension View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `.containerBackground(_:for:)` is iOS 17+. Older iOS uses the
|
/// Wraps the widget view in the Marianum palette + container background
|
||||||
/// implicit `.background(...)` model and renders fine without it.
|
/// so all subviews can read `\.widgetPalette` and so the widget renders
|
||||||
|
/// in our warm off-white / dark-clay instead of system grey.
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
func widgetContainerBackground() -> some View {
|
func widgetSurface(entry: TimetableEntry) -> some View {
|
||||||
|
WidgetSurface(entry: entry) { self }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct WidgetSurface<Content: View>: View {
|
||||||
|
let entry: TimetableEntry
|
||||||
|
@ViewBuilder let content: () -> Content
|
||||||
|
@Environment(\.colorScheme) private var colorScheme
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
let palette = WidgetPalette.resolve(
|
||||||
|
themeMode: entry.themeMode,
|
||||||
|
colorScheme: colorScheme
|
||||||
|
)
|
||||||
|
return AnyView(
|
||||||
|
background(content: content(), palette: palette)
|
||||||
|
.environment(\.widgetPalette, palette)
|
||||||
|
.widgetThemeOverride(entry.themeMode)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private func background<C: View>(content: C, palette: WidgetPalette) -> some View {
|
||||||
if #available(iOS 17.0, *) {
|
if #available(iOS 17.0, *) {
|
||||||
self.containerBackground(.fill.tertiary, for: .widget)
|
content.containerBackground(palette.background, for: .widget)
|
||||||
} else {
|
} else {
|
||||||
self
|
content.background(palette.background)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
/// Mirrors the Kotlin `WidgetPalette` in WidgetRenderer.kt so day/week widgets
|
||||||
|
/// look identical across platforms. All values are hex tokens from the in-app
|
||||||
|
/// LightAppTheme / DarkAppTheme — do not swap to system colors, the whole
|
||||||
|
/// point is platform-independent branding.
|
||||||
|
struct WidgetPalette {
|
||||||
|
let background: Color
|
||||||
|
let textPrimary: Color
|
||||||
|
let textSecondary: Color
|
||||||
|
let divider: Color
|
||||||
|
let breakBlock: Color
|
||||||
|
let watermarkOpacity: Double
|
||||||
|
|
||||||
|
static let light = WidgetPalette(
|
||||||
|
background: Color(red: 0xFC / 255, green: 0xF7 / 255, blue: 0xF5 / 255),
|
||||||
|
textPrimary: Color(red: 0x11 / 255, green: 0x11 / 255, blue: 0x11 / 255),
|
||||||
|
textSecondary: Color(red: 0x55 / 255, green: 0x55 / 255, blue: 0x55 / 255),
|
||||||
|
divider: Color.black.opacity(0x22 / 255.0),
|
||||||
|
breakBlock: Color.black.opacity(0x0C / 255.0),
|
||||||
|
watermarkOpacity: 0.014
|
||||||
|
)
|
||||||
|
|
||||||
|
static let dark = WidgetPalette(
|
||||||
|
background: Color(red: 0x1F / 255, green: 0x17 / 255, blue: 0x16 / 255),
|
||||||
|
textPrimary: Color(red: 0xF1 / 255, green: 0xF1 / 255, blue: 0xF1 / 255),
|
||||||
|
textSecondary: Color(red: 0xB0 / 255, green: 0xB0 / 255, blue: 0xB0 / 255),
|
||||||
|
divider: Color.white.opacity(0x33 / 255.0),
|
||||||
|
breakBlock: Color.white.opacity(0x14 / 255.0),
|
||||||
|
watermarkOpacity: 0.025
|
||||||
|
)
|
||||||
|
|
||||||
|
static func resolve(themeMode: String, colorScheme: ColorScheme) -> WidgetPalette {
|
||||||
|
let isDark: Bool
|
||||||
|
switch themeMode {
|
||||||
|
case "light": isDark = false
|
||||||
|
case "dark": isDark = true
|
||||||
|
default: isDark = colorScheme == .dark
|
||||||
|
}
|
||||||
|
return isDark ? .dark : .light
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct WidgetPaletteKey: EnvironmentKey {
|
||||||
|
static let defaultValue = WidgetPalette.light
|
||||||
|
}
|
||||||
|
|
||||||
|
extension EnvironmentValues {
|
||||||
|
var widgetPalette: WidgetPalette {
|
||||||
|
get { self[WidgetPaletteKey.self] }
|
||||||
|
set { self[WidgetPaletteKey.self] = newValue }
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user