import ActivityKit import SwiftUI import WidgetKit struct RecordingLiveActivityWidget: Widget { var body: some WidgetConfiguration { ActivityConfiguration(for: RecordingActivityAttributes.self) { context in lockScreenView(context) .activityBackgroundTint(Color.black.opacity(0.92)) .activitySystemActionForegroundColor(.white) .widgetURL(recordingURL(context.attributes)) } dynamicIsland: { context in DynamicIsland { DynamicIslandExpandedRegion(.leading) { Label("正在记录", systemImage: "record.circle.fill") .font(.caption.weight(.semibold)) .foregroundStyle(.red) } DynamicIslandExpandedRegion(.trailing) { elapsedTime(context.state) .font(.caption.monospacedDigit()) .foregroundStyle(.white) } DynamicIslandExpandedRegion(.bottom) { VStack(spacing: 8) { Text(context.attributes.title) .font(.caption) .lineLimit(1) .frame(maxWidth: .infinity, alignment: .leading) HStack(spacing: 8) { islandAction( label: context.state.isPaused ? "继续" : "暂停", systemImage: context.state.isPaused ? "play.fill" : "pause.fill", url: actionURL("toggle-pause", context.attributes) ) islandAction( label: "结束", systemImage: "stop.fill", url: actionURL("stop", context.attributes), tint: .red ) islandAction( label: "拍照", systemImage: "camera.fill", url: actionURL("photo", context.attributes) ) islandAction( label: "文字", systemImage: "note.text", url: actionURL("note", context.attributes) ) } } .foregroundStyle(.white) } } compactLeading: { Image(systemName: "record.circle.fill") .foregroundStyle(.red) } compactTrailing: { elapsedTime(context.state) .font(.caption2.monospacedDigit()) .frame(maxWidth: 48) } minimal: { Image(systemName: "record.circle.fill") .foregroundStyle(.red) } .widgetURL(recordingURL(context.attributes)) .keylineTint(.red) } } private func lockScreenView( _ context: ActivityViewContext ) -> some View { VStack(alignment: .leading, spacing: 12) { HStack(spacing: 10) { Image(systemName: context.state.isPaused ? "pause.circle.fill" : "record.circle.fill") .font(.title2) .foregroundStyle(context.state.isPaused ? .orange : .red) VStack(alignment: .leading, spacing: 2) { Text(context.attributes.title) .font(.headline) .foregroundStyle(.white) .lineLimit(1) Text(context.state.isPaused ? "记录已暂停" : "正在记录 · \(context.attributes.sourceName)") .font(.caption) .foregroundStyle(.white.opacity(0.65)) .lineLimit(1) } Spacer() elapsedTime(context.state) .font(.title3.monospacedDigit().weight(.medium)) .foregroundStyle(.white) } VStack(spacing: 8) { HStack(spacing: 10) { quickAction( title: context.state.isPaused ? "继续记录" : "暂停记录", systemImage: context.state.isPaused ? "play.fill" : "pause.fill", url: actionURL("toggle-pause", context.attributes) ) quickAction( title: "结束记录", systemImage: "stop.fill", url: actionURL("stop", context.attributes), tint: .red ) } HStack(spacing: 10) { quickAction( title: "拍照记录", systemImage: "camera.fill", url: actionURL("photo", context.attributes) ) quickAction( title: "文字记录", systemImage: "note.text", url: actionURL("note", context.attributes) ) } } } .padding(16) } @ViewBuilder private func elapsedTime( _ state: RecordingActivityAttributes.ContentState ) -> some View { if state.isPaused { Text(formattedDuration(state.elapsedSeconds)) } else { Text( timerInterval: state.timerStartDate...Date.distantFuture, pauseTime: nil, countsDown: false, showsHours: true ) } } private func quickAction( title: String, systemImage: String, url: URL, tint: Color = .white ) -> some View { Link(destination: url) { Label(title, systemImage: systemImage) .font(.caption.weight(.semibold)) .foregroundStyle(tint) .frame(maxWidth: .infinity) .padding(.vertical, 8) .background(.white.opacity(0.12), in: RoundedRectangle(cornerRadius: 8)) } } private func islandAction( label: String, systemImage: String, url: URL, tint: Color = .white ) -> some View { Link(destination: url) { Image(systemName: systemImage) .font(.caption.weight(.semibold)) .foregroundStyle(tint) .frame(maxWidth: .infinity) .padding(.vertical, 7) .background(.white.opacity(0.12), in: RoundedRectangle(cornerRadius: 7)) .accessibilityLabel(label) } } private func recordingURL(_ attributes: RecordingActivityAttributes) -> URL { URL(string: "celestiatrace://recording/\(attributes.sessionID.uuidString)")! } private func actionURL( _ action: String, _ attributes: RecordingActivityAttributes ) -> URL { URL(string: "celestiatrace://recording/\(attributes.sessionID.uuidString)/\(action)")! } private func formattedDuration(_ seconds: TimeInterval) -> String { let totalSeconds = max(0, Int(seconds)) return String( format: "%02d:%02d:%02d", totalSeconds / 3600, (totalSeconds % 3600) / 60, totalSeconds % 60 ) } }