react-native-ensys-camera-x 1.0.7

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.
Files changed (506) hide show
  1. package/CustomCamera.podspec +20 -0
  2. package/LICENSE +20 -0
  3. package/README.md +833 -0
  4. package/android/build.gradle +135 -0
  5. package/android/consumer-rules.pro +47 -0
  6. package/android/src/main/AndroidManifest.xml +72 -0
  7. package/android/src/main/java/android/print/PdfPrint.java +51 -0
  8. package/android/src/main/java/com/customcamera/AnimatedImageViewManager.kt +199 -0
  9. package/android/src/main/java/com/customcamera/AudioPlayerView.kt +322 -0
  10. package/android/src/main/java/com/customcamera/AudioRecorder.kt +506 -0
  11. package/android/src/main/java/com/customcamera/BarcodeScannerActivity.kt +361 -0
  12. package/android/src/main/java/com/customcamera/BlurBackgroundView.kt +199 -0
  13. package/android/src/main/java/com/customcamera/CacheCleanupManager.kt +386 -0
  14. package/android/src/main/java/com/customcamera/CameraAspectRatioMode.kt +77 -0
  15. package/android/src/main/java/com/customcamera/CameraAsset.kt +211 -0
  16. package/android/src/main/java/com/customcamera/CameraTheme.kt +479 -0
  17. package/android/src/main/java/com/customcamera/CompressionCacheStore.kt +504 -0
  18. package/android/src/main/java/com/customcamera/CompressionFileNameUtils.kt +7 -0
  19. package/android/src/main/java/com/customcamera/CompressionProfile.kt +195 -0
  20. package/android/src/main/java/com/customcamera/CustomCameraActivity.kt +7291 -0
  21. package/android/src/main/java/com/customcamera/CustomCameraFileProvider.kt +9 -0
  22. package/android/src/main/java/com/customcamera/CustomCameraModule.kt +2479 -0
  23. package/android/src/main/java/com/customcamera/CustomCameraPackage.kt +53 -0
  24. package/android/src/main/java/com/customcamera/DisplayUtils.kt +9 -0
  25. package/android/src/main/java/com/customcamera/DocumentPreviewView.kt +846 -0
  26. package/android/src/main/java/com/customcamera/DrawingView.kt +214 -0
  27. package/android/src/main/java/com/customcamera/EmojiPickerData.kt +228 -0
  28. package/android/src/main/java/com/customcamera/FilterStripAdapter.kt +158 -0
  29. package/android/src/main/java/com/customcamera/GifSupport.kt +215 -0
  30. package/android/src/main/java/com/customcamera/GlobalAudioPlayer.kt +320 -0
  31. package/android/src/main/java/com/customcamera/ImageExporter.kt +1394 -0
  32. package/android/src/main/java/com/customcamera/ImageFormatSupport.kt +163 -0
  33. package/android/src/main/java/com/customcamera/Localization.kt +206 -0
  34. package/android/src/main/java/com/customcamera/MediaAdapters.kt +758 -0
  35. package/android/src/main/java/com/customcamera/MediaCacheManager.kt +782 -0
  36. package/android/src/main/java/com/customcamera/MediaExportFormats.kt +201 -0
  37. package/android/src/main/java/com/customcamera/MediaFileIo.kt +310 -0
  38. package/android/src/main/java/com/customcamera/MediaMetadataStore.kt +494 -0
  39. package/android/src/main/java/com/customcamera/MediaPreviewEditor.kt +14865 -0
  40. package/android/src/main/java/com/customcamera/MediaPublisher.kt +423 -0
  41. package/android/src/main/java/com/customcamera/MediaStorageWorkflow.kt +722 -0
  42. package/android/src/main/java/com/customcamera/MediaStoreRepository.kt +378 -0
  43. package/android/src/main/java/com/customcamera/MediaThumbnailView.kt +286 -0
  44. package/android/src/main/java/com/customcamera/MediaUtils.kt +37 -0
  45. package/android/src/main/java/com/customcamera/NativeVideoViewManager.kt +845 -0
  46. package/android/src/main/java/com/customcamera/OfficeDocumentConverter.kt +2791 -0
  47. package/android/src/main/java/com/customcamera/PendingCaptureCrops.kt +88 -0
  48. package/android/src/main/java/com/customcamera/PowerPointPreview.kt +4077 -0
  49. package/android/src/main/java/com/customcamera/RecentPickerStore.kt +74 -0
  50. package/android/src/main/java/com/customcamera/SecureFileEraser.kt +411 -0
  51. package/android/src/main/java/com/customcamera/SecureKeyModule.kt +88 -0
  52. package/android/src/main/java/com/customcamera/SecureMediaDeleter.kt +493 -0
  53. package/android/src/main/java/com/customcamera/StickerPickerData.kt +107 -0
  54. package/android/src/main/java/com/customcamera/SvgDrawable.kt +175 -0
  55. package/android/src/main/java/com/customcamera/SvgIconViewManager.kt +77 -0
  56. package/android/src/main/java/com/customcamera/TextEntryController.kt +553 -0
  57. package/android/src/main/java/com/customcamera/ThumbnailLoader.kt +455 -0
  58. package/android/src/main/java/com/customcamera/UploadService.kt +909 -0
  59. package/android/src/main/java/com/customcamera/VideoEditUtils.kt +210 -0
  60. package/android/src/main/java/com/customcamera/VideoRotationTranscoder.kt +1992 -0
  61. package/android/src/main/java/com/customcamera/gif/AnimatedGifEncoder.kt +473 -0
  62. package/android/src/main/java/com/customcamera/gif/GifDecoder.kt +690 -0
  63. package/android/src/main/java/com/customcamera/gif/GifFramesDrawable.kt +160 -0
  64. package/android/src/main/java/com/customcamera/gif/GifHttp.kt +112 -0
  65. package/android/src/main/java/com/customcamera/gif/GifImageLoader.kt +186 -0
  66. package/android/src/main/java/com/customcamera/gif/GifModels.kt +74 -0
  67. package/android/src/main/java/com/customcamera/gif/GifProvider.kt +49 -0
  68. package/android/src/main/java/com/customcamera/gif/GifProviders.kt +27 -0
  69. package/android/src/main/java/com/customcamera/gif/GifRepository.kt +124 -0
  70. package/android/src/main/java/com/customcamera/gif/GifSafety.kt +33 -0
  71. package/android/src/main/java/com/customcamera/gif/GiphyGifProvider.kt +146 -0
  72. package/android/src/main/java/com/customcamera/gif/LZWEncoder.kt +258 -0
  73. package/android/src/main/java/com/customcamera/gif/NeuQuant.kt +435 -0
  74. package/android/src/main/java/com/customcamera/gif/TenorGifProvider.kt +147 -0
  75. package/android/src/main/res/drawable/bg_brush_selected.xml +5 -0
  76. package/android/src/main/res/drawable/bg_color_swatch.xml +5 -0
  77. package/android/src/main/res/drawable/bg_editor_toolbar.xml +6 -0
  78. package/android/src/main/res/drawable/bg_icon_circle.xml +5 -0
  79. package/android/src/main/res/drawable/bg_icon_circle_light.xml +5 -0
  80. package/android/src/main/res/drawable/bg_mode_tab_active.xml +6 -0
  81. package/android/src/main/res/drawable/bg_pill.xml +6 -0
  82. package/android/src/main/res/drawable/bg_recording_dot.xml +6 -0
  83. package/android/src/main/res/drawable/bg_selection_badge.xml +8 -0
  84. package/android/src/main/res/drawable/bg_send_button.xml +5 -0
  85. package/android/src/main/res/drawable/bg_shutter_inner.xml +5 -0
  86. package/android/src/main/res/drawable/bg_shutter_recording.xml +6 -0
  87. package/android/src/main/res/drawable/bg_shutter_ring.xml +8 -0
  88. package/android/src/main/res/drawable/bg_shutter_video_idle.xml +5 -0
  89. package/android/src/main/res/drawable/bg_thumbnail.xml +9 -0
  90. package/android/src/main/res/drawable/ic_aspect_ratio.xml +17 -0
  91. package/android/src/main/res/drawable/ic_audio.xml +10 -0
  92. package/android/src/main/res/drawable/ic_back.xml +10 -0
  93. package/android/src/main/res/drawable/ic_brush_medium.xml +12 -0
  94. package/android/src/main/res/drawable/ic_brush_thick.xml +12 -0
  95. package/android/src/main/res/drawable/ic_brush_thin.xml +12 -0
  96. package/android/src/main/res/drawable/ic_check.xml +17 -0
  97. package/android/src/main/res/drawable/ic_chevron_down.xml +10 -0
  98. package/android/src/main/res/drawable/ic_chevron_up.xml +13 -0
  99. package/android/src/main/res/drawable/ic_clear_drawing.xml +10 -0
  100. package/android/src/main/res/drawable/ic_close.xml +10 -0
  101. package/android/src/main/res/drawable/ic_cover.xml +9 -0
  102. package/android/src/main/res/drawable/ic_crop.xml +10 -0
  103. package/android/src/main/res/drawable/ic_delete.xml +9 -0
  104. package/android/src/main/res/drawable/ic_document.xml +10 -0
  105. package/android/src/main/res/drawable/ic_done.xml +10 -0
  106. package/android/src/main/res/drawable/ic_download.xml +10 -0
  107. package/android/src/main/res/drawable/ic_emoji.xml +10 -0
  108. package/android/src/main/res/drawable/ic_filter.xml +10 -0
  109. package/android/src/main/res/drawable/ic_flash_auto.xml +7 -0
  110. package/android/src/main/res/drawable/ic_flash_off.xml +23 -0
  111. package/android/src/main/res/drawable/ic_flash_on.xml +16 -0
  112. package/android/src/main/res/drawable/ic_flip.xml +15 -0
  113. package/android/src/main/res/drawable/ic_gallery.xml +10 -0
  114. package/android/src/main/res/drawable/ic_gif.xml +24 -0
  115. package/android/src/main/res/drawable/ic_lock.xml +19 -0
  116. package/android/src/main/res/drawable/ic_lock_open.xml +22 -0
  117. package/android/src/main/res/drawable/ic_marker.xml +10 -0
  118. package/android/src/main/res/drawable/ic_more_vert.xml +18 -0
  119. package/android/src/main/res/drawable/ic_overlay_close.xml +19 -0
  120. package/android/src/main/res/drawable/ic_overlay_resize.xml +18 -0
  121. package/android/src/main/res/drawable/ic_pause.xml +10 -0
  122. package/android/src/main/res/drawable/ic_photo_mode.xml +10 -0
  123. package/android/src/main/res/drawable/ic_play.xml +10 -0
  124. package/android/src/main/res/drawable/ic_redo.xml +10 -0
  125. package/android/src/main/res/drawable/ic_rotate.xml +9 -0
  126. package/android/src/main/res/drawable/ic_send.xml +10 -0
  127. package/android/src/main/res/drawable/ic_share.xml +10 -0
  128. package/android/src/main/res/drawable/ic_stat_upload.xml +10 -0
  129. package/android/src/main/res/drawable/ic_sticker.xml +24 -0
  130. package/android/src/main/res/drawable/ic_text.xml +10 -0
  131. package/android/src/main/res/drawable/ic_text_align_center.xml +11 -0
  132. package/android/src/main/res/drawable/ic_text_align_left.xml +11 -0
  133. package/android/src/main/res/drawable/ic_text_align_right.xml +11 -0
  134. package/android/src/main/res/drawable/ic_text_bg.xml +11 -0
  135. package/android/src/main/res/drawable/ic_trim.xml +23 -0
  136. package/android/src/main/res/drawable/ic_undo.xml +10 -0
  137. package/android/src/main/res/drawable/ic_video_mode.xml +10 -0
  138. package/android/src/main/res/drawable/ic_video_note.xml +13 -0
  139. package/android/src/main/res/drawable/ic_volume_off.xml +9 -0
  140. package/android/src/main/res/drawable/ic_volume_on.xml +9 -0
  141. package/android/src/main/res/drawable/sticker_heart.xml +9 -0
  142. package/android/src/main/res/drawable/sticker_smile.xml +9 -0
  143. package/android/src/main/res/drawable/sticker_star.xml +9 -0
  144. package/android/src/main/res/drawable/sticker_thumbsup.xml +9 -0
  145. package/android/src/main/res/values/colors.xml +34 -0
  146. package/android/src/main/res/values/ids.xml +15 -0
  147. package/android/src/main/res/values/strings.xml +15 -0
  148. package/android/src/main/res/values/styles.xml +20 -0
  149. package/android/src/main/res/xml/file_paths.xml +34 -0
  150. package/ios/CustomCamera.h +5 -0
  151. package/ios/CustomCamera.mm +214 -0
  152. package/lib/module/AudioPlayerContext.js +233 -0
  153. package/lib/module/AudioPlayerContext.js.map +1 -0
  154. package/lib/module/CameraPreview.js +304 -0
  155. package/lib/module/CameraPreview.js.map +1 -0
  156. package/lib/module/NativeAnimatedImageView.js +41 -0
  157. package/lib/module/NativeAnimatedImageView.js.map +1 -0
  158. package/lib/module/NativeAudioPlayerView.js +33 -0
  159. package/lib/module/NativeAudioPlayerView.js.map +1 -0
  160. package/lib/module/NativeBlurView.js +35 -0
  161. package/lib/module/NativeBlurView.js.map +1 -0
  162. package/lib/module/NativeCustomCamera.js +128 -0
  163. package/lib/module/NativeCustomCamera.js.map +1 -0
  164. package/lib/module/NativeCustomCamera.web.js +143 -0
  165. package/lib/module/NativeCustomCamera.web.js.map +1 -0
  166. package/lib/module/NativeCustomVideoView.js +5 -0
  167. package/lib/module/NativeCustomVideoView.js.map +1 -0
  168. package/lib/module/NativeDocumentPreviewView.js +156 -0
  169. package/lib/module/NativeDocumentPreviewView.js.map +1 -0
  170. package/lib/module/NativeMediaThumbnailView.js +110 -0
  171. package/lib/module/NativeMediaThumbnailView.js.map +1 -0
  172. package/lib/module/NativeSvgIcon.js +32 -0
  173. package/lib/module/NativeSvgIcon.js.map +1 -0
  174. package/lib/module/ResponsivePreviewContainer.js +171 -0
  175. package/lib/module/ResponsivePreviewContainer.js.map +1 -0
  176. package/lib/module/accessibility.js +61 -0
  177. package/lib/module/accessibility.js.map +1 -0
  178. package/lib/module/config.js +352 -0
  179. package/lib/module/config.js.map +1 -0
  180. package/lib/module/cpuOptimization.js +296 -0
  181. package/lib/module/cpuOptimization.js.map +1 -0
  182. package/lib/module/db/DatabaseManager.js +191 -0
  183. package/lib/module/db/DatabaseManager.js.map +1 -0
  184. package/lib/module/db/MediaMapper.js +284 -0
  185. package/lib/module/db/MediaMapper.js.map +1 -0
  186. package/lib/module/db/MediaRepository.js +242 -0
  187. package/lib/module/db/MediaRepository.js.map +1 -0
  188. package/lib/module/db/index.js +41 -0
  189. package/lib/module/db/index.js.map +1 -0
  190. package/lib/module/db/migrations.js +180 -0
  191. package/lib/module/db/migrations.js.map +1 -0
  192. package/lib/module/db/react-native-sqlcipher.d.js +2 -0
  193. package/lib/module/db/react-native-sqlcipher.d.js.map +1 -0
  194. package/lib/module/db/secureKey.js +87 -0
  195. package/lib/module/db/secureKey.js.map +1 -0
  196. package/lib/module/db/types.js +57 -0
  197. package/lib/module/db/types.js.map +1 -0
  198. package/lib/module/db/useMediaDatabase.js +123 -0
  199. package/lib/module/db/useMediaDatabase.js.map +1 -0
  200. package/lib/module/defaultIcons.js +2259 -0
  201. package/lib/module/defaultIcons.js.map +1 -0
  202. package/lib/module/deviceCapabilities.js +408 -0
  203. package/lib/module/deviceCapabilities.js.map +1 -0
  204. package/lib/module/errors.js +267 -0
  205. package/lib/module/errors.js.map +1 -0
  206. package/lib/module/iconConfig.js +796 -0
  207. package/lib/module/iconConfig.js.map +1 -0
  208. package/lib/module/icons.js +514 -0
  209. package/lib/module/icons.js.map +1 -0
  210. package/lib/module/index.js +251 -0
  211. package/lib/module/index.js.map +1 -0
  212. package/lib/module/jest-globals.d.js +2 -0
  213. package/lib/module/jest-globals.d.js.map +1 -0
  214. package/lib/module/library/MediaLibrary.js +107 -0
  215. package/lib/module/library/MediaLibrary.js.map +1 -0
  216. package/lib/module/library/MediaLibraryScreen.js +2597 -0
  217. package/lib/module/library/MediaLibraryScreen.js.map +1 -0
  218. package/lib/module/library/components/AudioPreview.js +74 -0
  219. package/lib/module/library/components/AudioPreview.js.map +1 -0
  220. package/lib/module/library/components/DocumentPreview.js +331 -0
  221. package/lib/module/library/components/DocumentPreview.js.map +1 -0
  222. package/lib/module/library/components/ExpandableFab.js +343 -0
  223. package/lib/module/library/components/ExpandableFab.js.map +1 -0
  224. package/lib/module/library/components/FileTiles.js +251 -0
  225. package/lib/module/library/components/FileTiles.js.map +1 -0
  226. package/lib/module/library/components/ImagePreview.js +70 -0
  227. package/lib/module/library/components/ImagePreview.js.map +1 -0
  228. package/lib/module/library/components/MediaMetadataHeader.js +175 -0
  229. package/lib/module/library/components/MediaMetadataHeader.js.map +1 -0
  230. package/lib/module/library/components/RecordAudioScreen.js +816 -0
  231. package/lib/module/library/components/RecordAudioScreen.js.map +1 -0
  232. package/lib/module/library/components/VideoPreview.js +453 -0
  233. package/lib/module/library/components/VideoPreview.js.map +1 -0
  234. package/lib/module/library/components/ViewerCounter.js +55 -0
  235. package/lib/module/library/components/ViewerCounter.js.map +1 -0
  236. package/lib/module/library/compressionEstimate.js +507 -0
  237. package/lib/module/library/compressionEstimate.js.map +1 -0
  238. package/lib/module/library/fileMetadata.js +196 -0
  239. package/lib/module/library/fileMetadata.js.map +1 -0
  240. package/lib/module/library/index.js +42 -0
  241. package/lib/module/library/index.js.map +1 -0
  242. package/lib/module/library/libraryIconRegistry.js +69 -0
  243. package/lib/module/library/libraryIconRegistry.js.map +1 -0
  244. package/lib/module/library/libraryIcons.js +858 -0
  245. package/lib/module/library/libraryIcons.js.map +1 -0
  246. package/lib/module/library/localizedFormat.js +181 -0
  247. package/lib/module/library/localizedFormat.js.map +1 -0
  248. package/lib/module/library/palette.js +225 -0
  249. package/lib/module/library/palette.js.map +1 -0
  250. package/lib/module/library/responsive.js +53 -0
  251. package/lib/module/library/responsive.js.map +1 -0
  252. package/lib/module/locales.js +416 -0
  253. package/lib/module/locales.js.map +1 -0
  254. package/lib/module/localization.js +1064 -0
  255. package/lib/module/localization.js.map +1 -0
  256. package/lib/module/logger.js +20 -0
  257. package/lib/module/logger.js.map +1 -0
  258. package/lib/module/lowMemoryCache.js +206 -0
  259. package/lib/module/lowMemoryCache.js.map +1 -0
  260. package/lib/module/mediaHistory.js +33 -0
  261. package/lib/module/mediaHistory.js.map +1 -0
  262. package/lib/module/mediaStorageService.js +402 -0
  263. package/lib/module/mediaStorageService.js.map +1 -0
  264. package/lib/module/memory.js +79 -0
  265. package/lib/module/memory.js.map +1 -0
  266. package/lib/module/nativeActions.js +307 -0
  267. package/lib/module/nativeActions.js.map +1 -0
  268. package/lib/module/nativeComponentSupport.js +61 -0
  269. package/lib/module/nativeComponentSupport.js.map +1 -0
  270. package/lib/module/networkPerformance.js +24 -0
  271. package/lib/module/networkPerformance.js.map +1 -0
  272. package/lib/module/package.json +1 -0
  273. package/lib/module/performance.js +104 -0
  274. package/lib/module/performance.js.map +1 -0
  275. package/lib/module/performanceMonitor.js +155 -0
  276. package/lib/module/performanceMonitor.js.map +1 -0
  277. package/lib/module/permissions.js +370 -0
  278. package/lib/module/permissions.js.map +1 -0
  279. package/lib/module/rendering.js +40 -0
  280. package/lib/module/rendering.js.map +1 -0
  281. package/lib/module/security.js +18 -0
  282. package/lib/module/security.js.map +1 -0
  283. package/lib/module/state.js +266 -0
  284. package/lib/module/state.js.map +1 -0
  285. package/lib/module/storage.js +400 -0
  286. package/lib/module/storage.js.map +1 -0
  287. package/lib/module/theme.js +390 -0
  288. package/lib/module/theme.js.map +1 -0
  289. package/lib/module/types.js +4 -0
  290. package/lib/module/types.js.map +1 -0
  291. package/lib/module/utils.js +115 -0
  292. package/lib/module/utils.js.map +1 -0
  293. package/lib/module/validation.js +288 -0
  294. package/lib/module/validation.js.map +1 -0
  295. package/lib/typescript/package.json +1 -0
  296. package/lib/typescript/src/AudioPlayerContext.d.ts +27 -0
  297. package/lib/typescript/src/AudioPlayerContext.d.ts.map +1 -0
  298. package/lib/typescript/src/CameraPreview.d.ts +13 -0
  299. package/lib/typescript/src/CameraPreview.d.ts.map +1 -0
  300. package/lib/typescript/src/NativeAnimatedImageView.d.ts +33 -0
  301. package/lib/typescript/src/NativeAnimatedImageView.d.ts.map +1 -0
  302. package/lib/typescript/src/NativeAudioPlayerView.d.ts +12 -0
  303. package/lib/typescript/src/NativeAudioPlayerView.d.ts.map +1 -0
  304. package/lib/typescript/src/NativeBlurView.d.ts +12 -0
  305. package/lib/typescript/src/NativeBlurView.d.ts.map +1 -0
  306. package/lib/typescript/src/NativeCustomCamera.d.ts +167 -0
  307. package/lib/typescript/src/NativeCustomCamera.d.ts.map +1 -0
  308. package/lib/typescript/src/NativeCustomCamera.web.d.ts +47 -0
  309. package/lib/typescript/src/NativeCustomCamera.web.d.ts.map +1 -0
  310. package/lib/typescript/src/NativeCustomVideoView.d.ts +30 -0
  311. package/lib/typescript/src/NativeCustomVideoView.d.ts.map +1 -0
  312. package/lib/typescript/src/NativeDocumentPreviewView.d.ts +48 -0
  313. package/lib/typescript/src/NativeDocumentPreviewView.d.ts.map +1 -0
  314. package/lib/typescript/src/NativeMediaThumbnailView.d.ts +54 -0
  315. package/lib/typescript/src/NativeMediaThumbnailView.d.ts.map +1 -0
  316. package/lib/typescript/src/NativeSvgIcon.d.ts +25 -0
  317. package/lib/typescript/src/NativeSvgIcon.d.ts.map +1 -0
  318. package/lib/typescript/src/ResponsivePreviewContainer.d.ts +92 -0
  319. package/lib/typescript/src/ResponsivePreviewContainer.d.ts.map +1 -0
  320. package/lib/typescript/src/accessibility.d.ts +20 -0
  321. package/lib/typescript/src/accessibility.d.ts.map +1 -0
  322. package/lib/typescript/src/config.d.ts +145 -0
  323. package/lib/typescript/src/config.d.ts.map +1 -0
  324. package/lib/typescript/src/cpuOptimization.d.ts +63 -0
  325. package/lib/typescript/src/cpuOptimization.d.ts.map +1 -0
  326. package/lib/typescript/src/db/DatabaseManager.d.ts +48 -0
  327. package/lib/typescript/src/db/DatabaseManager.d.ts.map +1 -0
  328. package/lib/typescript/src/db/MediaMapper.d.ts +77 -0
  329. package/lib/typescript/src/db/MediaMapper.d.ts.map +1 -0
  330. package/lib/typescript/src/db/MediaRepository.d.ts +66 -0
  331. package/lib/typescript/src/db/MediaRepository.d.ts.map +1 -0
  332. package/lib/typescript/src/db/index.d.ts +23 -0
  333. package/lib/typescript/src/db/index.d.ts.map +1 -0
  334. package/lib/typescript/src/db/migrations.d.ts +31 -0
  335. package/lib/typescript/src/db/migrations.d.ts.map +1 -0
  336. package/lib/typescript/src/db/secureKey.d.ts +10 -0
  337. package/lib/typescript/src/db/secureKey.d.ts.map +1 -0
  338. package/lib/typescript/src/db/types.d.ts +115 -0
  339. package/lib/typescript/src/db/types.d.ts.map +1 -0
  340. package/lib/typescript/src/db/useMediaDatabase.d.ts +41 -0
  341. package/lib/typescript/src/db/useMediaDatabase.d.ts.map +1 -0
  342. package/lib/typescript/src/defaultIcons.d.ts +139 -0
  343. package/lib/typescript/src/defaultIcons.d.ts.map +1 -0
  344. package/lib/typescript/src/deviceCapabilities.d.ts +92 -0
  345. package/lib/typescript/src/deviceCapabilities.d.ts.map +1 -0
  346. package/lib/typescript/src/errors.d.ts +95 -0
  347. package/lib/typescript/src/errors.d.ts.map +1 -0
  348. package/lib/typescript/src/iconConfig.d.ts +176 -0
  349. package/lib/typescript/src/iconConfig.d.ts.map +1 -0
  350. package/lib/typescript/src/icons.d.ts +217 -0
  351. package/lib/typescript/src/icons.d.ts.map +1 -0
  352. package/lib/typescript/src/index.d.ts +104 -0
  353. package/lib/typescript/src/index.d.ts.map +1 -0
  354. package/lib/typescript/src/library/MediaLibrary.d.ts +57 -0
  355. package/lib/typescript/src/library/MediaLibrary.d.ts.map +1 -0
  356. package/lib/typescript/src/library/MediaLibraryScreen.d.ts +51 -0
  357. package/lib/typescript/src/library/MediaLibraryScreen.d.ts.map +1 -0
  358. package/lib/typescript/src/library/components/AudioPreview.d.ts +6 -0
  359. package/lib/typescript/src/library/components/AudioPreview.d.ts.map +1 -0
  360. package/lib/typescript/src/library/components/DocumentPreview.d.ts +10 -0
  361. package/lib/typescript/src/library/components/DocumentPreview.d.ts.map +1 -0
  362. package/lib/typescript/src/library/components/ExpandableFab.d.ts +39 -0
  363. package/lib/typescript/src/library/components/ExpandableFab.d.ts.map +1 -0
  364. package/lib/typescript/src/library/components/FileTiles.d.ts +8 -0
  365. package/lib/typescript/src/library/components/FileTiles.d.ts.map +1 -0
  366. package/lib/typescript/src/library/components/ImagePreview.d.ts +5 -0
  367. package/lib/typescript/src/library/components/ImagePreview.d.ts.map +1 -0
  368. package/lib/typescript/src/library/components/MediaMetadataHeader.d.ts +16 -0
  369. package/lib/typescript/src/library/components/MediaMetadataHeader.d.ts.map +1 -0
  370. package/lib/typescript/src/library/components/RecordAudioScreen.d.ts +8 -0
  371. package/lib/typescript/src/library/components/RecordAudioScreen.d.ts.map +1 -0
  372. package/lib/typescript/src/library/components/VideoPreview.d.ts +20 -0
  373. package/lib/typescript/src/library/components/VideoPreview.d.ts.map +1 -0
  374. package/lib/typescript/src/library/components/ViewerCounter.d.ts +13 -0
  375. package/lib/typescript/src/library/components/ViewerCounter.d.ts.map +1 -0
  376. package/lib/typescript/src/library/compressionEstimate.d.ts +313 -0
  377. package/lib/typescript/src/library/compressionEstimate.d.ts.map +1 -0
  378. package/lib/typescript/src/library/fileMetadata.d.ts +33 -0
  379. package/lib/typescript/src/library/fileMetadata.d.ts.map +1 -0
  380. package/lib/typescript/src/library/index.d.ts +40 -0
  381. package/lib/typescript/src/library/index.d.ts.map +1 -0
  382. package/lib/typescript/src/library/libraryIconRegistry.d.ts +35 -0
  383. package/lib/typescript/src/library/libraryIconRegistry.d.ts.map +1 -0
  384. package/lib/typescript/src/library/libraryIcons.d.ts +85 -0
  385. package/lib/typescript/src/library/libraryIcons.d.ts.map +1 -0
  386. package/lib/typescript/src/library/localizedFormat.d.ts +49 -0
  387. package/lib/typescript/src/library/localizedFormat.d.ts.map +1 -0
  388. package/lib/typescript/src/library/palette.d.ts +104 -0
  389. package/lib/typescript/src/library/palette.d.ts.map +1 -0
  390. package/lib/typescript/src/library/responsive.d.ts +15 -0
  391. package/lib/typescript/src/library/responsive.d.ts.map +1 -0
  392. package/lib/typescript/src/locales.d.ts +103 -0
  393. package/lib/typescript/src/locales.d.ts.map +1 -0
  394. package/lib/typescript/src/localization.d.ts +1051 -0
  395. package/lib/typescript/src/localization.d.ts.map +1 -0
  396. package/lib/typescript/src/logger.d.ts +4 -0
  397. package/lib/typescript/src/logger.d.ts.map +1 -0
  398. package/lib/typescript/src/lowMemoryCache.d.ts +64 -0
  399. package/lib/typescript/src/lowMemoryCache.d.ts.map +1 -0
  400. package/lib/typescript/src/mediaHistory.d.ts +3 -0
  401. package/lib/typescript/src/mediaHistory.d.ts.map +1 -0
  402. package/lib/typescript/src/mediaStorageService.d.ts +155 -0
  403. package/lib/typescript/src/mediaStorageService.d.ts.map +1 -0
  404. package/lib/typescript/src/memory.d.ts +21 -0
  405. package/lib/typescript/src/memory.d.ts.map +1 -0
  406. package/lib/typescript/src/nativeActions.d.ts +174 -0
  407. package/lib/typescript/src/nativeActions.d.ts.map +1 -0
  408. package/lib/typescript/src/nativeComponentSupport.d.ts +30 -0
  409. package/lib/typescript/src/nativeComponentSupport.d.ts.map +1 -0
  410. package/lib/typescript/src/networkPerformance.d.ts +12 -0
  411. package/lib/typescript/src/networkPerformance.d.ts.map +1 -0
  412. package/lib/typescript/src/performance.d.ts +32 -0
  413. package/lib/typescript/src/performance.d.ts.map +1 -0
  414. package/lib/typescript/src/performanceMonitor.d.ts +45 -0
  415. package/lib/typescript/src/performanceMonitor.d.ts.map +1 -0
  416. package/lib/typescript/src/permissions.d.ts +132 -0
  417. package/lib/typescript/src/permissions.d.ts.map +1 -0
  418. package/lib/typescript/src/rendering.d.ts +9 -0
  419. package/lib/typescript/src/rendering.d.ts.map +1 -0
  420. package/lib/typescript/src/security.d.ts +6 -0
  421. package/lib/typescript/src/security.d.ts.map +1 -0
  422. package/lib/typescript/src/state.d.ts +102 -0
  423. package/lib/typescript/src/state.d.ts.map +1 -0
  424. package/lib/typescript/src/storage.d.ts +117 -0
  425. package/lib/typescript/src/storage.d.ts.map +1 -0
  426. package/lib/typescript/src/theme.d.ts +15 -0
  427. package/lib/typescript/src/theme.d.ts.map +1 -0
  428. package/lib/typescript/src/types.d.ts +802 -0
  429. package/lib/typescript/src/types.d.ts.map +1 -0
  430. package/lib/typescript/src/utils.d.ts +50 -0
  431. package/lib/typescript/src/utils.d.ts.map +1 -0
  432. package/lib/typescript/src/validation.d.ts +52 -0
  433. package/lib/typescript/src/validation.d.ts.map +1 -0
  434. package/package.json +204 -0
  435. package/react-native.config.js +17 -0
  436. package/src/AudioPlayerContext.tsx +220 -0
  437. package/src/CameraPreview.tsx +352 -0
  438. package/src/NativeAnimatedImageView.tsx +71 -0
  439. package/src/NativeAudioPlayerView.tsx +39 -0
  440. package/src/NativeBlurView.tsx +40 -0
  441. package/src/NativeCustomCamera.ts +305 -0
  442. package/src/NativeCustomCamera.web.ts +192 -0
  443. package/src/NativeCustomVideoView.ts +30 -0
  444. package/src/NativeDocumentPreviewView.tsx +196 -0
  445. package/src/NativeMediaThumbnailView.tsx +167 -0
  446. package/src/NativeSvgIcon.tsx +46 -0
  447. package/src/ResponsivePreviewContainer.tsx +227 -0
  448. package/src/accessibility.ts +80 -0
  449. package/src/config.ts +406 -0
  450. package/src/cpuOptimization.ts +365 -0
  451. package/src/db/DatabaseManager.ts +212 -0
  452. package/src/db/MediaMapper.ts +377 -0
  453. package/src/db/MediaRepository.ts +313 -0
  454. package/src/db/index.ts +73 -0
  455. package/src/db/migrations.ts +222 -0
  456. package/src/db/react-native-sqlcipher.d.ts +76 -0
  457. package/src/db/secureKey.ts +100 -0
  458. package/src/db/types.ts +123 -0
  459. package/src/db/useMediaDatabase.ts +191 -0
  460. package/src/defaultIcons.tsx +2390 -0
  461. package/src/deviceCapabilities.ts +438 -0
  462. package/src/errors.ts +350 -0
  463. package/src/iconConfig.tsx +962 -0
  464. package/src/icons.tsx +788 -0
  465. package/src/index.tsx +763 -0
  466. package/src/jest-globals.d.ts +9 -0
  467. package/src/library/MediaLibrary.tsx +141 -0
  468. package/src/library/MediaLibraryScreen.tsx +3003 -0
  469. package/src/library/components/AudioPreview.tsx +73 -0
  470. package/src/library/components/DocumentPreview.tsx +361 -0
  471. package/src/library/components/ExpandableFab.tsx +342 -0
  472. package/src/library/components/FileTiles.tsx +252 -0
  473. package/src/library/components/ImagePreview.tsx +72 -0
  474. package/src/library/components/MediaMetadataHeader.tsx +208 -0
  475. package/src/library/components/RecordAudioScreen.tsx +845 -0
  476. package/src/library/components/VideoPreview.tsx +478 -0
  477. package/src/library/components/ViewerCounter.tsx +71 -0
  478. package/src/library/compressionEstimate.ts +662 -0
  479. package/src/library/fileMetadata.ts +212 -0
  480. package/src/library/index.ts +66 -0
  481. package/src/library/libraryIconRegistry.tsx +86 -0
  482. package/src/library/libraryIcons.tsx +928 -0
  483. package/src/library/localizedFormat.ts +207 -0
  484. package/src/library/palette.ts +281 -0
  485. package/src/library/responsive.ts +49 -0
  486. package/src/locales.ts +381 -0
  487. package/src/localization.ts +1947 -0
  488. package/src/logger.ts +20 -0
  489. package/src/lowMemoryCache.ts +218 -0
  490. package/src/mediaHistory.ts +42 -0
  491. package/src/mediaStorageService.ts +546 -0
  492. package/src/memory.ts +102 -0
  493. package/src/nativeActions.ts +422 -0
  494. package/src/nativeComponentSupport.ts +60 -0
  495. package/src/networkPerformance.ts +30 -0
  496. package/src/performance.ts +137 -0
  497. package/src/performanceMonitor.ts +219 -0
  498. package/src/permissions.ts +403 -0
  499. package/src/rendering.ts +47 -0
  500. package/src/security.ts +19 -0
  501. package/src/state.ts +333 -0
  502. package/src/storage.ts +453 -0
  503. package/src/theme.tsx +467 -0
  504. package/src/types.ts +946 -0
  505. package/src/utils.ts +177 -0
  506. package/src/validation.ts +372 -0
