This commit is contained in:
2026-02-18 21:35:32 +01:00
parent 4b0cfaa699
commit 4e49741dfa
47 changed files with 3672 additions and 0 deletions

View File

@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "1.000",
"green" : "1.000",
"red" : "1.000"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Timer</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.widgetkit-extension</string>
</dict>
</dict>
</plist>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.application-groups</key>
<array>
<string>group.com.timetracker.app</string>
</array>
</dict>
</plist>

View File

@@ -0,0 +1,194 @@
import WidgetKit
import SwiftUI
struct TimerEntry: TimelineEntry {
let date: Date
let timer: WidgetTimer?
let projectName: String?
let projectColor: String?
}
struct WidgetTimer: Codable {
let id: String
let startTime: String
let projectId: String?
var elapsedTime: TimeInterval {
guard let start = ISO8601DateFormatter().date(from: startTime) else {
return 0
}
return Date().timeIntervalSince(start)
}
}
struct Provider: TimelineProvider {
private let appGroupIdentifier = "group.com.timetracker.app"
func placeholder(in context: Context) -> TimerEntry {
TimerEntry(
date: Date(),
timer: nil,
projectName: nil,
projectColor: nil
)
}
func getSnapshot(in context: Context, completion: @escaping (TimerEntry) -> Void) {
let entry = loadTimerEntry()
completion(entry)
}
func getTimeline(in context: Context, completion: @escaping (Timeline<TimerEntry>) -> Void) {
let entry = loadTimerEntry()
let nextUpdate = Calendar.current.date(byAdding: .minute, value: 15, to: Date())!
let timeline = Timeline(entries: [entry], policy: .after(nextUpdate))
completion(timeline)
}
private func loadTimerEntry() -> TimerEntry {
guard let userDefaults = UserDefaults(suiteName: appGroupIdentifier),
let data = userDefaults.data(forKey: "cachedTimer") else {
return TimerEntry(
date: Date(),
timer: nil,
projectName: nil,
projectColor: nil
)
}
do {
let timer = try JSONDecoder().decode(WidgetTimer.self, from: data)
return TimerEntry(
date: Date(),
timer: timer,
projectName: timer.projectId,
projectColor: nil
)
} catch {
return TimerEntry(
date: Date(),
timer: nil,
projectName: nil,
projectColor: nil
)
}
}
}
struct TimerWidgetEntryView: View {
var entry: Provider.Entry
@Environment(\.widgetFamily) var family
var body: some View {
switch family {
case .systemSmall:
smallWidget
case .systemMedium:
mediumWidget
default:
smallWidget
}
}
private var smallWidget: some View {
VStack(spacing: 8) {
if let timer = entry.timer {
Image(systemName: "timer")
.font(.title2)
.foregroundStyle(.green)
Text(timer.elapsedTime.formattedDuration)
.font(.system(size: 24, weight: .medium, design: .monospaced))
if let projectId = entry.projectName {
Text(projectId)
.font(.caption)
.foregroundStyle(.secondary)
.lineLimit(1)
}
} else {
Image(systemName: "timer")
.font(.title2)
.foregroundStyle(.secondary)
Text("No timer")
.font(.subheadline)
.foregroundStyle(.secondary)
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.containerBackground(for: .widget) {
Color(.systemBackground)
}
}
private var mediumWidget: some View {
HStack(spacing: 16) {
if let timer = entry.timer {
VStack(spacing: 4) {
Image(systemName: "timer")
.font(.title)
.foregroundStyle(.green)
Text(timer.elapsedTime.formattedDuration)
.font(.system(size: 28, weight: .medium, design: .monospaced))
}
VStack(alignment: .leading, spacing: 4) {
if let projectId = entry.projectName {
Text(projectId)
.font(.headline)
}
Text("Tap to open")
.font(.caption)
.foregroundStyle(.secondary)
}
} else {
VStack(spacing: 8) {
Image(systemName: "timer")
.font(.title)
.foregroundStyle(.secondary)
Text("No active timer")
.font(.subheadline)
.foregroundStyle(.secondary)
}
.frame(maxWidth: .infinity)
}
}
.padding()
.frame(maxWidth: .infinity, maxHeight: .infinity)
.containerBackground(for: .widget) {
Color(.systemBackground)
}
}
}
extension TimeInterval {
var formattedDuration: String {
let hours = Int(self) / 3600
let minutes = (Int(self) % 3600) / 60
let seconds = Int(self) % 60
if hours > 0 {
return String(format: "%d:%02d:%02d", hours, minutes, seconds)
} else {
return String(format: "%02d:%02d", minutes, seconds)
}
}
}
struct TimeTrackerWidget: Widget {
let kind: String = "TimeTrackerWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: Provider()) { entry in
TimerWidgetEntryView(entry: entry)
}
.configurationDisplayName("Timer")
.description("Shows your active timer.")
.supportedFamilies([.systemSmall, .systemMedium])
}
}

View File

@@ -0,0 +1,9 @@
import WidgetKit
import SwiftUI
@main
struct TimeTrackerWidgetBundle: WidgetBundle {
var body: some Widget {
TimeTrackerWidget()
}
}