tauri-plugin-live-activity 0.1.0-alpha.3 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.ko.md CHANGED
@@ -1,11 +1,12 @@
1
1
  # tauri-plugin-live-activity
2
2
 
3
- [![npm](https://img.shields.io/npm/v/tauri-plugin-live-activity/next)](https://www.npmjs.com/package/tauri-plugin-live-activity)
3
+ [![npm](https://img.shields.io/npm/v/tauri-plugin-live-activity)](https://www.npmjs.com/package/tauri-plugin-live-activity)
4
4
  [![crates.io](https://img.shields.io/crates/v/tauri-plugin-live-activity)](https://crates.io/crates/tauri-plugin-live-activity)
5
5
  [![CI](https://github.com/1eedaegon/tauri-plugin-live-activity/actions/workflows/ci.yml/badge.svg)](https://github.com/1eedaegon/tauri-plugin-live-activity/actions/workflows/ci.yml)
6
6
 
7
- Tauri 2에서 iOS Live Activity와 Android 진행 알림을 표시하는 커뮤니티 플러그인입니다.
8
- **초기 알파 버전입니다.** [English](https://github.com/1eedaegon/tauri-plugin-live-activity/blob/main/README.md)
7
+ Tauri 2에서 iOS Live Activities(ActivityKit·Dynamic Island)와 Android Live Updates를 표시하는 커뮤니티 플러그인입니다.
8
+ Android에서는 지원 조건에 따라 일반 진행 알림으로 표시합니다.
9
+ **1.0 이전에는 마이너 버전이 바뀔 때 API도 바뀔 수 있습니다.** [English](https://github.com/1eedaegon/tauri-plugin-live-activity/blob/main/README.md)
9
10
 
10
11
  | 플랫폼 | 표시 방식 |
11
12
  | --- | --- |
@@ -13,97 +14,567 @@ Tauri 2에서 iOS Live Activity와 Android 진행 알림을 표시하는 커뮤
13
14
  | Android API 26+ | 진행 알림 · API 36+에서 조건 충족 시 Live Update 승격 |
14
15
  | macOS / Windows / Linux | 미구현 |
15
16
 
17
+ ## 데모
18
+
16
19
  <table>
17
- <tr><th>Mochi 데모</th><th>iOS Live Activity</th><th>Android Live Update</th></tr>
20
+ <tr><th>iOS 잠금 화면</th><th>iOS Dynamic Island</th></tr>
21
+ <tr>
22
+ <td><img src="https://raw.githubusercontent.com/1eedaegon/tauri-plugin-live-activity/main/docs/images/ios-layout-paths.png" width="400" alt="iOS 잠금 화면에 SVG 경로로 그린 Mochi 얼굴"></td>
23
+ <td><img src="https://raw.githubusercontent.com/1eedaegon/tauri-plugin-live-activity/main/docs/images/ios-island-expanded.png" width="400" alt="펼친 Dynamic Island에 표시한 Mochi의 간식"></td>
24
+ </tr>
25
+ <tr><th>Android 알림</th><th>Samsung Now Bar</th></tr>
18
26
  <tr>
19
- <td><img src="https://raw.githubusercontent.com/1eedaegon/tauri-plugin-live-activity/main/docs/images/mochi-demo.png" width="240" alt="고양이 Mochi의 간식 배달을 보여 주는 Tauri 데모 앱"></td>
20
- <td><img src="https://raw.githubusercontent.com/1eedaegon/tauri-plugin-live-activity/main/docs/images/mochi-ios-live-activity.png" width="240" alt="iPhone 잠금 화면의 Mochi 간식 배달과 낮잠 타이머"></td>
21
- <td><img src="https://raw.githubusercontent.com/1eedaegon/tauri-plugin-live-activity/main/docs/images/mochi-android-live-activity.png" width="240" alt="Mochi의 간식 배달 진행률을 표시한 Android 알림"></td>
27
+ <td><img src="https://raw.githubusercontent.com/1eedaegon/tauri-plugin-live-activity/main/docs/images/android-layout-android.png" width="400" alt="진행 막대와 Wake Mochi 액션이 있는 Android 알림의 Mochi 낮잠"></td>
28
+ <td><img src="https://raw.githubusercontent.com/1eedaegon/tauri-plugin-live-activity/main/docs/images/samsung-now-bar.png" width="400" alt="잠긴 Galaxy S23의 One UI Now Bar에 표시한 Mochi의 간식 배달"></td>
22
29
  </tr>
23
30
  </table>
24
31
 
25
- iOS 27.2 시뮬레이터와 Android API 37 에뮬레이터에서 직접 캡처했습니다.
26
- 예제 앱이 전달한 간식 배달 진행률과 낮잠 시간을 실제 네이티브 UI로 표시한 화면입니다.
27
- [Dynamic Island 화면](https://raw.githubusercontent.com/1eedaegon/tauri-plugin-live-activity/main/docs/images/mochi-ios-dynamic-island.png)도 확인할 수 있습니다.
28
- 구형 Android에서는 같은 예제가 [일반 진행 알림](https://raw.githubusercontent.com/1eedaegon/tauri-plugin-live-activity/main/docs/images/mochi-android-notification-fallback.png)으로 표시됩니다.
32
+ 고양이 Mochi를 따라가는 [데모 앱](https://github.com/1eedaegon/tauri-plugin-live-activity/tree/main/examples/demo)에서
33
+ 캡처했습니다. iOS는 iPhone 17 시뮬레이터(iOS 26.3), Android는 Android 16(API 36) 에뮬레이터,
34
+ Now Bar는 잠긴 Galaxy S23(One UI 9, Android 17)입니다.
29
35
 
30
36
  ## 설치
31
37
 
32
- Tauri 2와 Rust 1.90 이상이 필요합니다.
33
- Android는 API 26 이상과 업데이트된 WebView가 필요하며, 빌드에는 Android SDK 37을 사용합니다.
34
- 기능별 OS 조건과 도구 버전은 [호환성 정책](https://github.com/1eedaegon/tauri-plugin-live-activity/blob/main/docs/compatibility.md)에 정리했습니다.
38
+ Tauri 2와 Rust 1.90 이상이 필요합니다. 아래 명령은 모두 앱의 루트 폴더에서 실행합니다.
39
+ npm 패키지와 crate는 같은 마이너 버전을 사용해야 합니다.
35
40
 
36
41
  ```sh
37
- npm install tauri-plugin-live-activity@next
42
+ npm i tauri-plugin-live-activity@0.2 && cargo add tauri-plugin-live-activity@0.2 --manifest-path src-tauri/Cargo.toml
43
+ for p in default allow-request-permissions allow-start allow-update allow-end allow-start-layout allow-update-layout allow-end-layout; do npx tauri permission add live-activity:$p; done
38
44
  ```
39
45
 
40
- `src-tauri/Cargo.toml`:
41
-
42
- ```toml
43
- [dependencies]
44
- tauri-plugin-live-activity = "=0.1.0-alpha.3"
45
- ```
46
-
47
- Tauri 빌더에 등록합니다.
46
+ 그다음 `src-tauri/src/lib.rs`에서 플러그인을 등록합니다.
48
47
 
49
48
  ```rust
50
49
  tauri::Builder::default()
51
50
  .plugin(tauri_plugin_live_activity::init())
52
- // ...
53
51
  ```
54
52
 
55
- 앱 capability의 `permissions`에 추가합니다.
53
+ ### iOS
56
54
 
57
- ```json
58
- [
59
- "live-activity:default",
60
- "live-activity:allow-request-permissions",
61
- "live-activity:allow-start",
62
- "live-activity:allow-update",
63
- "live-activity:allow-end"
64
- ]
55
+ 활동은 플러그인 템플릿으로 만든 위젯 확장이 그립니다. Mac에서 `tauri ios init`을 실행한 뒤,
56
+ 같은 셸에서 아래 명령을 차례로 실행합니다. 각 줄이 하는 일은 다음과 같습니다.
57
+
58
+ 1. 플러그인 폴더를 찾습니다. `jq`는 macOS 15부터 기본으로 들어 있습니다.
59
+ 2. 위젯, 버튼 인텐트, 확장의 Info.plist를 복사합니다. 이 Info.plist에는 `TauriLiveActivityLayout` = 2가
60
+ 들어 있습니다.
61
+ 3. `src-tauri/Info.ios.plist`에 `NSSupportsLiveActivities`를 추가합니다. Tauri가 이 파일을 앱의
62
+ Info.plist에 합칩니다.
63
+ 4. 플러그인 Swift 패키지가 쓰는 Tauri 의존성을 준비합니다. Apple silicon Mac에서는
64
+ `--target aarch64-apple-ios-sim`을 씁니다.
65
+
66
+ ```sh
67
+ LA=$(cargo metadata --format-version 1 --manifest-path src-tauri/Cargo.toml | jq -r '.packages[] | select(.name == "tauri-plugin-live-activity") | .manifest_path | rtrimstr("Cargo.toml")')
68
+ mkdir -p src-tauri/gen/apple/LiveActivityWidget && cp "$LA"templates/ios/* src-tauri/gen/apple/LiveActivityWidget/
69
+ /usr/libexec/PlistBuddy -c 'Add :NSSupportsLiveActivities bool true' src-tauri/Info.ios.plist
70
+ IPHONEOS_DEPLOYMENT_TARGET=16.1 cargo check --manifest-path src-tauri/Cargo.toml --target x86_64-apple-ios
65
71
  ```
66
72
 
67
- **iOS는 Widget Extension 설정이 필요합니다.**
68
- [iOS 설정](https://github.com/1eedaegon/tauri-plugin-live-activity/blob/main/docs/ios-integration.md) ·
69
- [Android 설정](https://github.com/1eedaegon/tauri-plugin-live-activity/blob/main/docs/android-integration.md)
73
+ Tauri는 XcodeGen으로 Xcode 프로젝트를 만들고, 새 타깃은 XcodeGen YAML 블록이라 한 줄로
74
+ 쓸 수 없습니다. 아래 블록을 붙여 넣고 마지막 줄을 실행합니다.
75
+
76
+ ```sh
77
+ cat > src-tauri/gen/apple/live-activity.yml <<EOF
78
+ packages:
79
+ LiveActivity: { path: "${LA}ios" }
80
+ targets:
81
+ $(sed -n 's/^name: //p' src-tauri/gen/apple/project.yml)_iOS:
82
+ sources: [LiveActivityWidget/LiveActivityActionIntent.swift]
83
+ dependencies: [{ target: LiveActivityWidget }]
84
+ settings: { base: { SWIFT_VERSION: "5.0" } }
85
+ LiveActivityWidget:
86
+ type: app-extension
87
+ platform: iOS
88
+ deploymentTarget: "16.1"
89
+ sources: [LiveActivityWidget]
90
+ dependencies: [{ package: LiveActivity, product: LiveActivityShared }, { package: LiveActivity, product: LiveActivityUI }]
91
+ info: { path: LiveActivityWidget/Info.plist, properties: { NSExtension: { NSExtensionPointIdentifier: com.apple.widgetkit-extension }, TauriLiveActivityLayout: 2 } }
92
+ settings: { base: { PRODUCT_BUNDLE_IDENTIFIER: $(jq -r .identifier src-tauri/tauri.conf.json).widget, SWIFT_VERSION: "5.0" } }
93
+ EOF
94
+ printf '\ninclude: [live-activity.yml]\n' >> src-tauri/gen/apple/project.yml && xcodegen generate --spec src-tauri/gen/apple/project.yml
95
+ ```
70
96
 
71
- ## 사용 예시
97
+ 인텐트 파일은 두 타깃에 모두 들어갑니다. 위젯은 이 파일로 버튼을 만들고, 시스템은 인텐트를
98
+ 앱에서 실행합니다. `tauri ios init`을 다시 실행하면 `project.yml`이 새로 써지므로 마지막 줄을
99
+ 다시 실행합니다. 확장의 버전은 1.0 (1)이 되니, App Store에 올리기 전에 앱과 같은 버전으로 맞춥니다.
100
+
101
+ ### Android
102
+
103
+ 알림 권한과 리시버는 플러그인 매니페스트가 추가합니다. 빌드에는 Android SDK 37이 필요합니다.
104
+ 최소 버전은 Android 8(API 26)인데, 새 Tauri 앱은 24로 시작합니다. 첫 줄은 `tauri android init`에
105
+ 쓰일 값을, 둘째 줄은 이미 만든 프로젝트의 값을 바꿉니다.
106
+
107
+ ```sh
108
+ node -e 'f="src-tauri/tauri.conf.json";c=require("./"+f);(c.bundle.android??={}).minSdkVersion=26;require("fs").writeFileSync(f,JSON.stringify(c,null,2)+"\n")'
109
+ perl -pi -e 's/minSdk = 2[0-5]$/minSdk = 26/' src-tauri/gen/android/app/build.gradle.kts
110
+ ```
111
+
112
+ ## 사용법
113
+
114
+ 플러그인은 앱이 보낸 상태를 표시합니다. 실제 작업, 백그라운드 실행, 갱신 주기는 앱이
115
+ 담당합니다. 활동이 어디에 보일지는 OS 권한, 집중 모드, 승격 규칙이 정합니다.
116
+ `fallback: 'none'`은 네이티브 백엔드를 쓸 수 없을 때 시작을 거부할 뿐, Android가 알림을
117
+ 승격하도록 강제하지는 못합니다.
118
+
119
+ 캡처는 iPhone 17 시뮬레이터(iOS 26.3)와 Android 16 에뮬레이터에서 했습니다. 데모는 여기
120
+ 코드에 Mochi의 색과 여백을 더했습니다.
121
+
122
+ ### 진행률
72
123
 
73
124
  ```ts
74
- import { start, update, end } from 'tauri-plugin-live-activity';
125
+ import { getCapabilities, requestPermissions, start, update, end } from 'tauri-plugin-live-activity';
126
+
127
+ const { platform } = await getCapabilities();
128
+ await requestPermissions(platform === 'android' ? 'notifications' : 'liveActivities');
75
129
 
76
- // 먼저 Android 알림 권한 또는 iOS Live Activity 설정을 확인합니다.
77
130
  const snack = await start({
78
131
  activityKey: 'mochi:snack',
79
132
  schemaVersion: 1,
80
133
  template: 'progress',
81
134
  fallback: 'notification',
82
- snapshot: {
83
- revision: 0, title: 'Mochi의 간식 배달', status: '작은 간식을 포장하는 중', progress: 0.15,
84
- },
135
+ snapshot: { revision: 0, title: 'Mochi’s snack delivery', status: 'Packing a tiny treat', progress: 0.15 },
85
136
  });
86
-
87
137
  await update({
88
138
  id: snack.id,
139
+ snapshot: { revision: 1, title: 'Mochi’s snack delivery', status: 'On the way to Mochi', progress: 0.65 },
140
+ });
141
+ await end({ id: snack.id, policy: { kind: 'immediate' } });
142
+ ```
143
+
144
+ 갱신할 때마다 `revision`을 올리고 snapshot 전체를 보냅니다. 앱을 다시 실행한 뒤에는 `list()`로
145
+ 활동을 다시 찾습니다.
146
+
147
+ <table>
148
+ <tr><th>iOS 26.3 시뮬레이터</th><th>Android 16 에뮬레이터</th></tr>
149
+ <tr>
150
+ <td><img src="https://raw.githubusercontent.com/1eedaegon/tauri-plugin-live-activity/main/docs/images/ios-v1-progress.png" width="400" alt="iOS 잠금 화면에서 65%인 Mochi의 간식 배달"></td>
151
+ <td><img src="https://raw.githubusercontent.com/1eedaegon/tauri-plugin-live-activity/main/docs/images/android-v1-progress.png" width="400" alt="Android 알림 창에서 진행 막대가 있는 Mochi의 간식 배달"></td>
152
+ </tr>
153
+ </table>
154
+
155
+ iOS의 고양이 그림은 데모 앱의 자체 위젯이 그립니다. 플러그인 템플릿은 같은 제목, 상태,
156
+ 진행 막대를 표시합니다.
157
+
158
+ ### 타이머
159
+
160
+ ```ts
161
+ import { start } from 'tauri-plugin-live-activity';
162
+
163
+ const now = Date.now();
164
+ await start({
165
+ activityKey: 'mochi:nap',
166
+ schemaVersion: 1,
167
+ template: 'timer',
168
+ fallback: 'notification',
89
169
  snapshot: {
90
- revision: 1, title: 'Mochi의 간식 배달', status: 'Mochi에게 가는 중', progress: 0.65,
170
+ revision: 0, title: 'Mochi’s nap', status: 'A little snooze, a little peace',
171
+ timer: { startedAt: now, endsAt: now + 5 * 60_000 },
91
172
  },
92
173
  });
174
+ ```
93
175
 
94
- await end({ id: snack.id, policy: { kind: 'immediate' } });
176
+ 시각은 Unix 밀리초입니다. 카운트다운은 시스템이 하므로 앱이 매초 보낼 것은 없습니다.
177
+
178
+ <table>
179
+ <tr><th>iOS 26.3 시뮬레이터</th><th>Android 16 에뮬레이터</th></tr>
180
+ <tr>
181
+ <td><img src="https://raw.githubusercontent.com/1eedaegon/tauri-plugin-live-activity/main/docs/images/ios-v1-timer.png" width="400" alt="iOS 잠금 화면에서 4:48부터 줄어드는 Mochi의 낮잠"></td>
182
+ <td><img src="https://raw.githubusercontent.com/1eedaegon/tauri-plugin-live-activity/main/docs/images/android-v1-timer.png" width="400" alt="Android 알림 머리글의 카운트다운과 Mochi의 낮잠"></td>
183
+ </tr>
184
+ </table>
185
+
186
+ ## 레이아웃
187
+
188
+ 레이아웃을 쓰면 앱이 활동의 화면을 직접 구성합니다. 노드 트리, 스타일 표, 값 맵을 보내면
189
+ iOS에서는 플러그인의 위젯 템플릿이 그리므로 앱에 SwiftUI 코드가 필요 없습니다. Android는
190
+ 그중 일부를 알림으로 보여 줍니다. 트리는 시작할 때 고정되고, 갱신할 때는 같은 키와 타입으로
191
+ 값만 보냅니다. 레이아웃·스타일·값은 ActivityKit의 4 KiB 한도를 함께 씁니다.
192
+
193
+ ### 텍스트, 값, 스타일
194
+
195
+ ```ts
196
+ import { layout, startLayout, updateLayout, endLayout } from 'tauri-plugin-live-activity';
197
+
198
+ const { vstack, bound, text } = layout;
199
+
200
+ const card = await startLayout({
201
+ activityKey: 'mochi:text',
202
+ schemaVersion: 2,
203
+ fallback: 'notification',
204
+ layout: {
205
+ lockScreen: vstack([
206
+ bound('title', { style: 'title' }),
207
+ bound('status', { style: 'status', unless: 'arrived' }),
208
+ text('Snack time! Made it home', { style: 'done', if: 'arrived' }),
209
+ ], { alignment: 'leading' }),
210
+ },
211
+ styles: {
212
+ title: { font: 'headline' },
213
+ status: { font: 'subheadline', color: 'secondary' },
214
+ done: { font: 'subheadline', weight: 'bold', color: '#C9654D' },
215
+ },
216
+ snapshot: { revision: 0, values: { title: 'Mochi’s snack', status: 'Packing a tiny treat', arrived: false } },
217
+ });
218
+
219
+ await updateLayout({
220
+ id: card.id,
221
+ snapshot: { revision: 1, values: { title: 'Mochi’s snack', status: 'Packing a tiny treat', arrived: true } },
222
+ });
223
+
224
+ await endLayout({ id: card.id, policy: { kind: 'immediate' } });
225
+ ```
226
+
227
+ `bound`는 값을, `text`는 고정 문자열을 표시합니다. `if`는 값이 참으로 평가될 때만, `unless`는
228
+ 그렇지 않을 때만 노드를 보여 줍니다. 아래 예시는 `fallback` 다음의 멤버만 보여 주며, 빌더는 모두
229
+ `layout`에 있습니다.
230
+
231
+ <table>
232
+ <tr><th></th><th>iOS 26.3 시뮬레이터</th><th>Android 16 에뮬레이터</th></tr>
233
+ <tr>
234
+ <td><code>arrived: false</code></td>
235
+ <td><img src="https://raw.githubusercontent.com/1eedaegon/tauri-plugin-live-activity/main/docs/images/ios-layout-text.png" width="380" alt="iOS 잠금 화면의 제목과 상태 텍스트"></td>
236
+ <td><img src="https://raw.githubusercontent.com/1eedaegon/tauri-plugin-live-activity/main/docs/images/android-layout-text.png" width="380" alt="같은 제목과 상태를 표시한 Android 알림"></td>
237
+ </tr>
238
+ <tr>
239
+ <td><code>arrived: true</code></td>
240
+ <td><img src="https://raw.githubusercontent.com/1eedaegon/tauri-plugin-live-activity/main/docs/images/ios-layout-text-arrived.png" width="380" alt="iOS에서 상태 대신 굵은 산호색 Snack time 텍스트"></td>
241
+ <td><img src="https://raw.githubusercontent.com/1eedaegon/tauri-plugin-live-activity/main/docs/images/android-layout-text-arrived.png" width="380" alt="Android에서 본문이 Snack time으로 바뀐 알림"></td>
242
+ </tr>
243
+ </table>
244
+
245
+ Android는 스타일을 버리고 첫 텍스트를 제목, 둘째 텍스트를 본문으로 씁니다.
246
+
247
+ ### SF Symbol
248
+
249
+ ```ts
250
+ layout: {
251
+ lockScreen: hstack([
252
+ symbol('cat.fill'),
253
+ symbol('heart.fill', { rendering: 'multicolor' }),
254
+ symbol('speaker.wave.3.fill', { variableBind: 'purr' }),
255
+ bound('title'),
256
+ ], { spacing: 12 }),
257
+ },
258
+ snapshot: { revision: 0, values: { title: 'Purr meter', purr: 0.35 } },
259
+ ```
260
+
261
+ `variableBind`는 0과 1 사이의 숫자로 가변 심볼을 채웁니다.
262
+
263
+ | iOS 26.3 시뮬레이터 | Android |
264
+ | --- | --- |
265
+ | <img src="https://raw.githubusercontent.com/1eedaegon/tauri-plugin-live-activity/main/docs/images/ios-layout-symbols.png" width="400" alt="iOS 잠금 화면의 고양이, 다색 하트, 35% 채운 스피커 심볼"> | 표시하지 않습니다. `degradedFeatures`에 `layout.symbol`로 남깁니다. |
266
+
267
+ ### 이미지
268
+
269
+ ```ts
270
+ layout: {
271
+ lockScreen: hstack([image('mochi', { resizable: true, style: 'portrait' }), bound('title')], { spacing: 12 }),
272
+ },
273
+ styles: { portrait: { frame: { width: 56, height: 56 } } },
274
+ snapshot: { revision: 0, values: { title: 'Mochi’s portrait' } },
275
+ ```
276
+
277
+ `image`는 위젯 확장의 에셋 카탈로그에 있는 그림의 이름입니다. `src-tauri/gen/apple/LiveActivityWidget`에
278
+ `Assets.xcassets` 폴더를 두고 `xcodegen generate --spec src-tauri/gen/apple/project.yml`을 실행하면
279
+ 위젯 타깃에 들어갑니다. 데모의 `mochi`는 SVG 파일에서 만든 PNG입니다.
280
+
281
+ | iOS 26.3 시뮬레이터 | Android |
282
+ | --- | --- |
283
+ | <img src="https://raw.githubusercontent.com/1eedaegon/tauri-plugin-live-activity/main/docs/images/ios-layout-image.png" width="400" alt="iOS 잠금 화면에 위젯 에셋 카탈로그에서 가져온 Mochi 초상화"> | 표시하지 않습니다. `layout.image`로 남깁니다. |
284
+
285
+ ### 경로(SVG path 데이터)
286
+
287
+ ```ts
288
+ const head = 'M14 40C12 32 11 22 14 14Q15 11 18 13L35 27Q50 23 65 27L82 13Q85 11 86 14C89 22 88 32 86 40C93 48 94 61 87 71C79 81 66 85 50 85C34 85 21 81 13 71C6 61 7 48 14 40Z';
289
+ layout: {
290
+ lockScreen: zstack([
291
+ path(head, [100, 90], { style: 'fur' }),
292
+ path(head, [100, 90], { stroke: 1.5 }),
293
+ path('M28 54Q33 58 38 54M62 54Q67 58 72 54', [100, 90], { stroke: 2 }),
294
+ path('M48 64Q50 62 52 64Q50 66 48 64Z', [100, 90], { style: 'nose' }),
295
+ ], { style: 'face' }),
296
+ },
297
+ styles: { face: { frame: { width: 60, height: 54 } }, fur: { color: '#F6E9CD' }, nose: { color: '#C9654D' } },
298
+ snapshot: { revision: 0, values: {} },
299
+ ```
300
+
301
+ `path`는 SVG `<path>`의 `d`를 `[너비, 높이]` 크기의 `box` 안에 비율을 지켜 그립니다. 기본은
302
+ 채우기이고, `stroke`로 선 굵기를 주면 선으로 그립니다. `d`는 `M L H V C S Q T Z` 명령을
303
+ 512바이트까지 받습니다. 호(`A`)는 지원하지 않으니 먼저 곡선으로 바꿉니다.
304
+
305
+ | iOS 26.3 시뮬레이터 | Android |
306
+ | --- | --- |
307
+ | <img src="https://raw.githubusercontent.com/1eedaegon/tauri-plugin-live-activity/main/docs/images/ios-layout-paths.png" width="400" alt="iOS 잠금 화면에 경로 네 개로 그린 Mochi 얼굴"> | 표시하지 않습니다. `layout.path`로 남깁니다. |
308
+
309
+ ### 게이지
310
+
311
+ ```ts
312
+ layout: {
313
+ lockScreen: hstack([gauge('bowl', { max: 100, kind: 'circular' }), gauge('bowl', { max: 100 })], { spacing: 12 }),
314
+ },
315
+ snapshot: { revision: 0, values: { bowl: 80 } },
316
+ ```
317
+
318
+ 게이지는 `min`과 `max`(생략하면 0과 1) 사이의 숫자를 원이나 막대로 보여 줍니다.
319
+
320
+ | iOS 26.3 시뮬레이터 | Android |
321
+ | --- | --- |
322
+ | <img src="https://raw.githubusercontent.com/1eedaegon/tauri-plugin-live-activity/main/docs/images/ios-layout-gauge.png" width="400" alt="iOS 잠금 화면에서 100 중 80을 가리키는 원형·막대 게이지"> | 표시하지 않습니다. `layout.gauge`로 남깁니다. |
323
+
324
+ ### 진행 표시
325
+
326
+ ```ts
327
+ layout: {
328
+ lockScreen: vstack([bound('title'), progress('done', { kind: 'circular' }), progress('done')]),
329
+ },
330
+ snapshot: { revision: 0, values: { title: 'Mochi’s snack delivery', done: 0.65 } },
331
+ ```
332
+
333
+ 진행 표시는 0부터 1까지의 숫자를 막대나 원으로 보여 줍니다.
334
+
335
+ <table>
336
+ <tr><th>iOS 26.3 시뮬레이터</th><th>Android 16 에뮬레이터</th></tr>
337
+ <tr>
338
+ <td><img src="https://raw.githubusercontent.com/1eedaegon/tauri-plugin-live-activity/main/docs/images/ios-layout-progress.png" width="400" alt="iOS 잠금 화면에서 65%인 원과 막대"></td>
339
+ <td><img src="https://raw.githubusercontent.com/1eedaegon/tauri-plugin-live-activity/main/docs/images/android-layout-progress.png" width="400" alt="Android 알림의 진행 막대가 된 첫 진행 표시"></td>
340
+ </tr>
341
+ </table>
342
+
343
+ Android는 첫 진행 표시를 알림의 진행 막대로 씁니다.
344
+
345
+ ### 타이머 텍스트
346
+
347
+ ```ts
348
+ layout: {
349
+ lockScreen: vstack([bound('title'), timerText('start', 'end')]),
350
+ },
351
+ snapshot: { revision: 0, values: { title: 'Mochi’s nap', start: Date.now(), end: Date.now() + 5 * 60_000 } },
352
+ ```
353
+
354
+ `timerText`는 `end` 값까지 거꾸로 세고, `countsDown: false`이면 `start`부터 셉니다.
355
+
356
+ <table>
357
+ <tr><th>iOS 26.3 시뮬레이터</th><th>Android 16 에뮬레이터</th></tr>
358
+ <tr>
359
+ <td><img src="https://raw.githubusercontent.com/1eedaegon/tauri-plugin-live-activity/main/docs/images/ios-layout-timer.png" width="400" alt="iOS 잠금 화면에서 4:50부터 줄어드는 Mochi의 낮잠"></td>
360
+ <td><img src="https://raw.githubusercontent.com/1eedaegon/tauri-plugin-live-activity/main/docs/images/android-layout-timer.png" width="400" alt="Android 알림 머리글의 크로노미터가 된 카운트다운"></td>
361
+ </tr>
362
+ </table>
363
+
364
+ Android는 첫 타이머를 알림의 크로노미터로 씁니다.
365
+
366
+ ### 스택
367
+
368
+ ```ts
369
+ layout: {
370
+ lockScreen: vstack([
371
+ hstack([zstack([symbol('circle.fill'), symbol('pawprint.fill', { style: 'mark' })]), bound('title'), spacer(), bound('eta')]),
372
+ divider(),
373
+ hstack([bound('from'), spacer(), bound('to')]),
374
+ ], { spacing: 10 }),
375
+ },
376
+ styles: { mark: { size: 13, color: 'white' } },
377
+ snapshot: { revision: 0, values: { title: 'Snack route', eta: '4 min', from: 'Kitchen', to: 'Mochi’s bowl' } },
95
378
  ```
96
379
 
97
- 갱신할 때는 `revision`을 늘리고 전체 snapshot을 보냅니다.
98
- 재실행 후에는 `list()`로 상태를 다시 조회합니다. 타이머는 `template: 'timer'`와
99
- `timer: { startedAt, endsAt }`을 사용하며, 시각 단위는 Unix 밀리초입니다.
380
+ `vstack`, `hstack`, `zstack`은 노드를 세로로, 가로로, 겹쳐서 놓습니다. `spacer`는 남는 공간을
381
+ 차지하고 `divider`는 구분선을 그립니다.
382
+
383
+ | iOS 26.3 시뮬레이터 | Android |
384
+ | --- | --- |
385
+ | <img src="https://raw.githubusercontent.com/1eedaegon/tauri-plugin-live-activity/main/docs/images/ios-layout-stacks.png" width="400" alt="iOS 잠금 화면의 발바닥 배지·제목·시간 한 줄, 구분선, 아래의 From과 To"> | 첫 두 텍스트만 제목과 본문으로 표시합니다. 캡처는 없습니다. |
100
386
 
101
- 플러그인은 상태를 표시합니다. 실제 작업과 백그라운드 실행은 호스트 앱이 담당합니다.
102
- 알림 표시와 승격 여부는 OS 설정에 따라 달라집니다.
387
+ ### 버튼
388
+
389
+ ```ts
390
+ layout: {
391
+ lockScreen: vstack([
392
+ bound('title'),
393
+ bound('status'),
394
+ hstack([button('pet', text('Pet Mochi')), button('done', text('All done'))], { spacing: 8 }),
395
+ ], { alignment: 'leading' }),
396
+ },
397
+ snapshot: { revision: 0, values: { title: 'Mochi wants attention', status: 'No pets yet', pets: 0 } },
398
+ ```
399
+
400
+ 버튼을 탭하면 버튼의 액션 ID가 앱에 전달됩니다. 탭만으로는 활동이 바뀌지 않으니 핸들러에서
401
+ 갱신하거나 종료합니다. 웹뷰가 실행 중이면 `onAction`이 탭을 받습니다.
402
+
403
+ ```ts
404
+ import { endLayout, onAction } from 'tauri-plugin-live-activity';
405
+
406
+ await onAction(async ({ actionId, activityId }) => {
407
+ if (actionId === 'done') await endLayout({ id: activityId, policy: { kind: 'immediate' } });
408
+ });
409
+ ```
410
+
411
+ 시스템이 탭을 처리하려고 앱을 백그라운드에서 실행하면 Tauri가 웹뷰를 만들지 않을 수 있습니다.
412
+ 그때도 동작해야 하는 탭은 Rust에서 처리합니다. 빌드한 `App`에 `run` 전에 핸들러를 등록합니다.
413
+
414
+ ```rust
415
+ use tauri_plugin_live_activity::{EndInput, EndPolicy, LiveActivityExt};
416
+
417
+ let app = tauri::Builder::default()
418
+ .plugin(tauri_plugin_live_activity::init())
419
+ .build(tauri::generate_context!())
420
+ .expect("error while building tauri application");
421
+ let handle = app.handle().clone();
422
+ app.live_activity().on_action(move |event| {
423
+ if event.action_id == "done" {
424
+ let input = EndInput { id: event.activity_id, policy: EndPolicy::Immediate {} };
425
+ let _ = handle.live_activity().end_layout(input);
426
+ }
427
+ });
428
+ app.run(|_, _| {});
429
+ ```
430
+
431
+ Rust 핸들러가 먼저 플러그인 스레드에서 실행되며, 핸들러 안에서 플러그인을 호출해도 됩니다.
432
+ `onAction`에는 `core:event:default` 권한이 필요한데, 새 앱에는 이미 들어 있습니다.
433
+
434
+ <table>
435
+ <tr><th></th><th>iOS 26.3 시뮬레이터</th><th>Android 16 에뮬레이터</th></tr>
436
+ <tr>
437
+ <td>시작</td>
438
+ <td><img src="https://raw.githubusercontent.com/1eedaegon/tauri-plugin-live-activity/main/docs/images/ios-layout-buttons-before.png" width="380" alt="iOS 잠금 화면의 Pet Mochi와 All done 버튼"></td>
439
+ <td><img src="https://raw.githubusercontent.com/1eedaegon/tauri-plugin-live-activity/main/docs/images/android-layout-buttons-before.png" width="380" alt="Android 알림 액션이 된 Pet Mochi와 All done"></td>
440
+ </tr>
441
+ <tr>
442
+ <td>Pet Mochi 탭 후</td>
443
+ <td><img src="https://raw.githubusercontent.com/1eedaegon/tauri-plugin-live-activity/main/docs/images/ios-layout-buttons-after.png" width="380" alt="iOS에서 한 번 탭한 뒤 Purring. 1 pet so far로 바뀐 상태"></td>
444
+ <td><img src="https://raw.githubusercontent.com/1eedaegon/tauri-plugin-live-activity/main/docs/images/android-layout-buttons-after.png" width="380" alt="Android에서 두 번 탭한 뒤 Purring. 2 pets so far로 바뀐 본문"></td>
445
+ </tr>
446
+ </table>
447
+
448
+ 데모의 Rust 핸들러가 쓰다듬은 횟수를 세어 상태를 갱신합니다. 버튼은 iOS 17 이상에서
449
+ 동작합니다(`getCapabilities()`의 `actions`). Android는 버튼을 세 개까지 알림 액션으로 보여 줍니다. Android에서 종료된 앱을
450
+ 다시 띄운 탭은 메모리에서 기다리다가, Tauri 런타임이 시작되지 않으면 사라집니다.
451
+
452
+ ### Dynamic Island
453
+
454
+ ```ts
455
+ layout: {
456
+ lockScreen: vstack([bound('title'), bound('status'), progress('done')]),
457
+ island: {
458
+ expanded: {
459
+ leading: image('mochi', { resizable: true, style: 'portrait' }),
460
+ center: bound('title'),
461
+ trailing: bound('eta'),
462
+ bottom: vstack([bound('status'), progress('done')]),
463
+ },
464
+ compactLeading: symbol('pawprint.fill'),
465
+ compactTrailing: bound('eta'),
466
+ minimal: progress('done', { kind: 'circular' }),
467
+ },
468
+ keyline: '#A3D9C2',
469
+ },
470
+ styles: { portrait: { frame: { width: 44, height: 44 } } },
471
+ snapshot: { revision: 0, values: { title: 'Mochi’s snack', status: 'Packing a tiny treat', done: 0.15, eta: '9 min' } },
472
+ ```
473
+
474
+ `island`로 각 영역에 노드를 따로 줍니다. 생략한 영역은 기본값을 씁니다. 펼친 화면은 잠금 화면
475
+ 트리를, compact leading과 minimal은 그 트리의 첫 심볼을 보여 줍니다. `keyline`은 아일랜드
476
+ 테두리 색입니다.
477
+
478
+ <table>
479
+ <tr><th>Compact</th><th>Minimal</th><th>Expanded</th></tr>
480
+ <tr>
481
+ <td><img src="https://raw.githubusercontent.com/1eedaegon/tauri-plugin-live-activity/main/docs/images/ios-island-compact.png" width="270" alt="compact Dynamic Island 왼쪽의 발바닥과 오른쪽의 9 min"></td>
482
+ <td><img src="https://raw.githubusercontent.com/1eedaegon/tauri-plugin-live-activity/main/docs/images/ios-island-minimal.png" width="180" alt="minimal Dynamic Island의 진행 원"></td>
483
+ <td><img src="https://raw.githubusercontent.com/1eedaegon/tauri-plugin-live-activity/main/docs/images/ios-island-expanded.png" width="265" alt="펼친 Dynamic Island의 초상화, 제목, 시간, 상태, 진행 막대"></td>
484
+ </tr>
485
+ </table>
486
+
487
+ iPhone 17 시뮬레이터(iOS 26.3)에서 캡처했습니다. Android에는 Dynamic Island가 없어서
488
+ `layout.island`로 남깁니다.
489
+
490
+ ### Android
491
+
492
+ 알림은 레이아웃을 그릴 수 없습니다. Android는 현재 값으로 잠금 화면 트리를 차례로 살펴
493
+ 아래 부분만 씁니다.
494
+
495
+ | 레이아웃 | 알림 |
496
+ | --- | --- |
497
+ | 첫 번째 `text` | 제목 |
498
+ | 두 번째 `text` | 본문 |
499
+ | 첫 번째 `progress` | 진행 막대 |
500
+ | 첫 번째 `timerText` | 크로노미터 |
501
+ | `button` 세 개까지 | 액션. 버튼 안의 첫 텍스트가 라벨 |
502
+ | `android.visibility` | 잠금 화면 공개 범위: `public`, `private`(기본), `secret` |
503
+
504
+ 나머지는 `degradedFeatures`에 남깁니다. 아래 예시라면 `layout.symbol`과 `layout.styles`입니다.
505
+
506
+ ```ts
507
+ layout: {
508
+ lockScreen: vstack([
509
+ hstack([symbol('moon.zzz.fill'), bound('title', { style: 'title' })], { spacing: 8 }),
510
+ bound('status'),
511
+ progress('sleep'),
512
+ button('wake', text('Wake Mochi')),
513
+ ], { alignment: 'leading' }),
514
+ },
515
+ styles: { title: { font: 'headline' } },
516
+ snapshot: { revision: 0, values: { title: 'Mochi’s nap', status: 'Dozing off', sleep: 0.2 } },
517
+ android: { visibility: 'public' },
518
+ ```
519
+
520
+ | iOS | Android 16 에뮬레이터 |
521
+ | --- | --- |
522
+ | 위 예시들처럼 작성한 그대로 그립니다. | <img src="https://raw.githubusercontent.com/1eedaegon/tauri-plugin-live-activity/main/docs/images/android-layout-android.png" width="400" alt="Android 알림 창에서 60%이고 Wake Mochi 액션이 있는 Mochi의 낮잠"> |
523
+
524
+ 캡처는 `Fast asleep`, `0.6`으로 한 번 갱신한 뒤의 화면입니다. Android 16 이상에서는 앱이 승격된
525
+ 알림을 보낼 수 있을 때 플러그인이 Live Update 승격을 요청하고, 그 결과는 `promotionState`에 나옵니다.
526
+
527
+ ### Samsung Now Bar
528
+
529
+ One UI는 Samsung의 허용 목록에 있는 앱의 승격된 알림만 Now Bar에 보여 줍니다. 사용자가 개발자
530
+ 옵션 "Live notifications for all apps"를 켜 두면 모든 앱이 대상이 됩니다. 목록에 넣을 앱은
531
+ Samsung이 비공개로 정하며, 공개된 신청 경로는 찾지 못했습니다. Android 17 이상에서 제목과
532
+ 본문을 Now Bar에 보이려면 플러그인의 One UI extra를 켭니다.
533
+
534
+ ```sh
535
+ node -e 'f="src-tauri/tauri.conf.json";c=require("./"+f);((c.plugins??={})["live-activity"]??={}).android={samsungNowBar:true};require("fs").writeFileSync(f,JSON.stringify(c,null,2)+"\n")'
536
+ ```
537
+
538
+ | Galaxy S23, One UI 9(Android 17), 잠금 상태 |
539
+ | --- |
540
+ | <img src="https://raw.githubusercontent.com/1eedaegon/tauri-plugin-live-activity/main/docs/images/samsung-now-bar.png" width="400" alt="One UI Now Bar에 표시한 Mochi’s snack delivery와 On the way to Mochi"> |
541
+
542
+ 데모의 Now Bar 예시(제목, 상태, 진행 막대에 `android: { visibility: 'public' }`)를 한 번 갱신한
543
+ 뒤, 개발자 옵션을 켠 상태에서 캡처했습니다. extra를 켠다고 허용 목록에 오르지는 않습니다. Samsung이 `RON` 플래그 없이 앱을
544
+ 목록에 넣었다면 아래 매니페스트 항목도 필요합니다. 플러그인은 이 항목을 추가하지 않습니다.
545
+
546
+ ```sh
547
+ f=src-tauri/gen/android/app/src/main/AndroidManifest.xml; grep -q ongoing_activity $f || perl -0pi -e 's|<application\b[^>]*>|$&\n <meta-data android:name="com.samsung.android.support.ongoing_activity" android:value="true" />|' $f
548
+ ```
549
+
550
+ ## 플러그인 설정
551
+
552
+ `tauri.conf.json`의 `plugins.live-activity`는 생략해도 됩니다.
553
+
554
+ ```json
555
+ { "plugins": { "live-activity": { "receipts": "redacted", "android": { "samsungNowBar": true } } } }
556
+ ```
557
+
558
+ - `receipts`: iOS는 수락한 요청마다 기록(receipt)을 남겨, 재실행 후에도 늦게 도착한
559
+ ActivityKit 조회 결과가 이전 revision을 되살리지 못하게 합니다. 기본값 `full`은 snapshot과
560
+ 그 텍스트를 저장합니다. `redacted`는 snapshot의 SHA-256 digest만 저장하는데, digest는 추측할
561
+ 수 없는 값만 가려 줍니다. Android는 아직 이 옵션을 무시하고 앱 전용 preferences에 기록 전체를
562
+ 저장합니다.
563
+ - `android.samsungNowBar`: [Samsung Now Bar](#samsung-now-bar)의 extra를 붙입니다. 생략하면 꺼져 있습니다.
564
+
565
+ ## 데모 실행
566
+
567
+ <img src="https://raw.githubusercontent.com/1eedaegon/tauri-plugin-live-activity/main/docs/images/ios-demo-picker.png" width="300" alt="예시마다 칩이 하나씩 있는 데모의 Layouts 탭">
568
+
569
+ 데모의 Layouts 탭에서 이 문서의 레이아웃 예시를 하나씩 시작할 수 있습니다. 모바일 도구를
570
+ 설정한 뒤 저장소 루트에서 실행합니다.
571
+
572
+ ```sh
573
+ npm ci && npm run build:demo && cd examples/demo && npm run tauri -- ios dev
574
+ ```
103
575
 
104
- [실행 가능한 예제](https://github.com/1eedaegon/tauri-plugin-live-activity/tree/main/examples/demo) ·
105
- [검증 결과](https://github.com/1eedaegon/tauri-plugin-live-activity/blob/main/docs/validation.md) ·
106
- [OS별 지원 범위](https://github.com/1eedaegon/tauri-plugin-live-activity/blob/main/docs/platform-coverage.md)
576
+ Android는 `android dev`를 씁니다. 요청 계약, 자세한 설정, 테스트 기록은
577
+ [docs](https://github.com/1eedaegon/tauri-plugin-live-activity/tree/main/docs)에 있습니다.
107
578
 
108
579
  ## 라이선스
109
580