@@ -0,0 +1,846 @@
1
+ package com.customcamera
2
+
3
+ import android.app.ActivityManager
4
+ import android.content.Context
5
+ import android.content.res.ColorStateList
6
+ import android.graphics.Bitmap
7
+ import android.graphics.BitmapFactory
8
+ import android.graphics.Color
9
+ import android.graphics.Typeface
10
+ import android.graphics.pdf.PdfRenderer
11
+ import android.net.Uri
12
+ import android.os.Handler
13
+ import android.os.Looper
14
+ import android.os.ParcelFileDescriptor
15
+ import android.util.LruCache
16
+ import android.util.TypedValue
17
+ import android.view.Gravity
18
+ import android.view.ViewGroup
19
+ import android.webkit.WebResourceRequest
20
+ import android.webkit.WebView
21
+ import android.webkit.WebViewClient
22
+ import android.widget.FrameLayout
23
+ import android.widget.ImageView
24
+ import android.widget.ProgressBar
25
+ import android.widget.ScrollView
26
+ import android.widget.TextView
27
+ import androidx.recyclerview.widget.LinearLayoutManager
28
+ import androidx.recyclerview.widget.RecyclerView
29
+ import com.facebook.react.bridge.Arguments
30
+ import com.facebook.react.bridge.ReactContext
31
+ import com.facebook.react.bridge.WritableMap
32
+ import com.facebook.react.uimanager.SimpleViewManager
33
+ import com.facebook.react.uimanager.ThemedReactContext
34
+ import com.facebook.react.uimanager.UIManagerHelper
35
+ import com.facebook.react.uimanager.annotations.ReactProp
36
+ import com.facebook.react.uimanager.events.Event
37
+ import java.io.ByteArrayOutputStream
38
+ import java.io.File
39
+ import java.io.FileOutputStream
40
+ import java.io.IOException
41
+ import java.util.concurrent.ExecutorService
42
+ import java.util.concurrent.Executors
43
+ import java.util.concurrent.RejectedExecutionException
44
+
45
+ /**
46
+ * ViewManager for a native, dependency-free IN-APP document preview used by the full-screen
47
+ * media viewer. Replaces the old "Open" → external ACTION_VIEW hand-off for the formats Android
48
+ * can render itself — the document is shown directly inside the existing preview screen, keeping
49
+ * the viewer's paging, counter and close chrome intact.
50
+ *
51
+ * Renderers (all standard Android framework — no third-party preview library):
52
+ * - `pdf` → [PdfRenderer] pages in a vertically scrolling [RecyclerView]. Pages are rasterized
53
+ * lazily on a single background thread at (capped) view width and held in a
54
+ * heap-bounded [LruCache], so a 200-page PDF costs only the on-screen pages.
55
+ * - `text` → plain text (TXT/LOG/…) in a scrollable [TextView]; `code` is the same renderer
56
+ * with a monospace face for structured text (JSON/XML/CSV). Reads are size-capped
57
+ * and binary content is rejected instead of rendered as garbage.
58
+ * - `html` → a locked-down [WebView] (JavaScript, file/content access and network loads all
59
+ * disabled; navigation blocked) fed the file's bytes directly, so it behaves as a
60
+ * static document renderer rather than a browser.
61
+ * - `image` → image-based documents decoded with inSampleSize down-sampling into an ImageView.
62
+ *
63
+ * The React side chooses the renderer via the `kind` prop (it owns the extension/MIME mapping and
64
+ * the unsupported-format info card) and listens for `onPreviewLoad` (`{pageCount}`) /
65
+ * `onPreviewError` (`{message}`) to fall back to the card when a file can't actually be rendered.
66
+ * `active` mirrors the viewer's visible-page gate (like the audio/video cells): PDF page bitmaps
67
+ * are only rasterized while the slide is the active page, so the pager's mounted-but-off-screen
68
+ * neighbours never stack up full-width page bitmaps on low-RAM devices.
69
+ */
70
+ class DocumentPreviewViewManager : SimpleViewManager<DocumentPreviewView>() {
71
+ override fun getName() = "CustomDocumentPreview"
72
+
73
+ override fun createViewInstance(reactContext: ThemedReactContext): DocumentPreviewView =
74
+ DocumentPreviewView(reactContext)
75
+
76
+ override fun onDropViewInstance(view: DocumentPreviewView) {
77
+ view.release()
78
+ super.onDropViewInstance(view)
79
+ }
80
+
81
+ @ReactProp(name = "src")
82
+ fun setSrc(view: DocumentPreviewView, src: String?) {
83
+ view.setSource(src)
84
+ }
85
+
86
+ @ReactProp(name = "kind")
87
+ fun setKind(view: DocumentPreviewView, kind: String?) {
88
+ view.setKind(kind)
89
+ }
90
+
91
+ @ReactProp(name = "active", defaultBoolean = true)
92
+ fun setActive(view: DocumentPreviewView, active: Boolean) {
93
+ view.setActive(active)
94
+ }
95
+
96
+ // Merge with the base constants (à la BaseViewManager) instead of replacing them, so
97
+ // framework events like topAccessibilityAction keep working on this view type.
98
+ override fun getExportedCustomDirectEventTypeConstants(): MutableMap<String, Any> {
99
+ val constants: MutableMap<String, Any> =
100
+ super.getExportedCustomDirectEventTypeConstants() ?: mutableMapOf()
101
+ constants["topPreviewLoad"] = mutableMapOf("registrationName" to "onPreviewLoad")
102
+ constants["topPreviewError"] = mutableMapOf("registrationName" to "onPreviewError")
103
+ return constants
104
+ }
105
+ }
106
+
107
+ /** Direct event carrying either the load result or a render failure to the React side. */
108
+ private class DocumentPreviewEvent(
109
+ surfaceId: Int,
110
+ viewId: Int,
111
+ private val name: String,
112
+ private val payload: WritableMap
113
+ ) : Event<DocumentPreviewEvent>(surfaceId, viewId) {
114
+ override fun getEventName() = name
115
+ override fun getEventData(): WritableMap = payload
116
+ // Load/error are one-shot state transitions — never merge two into one delivery.
117
+ override fun canCoalesce(): Boolean = false
118
+ }
119
+
120
+ class DocumentPreviewView(context: Context) : FrameLayout(context) {
121
+ private val mainHandler = Handler(Looper.getMainLooper())
122
+
123
+ private val theme = CameraTheme.get(context)
124
+ private val accentColor =
125
+ theme.componentColor("documentPreview", "accent", Color.parseColor("#4A56FF"))
126
+ private val textColor =
127
+ theme.componentColor("documentPreview", "text", Color.parseColor("#E2E8F0"))
128
+ private val textBackground =
129
+ theme.componentColor("documentPreview", "textBackground", Color.parseColor("#0B0D1F"))
130
+ /** Unrendered-page / web-view backing; stock white so pages read as paper. */
131
+ private val pageBackground =
132
+ theme.componentColor("documentPreview", "pageBackground", Color.WHITE)
133
+
134
+ /**
135
+ * Single worker thread owning ALL file/PdfRenderer access. PdfRenderer is not thread-safe, so
136
+ * serializing every open/render/close on one executor removes the need for locks entirely —
137
+ * a close posted after a render can never interleave with it.
138
+ */
139
+ private var executor: ExecutorService? = null
140
+
141
+ /**
142
+ * Monotonic load generation. Every async hop (worker job, main-thread post) captures the
143
+ * generation it was started under and bails if a newer load/reset has since superseded it,
144
+ * so a recycled or re-sourced view can never apply a stale document's content.
145
+ */
146
+ @Volatile private var generation = 0
147
+
148
+ private var currentSrc: String? = null
149
+ private var currentKind: String? = null
150
+ private var isActive = true
151
+ /** Set by [release]; no reload or worker may start afterwards (a posted reload could
152
+ * otherwise recreate the executor thread on a dropped view and leak it). */
153
+ private var released = false
154
+
155
+ // ── PDF state ──
156
+ private var pdfRenderer: PdfRenderer? = null
157
+ private var pdfFd: ParcelFileDescriptor? = null
158
+ private var tempPdfFile: File? = null
159
+ private var pageCache: LruCache<Int, Bitmap>? = null
160
+ private val pageAspects = HashMap<Int, Float>()
161
+ private val pendingPages = HashSet<Int>()
162
+ private var pdfAdapter: PageAdapter? = null
163
+ private var recycler: RecyclerView? = null
164
+
165
+ // ── HTML state ──
166
+ private var webView: WebView? = null
167
+
168
+ init {
169
+ setBackgroundColor(Color.TRANSPARENT)
170
+ }
171
+
172
+ private val measureAndLayout = Runnable {
173
+ measure(
174
+ MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
175
+ MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
176
+ )
177
+ layout(left, top, right, bottom)
178
+ }
179
+
180
+ override fun requestLayout() {
181
+ super.requestLayout()
182
+ post(measureAndLayout)
183
+ }
184
+
185
+ fun setSource(src: String?) {
186
+ if (src == currentSrc) return
187
+ currentSrc = src
188
+ scheduleReload()
189
+ }
190
+
191
+ fun setKind(kind: String?) {
192
+ if (kind == currentKind) return
193
+ currentKind = kind
194
+ scheduleReload()
195
+ }
196
+
197
+ fun setActive(active: Boolean) {
198
+ if (active == isActive) return
199
+ isActive = active
200
+ if (active) pdfAdapter?.notifyDataSetChanged()
201
+ }
202
+
203
+ fun release() {
204
+ released = true
205
+ resetContent()
206
+ executor?.shutdown()
207
+ executor = null
208
+ }
209
+
210
+ // ── Loading pipeline ─────────────────────────────────────────────
211
+
212
+ private var reloadScheduled = false
213
+
214
+ private fun scheduleReload() {
215
+ if (reloadScheduled) return
216
+ reloadScheduled = true
217
+ mainHandler.post {
218
+ reloadScheduled = false
219
+ reload()
220
+ }
221
+ }
222
+
223
+ private fun reload() {
224
+ if (released) return
225
+ resetContent()
226
+ val src = currentSrc ?: return
227
+ val kind = currentKind ?: return
228
+ val gen = generation
229
+ val uri = Uri.parse(src)
230
+ showLoading()
231
+ when (kind) {
232
+ "pdf" -> loadPdf(uri, gen)
233
+ "text" -> loadText(uri, monospace = false, gen = gen)
234
+ "code" -> loadText(uri, monospace = true, gen = gen)
235
+ "html" -> loadHtml(uri, gen)
236
+ "image" -> loadImage(uri, gen)
237
+ "ppt", "pptx" -> loadPowerPoint(uri, kind, gen)
238
+ "docx", "xlsx", "odt", "ods", "rtf", "csv", "epub" -> loadOffice(uri, kind, gen)
239
+ else -> postError(gen, Localization.getString("unsupportedPreviewKind", "Unsupported preview kind: {0}", kind))
240
+ }
241
+ }
242
+
243
+ private fun resetContent() {
244
+ generation++
245
+ removeAllViews()
246
+ recycler = null
247
+ pdfAdapter = null
248
+ pendingPages.clear()
249
+ pageAspects.clear()
250
+ pageCache?.evictAll()
251
+ pageCache = null
252
+ webView?.let { web ->
253
+ try { web.stopLoading(); web.destroy() } catch (_: Exception) {}
254
+ }
255
+ webView = null
256
+ powerPointPresentation?.release()
257
+ powerPointPresentation = null
258
+ powerPointAdapter = null
259
+ if (executor != null) {
260
+ runOnWorker {
261
+ try { pdfRenderer?.close() } catch (_: Exception) {}
262
+ try { pdfFd?.close() } catch (_: Exception) {}
263
+ pdfRenderer = null
264
+ pdfFd = null
265
+ tempPdfFile?.delete(); tempPdfFile = null
266
+ tempOfficeFile?.delete(); tempOfficeFile = null
267
+ }
268
+ }
269
+ }
270
+
271
+ private fun runOnWorker(block: () -> Unit) {
272
+ val exec = executor
273
+ ?: Executors.newSingleThreadExecutor { r ->
274
+ Thread(r, "DocumentPreview").apply { priority = Thread.NORM_PRIORITY - 1 }
275
+ }.also { executor = it }
276
+ try { exec.execute(block) } catch (_: RejectedExecutionException) {}
277
+ }
278
+
279
+ private fun showLoading() {
280
+ val spinner = ProgressBar(context).apply {
281
+ isIndeterminate = true
282
+ indeterminateTintList = ColorStateList.valueOf(accentColor)
283
+ }
284
+ removeAllViews()
285
+ addView(spinner, LayoutParams(dp(40), dp(40), Gravity.CENTER))
286
+ }
287
+
288
+ private fun attachContent(
289
+ view: android.view.View,
290
+ params: LayoutParams = LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
291
+ ) {
292
+ removeAllViews()
293
+ addView(view, params)
294
+ }
295
+
296
+ private fun postError(gen: Int, message: String) {
297
+ mainHandler.post {
298
+ if (gen != generation) return@post
299
+ removeAllViews()
300
+ emitEvent("topPreviewError", Arguments.createMap().apply { putString("message", message) })
301
+ }
302
+ }
303
+
304
+ private fun emitLoaded(pageCount: Int) {
305
+ emitEvent("topPreviewLoad", Arguments.createMap().apply { putInt("pageCount", pageCount) })
306
+ }
307
+
308
+ private fun emitEvent(name: String, payload: WritableMap) {
309
+ val reactContext = context as? ReactContext ?: return
310
+ val dispatcher = UIManagerHelper.getEventDispatcherForReactTag(reactContext, id) ?: return
311
+ dispatcher.dispatchEvent(
312
+ DocumentPreviewEvent(UIManagerHelper.getSurfaceId(reactContext), id, name, payload)
313
+ )
314
+ }
315
+
316
+ // ── PDF ──────────────────────────────────────────────────────────
317
+
318
+ private fun loadPdf(uri: Uri, gen: Int) {
319
+ runOnWorker {
320
+ if (gen != generation) return@runOnWorker
321
+ val fd: ParcelFileDescriptor
322
+ val renderer: PdfRenderer
323
+ try {
324
+ fd = openSeekableFd(uri)
325
+ renderer = PdfRenderer(fd)
326
+ } catch (e: Exception) {
327
+ postError(gen, Localization.getString("couldNotOpenPdf", "Could not open PDF: {0}", e.message ?: ""))
328
+ return@runOnWorker
329
+ }
330
+ if (gen != generation) {
331
+ try { renderer.close() } catch (_: Exception) {}
332
+ try { fd.close() } catch (_: Exception) {}
333
+ return@runOnWorker
334
+ }
335
+ pdfRenderer = renderer
336
+ pdfFd = fd
337
+ val pageCount = renderer.pageCount
338
+ mainHandler.post {
339
+ if (gen != generation) return@post
340
+ showPdfUi(pageCount)
341
+ }
342
+ }
343
+ }
344
+
345
+ private fun openSeekableFd(uri: Uri): ParcelFileDescriptor {
346
+ val resolver = context.contentResolver
347
+ try {
348
+ val direct = resolver.openFileDescriptor(uri, "r")
349
+ if (direct != null) {
350
+ if (direct.statSize >= 0) return direct
351
+ try { direct.close() } catch (_: Exception) {}
352
+ }
353
+ } catch (_: Exception) {}
354
+ val input = resolver.openInputStream(uri)
355
+ ?: throw IOException("Cannot open document stream")
356
+ val temp = File.createTempFile("doc_preview_", ".pdf", context.cacheDir)
357
+ input.use { stream -> FileOutputStream(temp).use { out -> stream.copyTo(out) } }
358
+ tempPdfFile = temp
359
+ return ParcelFileDescriptor.open(temp, ParcelFileDescriptor.MODE_READ_ONLY)
360
+ }
361
+
362
+ private fun showPdfUi(pageCount: Int) {
363
+ val cacheKb = (Runtime.getRuntime().maxMemory() / 1024L / 16L).toInt().coerceAtLeast(4096)
364
+ pageCache = object : LruCache<Int, Bitmap>(cacheKb) {
365
+ override fun sizeOf(key: Int, value: Bitmap): Int = value.byteCount / 1024
366
+ }
367
+ val rv = RecyclerView(context).apply {
368
+ layoutManager = LinearLayoutManager(context)
369
+ itemAnimator = null
370
+ }
371
+ val adapter = PageAdapter(pageCount)
372
+ rv.adapter = adapter
373
+ pdfAdapter = adapter
374
+ recycler = rv
375
+ attachContent(rv, LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.CENTER))
376
+ emitLoaded(pageCount)
377
+ }
378
+
379
+ private fun pageRenderWidth(): Int {
380
+ val base = recycler?.let { it.width - it.paddingLeft - it.paddingRight }?.takeIf { it > 0 }
381
+ ?: width.takeIf { it > 0 }
382
+ ?: resources.displayMetrics.widthPixels
383
+ return base.coerceIn(240, maxRenderDimension())
384
+ }
385
+
386
+ private fun maxRenderDimension(): Int {
387
+ val am = context.getSystemService(Context.ACTIVITY_SERVICE) as? ActivityManager
388
+ return if (am?.isLowRamDevice == true) 1080 else 1600
389
+ }
390
+
391
+ private fun requestPageRender(index: Int) {
392
+ if (!isActive) return
393
+ if (!pendingPages.add(index)) return
394
+ val gen = generation
395
+ val targetWidth = pageRenderWidth()
396
+ runOnWorker {
397
+ if (gen != generation) { mainHandler.post { pendingPages.remove(index) }; return@runOnWorker }
398
+ val renderer = pdfRenderer
399
+ if (renderer == null) { mainHandler.post { pendingPages.remove(index) }; return@runOnWorker }
400
+ var bitmap: Bitmap? = null
401
+ var aspect = DEFAULT_PAGE_ASPECT
402
+ try {
403
+ val page = renderer.openPage(index)
404
+ try {
405
+ var scale = targetWidth.toFloat() / page.width
406
+ if (page.height * scale > MAX_PAGE_BITMAP_HEIGHT) scale = MAX_PAGE_BITMAP_HEIGHT.toFloat() / page.height
407
+ val w = (page.width * scale).toInt().coerceAtLeast(1)
408
+ val h = (page.height * scale).toInt().coerceAtLeast(1)
409
+ val bmp = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
410
+ bmp.eraseColor(pageBackground)
411
+ page.render(bmp, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY)
412
+ aspect = page.height.toFloat() / page.width.toFloat()
413
+ bitmap = bmp
414
+ } finally { page.close() }
415
+ } catch (_: Throwable) { bitmap?.recycle(); bitmap = null }
416
+ val rendered = bitmap
417
+ mainHandler.post {
418
+ pendingPages.remove(index)
419
+ if (gen != generation) { rendered?.recycle(); return@post }
420
+ if (rendered != null) { pageAspects[index] = aspect; pageCache?.put(index, rendered); pdfAdapter?.notifyItemChanged(index) }
421
+ }
422
+ }
423
+ }
424
+
425
+ private inner class PageAdapter(private val pages: Int) : RecyclerView.Adapter<PageAdapter.PageHolder>() {
426
+ inner class PageHolder(val image: ImageView) : RecyclerView.ViewHolder(image)
427
+ override fun getItemCount(): Int = pages
428
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PageHolder {
429
+ val image = ImageView(parent.context).apply {
430
+ setBackgroundColor(pageBackground); scaleType = ImageView.ScaleType.FIT_XY
431
+ layoutParams = RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
432
+ }
433
+ return PageHolder(image)
434
+ }
435
+ override fun onBindViewHolder(holder: PageHolder, position: Int) {
436
+ val listWidth = pageRenderWidth()
437
+ val aspect = pageAspects[position] ?: DEFAULT_PAGE_ASPECT
438
+ val lp = holder.image.layoutParams as RecyclerView.LayoutParams
439
+ lp.height = (listWidth * aspect).toInt().coerceAtLeast(1)
440
+ lp.bottomMargin = if (position == pages - 1) 0 else dp(6)
441
+ holder.image.layoutParams = lp
442
+ val cached = pageCache?.get(position)
443
+ if (cached != null) holder.image.setImageBitmap(cached) else { holder.image.setImageBitmap(null); requestPageRender(position) }
444
+ }
445
+ override fun onViewRecycled(holder: PageHolder) { holder.image.setImageBitmap(null); super.onViewRecycled(holder) }
446
+ }
447
+
448
+ // ── Plain / structured text ──────────────────────────────────────
449
+
450
+ private fun loadText(uri: Uri, monospace: Boolean, gen: Int) {
451
+ runOnWorker {
452
+ if (gen != generation) return@runOnWorker
453
+ val text: String; val truncated: Boolean
454
+ try {
455
+ val (bytes, wasTruncated) = readCapped(uri, MAX_TEXT_BYTES)
456
+ if (looksBinary(bytes)) { postError(gen, Localization.getString("fileNotPreviewableText", "File is not previewable text")); return@runOnWorker }
457
+ text = String(bytes, Charsets.UTF_8).removePrefix("\uFEFF"); truncated = wasTruncated
458
+ } catch (e: Exception) { postError(gen, Localization.getString("couldNotReadFile", "Could not read file: {0}", e.message ?: "")); return@runOnWorker }
459
+ mainHandler.post { if (gen != generation) return@post; showTextUi(text, monospace, truncated) }
460
+ }
461
+ }
462
+
463
+ private fun showTextUi(text: String, monospace: Boolean, truncated: Boolean) {
464
+ val body = buildString { append(text.ifBlank { Localization.getString("emptyFile", "(empty file)") }); if (truncated) append("\n\n… " + Localization.getString("previewTruncated", "preview truncated")) }
465
+ val textView = TextView(context).apply {
466
+ setTextColor(textColor); setTextSize(TypedValue.COMPLEX_UNIT_SP, if (monospace) 12.5f else 14f)
467
+ typeface = if (monospace) Typeface.MONOSPACE else Typeface.DEFAULT
468
+ setLineSpacing(0f, 1.35f); setPadding(dp(16), dp(14), dp(16), dp(24)); this.text = body
469
+ }
470
+ val scroll = ScrollView(context).apply {
471
+ isFillViewport = true; setBackgroundColor(textBackground); isVerticalScrollBarEnabled = true
472
+ addView(textView, ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT))
473
+ }
474
+ attachContent(scroll); emitLoaded(0)
475
+ }
476
+
477
+ // ── HTML ─────────────────────────────────────────────────────────
478
+
479
+ private fun loadHtml(uri: Uri, gen: Int) {
480
+ runOnWorker {
481
+ if (gen != generation) return@runOnWorker
482
+ val html: String
483
+ try { val (bytes, _) = readCapped(uri, MAX_HTML_BYTES); html = String(bytes, Charsets.UTF_8).removePrefix("\uFEFF") }
484
+ catch (e: Exception) { postError(gen, Localization.getString("couldNotReadFile", "Could not read file: {0}", e.message ?: "")); return@runOnWorker }
485
+ mainHandler.post { if (gen != generation) return@post; showHtmlUi(html, gen) }
486
+ }
487
+ }
488
+
489
+ private fun showHtmlUi(html: String, gen: Int, wideViewport: Boolean = false, pageCount: Int = 0) {
490
+ val web = try { WebView(context) } catch (e: Exception) { postError(gen, Localization.getString("webViewUnavailable", "WebView unavailable: {0}", e.message ?: "")); return }
491
+ web.settings.apply {
492
+ javaScriptEnabled = false; allowFileAccess = false; allowContentAccess = false
493
+ blockNetworkLoads = true; loadWithOverviewMode = true; useWideViewPort = wideViewport
494
+ builtInZoomControls = true; displayZoomControls = false
495
+ }
496
+ web.webViewClient = object : WebViewClient() { override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest) = true }
497
+ web.setBackgroundColor(pageBackground)
498
+ // Overscroll glow is pure overdraw on a static document; dropping it removes work per frame.
499
+ web.overScrollMode = android.view.View.OVER_SCROLL_NEVER
500
+ web.loadDataWithBaseURL(null, html, "text/html", "utf-8", null)
501
+ webView = web
502
+ // MATCH_PARENT (an EXACTLY height), NOT wrap_content. A WebView measured with wrap_content
503
+ // (an AT_MOST spec) drops out of Chromium's GPU-composited *tiled* scrolling and re-rasterizes
504
+ // its whole content on every scroll frame — that was the "laggy, not smooth" scroll on every
505
+ // non-PDF (Office) document. A bounded height puts it back on the fast composited path and lets
506
+ // the WebView scroll its own overflow internally. (Short documents now top-align instead of
507
+ // centring, which reads correctly for a document; do NOT restore wrap_content to re-centre.)
508
+ attachContent(web)
509
+ emitLoaded(pageCount)
510
+ }
511
+
512
+ private fun buildPowerPointSlideHtml(file: File, extractedText: String): String {
513
+ val segments = extractedText.split(Regex("\n\\s*\n+")).map { it.trim() }.filter { it.isNotBlank() }
514
+ val slideCss = ".pptx-slide{font-family:Calibri,\"Segoe UI\",sans-serif;color:#0F172A;background:#FFFFFF;border-radius:6pt;padding:20pt 24pt;line-height:1.6;box-shadow:0 4px 12px rgba(0,0,0,0.15);margin:12pt auto;box-sizing:border-box;}" +
515
+ ".pptx-slide h1{font-size:20pt;font-weight:700;color:#0F172A;margin:0 0 10pt 0;border-bottom:2pt solid #E2E8F0;padding-bottom:8pt;}" +
516
+ ".pptx-slide .slide-num{display:inline-block;background:#4A56FF;color:#FFF;font-size:9pt;font-weight:700;padding:2pt 8pt;border-radius:4pt;margin-bottom:10pt;letter-spacing:0.5pt;}" +
517
+ ".pptx-slide .slide-text{font-size:12pt;color:#1E293B;white-space:pre-wrap;word-wrap:break-word;line-height:1.6;}"
518
+ val sb = StringBuilder()
519
+ if (segments.size <= 1) {
520
+ sb.append("<div class=\"sheet pptx-slide\"><div class=\"slide-num\">${Localization.getString("slideNumberLabel", "SLIDE {0}", 1)}</div><div class=\"slide-text\">${Ox.esc(extractedText)}</div></div>")
521
+ } else {
522
+ for ((idx, segment) in segments.take(50).withIndex()) {
523
+ sb.append("<div class=\"sheet pptx-slide\"><div class=\"slide-num\">${Localization.getString("slideNumberLabel", "SLIDE {0}", idx + 1)}</div><div class=\"slide-text\">${Ox.esc(segment)}</div></div>")
524
+ }
525
+ }
526
+ return Ox.shell(612f, slideCss, sb.toString())
527
+ }
528
+
529
+ // ── PowerPoint presentations (PPT/PPTX) ──────────────────────────
530
+
531
+ private var powerPointPresentation: PowerPointPresentation? = null
532
+ private var powerPointAdapter: PowerPointPreviewAdapter? = null
533
+
534
+ private fun loadPowerPoint(uri: Uri, kind: String, gen: Int) {
535
+ runOnWorker {
536
+ if (gen != generation) return@runOnWorker
537
+ val src: File
538
+ try {
539
+ src = materializeFile(uri, kind)
540
+ } catch (e: Throwable) {
541
+ val name = uri.lastPathSegment ?: Localization.getString("powerPointPresentationTitle", "PowerPoint presentation")
542
+ val errorPresentation = PowerPointDocumentProcessor.errorPresentation(
543
+ name,
544
+ Localization.getString("couldNotOpenPresentation", "Could not open presentation: {0}", e.message ?: "")
545
+ )
546
+ mainHandler.post {
547
+ if (gen != generation) return@post
548
+ showPowerPointUi(errorPresentation)
549
+ }
550
+ return@runOnWorker
551
+ }
552
+
553
+ val presentation: PowerPointPresentation
554
+ try {
555
+ presentation = PowerPointDocumentProcessor.process(src, kind)
556
+ } catch (e: Throwable) {
557
+ val errorPresentation = PowerPointDocumentProcessor.errorPresentation(
558
+ src.name,
559
+ Localization.getString("couldNotReadPowerPoint", "Could not read PowerPoint presentation: {0}", e.message ?: "")
560
+ )
561
+ mainHandler.post {
562
+ if (gen != generation) return@post
563
+ showPowerPointUi(errorPresentation)
564
+ }
565
+ return@runOnWorker
566
+ }
567
+
568
+ if (gen != generation) {
569
+ presentation.release()
570
+ return@runOnWorker
571
+ }
572
+ mainHandler.post {
573
+ if (gen != generation) {
574
+ presentation.release()
575
+ return@post
576
+ }
577
+ showPowerPointUi(presentation)
578
+ }
579
+ }
580
+ }
581
+
582
+ private fun showPowerPointUi(presentation: PowerPointPresentation) {
583
+ powerPointPresentation?.release()
584
+ powerPointPresentation = presentation
585
+ val rv = RecyclerView(context).apply {
586
+ layoutManager = LinearLayoutManager(context)
587
+ itemAnimator = null
588
+ setBackgroundColor(Color.TRANSPARENT)
589
+ }
590
+ val adapter = PowerPointPreviewAdapter(context, presentation) { pageRenderWidth() }
591
+ rv.adapter = adapter
592
+ recycler = rv
593
+ powerPointAdapter = adapter
594
+ attachContent(rv, LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.CENTER))
595
+ emitLoaded(presentation.slides.size)
596
+ }
597
+
598
+ // ── Office documents (DOCX/XLSX/ODT/ODS/RTF/CSV/EPUB) ──────
599
+
600
+ private var tempOfficeFile: File? = null
601
+
602
+ private fun loadOffice(uri: Uri, kind: String, gen: Int) {
603
+ runOnWorker {
604
+ if (gen != generation) return@runOnWorker
605
+ val src: File
606
+ try { src = materializeFile(uri, kind) } catch (e: Throwable) { postError(gen, Localization.getString("couldNotOpenDocument", "Could not open document: {0}", e.message ?: "")); return@runOnWorker }
607
+ val conversion: OfficeDocumentConverter.Conversion
608
+ try { conversion = OfficeDocumentConverter.convert(src, kind) }
609
+ catch (e: Throwable) {
610
+ val fallback = extractOfficeTextFallback(src)
611
+ if (fallback != null) { mainHandler.post { if (gen != generation) return@post; showHtmlUi(fallback, gen) }; return@runOnWorker }
612
+ postError(gen, Localization.getString("couldNotConvertDocument", "Could not convert document: {0}", e.message ?: "")); return@runOnWorker
613
+ }
614
+ if (gen != generation) return@runOnWorker
615
+ mainHandler.post { if (gen != generation) return@post; showHtmlUi(conversion.html, gen, wideViewport = true, pageCount = conversion.pageCount) }
616
+ }
617
+ }
618
+
619
+ /**
620
+ * Best-effort fallback for office documents. Three strategies in order:
621
+ *
622
+ * 1. ZIP XML text extraction — works for OOXML/ODF containers (.docx, .xlsx, .odt, etc.)
623
+ * when the primary converter failed. Extracts slide/document text and renders as HTML.
624
+ *
625
+ * 2. Binary-era text extraction + info card — for .doc / .xls (proprietary binary OLE
626
+ * formats). Extracts readable text runs from the binary and renders them as slide-like HTML,
627
+ * topped with a neutral info note suggesting the external app for full fidelity. This gives
628
+ * users *something* to look at rather than a bare "unsupported" message.
629
+ *
630
+ * 3. Generic informational card — catch-all for any other unextractable format.
631
+ */
632
+ private fun extractOfficeTextFallback(file: File): String? {
633
+ // ── Strategy 1: ZIP XML text extraction ──
634
+ try {
635
+ java.util.zip.ZipFile(file).use { zip ->
636
+ val sb = StringBuilder()
637
+ val entries = zip.entries()
638
+ while (entries.hasMoreElements()) {
639
+ val entry = entries.nextElement()
640
+ val name = entry.name
641
+ if (!name.endsWith(".xml") && !name.endsWith(".txt")) continue
642
+ val isContent = name.matches(Regex("ppt/slides/slide\\d+\\.xml")) ||
643
+ name == "word/document.xml" || name == "xl/sharedStrings.xml" ||
644
+ name == "content.xml" || name.endsWith("/document.xml")
645
+ if (!isContent) continue
646
+ zip.getInputStream(entry).use { stream ->
647
+ val xml = String(stream.readBytes(), Charsets.UTF_8)
648
+ val text = xml.replace(Regex("<[^>]+>"), " ").replace(Regex("\\s+"), " ").trim()
649
+ if (text.isNotBlank()) sb.appendLine(text).appendLine()
650
+ }
651
+ }
652
+ val extracted = sb.toString().trim()
653
+ if (extracted.length > 20) return buildPowerPointSlideHtml(file, extracted)
654
+ }
655
+ } catch (_: Throwable) { /* Not a valid ZIP — fall through. */ }
656
+
657
+ // ── Strategy 2: Binary text extraction + info card ──
658
+ val ext = file.name.substringAfterLast('.', "").lowercase()
659
+ if (ext in setOf("doc", "xls")) {
660
+ val extractedText = extractBinaryText(file)
661
+ val formatLabel = when (ext) { "ppt" -> "PowerPoint"; "doc" -> "Word"; "xls" -> "Excel"; else -> "Office" }
662
+ val sizeKb = (file.length() / 1024)
663
+ val sizeLabel = if (sizeKb > 1024) "${sizeKb / 1024} MB" else "$sizeKb KB"
664
+
665
+ val infoCss = ".legacy-info{font-family:-apple-system,sans-serif;color:#0F172A;}" +
666
+ ".legacy-info .info-card{background:#FFFFFF;border-radius:6pt;padding:16pt 20pt;margin:12pt auto;box-shadow:0 2px 8px rgba(0,0,0,0.1);text-align:center;max-width:520pt;}" +
667
+ ".legacy-info .info-card h2{font-size:14pt;margin:0 0 4pt 0;color:#0F172A;}" +
668
+ ".legacy-info .info-card .meta{color:#64748B;font-size:10pt;margin-bottom:8pt;}" +
669
+ ".legacy-info .info-card .msg{color:#334155;font-size:11pt;line-height:1.5;}" +
670
+ ".pptx-slide{font-family:Calibri,\"Segoe UI\",sans-serif;color:#0F172A;background:#FFFFFF;border-radius:6pt;padding:16pt 20pt;line-height:1.5;box-shadow:0 2px 8px rgba(0,0,0,0.1);margin:10pt auto;box-sizing:border-box;}" +
671
+ ".pptx-slide .slide-num{display:inline-block;background:#4A56FF;color:#FFF;font-size:8pt;font-weight:700;padding:2pt 6pt;border-radius:3pt;margin-bottom:6pt;}" +
672
+ ".pptx-slide .slide-text{font-size:11pt;color:#1E293B;white-space:pre-wrap;word-wrap:break-word;}"
673
+
674
+ val body = StringBuilder()
675
+ // Info card at the top
676
+ body.append("<div class=\"info-card\">")
677
+ body.append("<h2>${Ox.esc(file.name)}</h2>")
678
+ body.append("<div class=\"meta\">$formatLabel · $sizeLabel</div>")
679
+ body.append("<div class=\"msg\">" + Localization.getString("officeBinaryPreviewNote", "Text preview shown below — open in PowerPoint, Google Slides, or WPS Office for full formatting and images.") + "</div>")
680
+ body.append("</div>")
681
+
682
+ if (extractedText.length > 20) {
683
+ // Render extracted text as slides
684
+ val segments = extractedText.split(Regex("\n\\s*\n+")).map { it.trim() }.filter { it.isNotBlank() }
685
+ if (segments.size <= 1) {
686
+ body.append("<div class=\"pptx-slide\"><div class=\"slide-num\">${Localization.getString("slideNumberLabel", "SLIDE {0}", 1)}</div><div class=\"slide-text\">${Ox.esc(extractedText)}</div></div>")
687
+ } else {
688
+ for ((idx, segment) in segments.take(50).withIndex()) {
689
+ body.append("<div class=\"pptx-slide\"><div class=\"slide-num\">${Localization.getString("slideNumberLabel", "SLIDE {0}", idx + 1)}</div><div class=\"slide-text\">${Ox.esc(segment)}</div></div>")
690
+ }
691
+ }
692
+ } else {
693
+ // No extractable text — show the info card alone
694
+ body.append("<div class=\"info-card\" style=\"margin-top:0;\"><div class=\"msg\">" + Localization.getString("unableToExtractBinaryText", "Unable to extract text from this binary file. Open in PowerPoint for the best experience.") + "</div></div>")
695
+ }
696
+
697
+ return Ox.shell(612f, infoCss, body.toString())
698
+ }
699
+
700
+ // ── Strategy 3: Generic card ──
701
+ val formatLabel = when (ext.uppercase()) {
702
+ "PPT", "PPTX" -> Localization.getString("docTypePowerpoint", "PowerPoint Presentation")
703
+ "DOC", "DOCX" -> Localization.getString("docTypeWord", "Word Document")
704
+ "XLS", "XLSX" -> Localization.getString("docTypeExcel", "Excel Spreadsheet")
705
+ else -> Localization.getString("docTypeOffice", "Office Document")
706
+ }
707
+ val sizeKb = (file.length() / 1024)
708
+ val sizeLabel = if (sizeKb > 1024) "${sizeKb / 1024} MB" else "$sizeKb KB"
709
+ return Ox.shell(612f,
710
+ ".fallback{font-family:-apple-system,sans-serif;padding:24pt;background:#FFFFFF;color:#0F172A;text-align:center;border-radius:6pt;margin:16pt auto;box-shadow:0 4px 12px rgba(0,0,0,0.15);}" +
711
+ ".fallback .icon{font-size:48pt;margin-bottom:12pt;}.fallback h2{font-size:16pt;margin:0 0 6pt 0;}" +
712
+ ".fallback .format{display:inline-block;background:#E2E8F0;color:#334155;font-size:10pt;font-weight:600;padding:3pt 8pt;border-radius:4pt;margin-bottom:10pt;}" +
713
+ ".fallback .size{color:#64748B;font-size:10pt;margin-bottom:16pt;}" +
714
+ ".fallback .msg{color:#334155;font-size:12pt;line-height:1.6;max-width:400pt;margin:0 auto;}",
715
+ "<div class=\"sheet fallback\"><div class=\"icon\">📊</div><h2>${Ox.esc(file.name)}</h2>" +
716
+ "<div class=\"format\">$formatLabel</div><div class=\"size\">$sizeLabel</div>" +
717
+ "<p class=\"msg\">" + Localization.getString("openInOfficeAppNote", "Open this file in PowerPoint, Google Slides, or WPS Office for the best viewing experience.") + "</p></div>")
718
+ }
719
+
720
+ /** Scan a binary-era Office file for readable UTF-16LE and ASCII text runs. */
721
+ private fun extractBinaryText(file: File): String {
722
+ // Skip files that are too large to scan safely in memory.
723
+ if (file.length() > MAX_BINARY_SCAN_BYTES) return ""
724
+ val sb = StringBuilder()
725
+ try {
726
+ val bytes = file.inputStream().use { it.readBytes() }
727
+ var i = 0
728
+ while (i < bytes.size - 3) {
729
+ if (isUtf16LeRun(bytes, i)) {
730
+ val run = extractUtf16LeRun(bytes, i)
731
+ if (run.length >= 2) sb.appendLine(run)
732
+ i += run.length * 2
733
+ } else if (isAsciiRun(bytes, i)) {
734
+ val run = extractAsciiRun(bytes, i)
735
+ if (run.length >= 4) sb.appendLine(run)
736
+ i += run.length
737
+ } else { i++ }
738
+ }
739
+ return sb.toString()
740
+ .replace(Regex("\u0000"), "")
741
+ .replace(Regex("[\\x00-\\x08\\x0E-\\x1F]"), " ")
742
+ .replace(Regex(" {3,}"), " ")
743
+ .trim()
744
+ .lines().filter { it.isNotBlank() }.joinToString("\n\n")
745
+ } catch (_: Throwable) { return "" }
746
+ }
747
+
748
+ private fun isUtf16LeRun(bytes: ByteArray, pos: Int): Boolean {
749
+ if (pos + 3 >= bytes.size) return false
750
+ val hi1 = bytes[pos + 1].toInt() and 0xFF; val hi2 = bytes[pos + 3].toInt() and 0xFF
751
+ return hi1 == 0 && hi2 == 0 && isPrintableChar(bytes[pos].toInt() and 0xFF) && isPrintableChar(bytes[pos + 2].toInt() and 0xFF)
752
+ }
753
+
754
+ private fun extractUtf16LeRun(bytes: ByteArray, start: Int): String {
755
+ val sb = StringBuilder(); var i = start
756
+ while (i + 1 < bytes.size) {
757
+ val lo = bytes[i].toInt() and 0xFF; val hi = bytes[i + 1].toInt() and 0xFF
758
+ if (hi != 0 || !isPrintableChar(lo)) break
759
+ sb.append(lo.toChar()); i += 2
760
+ }
761
+ return sb.toString()
762
+ }
763
+
764
+ private fun isAsciiRun(bytes: ByteArray, pos: Int): Boolean = pos < bytes.size && (bytes[pos].toInt() and 0xFF) in 0x20..0x7E
765
+
766
+ private fun extractAsciiRun(bytes: ByteArray, start: Int): String {
767
+ val sb = StringBuilder(); var i = start
768
+ while (i < bytes.size && (bytes[i].toInt() and 0xFF) in 0x20..0x7E) { sb.append((bytes[i].toInt() and 0xFF).toChar()); i++ }
769
+ return sb.toString()
770
+ }
771
+
772
+ private fun isPrintableChar(b: Int): Boolean = b in 0x20..0x7E || b == 0x0A || b == 0x0D || (b in 0x80..0xFF)
773
+
774
+ private fun materializeFile(uri: Uri, kind: String): File {
775
+ if (uri.scheme == "file") { uri.path?.let { path -> val f = File(path); if (f.canRead()) return f } }
776
+ val input = context.contentResolver.openInputStream(uri) ?: throw IOException("Cannot open document stream")
777
+ val temp = File.createTempFile("doc_preview_", ".$kind", context.cacheDir)
778
+ input.use { stream -> FileOutputStream(temp).use { out -> stream.copyTo(out) } }
779
+ tempOfficeFile = temp
780
+ return temp
781
+ }
782
+
783
+ // ── Image-based documents ────────────────────────────────────────
784
+
785
+ private fun loadImage(uri: Uri, gen: Int) {
786
+ if (width > 0) { decodeImage(uri, gen, width, height); return }
787
+ addOnLayoutChangeListener(object : OnLayoutChangeListener {
788
+ override fun onLayoutChange(v: android.view.View, l: Int, t: Int, r: Int, b: Int, ol: Int, ot: Int, or2: Int, ob: Int) {
789
+ if (r - l <= 0) return; v.removeOnLayoutChangeListener(this)
790
+ if (gen != generation) return; decodeImage(uri, gen, r - l, b - t)
791
+ }
792
+ })
793
+ }
794
+
795
+ private fun decodeImage(uri: Uri, gen: Int, viewW: Int, viewH: Int) {
796
+ val maxDim = maxRenderDimension(); val targetW = viewW.coerceIn(1, maxDim); val targetH = (if (viewH > 0) viewH else viewW).coerceIn(1, maxDim)
797
+ runOnWorker {
798
+ if (gen != generation) return@runOnWorker
799
+ val bitmap: Bitmap?
800
+ try {
801
+ val resolver = context.contentResolver
802
+ val bounds = BitmapFactory.Options().apply { inJustDecodeBounds = true }
803
+ resolver.openInputStream(uri)?.use { BitmapFactory.decodeStream(it, null, bounds) }
804
+ if (bounds.outWidth <= 0 || bounds.outHeight <= 0) { postError(gen, Localization.getString("notDecodableImage", "Not a decodable image")); return@runOnWorker }
805
+ var sample = 1
806
+ while (bounds.outWidth / (sample * 2) >= targetW && bounds.outHeight / (sample * 2) >= targetH) sample *= 2
807
+ val opts = BitmapFactory.Options().apply { inSampleSize = sample }
808
+ bitmap = resolver.openInputStream(uri)?.use { BitmapFactory.decodeStream(it, null, opts) }
809
+ } catch (e: Throwable) { postError(gen, Localization.getString("couldNotDecodeImage", "Could not decode image: {0}", e.message ?: "")); return@runOnWorker }
810
+ if (bitmap == null) { postError(gen, Localization.getString("couldNotDecodeImageGeneric", "Could not decode image")); return@runOnWorker }
811
+ mainHandler.post {
812
+ if (gen != generation) { bitmap.recycle(); return@post }
813
+ val imageView = ImageView(context).apply { scaleType = ImageView.ScaleType.FIT_CENTER; setImageBitmap(bitmap) }
814
+ attachContent(imageView); emitLoaded(0)
815
+ }
816
+ }
817
+ }
818
+
819
+ // ── Shared IO helpers ────────────────────────────────────────────
820
+
821
+ private fun readCapped(uri: Uri, cap: Int): Pair<ByteArray, Boolean> {
822
+ val input = context.contentResolver.openInputStream(uri) ?: throw IOException("Cannot open document stream")
823
+ input.use { stream ->
824
+ val buffer = ByteArrayOutputStream(minOf(cap, 256 * 1024)); val chunk = ByteArray(64 * 1024); var total = 0; var truncated = false
825
+ while (true) {
826
+ val read = stream.read(chunk); if (read == -1) break
827
+ val remaining = cap - total
828
+ if (read >= remaining) { buffer.write(chunk, 0, remaining); total += remaining; truncated = read > remaining || stream.read() != -1; break }
829
+ buffer.write(chunk, 0, read); total += read
830
+ }
831
+ return buffer.toByteArray() to truncated
832
+ }
833
+ }
834
+
835
+ private fun looksBinary(bytes: ByteArray): Boolean { val checkLen = minOf(bytes.size, 8192); for (i in 0 until checkLen) { if (bytes[i] == 0.toByte()) return true }; return false }
836
+
837
+ private fun dp(value: Int): Int = (value * resources.displayMetrics.density).toInt()
838
+
839
+ companion object {
840
+ private const val DEFAULT_PAGE_ASPECT = 1.4142f
841
+ private const val MAX_PAGE_BITMAP_HEIGHT = 4096
842
+ private const val MAX_TEXT_BYTES = 256 * 1024
843
+ private const val MAX_HTML_BYTES = 2 * 1024 * 1024
844
+ private const val MAX_BINARY_SCAN_BYTES = 2 * 1024 * 1024
845
+ }
846
+ }