712 lines
28 KiB
Swift
712 lines
28 KiB
Swift
//
|
|
// SolianWidgetExtension.swift
|
|
// SolianWidgetExtension
|
|
//
|
|
// Created by LittleSheep on 2026/1/3.
|
|
//
|
|
|
|
import WidgetKit
|
|
import SwiftUI
|
|
|
|
struct CheckInTip: Codable {
|
|
let isPositive: Bool
|
|
let title: String
|
|
let content: String
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case isPositive = "is_positive"
|
|
case title
|
|
case content
|
|
}
|
|
}
|
|
|
|
struct CheckInAccount: Codable {
|
|
let id: String
|
|
let nick: String?
|
|
let profile: CheckInProfile?
|
|
}
|
|
|
|
struct CheckInProfile: Codable {
|
|
let picture: String?
|
|
}
|
|
|
|
struct CheckInResult: Codable {
|
|
let id: String
|
|
let level: Int
|
|
let rewardPoints: Int
|
|
let rewardExperience: Int
|
|
let tips: [CheckInTip]
|
|
let accountId: String
|
|
let account: CheckInAccount?
|
|
let createdAt: String
|
|
let updatedAt: String
|
|
let deletedAt: String?
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case id
|
|
case level
|
|
case rewardPoints = "reward_points"
|
|
case rewardExperience = "reward_experience"
|
|
case tips
|
|
case accountId = "account_id"
|
|
case account
|
|
case createdAt = "created_at"
|
|
case updatedAt = "updated_at"
|
|
case deletedAt = "deleted_at"
|
|
}
|
|
|
|
var createdDate: Date? {
|
|
ISO8601DateFormatter().date(from: createdAt)
|
|
}
|
|
}
|
|
|
|
struct NotableDay: Codable {
|
|
let date: String
|
|
let localName: String
|
|
let globalName: String
|
|
let countryCode: String?
|
|
let localizableKey: String?
|
|
let holidays: [Int]?
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case date
|
|
case localName = "local_name"
|
|
case globalName = "global_name"
|
|
case countryCode = "country_code"
|
|
case localizableKey = "localizable_key"
|
|
case holidays
|
|
}
|
|
|
|
var notableDate: Date? {
|
|
ISO8601DateFormatter().date(from: date)
|
|
}
|
|
|
|
var isToday: Bool {
|
|
guard let notableDate = notableDate else { return false }
|
|
let calendar = Calendar.current
|
|
return calendar.isDateInToday(notableDate)
|
|
}
|
|
}
|
|
|
|
class CheckInService {
|
|
private let networkService = WidgetNetworkService()
|
|
|
|
func fetchCheckInResult() async throws -> CheckInResult? {
|
|
return try await networkService.makeRequest(path: "/pass/accounts/me/check-in")
|
|
}
|
|
}
|
|
|
|
class NotableDayService {
|
|
private let networkService = WidgetNetworkService()
|
|
|
|
func fetchRecentNotableDay() async throws -> NotableDay? {
|
|
print("[WidgetKit] [NotableDayService] Fetching recent notable day...")
|
|
do {
|
|
let result: [NotableDay]? = try await networkService.makeRequest(path: "/pass/notable/me/recent")
|
|
print("[WidgetKit] [NotableDayService] Result: \(String(describing: result))")
|
|
|
|
guard let result = result else {
|
|
print("[WidgetKit] [NotableDayService] Result is nil")
|
|
return nil
|
|
}
|
|
|
|
print("[WidgetKit] [NotableDayService] Result count: \(result.count)")
|
|
|
|
guard result.isEmpty == false else {
|
|
print("[WidgetKit] [NotableDayService] No notable days found")
|
|
return nil
|
|
}
|
|
|
|
let firstDay = result.first!
|
|
print("[WidgetKit] [NotableDayService] First notable day: \(firstDay.localName), date: \(firstDay.date)")
|
|
|
|
return firstDay
|
|
} catch let decodingError as DecodingError {
|
|
print("[WidgetKit] [NotableDayService] Decoding error, trying as single object...")
|
|
print("[WidgetKit] [NotableDayService] Error: \(decodingError.localizedDescription)")
|
|
|
|
switch decodingError {
|
|
case .typeMismatch(let type, let context):
|
|
print("[WidgetKit] [NotableDayService] Type mismatch: expected \(type), context: \(context.debugDescription)")
|
|
case .valueNotFound(let type, let context):
|
|
print("[WidgetKit] [NotableDayService] Value not found: type \(type), context: \(context.debugDescription)")
|
|
case .keyNotFound(let key, let context):
|
|
print("[WidgetKit] [NotableDayService] Key not found: \(key), context: \(context.debugDescription)")
|
|
case .dataCorrupted(let context):
|
|
print("[WidgetKit] [NotableDayService] Data corrupted: \(context.debugDescription)")
|
|
@unknown default:
|
|
print("[WidgetKit] [NotableDayService] Unknown decoding error")
|
|
}
|
|
|
|
do {
|
|
let singleResult: NotableDay? = try await networkService.makeRequest(path: "/pass/notable/me/recent")
|
|
print("[WidgetKit] [NotableDayService] Single object decode succeeded: \(singleResult?.localName ?? "nil")")
|
|
return singleResult
|
|
} catch {
|
|
print("[WidgetKit] [NotableDayService] Single object decode also failed: \(error.localizedDescription)")
|
|
throw decodingError
|
|
}
|
|
} catch {
|
|
print("[WidgetKit] [NotableDayService] Error fetching notable day: \(error.localizedDescription)")
|
|
print("[WidgetKit] [NotableDayService] Error type: \(type(of: error))")
|
|
throw error
|
|
}
|
|
}
|
|
}
|
|
|
|
struct CheckInEntry: TimelineEntry {
|
|
let date: Date
|
|
let result: CheckInResult?
|
|
let notableDay: NotableDay?
|
|
let error: String?
|
|
let isLoading: Bool
|
|
|
|
static func placeholder() -> CheckInEntry {
|
|
CheckInEntry(date: Date(), result: nil, notableDay: nil, error: nil, isLoading: true)
|
|
}
|
|
}
|
|
|
|
struct Provider: TimelineProvider {
|
|
private let apiService = CheckInService()
|
|
|
|
func placeholder(in context: Context) -> CheckInEntry {
|
|
CheckInEntry.placeholder()
|
|
}
|
|
|
|
func getSnapshot(in context: Context, completion: @escaping (CheckInEntry) -> ()) {
|
|
Task {
|
|
print("[WidgetKit] [Provider] Getting snapshot...")
|
|
async let checkInResult = try? await apiService.fetchCheckInResult()
|
|
async let notableDay = try? await NotableDayService().fetchRecentNotableDay()
|
|
|
|
let result = try? await checkInResult
|
|
let day = try? await notableDay
|
|
|
|
print("[WidgetKit] [Provider] Snapshot - CheckIn: \(result != nil ? "Found" : "Not found"), NotableDay: \(day != nil ? "Found" : "Not found")")
|
|
|
|
let entry = CheckInEntry(date: Date(), result: result, notableDay: day, error: nil, isLoading: false)
|
|
completion(entry)
|
|
}
|
|
}
|
|
|
|
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
|
|
Task {
|
|
let currentDate = Date()
|
|
print("[WidgetKit] [Provider] Getting timeline at \(currentDate)...")
|
|
|
|
do {
|
|
async let checkInResult = try await apiService.fetchCheckInResult()
|
|
async let notableDay = try await NotableDayService().fetchRecentNotableDay()
|
|
|
|
let result = try await checkInResult
|
|
let day = try await notableDay
|
|
|
|
print("[WidgetKit] [Provider] Timeline - CheckIn: \(result != nil ? "Found" : "Not found"), NotableDay: \(day != nil ? "Found" : "Not found")")
|
|
|
|
let entry = CheckInEntry(date: currentDate, result: result, notableDay: day, error: nil, isLoading: false)
|
|
|
|
let nextUpdateDate: Date
|
|
if let result = result, let createdDate = result.createdDate {
|
|
let calendar = Calendar.current
|
|
if let tomorrow = calendar.date(byAdding: .day, value: 1, to: createdDate) {
|
|
nextUpdateDate = min(tomorrow, calendar.date(byAdding: .hour, value: 1, to: currentDate)!)
|
|
} else {
|
|
nextUpdateDate = calendar.date(byAdding: .hour, value: 1, to: currentDate)!
|
|
}
|
|
} else {
|
|
nextUpdateDate = Calendar.current.date(byAdding: .minute, value: 30, to: currentDate)!
|
|
}
|
|
|
|
print("[WidgetKit] [Provider] Next update at: \(nextUpdateDate)")
|
|
let timeline = Timeline(entries: [entry], policy: .after(nextUpdateDate))
|
|
completion(timeline)
|
|
} catch {
|
|
print("[WidgetKit] [Provider] Error in getTimeline: \(error.localizedDescription)")
|
|
let entry = CheckInEntry(date: currentDate, result: nil, notableDay: nil, error: error.localizedDescription, isLoading: false)
|
|
let nextUpdate = Calendar.current.date(byAdding: .minute, value: 10, to: currentDate)!
|
|
let timeline = Timeline(entries: [entry], policy: .after(nextUpdate))
|
|
completion(timeline)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
struct CheckInWidgetEntryView: View {
|
|
var entry: Provider.Entry
|
|
@Environment(\.widgetFamily) var family
|
|
|
|
var body: some View {
|
|
if let result = entry.result {
|
|
CheckedInView(result: result, notableDay: entry.notableDay)
|
|
} else if entry.isLoading {
|
|
LoadingView()
|
|
} else if let error = entry.error {
|
|
ErrorView(error: error)
|
|
} else {
|
|
NotCheckedInView(notableDay: entry.notableDay)
|
|
}
|
|
}
|
|
|
|
private func getLevelName(for level: Int) -> String {
|
|
let key = "checkInResultT\(level)"
|
|
return NSLocalizedString(key, comment: "Check-in result level name")
|
|
}
|
|
|
|
@ViewBuilder
|
|
private func NotableDayView(notableDay: NotableDay) -> some View {
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
if !notableDay.isToday {
|
|
Text(NSLocalizedString("notableDayUpcoming", comment: "Upcoming"))
|
|
.font(.caption2)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
HStack(spacing: isAccessory ? 8 : 6) {
|
|
Image(systemName: "sparkles")
|
|
.foregroundColor(.orange)
|
|
.font(isAccessory ? .caption : .subheadline)
|
|
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
if notableDay.isToday {
|
|
Text(String(format: NSLocalizedString("notableDayToday", comment: "{name} is today!"), notableDay.localName))
|
|
.font(isAccessory ? .caption : .footnote)
|
|
.fontWeight(.bold)
|
|
.foregroundColor(.primary)
|
|
.lineLimit(2)
|
|
} else {
|
|
if let notableDate = notableDay.notableDate {
|
|
let dateString = isCompact ? formatDateCompact(notableDate) : formatDateRegular(notableDate)
|
|
Text(String(format: NSLocalizedString("notableDayIs", comment: "{date} is {name}"), dateString, notableDay.localName))
|
|
.font(isAccessory ? .caption : .footnote)
|
|
.fontWeight(.semibold)
|
|
.foregroundColor(.primary)
|
|
.lineLimit(2)
|
|
} else {
|
|
Text(notableDay.localName)
|
|
.font(isAccessory ? .caption : .footnote)
|
|
.fontWeight(.semibold)
|
|
.foregroundColor(.primary)
|
|
.lineLimit(2)
|
|
}
|
|
}
|
|
}
|
|
|
|
Spacer()
|
|
}
|
|
|
|
if notableDay.isToday && !isAccessory {
|
|
HStack(spacing: 4) {
|
|
Image(systemName: "star.fill")
|
|
.font(.caption2)
|
|
.foregroundColor(.orange)
|
|
Text(NSLocalizedString("today", comment: "Today"))
|
|
.font(.caption2)
|
|
.foregroundColor(.secondary)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private var isCompact: Bool {
|
|
family == .systemSmall || isAccessory
|
|
}
|
|
|
|
private func formatDateCompact(_ date: Date) -> String {
|
|
let formatter = DateFormatter()
|
|
formatter.dateFormat = "M/d"
|
|
return formatter.string(from: date)
|
|
}
|
|
|
|
private func formatDateRegular(_ date: Date) -> String {
|
|
let formatter = DateFormatter()
|
|
formatter.dateFormat = "MMM d, yyyy"
|
|
return formatter.string(from: date)
|
|
}
|
|
|
|
@ViewBuilder
|
|
private func CheckedInView(result: CheckInResult, notableDay: NotableDay?) -> some View {
|
|
Link(destination: URL(string: "solian://dashboard")!) {
|
|
VStack(alignment: .leading, spacing: isAccessory ? 2 : 8) {
|
|
HStack(spacing: 8) {
|
|
Image(systemName: "flame.fill")
|
|
.foregroundColor(.orange)
|
|
.font(isAccessory ? .caption : .title3)
|
|
Text(getLevelName(for: result.level))
|
|
.font(isAccessory ? .caption2 : .headline)
|
|
.fontWeight(.bold)
|
|
Spacer()
|
|
}
|
|
|
|
if !result.tips.isEmpty {
|
|
if isAccessory {
|
|
let positiveTips = result.tips.filter { $0.isPositive }
|
|
let negativeTips = result.tips.filter { !$0.isPositive }
|
|
|
|
HStack(spacing: 2) {
|
|
if let positiveTip = positiveTips.first {
|
|
HStack(spacing: 8) {
|
|
Image(systemName: "hand.thumbsup.fill")
|
|
.font(.caption2)
|
|
.foregroundColor(.secondary)
|
|
Text(positiveTip.title)
|
|
.font(.caption2)
|
|
.foregroundColor(.primary)
|
|
.lineLimit(1)
|
|
}
|
|
}
|
|
if let negativeTip = negativeTips.first {
|
|
HStack(spacing: 8) {
|
|
Image(systemName: "hand.thumbsdown.fill")
|
|
.font(.caption2)
|
|
.foregroundColor(.secondary)
|
|
Text(negativeTip.title)
|
|
.font(.caption2)
|
|
.foregroundColor(.primary)
|
|
.lineLimit(1)
|
|
}
|
|
}
|
|
Spacer()
|
|
}
|
|
} else if family == .systemSmall {
|
|
let positiveTips = result.tips.filter { $0.isPositive }
|
|
let negativeTips = result.tips.filter { !$0.isPositive }
|
|
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
if let positiveTip = positiveTips.first {
|
|
HStack(spacing: 4) {
|
|
Image(systemName: "hand.thumbsup.fill")
|
|
.font(.caption)
|
|
.foregroundColor(.green.opacity(0.8))
|
|
Text(positiveTip.title)
|
|
.font(.caption)
|
|
.foregroundColor(.primary)
|
|
Spacer()
|
|
}
|
|
}
|
|
if let negativeTip = negativeTips.first {
|
|
HStack(spacing: 4) {
|
|
Image(systemName: "hand.thumbsdown.fill")
|
|
.font(.caption)
|
|
.foregroundColor(.red.opacity(0.8))
|
|
Text(negativeTip.title)
|
|
.font(.caption)
|
|
.foregroundColor(.primary)
|
|
Spacer()
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
let positiveTips = result.tips.filter { $0.isPositive }
|
|
let negativeTips = result.tips.filter { !$0.isPositive }
|
|
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
if !positiveTips.isEmpty {
|
|
HStack(spacing: 6) {
|
|
Image(systemName: "hand.thumbsup.fill")
|
|
.font(.caption)
|
|
.foregroundColor(.green.opacity(0.8))
|
|
ForEach(Array(positiveTips.prefix(3)), id: \.title) { tip in
|
|
Text(tip.title)
|
|
.font(.caption)
|
|
.foregroundColor(.primary)
|
|
if tip.title != positiveTips.last?.title {
|
|
Text("•")
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
}
|
|
}
|
|
Spacer()
|
|
}
|
|
}
|
|
|
|
if !negativeTips.isEmpty {
|
|
HStack(spacing: 6) {
|
|
Image(systemName: "hand.thumbsdown.fill")
|
|
.font(.caption)
|
|
.foregroundColor(.red.opacity(0.8))
|
|
ForEach(Array(negativeTips.prefix(3)), id: \.title) { tip in
|
|
Text(tip.title)
|
|
.font(.caption)
|
|
.foregroundColor(.primary)
|
|
if tip.title != negativeTips.last?.title {
|
|
Text("•")
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
}
|
|
}
|
|
Spacer()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else if !isAccessory && family != .systemSmall {
|
|
Text("No fortune today")
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
}
|
|
|
|
if let notableDay = notableDay {
|
|
NotableDayView(notableDay: notableDay)
|
|
}
|
|
|
|
if family == .systemLarge {
|
|
Spacer()
|
|
WidgetFooter()
|
|
|
|
}
|
|
}
|
|
.padding(isAccessory ? 0 : (family == .systemSmall ? 6 : 12))
|
|
}
|
|
}
|
|
|
|
@ViewBuilder
|
|
private func WidgetFooter() -> some View {
|
|
HStack {
|
|
Text("Solian")
|
|
.font(.caption2)
|
|
.fontWeight(.medium)
|
|
.foregroundColor(.secondary)
|
|
Spacer()
|
|
}
|
|
}
|
|
|
|
private var isAccessory: Bool {
|
|
if #available(iOS 16.0, *) {
|
|
if case .accessoryRectangular = family {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
@ViewBuilder
|
|
private func NotCheckedInView(notableDay: NotableDay?) -> some View {
|
|
Link(destination: URL(string: "solian://dashboard")!) {
|
|
VStack(alignment: .leading, spacing: isAccessory ? 2 : 8) {
|
|
HStack(spacing: 8) {
|
|
Image(systemName: "flame.fill")
|
|
.foregroundColor(.secondary)
|
|
.font(isAccessory ? .caption : .title3)
|
|
Text(NSLocalizedString("checkIn", comment: "Check In"))
|
|
.font(isAccessory ? .caption2 : .headline)
|
|
.fontWeight(.bold)
|
|
Spacer()
|
|
}
|
|
|
|
if !isAccessory {
|
|
Text(NSLocalizedString("tapToCheckIn", comment: "Tap to check in today"))
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
|
|
if let notableDay = notableDay {
|
|
NotableDayView(notableDay: notableDay)
|
|
}
|
|
|
|
Spacer()
|
|
|
|
WidgetFooter()
|
|
} else if let notableDay = notableDay {
|
|
NotableDayView(notableDay: notableDay)
|
|
}
|
|
}
|
|
.padding(isAccessory ? 0 : (family == .systemSmall ? 6 : 12))
|
|
}
|
|
}
|
|
|
|
@ViewBuilder
|
|
private func LoadingView() -> some View {
|
|
VStack(alignment: .leading, spacing: isAccessory ? 2 : 8) {
|
|
HStack(spacing: 4) {
|
|
ProgressView()
|
|
.scaleEffect(isAccessory ? 0.6 : 0.8)
|
|
Text(NSLocalizedString("loading", comment: "Loading..."))
|
|
.font(isAccessory ? .caption2 : .caption)
|
|
.foregroundColor(.secondary)
|
|
Spacer()
|
|
}
|
|
|
|
if !isAccessory {
|
|
Spacer()
|
|
WidgetFooter()
|
|
}
|
|
}
|
|
.padding(isAccessory ? 0 : 12)
|
|
}
|
|
|
|
@ViewBuilder
|
|
private func ErrorView(error: String) -> some View {
|
|
Link(destination: URL(string: "solian://dashboard")!) {
|
|
VStack(alignment: .leading, spacing: isAccessory ? 2 : 8) {
|
|
HStack(spacing: 4) {
|
|
Image(systemName: "exclamationmark.triangle")
|
|
.foregroundColor(.secondary)
|
|
.font(isAccessory ? .caption : .title3)
|
|
Text(NSLocalizedString("error", comment: "Error"))
|
|
.font(isAccessory ? .caption2 : .headline)
|
|
Spacer()
|
|
}
|
|
|
|
if !isAccessory {
|
|
Text(NSLocalizedString("openAppToRefresh", comment: "Open app to refresh"))
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
|
|
Text(error)
|
|
.font(.footnote)
|
|
.foregroundStyle(.secondary)
|
|
.lineLimit(nil)
|
|
.multilineTextAlignment(.leading)
|
|
|
|
Spacer()
|
|
|
|
WidgetFooter()
|
|
}
|
|
}
|
|
.padding(isAccessory ? 0 : (family == .systemSmall ? 6 : 12))
|
|
}
|
|
}
|
|
}
|
|
|
|
struct CheckInWidgetRootView: View {
|
|
var entry: Provider.Entry
|
|
@Environment(\.colorScheme) var colorScheme
|
|
|
|
var body: some View {
|
|
if #available(iOS 17.0, *) {
|
|
ZStack {
|
|
CheckInWidgetEntryView(entry: entry)
|
|
|
|
if entry.result != nil || entry.notableDay != nil {
|
|
GeometryReader { geometry in
|
|
Image(colorScheme == .dark ? "CloudyLambDark" : "CloudyLamb")
|
|
.resizable()
|
|
.aspectRatio(contentMode: .fit)
|
|
.frame(
|
|
width: geometry.size.width * 0.9,
|
|
height: geometry.size.width * 0.9
|
|
)
|
|
.opacity(0.18)
|
|
.mask(
|
|
LinearGradient(
|
|
gradient: Gradient(colors: [
|
|
Color.white,
|
|
Color.white,
|
|
Color.clear
|
|
]),
|
|
startPoint: .topLeading,
|
|
endPoint: .bottomTrailing
|
|
)
|
|
)
|
|
.position(
|
|
x: geometry.size.width * 0.9,
|
|
y: 20
|
|
)
|
|
}
|
|
.allowsHitTesting(false)
|
|
}
|
|
}
|
|
.containerBackground(.fill.tertiary, for: .widget)
|
|
.padding(.vertical, 8)
|
|
} else {
|
|
CheckInWidgetEntryView(entry: entry)
|
|
.padding()
|
|
.background()
|
|
}
|
|
}
|
|
}
|
|
|
|
struct SolianCheckInWidget: Widget {
|
|
let kind: String = "SolianCheckInWidget"
|
|
|
|
var body: some WidgetConfiguration {
|
|
StaticConfiguration(kind: kind, provider: Provider()) { entry in
|
|
CheckInWidgetRootView(entry: entry)
|
|
}
|
|
.configurationDisplayName("Check In")
|
|
.description("View your daily check-in status")
|
|
.supportedFamilies(supportedFamilies)
|
|
}
|
|
|
|
private var supportedFamilies: [WidgetFamily] {
|
|
#if os(iOS)
|
|
return [.systemSmall, .systemMedium, .systemLarge, .accessoryRectangular]
|
|
#else
|
|
return [.systemSmall, .systemMedium, .systemLarge]
|
|
#endif
|
|
}
|
|
}
|
|
|
|
#Preview(as: .systemSmall) {
|
|
SolianCheckInWidget()
|
|
} timeline: {
|
|
CheckInEntry(date: .now, result: nil, notableDay: nil, error: nil, isLoading: false)
|
|
}
|
|
|
|
#Preview(as: .systemMedium) {
|
|
SolianCheckInWidget()
|
|
} timeline: {
|
|
CheckInEntry(
|
|
date: .now,
|
|
result: CheckInResult(
|
|
id: "test-id",
|
|
level: 2,
|
|
rewardPoints: 10,
|
|
rewardExperience: 100,
|
|
tips: [
|
|
CheckInTip(isPositive: true, title: "Good Luck", content: "Great day"),
|
|
CheckInTip(isPositive: true, title: "Creative", content: "Inspiration"),
|
|
CheckInTip(isPositive: false, title: "Shopping", content: "Expensive")
|
|
],
|
|
accountId: "account-id",
|
|
account: nil,
|
|
createdAt: ISO8601DateFormatter().string(from: Date()),
|
|
updatedAt: ISO8601DateFormatter().string(from: Date()),
|
|
deletedAt: nil
|
|
),
|
|
notableDay: NotableDay(
|
|
date: ISO8601DateFormatter().string(from: Calendar.current.date(byAdding: .day, value: 5, to: Date())!),
|
|
localName: "Christmas",
|
|
globalName: "Christmas",
|
|
countryCode: nil,
|
|
localizableKey: nil,
|
|
holidays: []
|
|
),
|
|
error: nil,
|
|
isLoading: false
|
|
)
|
|
}
|
|
|
|
#if os(iOS)
|
|
#Preview(as: .accessoryRectangular) {
|
|
SolianCheckInWidget()
|
|
} timeline: {
|
|
CheckInEntry(
|
|
date: .now,
|
|
result: CheckInResult(
|
|
id: "test-id",
|
|
level: 4,
|
|
rewardPoints: 50,
|
|
rewardExperience: 500,
|
|
tips: [
|
|
CheckInTip(isPositive: true, title: "Lucky", content: "Great fortune"),
|
|
CheckInTip(isPositive: true, title: "Success", content: "Opportunity")
|
|
],
|
|
accountId: "account-id",
|
|
account: nil,
|
|
createdAt: ISO8601DateFormatter().string(from: Date()),
|
|
updatedAt: ISO8601DateFormatter().string(from: Date()),
|
|
deletedAt: nil
|
|
),
|
|
notableDay: NotableDay(
|
|
date: ISO8601DateFormatter().string(from: Date()),
|
|
localName: "New Year",
|
|
globalName: "New Year",
|
|
countryCode: nil,
|
|
localizableKey: nil,
|
|
holidays: []
|
|
),
|
|
error: nil,
|
|
isLoading: false
|
|
)
|
|
}
|
|
#endif
|