| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202 |
- 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<RecordingActivityAttributes>
- ) -> 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
- )
- }
- }
|