diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 39f5dec2..eec4a04a 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -631,6 +631,10 @@ en, Base, "zh-Hans", + es, + ja, + ko, + "zh-Hant", ); mainGroup = 97C146E51CF9000F007C117D; preferredProjectObjectVersion = 77; diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/SolianShareExtension.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/SolianShareExtension.xcscheme index 3d28553a..e36c94d6 100644 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/SolianShareExtension.xcscheme +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/SolianShareExtension.xcscheme @@ -1,6 +1,7 @@ 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, error: nil, isLoading: true) + CheckInEntry(date: Date(), result: nil, notableDay: nil, error: nil, isLoading: true) } } @@ -195,22 +300,38 @@ struct Provider: TimelineProvider { func placeholder(in context: Context) -> CheckInEntry { CheckInEntry.placeholder() } - + func getSnapshot(in context: Context, completion: @escaping (CheckInEntry) -> ()) { Task { - let result = try? await apiService.fetchCheckInResult() - let entry = CheckInEntry(date: Date(), result: result, error: nil, isLoading: false) + 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) -> ()) { Task { let currentDate = Date() + print("[WidgetKit] [Provider] Getting timeline at \(currentDate)...") do { - let result = try await apiService.fetchCheckInResult() - let entry = CheckInEntry(date: currentDate, result: result, error: nil, isLoading: false) + 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 { @@ -224,10 +345,12 @@ struct Provider: TimelineProvider { 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 { - let entry = CheckInEntry(date: currentDate, result: nil, error: error.localizedDescription, isLoading: false) + 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) @@ -239,16 +362,16 @@ struct Provider: TimelineProvider { struct CheckInWidgetEntryView: View { var entry: Provider.Entry @Environment(\.widgetFamily) var family - + var body: some View { if let result = entry.result { - CheckedInView(result: result) + CheckedInView(result: result, notableDay: entry.notableDay) } else if entry.isLoading { LoadingView() } else if let error = entry.error { ErrorView(error: error) } else { - NotCheckedInView() + NotCheckedInView(notableDay: entry.notableDay) } } @@ -258,12 +381,82 @@ struct CheckInWidgetEntryView: View { } @ViewBuilder - private func CheckedInView(result: CheckInResult) -> some View { + 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: 4) { + HStack(spacing: 8) { Image(systemName: "flame.fill") - .foregroundColor(.secondary) + .foregroundColor(.orange) .font(isAccessory ? .caption : .title3) Text(getLevelName(for: result.level)) .font(isAccessory ? .caption2 : .headline) @@ -276,9 +469,9 @@ struct CheckInWidgetEntryView: View { let positiveTips = result.tips.filter { $0.isPositive } let negativeTips = result.tips.filter { !$0.isPositive } - VStack(alignment: .leading, spacing: 1) { + HStack(spacing: 2) { if let positiveTip = positiveTips.first { - HStack(spacing: 2) { + HStack(spacing: 8) { Image(systemName: "hand.thumbsup.fill") .font(.caption2) .foregroundColor(.secondary) @@ -289,7 +482,7 @@ struct CheckInWidgetEntryView: View { } } if let negativeTip = negativeTips.first { - HStack(spacing: 2) { + HStack(spacing: 8) { Image(systemName: "hand.thumbsdown.fill") .font(.caption2) .foregroundColor(.secondary) @@ -299,6 +492,7 @@ struct CheckInWidgetEntryView: View { .lineLimit(1) } } + Spacer() } } else if family == .systemSmall { let positiveTips = result.tips.filter { $0.isPositive } @@ -309,7 +503,7 @@ struct CheckInWidgetEntryView: View { HStack(spacing: 4) { Image(systemName: "hand.thumbsup.fill") .font(.caption) - .foregroundColor(.secondary) + .foregroundColor(.green.opacity(0.8)) Text(positiveTip.title) .font(.caption) .foregroundColor(.primary) @@ -320,7 +514,7 @@ struct CheckInWidgetEntryView: View { HStack(spacing: 4) { Image(systemName: "hand.thumbsdown.fill") .font(.caption) - .foregroundColor(.secondary) + .foregroundColor(.red.opacity(0.8)) Text(negativeTip.title) .font(.caption) .foregroundColor(.primary) @@ -337,7 +531,7 @@ struct CheckInWidgetEntryView: View { HStack(spacing: 6) { Image(systemName: "hand.thumbsup.fill") .font(.caption) - .foregroundColor(.secondary) + .foregroundColor(.green.opacity(0.8)) ForEach(Array(positiveTips.prefix(3)), id: \.title) { tip in Text(tip.title) .font(.caption) @@ -356,7 +550,7 @@ struct CheckInWidgetEntryView: View { HStack(spacing: 6) { Image(systemName: "hand.thumbsdown.fill") .font(.caption) - .foregroundColor(.secondary) + .foregroundColor(.red.opacity(0.8)) ForEach(Array(negativeTips.prefix(3)), id: \.title) { tip in Text(tip.title) .font(.caption) @@ -378,9 +572,14 @@ struct CheckInWidgetEntryView: View { .foregroundColor(.secondary) } - if !isAccessory { + if let notableDay = notableDay { + NotableDayView(notableDay: notableDay) + } + + if family == .systemLarge { Spacer() WidgetFooter() + } } .padding(isAccessory ? 0 : (family == .systemSmall ? 6 : 12)) @@ -408,7 +607,7 @@ struct CheckInWidgetEntryView: View { } @ViewBuilder - private func NotCheckedInView() -> some View { + private func NotCheckedInView(notableDay: NotableDay?) -> some View { Link(destination: URL(string: "solian://dashboard")!) { VStack(alignment: .leading, spacing: isAccessory ? 2 : 8) { HStack(spacing: 4) { @@ -426,9 +625,15 @@ struct CheckInWidgetEntryView: View { .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)) @@ -491,12 +696,13 @@ struct CheckInWidgetEntryView: View { struct SolianWidgetExtension: Widget { let kind: String = "SolianWidgetExtension" - + var body: some WidgetConfiguration { StaticConfiguration(kind: kind, provider: Provider()) { entry in if #available(iOS 17.0, *) { CheckInWidgetEntryView(entry: entry) .containerBackground(.fill.tertiary, for: .widget) + .padding(.vertical, 8) } else { CheckInWidgetEntryView(entry: entry) .padding() @@ -509,18 +715,18 @@ struct SolianWidgetExtension: Widget { } private var supportedFamilies: [WidgetFamily] { - #if os(iOS) +#if os(iOS) return [.systemSmall, .systemMedium, .systemLarge, .accessoryRectangular] - #else +#else return [.systemSmall, .systemMedium, .systemLarge] - #endif +#endif } } #Preview(as: .systemSmall) { SolianWidgetExtension() } timeline: { - CheckInEntry(date: .now, result: nil, error: nil, isLoading: false) + CheckInEntry(date: .now, result: nil, notableDay: nil, error: nil, isLoading: false) } #Preview(as: .systemMedium) { @@ -544,6 +750,14 @@ struct SolianWidgetExtension: Widget { 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 ) @@ -570,6 +784,14 @@ struct SolianWidgetExtension: Widget { 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 ) diff --git a/ios/SolianWidgetExtension/en.lproj/Localizable.strings b/ios/SolianWidgetExtension/en.lproj/Localizable.strings new file mode 100644 index 00000000..7287037f --- /dev/null +++ b/ios/SolianWidgetExtension/en.lproj/Localizable.strings @@ -0,0 +1,23 @@ +/* Check In Level Names */ +"checkInResultT0" = "Great Misfortune"; +"checkInResultT1" = "Misfortune"; +"checkInResultT2" = "Moderate"; +"checkInResultT3" = "Fortune"; +"checkInResultT4" = "Great Fortune"; +"checkInResultT5" = "Special"; + +/* Widget UI Strings */ +"checkIn" = "Check In"; +"tapToCheckIn" = "Tap to check in today"; +"error" = "Error"; +"openAppToRefresh" = "Open app to refresh"; +"loading" = "Loading..."; +"rewardPoints" = "%d"; +"rewardExperience" = "%d EXP"; +"footer" = "Solian Check In"; + +/* Notable Day Strings */ +"notableDayToday" = "%@ is today!"; +"notableDayIs" = "%@ is %@"; +"notableDayUpcoming" = "Upcoming"; +"today" = "Today"; diff --git a/ios/SolianWidgetExtension/es.lproj/Localizable.strings b/ios/SolianWidgetExtension/es.lproj/Localizable.strings new file mode 100644 index 00000000..e412fb36 --- /dev/null +++ b/ios/SolianWidgetExtension/es.lproj/Localizable.strings @@ -0,0 +1,22 @@ +/* Check In Level Names */ +"checkInResultT0" = "Gran Desventura"; +"checkInResultT1" = "Desventura"; +"checkInResultT2" = "Moderado"; +"checkInResultT3" = "Buena Fortuna"; +"checkInResultT4" = "Gran Fortuna"; +"checkInResultT5" = "Especial"; + +/* Widget UI Strings */ +"checkIn" = "Registrar"; +"tapToCheckIn" = "Toca para registrar hoy"; +"error" = "Error"; +"openAppToRefresh" = "Abre la aplicación para actualizar"; +"loading" = "Cargando..."; +"rewardPoints" = "%d"; +"rewardExperience" = "%d EXP"; +"footer" = "Registro Solian"; + +/* Notable Day Strings */ +"notableDayToday" = "%@ es hoy!"; +"notableDayIs" = "%@ es %@"; +"today" = "Hoy"; diff --git a/ios/SolianWidgetExtension/ja.lproj/Localizable.strings b/ios/SolianWidgetExtension/ja.lproj/Localizable.strings new file mode 100644 index 00000000..f1901b02 --- /dev/null +++ b/ios/SolianWidgetExtension/ja.lproj/Localizable.strings @@ -0,0 +1,22 @@ +/* Check In Level Names */ +"checkInResultT0" = "大凶"; +"checkInResultT1" = "凶"; +"checkInResultT2" = "中平"; +"checkInResultT3" = "吉"; +"checkInResultT4" = "大吉"; +"checkInResultT5" = "特殊"; + +/* Widget UI Strings */ +"checkIn" = "チェックイン"; +"tapToCheckIn" = "タップして今日チェックイン"; +"error" = "エラー"; +"openAppToRefresh" = "アプリを開いて更新"; +"loading" = "読み込み中..."; +"rewardPoints" = "%d"; +"rewardExperience" = "%d 経験値"; +"footer" = "Solian チェックイン"; + +/* Notable Day Strings */ +"notableDayToday" = "%@は今日です!"; +"notableDayIs" = "%@は%@です"; +"today" = "今日"; diff --git a/ios/SolianWidgetExtension/ko.lproj/Localizable.strings b/ios/SolianWidgetExtension/ko.lproj/Localizable.strings new file mode 100644 index 00000000..dd1e0971 --- /dev/null +++ b/ios/SolianWidgetExtension/ko.lproj/Localizable.strings @@ -0,0 +1,22 @@ +/* Check In Level Names */ +"checkInResultT0" = "대흉"; +"checkInResultT1" = "흉"; +"checkInResultT2" = "중평"; +"checkInResultT3" = "길"; +"checkInResultT4" = "대길"; +"checkInResultT5" = "특수"; + +/* Widget UI Strings */ +"checkIn" = "체크인"; +"tapToCheckIn" = "탭하여 오늘 체크인"; +"error" = "오류"; +"openAppToRefresh" = "앱을 열어 새로고침"; +"loading" = "로딩 중..."; +"rewardPoints" = "%d"; +"rewardExperience" = "%d 경험치"; +"footer" = "Solian 체크인"; + +/* Notable Day Strings */ +"notableDayToday" = "%@ 오늘입니다!"; +"notableDayIs" = "%@ 은/는 %@"; +"today" = "오늘"; diff --git a/ios/SolianWidgetExtension/zh-Hans.lproj/Localizable.strings b/ios/SolianWidgetExtension/zh-Hans.lproj/Localizable.strings index 319b7dce..4c71e100 100644 --- a/ios/SolianWidgetExtension/zh-Hans.lproj/Localizable.strings +++ b/ios/SolianWidgetExtension/zh-Hans.lproj/Localizable.strings @@ -14,3 +14,10 @@ "loading" = "加载中..."; "rewardPoints" = "%d"; "rewardExperience" = "%d 经验值"; +"footer" = "Solian 签到"; + +/* Notable Day Strings */ +"notableDayToday" = "%@是今天!"; +"notableDayIs" = "%@ 是 %@"; +"notableDayUpcoming" = "接下来"; +"today" = "今天"; diff --git a/ios/SolianWidgetExtension/zh-Hant.lproj/Localizable.strings b/ios/SolianWidgetExtension/zh-Hant.lproj/Localizable.strings new file mode 100644 index 00000000..28390d17 --- /dev/null +++ b/ios/SolianWidgetExtension/zh-Hant.lproj/Localizable.strings @@ -0,0 +1,22 @@ +/* Check In Level Names */ +"checkInResultT0" = "大凶"; +"checkInResultT1" = "凶"; +"checkInResultT2" = "中平"; +"checkInResultT3" = "吉"; +"checkInResultT4" = "大吉"; +"checkInResultT5" = "特殊"; + +/* Widget UI Strings */ +"checkIn" = "打卡"; +"tapToCheckIn" = "點擊今日打卡"; +"error" = "錯誤"; +"openAppToRefresh" = "打開應用以刷新"; +"loading" = "載入中..."; +"rewardPoints" = "%d"; +"rewardExperience" = "%d 經驗值"; +"footer" = "Solian 簽到"; + +/* Notable Day Strings */ +"notableDayToday" = "%@是今天!"; +"notableDayIs" = "%@ 是 %@"; +"today" = "今天";