RecordingLiveActivityWidget.swift 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157
  1. import ActivityKit
  2. import SwiftUI
  3. import WidgetKit
  4. struct RecordingLiveActivityWidget: Widget {
  5. var body: some WidgetConfiguration {
  6. ActivityConfiguration(for: RecordingActivityAttributes.self) { context in
  7. lockScreenView(context)
  8. .activityBackgroundTint(Color.black.opacity(0.92))
  9. .activitySystemActionForegroundColor(.white)
  10. .widgetURL(recordingURL(context.attributes))
  11. } dynamicIsland: { context in
  12. DynamicIsland {
  13. DynamicIslandExpandedRegion(.leading) {
  14. Label("正在记录", systemImage: "record.circle.fill")
  15. .font(.caption.weight(.semibold))
  16. .foregroundStyle(.red)
  17. }
  18. DynamicIslandExpandedRegion(.trailing) {
  19. elapsedTime(context.state)
  20. .font(.caption.monospacedDigit())
  21. .foregroundStyle(.white)
  22. }
  23. DynamicIslandExpandedRegion(.bottom) {
  24. HStack {
  25. Text(context.attributes.title)
  26. .font(.caption)
  27. .lineLimit(1)
  28. Spacer()
  29. quickAction(
  30. title: "拍照",
  31. systemImage: "camera.fill",
  32. url: actionURL("photo", context.attributes)
  33. )
  34. quickAction(
  35. title: "文字",
  36. systemImage: "note.text",
  37. url: actionURL("note", context.attributes)
  38. )
  39. }
  40. .foregroundStyle(.white)
  41. }
  42. } compactLeading: {
  43. Image(systemName: "record.circle.fill")
  44. .foregroundStyle(.red)
  45. } compactTrailing: {
  46. elapsedTime(context.state)
  47. .font(.caption2.monospacedDigit())
  48. .frame(maxWidth: 48)
  49. } minimal: {
  50. Image(systemName: "record.circle.fill")
  51. .foregroundStyle(.red)
  52. }
  53. .widgetURL(recordingURL(context.attributes))
  54. .keylineTint(.red)
  55. }
  56. }
  57. private func lockScreenView(
  58. _ context: ActivityViewContext<RecordingActivityAttributes>
  59. ) -> some View {
  60. VStack(alignment: .leading, spacing: 12) {
  61. HStack(spacing: 10) {
  62. Image(systemName: context.state.isPaused ? "pause.circle.fill" : "record.circle.fill")
  63. .font(.title2)
  64. .foregroundStyle(context.state.isPaused ? .orange : .red)
  65. VStack(alignment: .leading, spacing: 2) {
  66. Text(context.attributes.title)
  67. .font(.headline)
  68. .foregroundStyle(.white)
  69. .lineLimit(1)
  70. Text(context.state.isPaused ? "记录已暂停" : "正在记录 · \(context.attributes.sourceName)")
  71. .font(.caption)
  72. .foregroundStyle(.white.opacity(0.65))
  73. .lineLimit(1)
  74. }
  75. Spacer()
  76. elapsedTime(context.state)
  77. .font(.title3.monospacedDigit().weight(.medium))
  78. .foregroundStyle(.white)
  79. }
  80. HStack(spacing: 10) {
  81. quickAction(
  82. title: "拍照记录",
  83. systemImage: "camera.fill",
  84. url: actionURL("photo", context.attributes)
  85. )
  86. quickAction(
  87. title: "文字记录",
  88. systemImage: "note.text",
  89. url: actionURL("note", context.attributes)
  90. )
  91. }
  92. }
  93. .padding(16)
  94. }
  95. @ViewBuilder
  96. private func elapsedTime(
  97. _ state: RecordingActivityAttributes.ContentState
  98. ) -> some View {
  99. if state.isPaused {
  100. Text(formattedDuration(state.elapsedSeconds))
  101. } else {
  102. Text(
  103. timerInterval: state.timerStartDate...Date.distantFuture,
  104. pauseTime: nil,
  105. countsDown: false,
  106. showsHours: true
  107. )
  108. }
  109. }
  110. private func quickAction(
  111. title: String,
  112. systemImage: String,
  113. url: URL
  114. ) -> some View {
  115. Link(destination: url) {
  116. Label(title, systemImage: systemImage)
  117. .font(.caption.weight(.semibold))
  118. .foregroundStyle(.white)
  119. .frame(maxWidth: .infinity)
  120. .padding(.vertical, 8)
  121. .background(.white.opacity(0.12), in: RoundedRectangle(cornerRadius: 8))
  122. }
  123. }
  124. private func recordingURL(_ attributes: RecordingActivityAttributes) -> URL {
  125. URL(string: "celestiatrace://recording/\(attributes.sessionID.uuidString)")!
  126. }
  127. private func actionURL(
  128. _ action: String,
  129. _ attributes: RecordingActivityAttributes
  130. ) -> URL {
  131. URL(string: "celestiatrace://recording/\(attributes.sessionID.uuidString)/\(action)")!
  132. }
  133. private func formattedDuration(_ seconds: TimeInterval) -> String {
  134. let totalSeconds = max(0, Int(seconds))
  135. return String(
  136. format: "%02d:%02d:%02d",
  137. totalSeconds / 3600,
  138. (totalSeconds % 3600) / 60,
  139. totalSeconds % 60
  140. )
  141. }
  142. }