react-native-attachment-kit 1.0.6 → 1.0.9
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/CHANGELOG.md +8 -0
- package/README.md +41 -3
- package/android/CMakeLists.txt +3 -0
- package/android/build.gradle +11 -2
- package/android/src/main/libs/rnattachmentkit-release.aar +0 -0
- package/android/src/newarch/java/com/rnattachmentkit/RNAttachmentKitModule.kt +48 -2
- package/android/src/oldarch/java/com/rnattachmentkit/RNAttachmentKitModule.kt +48 -2
- package/ios/RNAttachmentKit.swift +160 -5
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/Assets.car +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios.abi.json +1283 -810
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios.private.swiftinterface +12 -1
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios.swiftdoc +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios.swiftinterface +12 -1
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/RNAttachmentKit +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/_CodeSignature/CodeDirectory +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/_CodeSignature/CodeResources +12 -12
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/_CodeSignature/CodeSignature +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Assets.car +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios-simulator.abi.json +1283 -810
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface +12 -1
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios-simulator.swiftdoc +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios-simulator.swiftinterface +12 -1
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/x86_64-apple-ios-simulator.abi.json +1283 -810
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/x86_64-apple-ios-simulator.private.swiftinterface +12 -1
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/x86_64-apple-ios-simulator.swiftdoc +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/x86_64-apple-ios-simulator.swiftinterface +12 -1
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/RNAttachmentKit +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/_CodeSignature/CodeDirectory +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/_CodeSignature/CodeResources +22 -22
- package/package.json +2 -2
- package/src/index.ts +1 -1
- package/src/types.ts +34 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.8 — 2026-10-16
|
|
4
|
+
|
|
5
|
+
- **AI screens are the same in every app (iOS + Android)**. Generate: woman / man, country, age (up to the 80s), style, situation, aspect ratio + Create, sample chips and the chat input; chat text is sent together with the chosen settings, and tapping a sample shows a summary of the settings before creating. The glamour situations / samples and the detail options were removed from the generate screen. Edit: the full editing screen everywhere. Generated and edited images are stored for the gallery's "AI 이미지" tab in every app.
|
|
6
|
+
- iOS: the "AI 이미지" tab expands / collapses the sheet like the gallery grid.
|
|
7
|
+
- iOS: an edited (trimmed) video's first poster (`onProcessingStart` `thumbnailUri`) is taken from the trimmed start, with crop and drawing applied.
|
|
8
|
+
- iOS: closing the AI screen right after using a menu no longer leaves a stray button over the gallery.
|
|
9
|
+
- Android image viewer: video controls no longer flash on photo pages while the viewer opens.
|
|
10
|
+
|
|
3
11
|
## 1.0.6 — 2026-10-14
|
|
4
12
|
|
|
5
13
|
- **Video in the picker, editor and viewer (iOS + Android)**: `openGallery({ mediaType: "all" | "video", video })` picks videos; the editor gets video pages (trim, mute, quality, crop, drawing/stickers/text, pre-encoding); `openViewer({ images, videos })` plays videos (progress pill, mute/options, remote video cache, pinch zoom). Sending progress is reported as events. `video.posterFormat` (`"webp"` default / `"jpg"`). Video limits are checked before sending.
|
package/README.md
CHANGED
|
@@ -98,7 +98,7 @@ Enable on the key: **Maps SDK for Android**, and **Places API (New)** for place
|
|
|
98
98
|
import AttachmentKit from 'react-native-attachment-kit'
|
|
99
99
|
|
|
100
100
|
// Once at app start
|
|
101
|
-
await AttachmentKit.init('YOUR_LICENSE_KEY', { // '' = evaluation (editor watermark)
|
|
101
|
+
await AttachmentKit.init('YOUR_LICENSE_KEY', { // '' = evaluation: photos only (editor watermark; no location / video / AI)
|
|
102
102
|
languageCode: 'ko', // any app language code; see Localization
|
|
103
103
|
themeColor: '#10b981',
|
|
104
104
|
appearance: 'system', // 'system' | 'light' | 'dark'
|
|
@@ -147,6 +147,7 @@ Opens the gallery bottom sheet. Resolves with the picked images (`[]` when the u
|
|
|
147
147
|
| `profileMode` | `boolean` | `false` | 1:1 (circle) crop for profile photos. |
|
|
148
148
|
| `maxWidth` / `maxHeight` | `number` | `1024` | Output images are scaled down to fit. |
|
|
149
149
|
| `outputFormat` | `'webp' \| 'jpg' \| 'png'` | `'webp'` | Output file format. (Not in the TS type yet — pass it with a cast.) |
|
|
150
|
+
| `thumbnail` | `ThumbnailOptions` | — | Also make a small photo thumbnail for each photo — see [Photo thumbnails](#photo-thumbnails). Omitted: no photo thumbnail (same as before). |
|
|
150
151
|
| `themeColor` / `languageCode` / `appearance` | | defaults | Per-call overrides. |
|
|
151
152
|
| `doneButtonText` / `allItemsText` / `selectedItemsText` / `recentsAlbumText` | `string` | translated | Label overrides. |
|
|
152
153
|
| `asyncProcessing` | `boolean` | `false` | Event-driven sending: each item is delivered through `onImageProcessed` as soon as it is ready (the Promise still resolves with everything at the end), and videos show no progress HUD — draw progress in your own UI. See [Sending progress](#sending-progress-events). |
|
|
@@ -172,6 +173,32 @@ Opens the gallery bottom sheet. Resolves with the picked images (`[]` when the u
|
|
|
172
173
|
| `type` | `string` | MIME type. |
|
|
173
174
|
| `caption?` | `string` | Caption typed in the editor. |
|
|
174
175
|
| `fileName?` / `fileExtension?` / `fileSize?` | | Output file info (bytes). |
|
|
176
|
+
| `thumbnailUri?` / `thumbnailWidth?` / `thumbnailHeight?` | | Photo: the thumbnail when the `thumbnail` option is set. Video: the poster (always, see `video.posterFormat`). |
|
|
177
|
+
| `thumbnailFileSize?` | `number` | Photo thumbnail file size (bytes). |
|
|
178
|
+
|
|
179
|
+
#### Photo thumbnails
|
|
180
|
+
|
|
181
|
+
Pass `thumbnail` to `openGallery` (or `openEditor`) to get a small thumbnail next to each photo — e.g. to upload both the full image and a list-size image in one request.
|
|
182
|
+
|
|
183
|
+
```ts
|
|
184
|
+
// Profile avatar: exactly 200×200, center-cropped
|
|
185
|
+
await AttachmentKit.openGallery({ maxWidth: 512, maxHeight: 512, thumbnail: { maxWidth: 200, mode: 'cover' } })
|
|
186
|
+
// Moments list: keep the aspect ratio, longest side ≤ 360
|
|
187
|
+
await AttachmentKit.openGallery({ maxSelection: 10, thumbnail: { maxWidth: 360 } })
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
| Option | Type | Default | Description |
|
|
191
|
+
|---|---|---|---|
|
|
192
|
+
| `maxWidth` | `number` (px) | **required** | Box width. |
|
|
193
|
+
| `maxHeight` | `number` (px) | `maxWidth` | Box height. |
|
|
194
|
+
| `mode` | `'fit' \| 'cover'` | `'fit'` | `'fit'`: keep the aspect ratio inside the box. `'cover'`: fill the box and crop the center → exactly `maxWidth × maxHeight`. |
|
|
195
|
+
| `quality` | `number` 0–1 | `0.8` | Encoding quality. |
|
|
196
|
+
| `format` | `'webp' \| 'jpg'` | `'webp'` | Falls back to jpg if webp encoding fails (check the extension of `thumbnailUri`). |
|
|
197
|
+
|
|
198
|
+
- Made from the same image as `uri` (orientation, crop, drawing and filter applied) — the original is not decoded again. No EXIF / GPS is written.
|
|
199
|
+
- Never enlarged: a 100×80 photo gives 100×80 with `{ maxWidth: 360 }` and 80×80 with `{ maxWidth: 200, mode: 'cover' }`. iOS and Android give the same size.
|
|
200
|
+
- Also in `onImageProcessed` (`image.thumbnailUri` …). Video posters keep their own rules (longest side 512, `video.posterFormat`) and ignore this option.
|
|
201
|
+
- Files: `turbo_picker_thumb_<time>_<index>.webp` next to the output photo; deleted after 24 h like the other temp files.
|
|
175
202
|
|
|
176
203
|
#### `closeGallery(): Promise<boolean>`
|
|
177
204
|
Closes the picker from code. `true` if it was open.
|
|
@@ -262,6 +289,7 @@ Both callbacks fire before `openGallery` resolves; their listeners are removed w
|
|
|
262
289
|
| `uri` | `string` | Image to edit (`originalUri` from a picker result). |
|
|
263
290
|
| `editedFileUri?` | `string` | A previous edit to continue from. |
|
|
264
291
|
| `maxWidth` / `maxHeight` | `number` | Output bounds. |
|
|
292
|
+
| `thumbnail?` | `ThumbnailOptions` | Also make a thumbnail of the edited result (crop and drawing applied). See [Photo thumbnails](#photo-thumbnails). |
|
|
265
293
|
| `themeColor` / `appearance` | | Per-call overrides. |
|
|
266
294
|
| `openDuration` / `closeDuration` | `number` (ms) | Animation durations. |
|
|
267
295
|
|
|
@@ -463,7 +491,7 @@ A native grid built for thousands of photos. It opens like the picker (same bott
|
|
|
463
491
|
|
|
464
492
|
### AI (generate & edit)
|
|
465
493
|
|
|
466
|
-
The gallery's ✦ button (AI image generation) and the editor's 6th tool **AI** (AI photo editing) use Google Gemini. They are **off until you call `setAIConfig({ enabled: true, ... })`**.
|
|
494
|
+
The gallery's ✦ button (AI image generation) and the editor's 6th tool **AI** (AI photo editing) use Google Gemini. They are **off until you call `setAIConfig({ enabled: true, ... })`**. The screens are the same in every app. Generate has woman / man, country, age, style and situation pickers, an aspect-ratio bar, sample chips and the chat input; chat text is sent together with the chosen settings. Edit is the full editing screen: edit choices, options, recommendation chips and the chat input. Generated and edited images are kept on the device and listed in the gallery's "AI 이미지" tab.
|
|
467
495
|
|
|
468
496
|
```ts
|
|
469
497
|
await AttachmentKit.setAIConfig({
|
|
@@ -562,6 +590,7 @@ app.post('/ai/generate', auth, express.json({ limit: '20mb' }), async (req, res)
|
|
|
562
590
|
|---|---|---|---|
|
|
563
591
|
| Files from the File tab | `tmp/AttachmentKitFiles/<uuid>/<name>` | `cache/AttachmentKitFiles/<uuid>/<name>` | Deleted after 24 h (next gallery open); the OS may purge earlier — copy/upload promptly. |
|
|
564
592
|
| Location snapshots | `tmp/AttachmentKitLocation/` | `cache/AttachmentKitLocation/` | Deleted after 24 h. |
|
|
593
|
+
| Photo thumbnails (`thumbnail` option) | `Library/Caches/RNAttachmentKit/turbo_picker_thumb_*` | `cache/turbo_picker_thumb_*` | Deleted after 24 h (next gallery open). |
|
|
565
594
|
| Location thumbnails | `Library/Caches/RNAttachmentKit/LocationThumbs/` | `cache/RNAttachmentKitLocationThumbs/` | 300 max → 250, least recently used. `clearLocationThumbnailCache()`. |
|
|
566
595
|
| Reverse-geocode results | `Library/Caches/RNAttachmentKit/Geocode/` | `cache/RNAttachmentKitGeocode/` | 1000 max → 800, least recently used. `clearReverseGeocodeCache()`. |
|
|
567
596
|
|
|
@@ -569,7 +598,16 @@ app.post('/ai/generate', auth, express.json({ limit: '20mb' }), async (req, res)
|
|
|
569
598
|
|
|
570
599
|
## 📜 License & Contact
|
|
571
600
|
|
|
572
|
-
`react-native-attachment-kit` is released under the **MIT License**.
|
|
601
|
+
`react-native-attachment-kit` is released under the **MIT License**. A license key passed to `init()` (one key per app ID — bundle ID / package name) removes the editor watermark and unlocks location, video and AI.
|
|
602
|
+
|
|
603
|
+
Without a valid key (evaluation), only photos work: picking, editing (with watermark) and viewing.
|
|
604
|
+
|
|
605
|
+
| Feature | Without a license key |
|
|
606
|
+
|---|---|
|
|
607
|
+
| Attach menu Location tab, ✦ AI generate, editor AI tool, "AI 이미지" tab | Visible; tapping shows a "License required" notice |
|
|
608
|
+
| `openGallery({ mediaType: 'video' \| 'all' })` | Photos only; holding the shutter to record shows the notice |
|
|
609
|
+
| `openMapViewer`, `renderLocationThumbnail`, `reverseGeocode`, `openViewer` with `videos`, `openMediaGallery` / `appendGalleryItems` with video items | Rejected with `E_LICENSE_REQUIRED` |
|
|
610
|
+
| `setAIConfig` | Accepted (the AI buttons appear and show the notice) |
|
|
573
611
|
|
|
574
612
|
🛒 **License**: [https://license.rnturboimagepicker.usomnia.co.kr](https://license.rnturboimagepicker.usomnia.co.kr)
|
|
575
613
|
📧 **Contact**: [contact@usomnia.co.kr](mailto:contact@usomnia.co.kr)
|
package/android/CMakeLists.txt
CHANGED
|
@@ -3,6 +3,9 @@ set(CMAKE_VERBOSE_MAKEFILE ON)
|
|
|
3
3
|
|
|
4
4
|
project(react_codegen_RNAttachmentKitSpec)
|
|
5
5
|
|
|
6
|
+
# Android 15+ 16KB 페이지 기기 지원 (Google Play: targetSdk 35+ 업데이트 필수). NDK r27 이하는 기본이 4KB 정렬이라 명시
|
|
7
|
+
string(APPEND CMAKE_SHARED_LINKER_FLAGS " -Wl,-z,max-page-size=16384")
|
|
8
|
+
|
|
6
9
|
set(GENERATED_SRC_DIR "${CMAKE_CURRENT_SOURCE_DIR}/build/generated/source/codegen/jni")
|
|
7
10
|
|
|
8
11
|
if(NOT COMMAND target_compile_reactnative_options)
|
package/android/build.gradle
CHANGED
|
@@ -46,10 +46,19 @@ if (project != rootProject) {
|
|
|
46
46
|
android {
|
|
47
47
|
namespace "com.rnattachmentkit.react"
|
|
48
48
|
compileSdkVersion 34
|
|
49
|
+
// 16KB 페이지 지원: 앱과 같은 NDK (RN 템플릿의 rootProject.ext.ndkVersion, r27+ 권장)
|
|
50
|
+
if (rootProject.ext.has("ndkVersion")) {
|
|
51
|
+
ndkVersion rootProject.ext.ndkVersion
|
|
52
|
+
}
|
|
49
53
|
|
|
50
54
|
defaultConfig {
|
|
51
55
|
minSdkVersion 24
|
|
52
56
|
targetSdkVersion 34
|
|
57
|
+
externalNativeBuild {
|
|
58
|
+
cmake {
|
|
59
|
+
arguments "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON"
|
|
60
|
+
}
|
|
61
|
+
}
|
|
53
62
|
}
|
|
54
63
|
compileOptions {
|
|
55
64
|
sourceCompatibility JavaVersion.VERSION_17
|
|
@@ -126,8 +135,8 @@ if (project != rootProject) {
|
|
|
126
135
|
implementation "androidx.media3:media3-transformer:${media3_version}"
|
|
127
136
|
implementation "androidx.media3:media3-effect:${media3_version}"
|
|
128
137
|
|
|
129
|
-
// CameraX dependencies
|
|
130
|
-
def camerax_version = "1.
|
|
138
|
+
// CameraX dependencies — 1.4.0+ 부터 libimage_processing_util_jni.so 가 16KB 정렬 (AAR 쪽과 같은 버전)
|
|
139
|
+
def camerax_version = "1.4.2"
|
|
131
140
|
implementation "androidx.camera:camera-core:${camerax_version}"
|
|
132
141
|
implementation "androidx.camera:camera-camera2:${camerax_version}"
|
|
133
142
|
implementation "androidx.camera:camera-lifecycle:${camerax_version}"
|
|
Binary file
|
|
@@ -59,10 +59,13 @@ class RNAttachmentKitModule(reactContext: ReactApplicationContext) :
|
|
|
59
59
|
val maxWidth: Int, val maxHeight: Int, val outputFormat: String,
|
|
60
60
|
val videoOptions: com.rnattachmentkit.VideoExportOptions,
|
|
61
61
|
val states: Map<String, com.rnattachmentkit.VideoEditState>,
|
|
62
|
-
val showHUD: Boolean, val themeColor: Int?, val language: String
|
|
62
|
+
val showHUD: Boolean, val themeColor: Int?, val language: String,
|
|
63
|
+
val thumbnail: com.rnattachmentkit.PhotoThumbnail.Options?
|
|
63
64
|
)
|
|
64
65
|
private fun sendSnapshot() = SendSnapshot(maxWidth, maxHeight, outputFormat, videoOptions, HashMap(lastVideoEditStates),
|
|
65
|
-
showVideoHUD, lastThemeColorInt, lastLanguageCode)
|
|
66
|
+
showVideoHUD, lastThemeColorInt, lastLanguageCode, thumbnailOptions)
|
|
67
|
+
/** 사진 썸네일 (GalleryOptions.thumbnail / EditorOptions.thumbnail). null 이면 만들지 않음 (기존과 같음) */
|
|
68
|
+
private var thumbnailOptions: com.rnattachmentkit.PhotoThumbnail.Options? = null
|
|
66
69
|
/** 동영상 변환 진행 HUD — video.showProgressHUD, 없으면 asyncProcessing 이 아닐 때만 (이벤트로 말풍선에 그리는 앱은 HUD 없이) */
|
|
67
70
|
private var showVideoHUD = true
|
|
68
71
|
/** 시트가 닫힐 때 넘겨받은 동영상 편집 상태 (원본 uri 문자열 → 품질·음소거) */
|
|
@@ -242,6 +245,9 @@ class RNAttachmentKitModule(reactContext: ReactApplicationContext) :
|
|
|
242
245
|
|
|
243
246
|
if (options.hasKey("maxWidth")) maxWidth = options.getInt("maxWidth")
|
|
244
247
|
if (options.hasKey("maxHeight")) maxHeight = options.getInt("maxHeight")
|
|
248
|
+
thumbnailOptions = com.rnattachmentkit.PhotoThumbnail.parse(
|
|
249
|
+
if (options.hasKey("thumbnail") && !options.isNull("thumbnail")) options.getMap("thumbnail")?.toHashMap() else null
|
|
250
|
+
)
|
|
245
251
|
|
|
246
252
|
editImagePromise = promise
|
|
247
253
|
|
|
@@ -350,6 +356,10 @@ class RNAttachmentKitModule(reactContext: ReactApplicationContext) :
|
|
|
350
356
|
videoOptions = com.rnattachmentkit.VideoExportOptions.fromMap(
|
|
351
357
|
if (options.hasKey("video") && !options.isNull("video")) options.getMap("video")?.toHashMap() else null
|
|
352
358
|
)
|
|
359
|
+
// 사진 썸네일 (동영상 포스터 규칙은 그대로 — video.posterFormat, 긴 변 512)
|
|
360
|
+
thumbnailOptions = com.rnattachmentkit.PhotoThumbnail.parse(
|
|
361
|
+
if (options.hasKey("thumbnail") && !options.isNull("thumbnail")) options.getMap("thumbnail")?.toHashMap() else null
|
|
362
|
+
)
|
|
353
363
|
run {
|
|
354
364
|
val async = options.hasKey("asyncProcessing") && !options.isNull("asyncProcessing") && options.getBoolean("asyncProcessing")
|
|
355
365
|
val v = if (options.hasKey("video") && !options.isNull("video")) options.getMap("video") else null
|
|
@@ -528,6 +538,14 @@ class RNAttachmentKitModule(reactContext: ReactApplicationContext) :
|
|
|
528
538
|
}
|
|
529
539
|
|
|
530
540
|
override fun openViewer(options: ReadableMap, promise: Promise) {
|
|
541
|
+
// 동영상 페이지는 라이선스 키가 있어야
|
|
542
|
+
if (!LicenseGate.isLicensed && options.hasKey("videos")) {
|
|
543
|
+
val va = options.getArray("videos")
|
|
544
|
+
val anyVideo = va != null && (0 until va.size()).any { i ->
|
|
545
|
+
va.getType(i) == com.facebook.react.bridge.ReadableType.Map && va.getMap(i)?.let { it.hasKey("uri") && !it.isNull("uri") && !it.getString("uri").isNullOrEmpty() } == true
|
|
546
|
+
}
|
|
547
|
+
if (anyVideo) { promise.reject(LicenseGate.ERROR_CODE, LicenseGate.ERROR_MESSAGE); return }
|
|
548
|
+
}
|
|
531
549
|
val activity = getCurrentActivity()
|
|
532
550
|
if (activity == null) {
|
|
533
551
|
promise.reject(E_NO_ACTIVITY, "Activity doesn't exist")
|
|
@@ -964,6 +982,7 @@ class RNAttachmentKitModule(reactContext: ReactApplicationContext) :
|
|
|
964
982
|
val showVideoHUD = snap.showHUD
|
|
965
983
|
val lastThemeColorInt = snap.themeColor
|
|
966
984
|
val lastLanguageCode = snap.language
|
|
985
|
+
val thumbnailOptions = snap.thumbnail
|
|
967
986
|
val processingId = java.util.UUID.randomUUID().toString()
|
|
968
987
|
val results = mutableListOf<com.facebook.react.bridge.WritableMap>()
|
|
969
988
|
val contentResolver = reactApplicationContext.contentResolver
|
|
@@ -1212,6 +1231,10 @@ class RNAttachmentKitModule(reactContext: ReactApplicationContext) :
|
|
|
1212
1231
|
FileOutputStream(tempFile).use { out ->
|
|
1213
1232
|
processedBitmap.compress(compressFormat, 85, out)
|
|
1214
1233
|
}
|
|
1234
|
+
// 썸네일: 같은 비트맵(방향·크롭·그리기 반영)에서 — 원본을 다시 읽지 않음
|
|
1235
|
+
val thumb = thumbnailOptions?.let {
|
|
1236
|
+
com.rnattachmentkit.PhotoThumbnail.make(processedBitmap, it, reactApplicationContext.cacheDir, currentIndex)
|
|
1237
|
+
}
|
|
1215
1238
|
|
|
1216
1239
|
val finalWidth = processedBitmap.width
|
|
1217
1240
|
val finalHeight = processedBitmap.height
|
|
@@ -1237,6 +1260,12 @@ class RNAttachmentKitModule(reactContext: ReactApplicationContext) :
|
|
|
1237
1260
|
putString("originalUri", uri.toString())
|
|
1238
1261
|
putInt("originalWidth", originalWidth)
|
|
1239
1262
|
putInt("originalHeight", originalHeight)
|
|
1263
|
+
if (thumb != null) {
|
|
1264
|
+
putString("thumbnailUri", Uri.fromFile(thumb.file).toString())
|
|
1265
|
+
putInt("thumbnailWidth", thumb.width)
|
|
1266
|
+
putInt("thumbnailHeight", thumb.height)
|
|
1267
|
+
putDouble("thumbnailFileSize", thumb.fileSize.toDouble())
|
|
1268
|
+
}
|
|
1240
1269
|
}
|
|
1241
1270
|
}
|
|
1242
1271
|
val imageInfo = createImageInfo()
|
|
@@ -1342,6 +1371,10 @@ class RNAttachmentKitModule(reactContext: ReactApplicationContext) :
|
|
|
1342
1371
|
return
|
|
1343
1372
|
}
|
|
1344
1373
|
val o: Map<String, Any?> = options.toHashMap()
|
|
1374
|
+
// 동영상 항목은 라이선스 키가 있어야
|
|
1375
|
+
if (!LicenseGate.isLicensed && MediaGalleryItem.listFrom(o["items"]).any { it.isVideo }) {
|
|
1376
|
+
promise.reject(LicenseGate.ERROR_CODE, LicenseGate.ERROR_MESSAGE); return
|
|
1377
|
+
}
|
|
1345
1378
|
com.facebook.react.bridge.UiThreadUtil.runOnUiThread {
|
|
1346
1379
|
try {
|
|
1347
1380
|
val systemDark = (activity.resources.configuration.uiMode and
|
|
@@ -1425,6 +1458,9 @@ class RNAttachmentKitModule(reactContext: ReactApplicationContext) :
|
|
|
1425
1458
|
p["nextCursor"] as? String,
|
|
1426
1459
|
p["hasMore"] as? Boolean ?: false
|
|
1427
1460
|
)
|
|
1461
|
+
if (!LicenseGate.isLicensed && page.items.any { it.isVideo }) {
|
|
1462
|
+
promise.reject(LicenseGate.ERROR_CODE, LicenseGate.ERROR_MESSAGE); return
|
|
1463
|
+
}
|
|
1428
1464
|
com.facebook.react.bridge.UiThreadUtil.runOnUiThread {
|
|
1429
1465
|
mediaGalleries[galleryId]?.store?.appendExternal(page)
|
|
1430
1466
|
promise.resolve(null)
|
|
@@ -1503,6 +1539,8 @@ class RNAttachmentKitModule(reactContext: ReactApplicationContext) :
|
|
|
1503
1539
|
}
|
|
1504
1540
|
|
|
1505
1541
|
override fun openMapViewer(options: ReadableMap, promise: Promise) {
|
|
1542
|
+
// 위치 기능은 라이선스 키가 있어야
|
|
1543
|
+
if (!LicenseGate.isLicensed) { promise.reject(LicenseGate.ERROR_CODE, LicenseGate.ERROR_MESSAGE); return }
|
|
1506
1544
|
val activity = getCurrentActivity() as? FragmentActivity
|
|
1507
1545
|
if (activity == null) {
|
|
1508
1546
|
promise.reject("NO_ACTIVITY", "Activity is not a FragmentActivity")
|
|
@@ -1585,6 +1623,8 @@ class RNAttachmentKitModule(reactContext: ReactApplicationContext) :
|
|
|
1585
1623
|
*/
|
|
1586
1624
|
@ReactMethod
|
|
1587
1625
|
override fun renderLocationThumbnail(options: ReadableMap, promise: Promise) {
|
|
1626
|
+
// 위치 기능은 라이선스 키가 있어야
|
|
1627
|
+
if (!LicenseGate.isLicensed) { promise.reject(LicenseGate.ERROR_CODE, LicenseGate.ERROR_MESSAGE); return }
|
|
1588
1628
|
fun str(key: String) = if (options.hasKey(key) && !options.isNull(key)) options.getString(key) else null
|
|
1589
1629
|
fun num(key: String) = if (options.hasKey(key) && !options.isNull(key)) options.getDouble(key) else null
|
|
1590
1630
|
val lat = num("latitude")
|
|
@@ -1685,6 +1725,10 @@ class RNAttachmentKitModule(reactContext: ReactApplicationContext) :
|
|
|
1685
1725
|
// ── AI 기능 설정·잔액 (넘긴 값만 바뀜). 사용·충전 요청은 onAIUsage / onAIChargeRequired 이벤트로 알림 ──
|
|
1686
1726
|
|
|
1687
1727
|
override fun setAIConfig(config: ReadableMap, promise: Promise) {
|
|
1728
|
+
// 라이선스 키가 없어도 설정은 받아 둠 (AI 버튼은 보이고, 누르면 "라이선스 필요" 안내)
|
|
1729
|
+
if (!LicenseGate.isLicensed && config.hasKey("enabled") && !config.isNull("enabled") && config.getBoolean("enabled")) {
|
|
1730
|
+
android.util.Log.w("RNAttachmentKit", "AI needs a license key — the AI buttons show a license notice until init() gets a valid key")
|
|
1731
|
+
}
|
|
1688
1732
|
AIBilling.apply(config.toHashMap())
|
|
1689
1733
|
val ctx = reactApplicationContext
|
|
1690
1734
|
fun emit(name: String, body: Map<String, Any>) {
|
|
@@ -1702,6 +1746,8 @@ class RNAttachmentKitModule(reactContext: ReactApplicationContext) :
|
|
|
1702
1746
|
// ── 역지오코딩 (기기 Geocoder, 디스크 캐시) ──
|
|
1703
1747
|
|
|
1704
1748
|
override fun reverseGeocode(options: ReadableMap, promise: Promise) {
|
|
1749
|
+
// 위치 기능은 라이선스 키가 있어야
|
|
1750
|
+
if (!LicenseGate.isLicensed) { promise.reject(LicenseGate.ERROR_CODE, LicenseGate.ERROR_MESSAGE); return }
|
|
1705
1751
|
val lat = if (options.hasKey("latitude") && !options.isNull("latitude")) options.getDouble("latitude") else Double.NaN
|
|
1706
1752
|
val lng = if (options.hasKey("longitude") && !options.isNull("longitude")) options.getDouble("longitude") else Double.NaN
|
|
1707
1753
|
val lang = if (options.hasKey("languageCode") && !options.isNull("languageCode")) options.getString("languageCode") else null
|
|
@@ -58,10 +58,13 @@ class RNAttachmentKitModule(reactContext: ReactApplicationContext) :
|
|
|
58
58
|
val maxWidth: Int, val maxHeight: Int, val outputFormat: String,
|
|
59
59
|
val videoOptions: com.rnattachmentkit.VideoExportOptions,
|
|
60
60
|
val states: Map<String, com.rnattachmentkit.VideoEditState>,
|
|
61
|
-
val showHUD: Boolean, val themeColor: Int?, val language: String
|
|
61
|
+
val showHUD: Boolean, val themeColor: Int?, val language: String,
|
|
62
|
+
val thumbnail: com.rnattachmentkit.PhotoThumbnail.Options?
|
|
62
63
|
)
|
|
63
64
|
private fun sendSnapshot() = SendSnapshot(maxWidth, maxHeight, outputFormat, videoOptions, HashMap(lastVideoEditStates),
|
|
64
|
-
showVideoHUD, lastThemeColorInt, lastLanguageCode)
|
|
65
|
+
showVideoHUD, lastThemeColorInt, lastLanguageCode, thumbnailOptions)
|
|
66
|
+
/** 사진 썸네일 (GalleryOptions.thumbnail / EditorOptions.thumbnail). null 이면 만들지 않음 (기존과 같음) */
|
|
67
|
+
private var thumbnailOptions: com.rnattachmentkit.PhotoThumbnail.Options? = null
|
|
65
68
|
/** 동영상 변환 진행 HUD — video.showProgressHUD, 없으면 asyncProcessing 이 아닐 때만 (이벤트로 말풍선에 그리는 앱은 HUD 없이) */
|
|
66
69
|
private var showVideoHUD = true
|
|
67
70
|
/** 시트가 닫힐 때 넘겨받은 동영상 편집 상태 (원본 uri 문자열 → 품질·음소거) */
|
|
@@ -180,6 +183,9 @@ class RNAttachmentKitModule(reactContext: ReactApplicationContext) :
|
|
|
180
183
|
|
|
181
184
|
if (options.hasKey("maxWidth")) maxWidth = options.getInt("maxWidth")
|
|
182
185
|
if (options.hasKey("maxHeight")) maxHeight = options.getInt("maxHeight")
|
|
186
|
+
thumbnailOptions = com.rnattachmentkit.PhotoThumbnail.parse(
|
|
187
|
+
if (options.hasKey("thumbnail") && !options.isNull("thumbnail")) options.getMap("thumbnail")?.toHashMap() else null
|
|
188
|
+
)
|
|
183
189
|
|
|
184
190
|
editImagePromise = promise
|
|
185
191
|
|
|
@@ -288,6 +294,10 @@ class RNAttachmentKitModule(reactContext: ReactApplicationContext) :
|
|
|
288
294
|
videoOptions = com.rnattachmentkit.VideoExportOptions.fromMap(
|
|
289
295
|
if (options.hasKey("video") && !options.isNull("video")) options.getMap("video")?.toHashMap() else null
|
|
290
296
|
)
|
|
297
|
+
// 사진 썸네일 (동영상 포스터 규칙은 그대로 — video.posterFormat, 긴 변 512)
|
|
298
|
+
thumbnailOptions = com.rnattachmentkit.PhotoThumbnail.parse(
|
|
299
|
+
if (options.hasKey("thumbnail") && !options.isNull("thumbnail")) options.getMap("thumbnail")?.toHashMap() else null
|
|
300
|
+
)
|
|
291
301
|
run {
|
|
292
302
|
val async = options.hasKey("asyncProcessing") && !options.isNull("asyncProcessing") && options.getBoolean("asyncProcessing")
|
|
293
303
|
val v = if (options.hasKey("video") && !options.isNull("video")) options.getMap("video") else null
|
|
@@ -536,6 +546,14 @@ class RNAttachmentKitModule(reactContext: ReactApplicationContext) :
|
|
|
536
546
|
|
|
537
547
|
@ReactMethod
|
|
538
548
|
override fun openViewer(options: ReadableMap, promise: Promise) {
|
|
549
|
+
// 동영상 페이지는 라이선스 키가 있어야
|
|
550
|
+
if (!LicenseGate.isLicensed && options.hasKey("videos")) {
|
|
551
|
+
val va = options.getArray("videos")
|
|
552
|
+
val anyVideo = va != null && (0 until va.size()).any { i ->
|
|
553
|
+
va.getType(i) == com.facebook.react.bridge.ReadableType.Map && va.getMap(i)?.let { it.hasKey("uri") && !it.isNull("uri") && !it.getString("uri").isNullOrEmpty() } == true
|
|
554
|
+
}
|
|
555
|
+
if (anyVideo) { promise.reject(LicenseGate.ERROR_CODE, LicenseGate.ERROR_MESSAGE); return }
|
|
556
|
+
}
|
|
539
557
|
val activity = getCurrentActivity()
|
|
540
558
|
if (activity == null) {
|
|
541
559
|
promise.reject(E_NO_ACTIVITY, "Activity doesn't exist")
|
|
@@ -948,6 +966,7 @@ class RNAttachmentKitModule(reactContext: ReactApplicationContext) :
|
|
|
948
966
|
val showVideoHUD = snap.showHUD
|
|
949
967
|
val lastThemeColorInt = snap.themeColor
|
|
950
968
|
val lastLanguageCode = snap.language
|
|
969
|
+
val thumbnailOptions = snap.thumbnail
|
|
951
970
|
val processingId = java.util.UUID.randomUUID().toString()
|
|
952
971
|
val results = mutableListOf<com.facebook.react.bridge.WritableMap>()
|
|
953
972
|
val contentResolver = reactApplicationContext.contentResolver
|
|
@@ -1196,6 +1215,10 @@ class RNAttachmentKitModule(reactContext: ReactApplicationContext) :
|
|
|
1196
1215
|
FileOutputStream(tempFile).use { out ->
|
|
1197
1216
|
processedBitmap.compress(compressFormat, 85, out)
|
|
1198
1217
|
}
|
|
1218
|
+
// 썸네일: 같은 비트맵(방향·크롭·그리기 반영)에서 — 원본을 다시 읽지 않음
|
|
1219
|
+
val thumb = thumbnailOptions?.let {
|
|
1220
|
+
com.rnattachmentkit.PhotoThumbnail.make(processedBitmap, it, reactApplicationContext.cacheDir, currentIndex)
|
|
1221
|
+
}
|
|
1199
1222
|
|
|
1200
1223
|
val finalWidth = processedBitmap.width
|
|
1201
1224
|
val finalHeight = processedBitmap.height
|
|
@@ -1221,6 +1244,12 @@ class RNAttachmentKitModule(reactContext: ReactApplicationContext) :
|
|
|
1221
1244
|
putString("originalUri", uri.toString())
|
|
1222
1245
|
putInt("originalWidth", originalWidth)
|
|
1223
1246
|
putInt("originalHeight", originalHeight)
|
|
1247
|
+
if (thumb != null) {
|
|
1248
|
+
putString("thumbnailUri", Uri.fromFile(thumb.file).toString())
|
|
1249
|
+
putInt("thumbnailWidth", thumb.width)
|
|
1250
|
+
putInt("thumbnailHeight", thumb.height)
|
|
1251
|
+
putDouble("thumbnailFileSize", thumb.fileSize.toDouble())
|
|
1252
|
+
}
|
|
1224
1253
|
}
|
|
1225
1254
|
}
|
|
1226
1255
|
val imageInfo = createImageInfo()
|
|
@@ -1327,6 +1356,10 @@ class RNAttachmentKitModule(reactContext: ReactApplicationContext) :
|
|
|
1327
1356
|
return
|
|
1328
1357
|
}
|
|
1329
1358
|
val o: Map<String, Any?> = options.toHashMap()
|
|
1359
|
+
// 동영상 항목은 라이선스 키가 있어야
|
|
1360
|
+
if (!LicenseGate.isLicensed && MediaGalleryItem.listFrom(o["items"]).any { it.isVideo }) {
|
|
1361
|
+
promise.reject(LicenseGate.ERROR_CODE, LicenseGate.ERROR_MESSAGE); return
|
|
1362
|
+
}
|
|
1330
1363
|
com.facebook.react.bridge.UiThreadUtil.runOnUiThread {
|
|
1331
1364
|
try {
|
|
1332
1365
|
val systemDark = (activity.resources.configuration.uiMode and
|
|
@@ -1412,6 +1445,9 @@ class RNAttachmentKitModule(reactContext: ReactApplicationContext) :
|
|
|
1412
1445
|
p["nextCursor"] as? String,
|
|
1413
1446
|
p["hasMore"] as? Boolean ?: false
|
|
1414
1447
|
)
|
|
1448
|
+
if (!LicenseGate.isLicensed && page.items.any { it.isVideo }) {
|
|
1449
|
+
promise.reject(LicenseGate.ERROR_CODE, LicenseGate.ERROR_MESSAGE); return
|
|
1450
|
+
}
|
|
1415
1451
|
com.facebook.react.bridge.UiThreadUtil.runOnUiThread {
|
|
1416
1452
|
mediaGalleries[galleryId]?.store?.appendExternal(page)
|
|
1417
1453
|
promise.resolve(null)
|
|
@@ -1488,6 +1524,8 @@ class RNAttachmentKitModule(reactContext: ReactApplicationContext) :
|
|
|
1488
1524
|
|
|
1489
1525
|
@ReactMethod
|
|
1490
1526
|
fun openMapViewer(options: ReadableMap, promise: Promise) {
|
|
1527
|
+
// 위치 기능은 라이선스 키가 있어야
|
|
1528
|
+
if (!LicenseGate.isLicensed) { promise.reject(LicenseGate.ERROR_CODE, LicenseGate.ERROR_MESSAGE); return }
|
|
1491
1529
|
val activity = getCurrentActivity() as? FragmentActivity
|
|
1492
1530
|
if (activity == null) {
|
|
1493
1531
|
promise.reject("NO_ACTIVITY", "Activity is not a FragmentActivity")
|
|
@@ -1571,6 +1609,8 @@ class RNAttachmentKitModule(reactContext: ReactApplicationContext) :
|
|
|
1571
1609
|
*/
|
|
1572
1610
|
@ReactMethod
|
|
1573
1611
|
fun renderLocationThumbnail(options: ReadableMap, promise: Promise) {
|
|
1612
|
+
// 위치 기능은 라이선스 키가 있어야
|
|
1613
|
+
if (!LicenseGate.isLicensed) { promise.reject(LicenseGate.ERROR_CODE, LicenseGate.ERROR_MESSAGE); return }
|
|
1574
1614
|
fun str(key: String) = if (options.hasKey(key) && !options.isNull(key)) options.getString(key) else null
|
|
1575
1615
|
fun num(key: String) = if (options.hasKey(key) && !options.isNull(key)) options.getDouble(key) else null
|
|
1576
1616
|
val lat = num("latitude")
|
|
@@ -1672,6 +1712,10 @@ class RNAttachmentKitModule(reactContext: ReactApplicationContext) :
|
|
|
1672
1712
|
|
|
1673
1713
|
@ReactMethod
|
|
1674
1714
|
fun setAIConfig(config: ReadableMap, promise: Promise) {
|
|
1715
|
+
// 라이선스 키가 없어도 설정은 받아 둠 (AI 버튼은 보이고, 누르면 "라이선스 필요" 안내)
|
|
1716
|
+
if (!LicenseGate.isLicensed && config.hasKey("enabled") && !config.isNull("enabled") && config.getBoolean("enabled")) {
|
|
1717
|
+
android.util.Log.w("RNAttachmentKit", "AI needs a license key — the AI buttons show a license notice until init() gets a valid key")
|
|
1718
|
+
}
|
|
1675
1719
|
AIBilling.apply(config.toHashMap())
|
|
1676
1720
|
val ctx = reactApplicationContext
|
|
1677
1721
|
fun emit(name: String, body: Map<String, Any>) {
|
|
@@ -1690,6 +1734,8 @@ class RNAttachmentKitModule(reactContext: ReactApplicationContext) :
|
|
|
1690
1734
|
|
|
1691
1735
|
@ReactMethod
|
|
1692
1736
|
fun reverseGeocode(options: ReadableMap, promise: Promise) {
|
|
1737
|
+
// 위치 기능은 라이선스 키가 있어야
|
|
1738
|
+
if (!LicenseGate.isLicensed) { promise.reject(LicenseGate.ERROR_CODE, LicenseGate.ERROR_MESSAGE); return }
|
|
1693
1739
|
val lat = if (options.hasKey("latitude") && !options.isNull("latitude")) options.getDouble("latitude") else Double.NaN
|
|
1694
1740
|
val lng = if (options.hasKey("longitude") && !options.isNull("longitude")) options.getDouble("longitude") else Double.NaN
|
|
1695
1741
|
val lang = if (options.hasKey("languageCode") && !options.isNull("languageCode")) options.getString("languageCode") else null
|