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.
- package/CustomCamera.podspec +20 -0
- package/LICENSE +20 -0
- package/README.md +833 -0
- package/android/build.gradle +135 -0
- package/android/consumer-rules.pro +47 -0
- package/android/src/main/AndroidManifest.xml +72 -0
- package/android/src/main/java/android/print/PdfPrint.java +51 -0
- package/android/src/main/java/com/customcamera/AnimatedImageViewManager.kt +199 -0
- package/android/src/main/java/com/customcamera/AudioPlayerView.kt +322 -0
- package/android/src/main/java/com/customcamera/AudioRecorder.kt +506 -0
- package/android/src/main/java/com/customcamera/BarcodeScannerActivity.kt +361 -0
- package/android/src/main/java/com/customcamera/BlurBackgroundView.kt +199 -0
- package/android/src/main/java/com/customcamera/CacheCleanupManager.kt +386 -0
- package/android/src/main/java/com/customcamera/CameraAspectRatioMode.kt +77 -0
- package/android/src/main/java/com/customcamera/CameraAsset.kt +211 -0
- package/android/src/main/java/com/customcamera/CameraTheme.kt +479 -0
- package/android/src/main/java/com/customcamera/CompressionCacheStore.kt +504 -0
- package/android/src/main/java/com/customcamera/CompressionFileNameUtils.kt +7 -0
- package/android/src/main/java/com/customcamera/CompressionProfile.kt +195 -0
- package/android/src/main/java/com/customcamera/CustomCameraActivity.kt +7291 -0
- package/android/src/main/java/com/customcamera/CustomCameraFileProvider.kt +9 -0
- package/android/src/main/java/com/customcamera/CustomCameraModule.kt +2479 -0
- package/android/src/main/java/com/customcamera/CustomCameraPackage.kt +53 -0
- package/android/src/main/java/com/customcamera/DisplayUtils.kt +9 -0
- package/android/src/main/java/com/customcamera/DocumentPreviewView.kt +846 -0
- package/android/src/main/java/com/customcamera/DrawingView.kt +214 -0
- package/android/src/main/java/com/customcamera/EmojiPickerData.kt +228 -0
- package/android/src/main/java/com/customcamera/FilterStripAdapter.kt +158 -0
- package/android/src/main/java/com/customcamera/GifSupport.kt +215 -0
- package/android/src/main/java/com/customcamera/GlobalAudioPlayer.kt +320 -0
- package/android/src/main/java/com/customcamera/ImageExporter.kt +1394 -0
- package/android/src/main/java/com/customcamera/ImageFormatSupport.kt +163 -0
- package/android/src/main/java/com/customcamera/Localization.kt +206 -0
- package/android/src/main/java/com/customcamera/MediaAdapters.kt +758 -0
- package/android/src/main/java/com/customcamera/MediaCacheManager.kt +782 -0
- package/android/src/main/java/com/customcamera/MediaExportFormats.kt +201 -0
- package/android/src/main/java/com/customcamera/MediaFileIo.kt +310 -0
- package/android/src/main/java/com/customcamera/MediaMetadataStore.kt +494 -0
- package/android/src/main/java/com/customcamera/MediaPreviewEditor.kt +14865 -0
- package/android/src/main/java/com/customcamera/MediaPublisher.kt +423 -0
- package/android/src/main/java/com/customcamera/MediaStorageWorkflow.kt +722 -0
- package/android/src/main/java/com/customcamera/MediaStoreRepository.kt +378 -0
- package/android/src/main/java/com/customcamera/MediaThumbnailView.kt +286 -0
- package/android/src/main/java/com/customcamera/MediaUtils.kt +37 -0
- package/android/src/main/java/com/customcamera/NativeVideoViewManager.kt +845 -0
- package/android/src/main/java/com/customcamera/OfficeDocumentConverter.kt +2791 -0
- package/android/src/main/java/com/customcamera/PendingCaptureCrops.kt +88 -0
- package/android/src/main/java/com/customcamera/PowerPointPreview.kt +4077 -0
- package/android/src/main/java/com/customcamera/RecentPickerStore.kt +74 -0
- package/android/src/main/java/com/customcamera/SecureFileEraser.kt +411 -0
- package/android/src/main/java/com/customcamera/SecureKeyModule.kt +88 -0
- package/android/src/main/java/com/customcamera/SecureMediaDeleter.kt +493 -0
- package/android/src/main/java/com/customcamera/StickerPickerData.kt +107 -0
- package/android/src/main/java/com/customcamera/SvgDrawable.kt +175 -0
- package/android/src/main/java/com/customcamera/SvgIconViewManager.kt +77 -0
- package/android/src/main/java/com/customcamera/TextEntryController.kt +553 -0
- package/android/src/main/java/com/customcamera/ThumbnailLoader.kt +455 -0
- package/android/src/main/java/com/customcamera/UploadService.kt +909 -0
- package/android/src/main/java/com/customcamera/VideoEditUtils.kt +210 -0
- package/android/src/main/java/com/customcamera/VideoRotationTranscoder.kt +1992 -0
- package/android/src/main/java/com/customcamera/gif/AnimatedGifEncoder.kt +473 -0
- package/android/src/main/java/com/customcamera/gif/GifDecoder.kt +690 -0
- package/android/src/main/java/com/customcamera/gif/GifFramesDrawable.kt +160 -0
- package/android/src/main/java/com/customcamera/gif/GifHttp.kt +112 -0
- package/android/src/main/java/com/customcamera/gif/GifImageLoader.kt +186 -0
- package/android/src/main/java/com/customcamera/gif/GifModels.kt +74 -0
- package/android/src/main/java/com/customcamera/gif/GifProvider.kt +49 -0
- package/android/src/main/java/com/customcamera/gif/GifProviders.kt +27 -0
- package/android/src/main/java/com/customcamera/gif/GifRepository.kt +124 -0
- package/android/src/main/java/com/customcamera/gif/GifSafety.kt +33 -0
- package/android/src/main/java/com/customcamera/gif/GiphyGifProvider.kt +146 -0
- package/android/src/main/java/com/customcamera/gif/LZWEncoder.kt +258 -0
- package/android/src/main/java/com/customcamera/gif/NeuQuant.kt +435 -0
- package/android/src/main/java/com/customcamera/gif/TenorGifProvider.kt +147 -0
- package/android/src/main/res/drawable/bg_brush_selected.xml +5 -0
- package/android/src/main/res/drawable/bg_color_swatch.xml +5 -0
- package/android/src/main/res/drawable/bg_editor_toolbar.xml +6 -0
- package/android/src/main/res/drawable/bg_icon_circle.xml +5 -0
- package/android/src/main/res/drawable/bg_icon_circle_light.xml +5 -0
- package/android/src/main/res/drawable/bg_mode_tab_active.xml +6 -0
- package/android/src/main/res/drawable/bg_pill.xml +6 -0
- package/android/src/main/res/drawable/bg_recording_dot.xml +6 -0
- package/android/src/main/res/drawable/bg_selection_badge.xml +8 -0
- package/android/src/main/res/drawable/bg_send_button.xml +5 -0
- package/android/src/main/res/drawable/bg_shutter_inner.xml +5 -0
- package/android/src/main/res/drawable/bg_shutter_recording.xml +6 -0
- package/android/src/main/res/drawable/bg_shutter_ring.xml +8 -0
- package/android/src/main/res/drawable/bg_shutter_video_idle.xml +5 -0
- package/android/src/main/res/drawable/bg_thumbnail.xml +9 -0
- package/android/src/main/res/drawable/ic_aspect_ratio.xml +17 -0
- package/android/src/main/res/drawable/ic_audio.xml +10 -0
- package/android/src/main/res/drawable/ic_back.xml +10 -0
- package/android/src/main/res/drawable/ic_brush_medium.xml +12 -0
- package/android/src/main/res/drawable/ic_brush_thick.xml +12 -0
- package/android/src/main/res/drawable/ic_brush_thin.xml +12 -0
- package/android/src/main/res/drawable/ic_check.xml +17 -0
- package/android/src/main/res/drawable/ic_chevron_down.xml +10 -0
- package/android/src/main/res/drawable/ic_chevron_up.xml +13 -0
- package/android/src/main/res/drawable/ic_clear_drawing.xml +10 -0
- package/android/src/main/res/drawable/ic_close.xml +10 -0
- package/android/src/main/res/drawable/ic_cover.xml +9 -0
- package/android/src/main/res/drawable/ic_crop.xml +10 -0
- package/android/src/main/res/drawable/ic_delete.xml +9 -0
- package/android/src/main/res/drawable/ic_document.xml +10 -0
- package/android/src/main/res/drawable/ic_done.xml +10 -0
- package/android/src/main/res/drawable/ic_download.xml +10 -0
- package/android/src/main/res/drawable/ic_emoji.xml +10 -0
- package/android/src/main/res/drawable/ic_filter.xml +10 -0
- package/android/src/main/res/drawable/ic_flash_auto.xml +7 -0
- package/android/src/main/res/drawable/ic_flash_off.xml +23 -0
- package/android/src/main/res/drawable/ic_flash_on.xml +16 -0
- package/android/src/main/res/drawable/ic_flip.xml +15 -0
- package/android/src/main/res/drawable/ic_gallery.xml +10 -0
- package/android/src/main/res/drawable/ic_gif.xml +24 -0
- package/android/src/main/res/drawable/ic_lock.xml +19 -0
- package/android/src/main/res/drawable/ic_lock_open.xml +22 -0
- package/android/src/main/res/drawable/ic_marker.xml +10 -0
- package/android/src/main/res/drawable/ic_more_vert.xml +18 -0
- package/android/src/main/res/drawable/ic_overlay_close.xml +19 -0
- package/android/src/main/res/drawable/ic_overlay_resize.xml +18 -0
- package/android/src/main/res/drawable/ic_pause.xml +10 -0
- package/android/src/main/res/drawable/ic_photo_mode.xml +10 -0
- package/android/src/main/res/drawable/ic_play.xml +10 -0
- package/android/src/main/res/drawable/ic_redo.xml +10 -0
- package/android/src/main/res/drawable/ic_rotate.xml +9 -0
- package/android/src/main/res/drawable/ic_send.xml +10 -0
- package/android/src/main/res/drawable/ic_share.xml +10 -0
- package/android/src/main/res/drawable/ic_stat_upload.xml +10 -0
- package/android/src/main/res/drawable/ic_sticker.xml +24 -0
- package/android/src/main/res/drawable/ic_text.xml +10 -0
- package/android/src/main/res/drawable/ic_text_align_center.xml +11 -0
- package/android/src/main/res/drawable/ic_text_align_left.xml +11 -0
- package/android/src/main/res/drawable/ic_text_align_right.xml +11 -0
- package/android/src/main/res/drawable/ic_text_bg.xml +11 -0
- package/android/src/main/res/drawable/ic_trim.xml +23 -0
- package/android/src/main/res/drawable/ic_undo.xml +10 -0
- package/android/src/main/res/drawable/ic_video_mode.xml +10 -0
- package/android/src/main/res/drawable/ic_video_note.xml +13 -0
- package/android/src/main/res/drawable/ic_volume_off.xml +9 -0
- package/android/src/main/res/drawable/ic_volume_on.xml +9 -0
- package/android/src/main/res/drawable/sticker_heart.xml +9 -0
- package/android/src/main/res/drawable/sticker_smile.xml +9 -0
- package/android/src/main/res/drawable/sticker_star.xml +9 -0
- package/android/src/main/res/drawable/sticker_thumbsup.xml +9 -0
- package/android/src/main/res/values/colors.xml +34 -0
- package/android/src/main/res/values/ids.xml +15 -0
- package/android/src/main/res/values/strings.xml +15 -0
- package/android/src/main/res/values/styles.xml +20 -0
- package/android/src/main/res/xml/file_paths.xml +34 -0
- package/ios/CustomCamera.h +5 -0
- package/ios/CustomCamera.mm +214 -0
- package/lib/module/AudioPlayerContext.js +233 -0
- package/lib/module/AudioPlayerContext.js.map +1 -0
- package/lib/module/CameraPreview.js +304 -0
- package/lib/module/CameraPreview.js.map +1 -0
- package/lib/module/NativeAnimatedImageView.js +41 -0
- package/lib/module/NativeAnimatedImageView.js.map +1 -0
- package/lib/module/NativeAudioPlayerView.js +33 -0
- package/lib/module/NativeAudioPlayerView.js.map +1 -0
- package/lib/module/NativeBlurView.js +35 -0
- package/lib/module/NativeBlurView.js.map +1 -0
- package/lib/module/NativeCustomCamera.js +128 -0
- package/lib/module/NativeCustomCamera.js.map +1 -0
- package/lib/module/NativeCustomCamera.web.js +143 -0
- package/lib/module/NativeCustomCamera.web.js.map +1 -0
- package/lib/module/NativeCustomVideoView.js +5 -0
- package/lib/module/NativeCustomVideoView.js.map +1 -0
- package/lib/module/NativeDocumentPreviewView.js +156 -0
- package/lib/module/NativeDocumentPreviewView.js.map +1 -0
- package/lib/module/NativeMediaThumbnailView.js +110 -0
- package/lib/module/NativeMediaThumbnailView.js.map +1 -0
- package/lib/module/NativeSvgIcon.js +32 -0
- package/lib/module/NativeSvgIcon.js.map +1 -0
- package/lib/module/ResponsivePreviewContainer.js +171 -0
- package/lib/module/ResponsivePreviewContainer.js.map +1 -0
- package/lib/module/accessibility.js +61 -0
- package/lib/module/accessibility.js.map +1 -0
- package/lib/module/config.js +352 -0
- package/lib/module/config.js.map +1 -0
- package/lib/module/cpuOptimization.js +296 -0
- package/lib/module/cpuOptimization.js.map +1 -0
- package/lib/module/db/DatabaseManager.js +191 -0
- package/lib/module/db/DatabaseManager.js.map +1 -0
- package/lib/module/db/MediaMapper.js +284 -0
- package/lib/module/db/MediaMapper.js.map +1 -0
- package/lib/module/db/MediaRepository.js +242 -0
- package/lib/module/db/MediaRepository.js.map +1 -0
- package/lib/module/db/index.js +41 -0
- package/lib/module/db/index.js.map +1 -0
- package/lib/module/db/migrations.js +180 -0
- package/lib/module/db/migrations.js.map +1 -0
- package/lib/module/db/react-native-sqlcipher.d.js +2 -0
- package/lib/module/db/react-native-sqlcipher.d.js.map +1 -0
- package/lib/module/db/secureKey.js +87 -0
- package/lib/module/db/secureKey.js.map +1 -0
- package/lib/module/db/types.js +57 -0
- package/lib/module/db/types.js.map +1 -0
- package/lib/module/db/useMediaDatabase.js +123 -0
- package/lib/module/db/useMediaDatabase.js.map +1 -0
- package/lib/module/defaultIcons.js +2259 -0
- package/lib/module/defaultIcons.js.map +1 -0
- package/lib/module/deviceCapabilities.js +408 -0
- package/lib/module/deviceCapabilities.js.map +1 -0
- package/lib/module/errors.js +267 -0
- package/lib/module/errors.js.map +1 -0
- package/lib/module/iconConfig.js +796 -0
- package/lib/module/iconConfig.js.map +1 -0
- package/lib/module/icons.js +514 -0
- package/lib/module/icons.js.map +1 -0
- package/lib/module/index.js +251 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/jest-globals.d.js +2 -0
- package/lib/module/jest-globals.d.js.map +1 -0
- package/lib/module/library/MediaLibrary.js +107 -0
- package/lib/module/library/MediaLibrary.js.map +1 -0
- package/lib/module/library/MediaLibraryScreen.js +2597 -0
- package/lib/module/library/MediaLibraryScreen.js.map +1 -0
- package/lib/module/library/components/AudioPreview.js +74 -0
- package/lib/module/library/components/AudioPreview.js.map +1 -0
- package/lib/module/library/components/DocumentPreview.js +331 -0
- package/lib/module/library/components/DocumentPreview.js.map +1 -0
- package/lib/module/library/components/ExpandableFab.js +343 -0
- package/lib/module/library/components/ExpandableFab.js.map +1 -0
- package/lib/module/library/components/FileTiles.js +251 -0
- package/lib/module/library/components/FileTiles.js.map +1 -0
- package/lib/module/library/components/ImagePreview.js +70 -0
- package/lib/module/library/components/ImagePreview.js.map +1 -0
- package/lib/module/library/components/MediaMetadataHeader.js +175 -0
- package/lib/module/library/components/MediaMetadataHeader.js.map +1 -0
- package/lib/module/library/components/RecordAudioScreen.js +816 -0
- package/lib/module/library/components/RecordAudioScreen.js.map +1 -0
- package/lib/module/library/components/VideoPreview.js +453 -0
- package/lib/module/library/components/VideoPreview.js.map +1 -0
- package/lib/module/library/components/ViewerCounter.js +55 -0
- package/lib/module/library/components/ViewerCounter.js.map +1 -0
- package/lib/module/library/compressionEstimate.js +507 -0
- package/lib/module/library/compressionEstimate.js.map +1 -0
- package/lib/module/library/fileMetadata.js +196 -0
- package/lib/module/library/fileMetadata.js.map +1 -0
- package/lib/module/library/index.js +42 -0
- package/lib/module/library/index.js.map +1 -0
- package/lib/module/library/libraryIconRegistry.js +69 -0
- package/lib/module/library/libraryIconRegistry.js.map +1 -0
- package/lib/module/library/libraryIcons.js +858 -0
- package/lib/module/library/libraryIcons.js.map +1 -0
- package/lib/module/library/localizedFormat.js +181 -0
- package/lib/module/library/localizedFormat.js.map +1 -0
- package/lib/module/library/palette.js +225 -0
- package/lib/module/library/palette.js.map +1 -0
- package/lib/module/library/responsive.js +53 -0
- package/lib/module/library/responsive.js.map +1 -0
- package/lib/module/locales.js +416 -0
- package/lib/module/locales.js.map +1 -0
- package/lib/module/localization.js +1064 -0
- package/lib/module/localization.js.map +1 -0
- package/lib/module/logger.js +20 -0
- package/lib/module/logger.js.map +1 -0
- package/lib/module/lowMemoryCache.js +206 -0
- package/lib/module/lowMemoryCache.js.map +1 -0
- package/lib/module/mediaHistory.js +33 -0
- package/lib/module/mediaHistory.js.map +1 -0
- package/lib/module/mediaStorageService.js +402 -0
- package/lib/module/mediaStorageService.js.map +1 -0
- package/lib/module/memory.js +79 -0
- package/lib/module/memory.js.map +1 -0
- package/lib/module/nativeActions.js +307 -0
- package/lib/module/nativeActions.js.map +1 -0
- package/lib/module/nativeComponentSupport.js +61 -0
- package/lib/module/nativeComponentSupport.js.map +1 -0
- package/lib/module/networkPerformance.js +24 -0
- package/lib/module/networkPerformance.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/performance.js +104 -0
- package/lib/module/performance.js.map +1 -0
- package/lib/module/performanceMonitor.js +155 -0
- package/lib/module/performanceMonitor.js.map +1 -0
- package/lib/module/permissions.js +370 -0
- package/lib/module/permissions.js.map +1 -0
- package/lib/module/rendering.js +40 -0
- package/lib/module/rendering.js.map +1 -0
- package/lib/module/security.js +18 -0
- package/lib/module/security.js.map +1 -0
- package/lib/module/state.js +266 -0
- package/lib/module/state.js.map +1 -0
- package/lib/module/storage.js +400 -0
- package/lib/module/storage.js.map +1 -0
- package/lib/module/theme.js +390 -0
- package/lib/module/theme.js.map +1 -0
- package/lib/module/types.js +4 -0
- package/lib/module/types.js.map +1 -0
- package/lib/module/utils.js +115 -0
- package/lib/module/utils.js.map +1 -0
- package/lib/module/validation.js +288 -0
- package/lib/module/validation.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/AudioPlayerContext.d.ts +27 -0
- package/lib/typescript/src/AudioPlayerContext.d.ts.map +1 -0
- package/lib/typescript/src/CameraPreview.d.ts +13 -0
- package/lib/typescript/src/CameraPreview.d.ts.map +1 -0
- package/lib/typescript/src/NativeAnimatedImageView.d.ts +33 -0
- package/lib/typescript/src/NativeAnimatedImageView.d.ts.map +1 -0
- package/lib/typescript/src/NativeAudioPlayerView.d.ts +12 -0
- package/lib/typescript/src/NativeAudioPlayerView.d.ts.map +1 -0
- package/lib/typescript/src/NativeBlurView.d.ts +12 -0
- package/lib/typescript/src/NativeBlurView.d.ts.map +1 -0
- package/lib/typescript/src/NativeCustomCamera.d.ts +167 -0
- package/lib/typescript/src/NativeCustomCamera.d.ts.map +1 -0
- package/lib/typescript/src/NativeCustomCamera.web.d.ts +47 -0
- package/lib/typescript/src/NativeCustomCamera.web.d.ts.map +1 -0
- package/lib/typescript/src/NativeCustomVideoView.d.ts +30 -0
- package/lib/typescript/src/NativeCustomVideoView.d.ts.map +1 -0
- package/lib/typescript/src/NativeDocumentPreviewView.d.ts +48 -0
- package/lib/typescript/src/NativeDocumentPreviewView.d.ts.map +1 -0
- package/lib/typescript/src/NativeMediaThumbnailView.d.ts +54 -0
- package/lib/typescript/src/NativeMediaThumbnailView.d.ts.map +1 -0
- package/lib/typescript/src/NativeSvgIcon.d.ts +25 -0
- package/lib/typescript/src/NativeSvgIcon.d.ts.map +1 -0
- package/lib/typescript/src/ResponsivePreviewContainer.d.ts +92 -0
- package/lib/typescript/src/ResponsivePreviewContainer.d.ts.map +1 -0
- package/lib/typescript/src/accessibility.d.ts +20 -0
- package/lib/typescript/src/accessibility.d.ts.map +1 -0
- package/lib/typescript/src/config.d.ts +145 -0
- package/lib/typescript/src/config.d.ts.map +1 -0
- package/lib/typescript/src/cpuOptimization.d.ts +63 -0
- package/lib/typescript/src/cpuOptimization.d.ts.map +1 -0
- package/lib/typescript/src/db/DatabaseManager.d.ts +48 -0
- package/lib/typescript/src/db/DatabaseManager.d.ts.map +1 -0
- package/lib/typescript/src/db/MediaMapper.d.ts +77 -0
- package/lib/typescript/src/db/MediaMapper.d.ts.map +1 -0
- package/lib/typescript/src/db/MediaRepository.d.ts +66 -0
- package/lib/typescript/src/db/MediaRepository.d.ts.map +1 -0
- package/lib/typescript/src/db/index.d.ts +23 -0
- package/lib/typescript/src/db/index.d.ts.map +1 -0
- package/lib/typescript/src/db/migrations.d.ts +31 -0
- package/lib/typescript/src/db/migrations.d.ts.map +1 -0
- package/lib/typescript/src/db/secureKey.d.ts +10 -0
- package/lib/typescript/src/db/secureKey.d.ts.map +1 -0
- package/lib/typescript/src/db/types.d.ts +115 -0
- package/lib/typescript/src/db/types.d.ts.map +1 -0
- package/lib/typescript/src/db/useMediaDatabase.d.ts +41 -0
- package/lib/typescript/src/db/useMediaDatabase.d.ts.map +1 -0
- package/lib/typescript/src/defaultIcons.d.ts +139 -0
- package/lib/typescript/src/defaultIcons.d.ts.map +1 -0
- package/lib/typescript/src/deviceCapabilities.d.ts +92 -0
- package/lib/typescript/src/deviceCapabilities.d.ts.map +1 -0
- package/lib/typescript/src/errors.d.ts +95 -0
- package/lib/typescript/src/errors.d.ts.map +1 -0
- package/lib/typescript/src/iconConfig.d.ts +176 -0
- package/lib/typescript/src/iconConfig.d.ts.map +1 -0
- package/lib/typescript/src/icons.d.ts +217 -0
- package/lib/typescript/src/icons.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +104 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/library/MediaLibrary.d.ts +57 -0
- package/lib/typescript/src/library/MediaLibrary.d.ts.map +1 -0
- package/lib/typescript/src/library/MediaLibraryScreen.d.ts +51 -0
- package/lib/typescript/src/library/MediaLibraryScreen.d.ts.map +1 -0
- package/lib/typescript/src/library/components/AudioPreview.d.ts +6 -0
- package/lib/typescript/src/library/components/AudioPreview.d.ts.map +1 -0
- package/lib/typescript/src/library/components/DocumentPreview.d.ts +10 -0
- package/lib/typescript/src/library/components/DocumentPreview.d.ts.map +1 -0
- package/lib/typescript/src/library/components/ExpandableFab.d.ts +39 -0
- package/lib/typescript/src/library/components/ExpandableFab.d.ts.map +1 -0
- package/lib/typescript/src/library/components/FileTiles.d.ts +8 -0
- package/lib/typescript/src/library/components/FileTiles.d.ts.map +1 -0
- package/lib/typescript/src/library/components/ImagePreview.d.ts +5 -0
- package/lib/typescript/src/library/components/ImagePreview.d.ts.map +1 -0
- package/lib/typescript/src/library/components/MediaMetadataHeader.d.ts +16 -0
- package/lib/typescript/src/library/components/MediaMetadataHeader.d.ts.map +1 -0
- package/lib/typescript/src/library/components/RecordAudioScreen.d.ts +8 -0
- package/lib/typescript/src/library/components/RecordAudioScreen.d.ts.map +1 -0
- package/lib/typescript/src/library/components/VideoPreview.d.ts +20 -0
- package/lib/typescript/src/library/components/VideoPreview.d.ts.map +1 -0
- package/lib/typescript/src/library/components/ViewerCounter.d.ts +13 -0
- package/lib/typescript/src/library/components/ViewerCounter.d.ts.map +1 -0
- package/lib/typescript/src/library/compressionEstimate.d.ts +313 -0
- package/lib/typescript/src/library/compressionEstimate.d.ts.map +1 -0
- package/lib/typescript/src/library/fileMetadata.d.ts +33 -0
- package/lib/typescript/src/library/fileMetadata.d.ts.map +1 -0
- package/lib/typescript/src/library/index.d.ts +40 -0
- package/lib/typescript/src/library/index.d.ts.map +1 -0
- package/lib/typescript/src/library/libraryIconRegistry.d.ts +35 -0
- package/lib/typescript/src/library/libraryIconRegistry.d.ts.map +1 -0
- package/lib/typescript/src/library/libraryIcons.d.ts +85 -0
- package/lib/typescript/src/library/libraryIcons.d.ts.map +1 -0
- package/lib/typescript/src/library/localizedFormat.d.ts +49 -0
- package/lib/typescript/src/library/localizedFormat.d.ts.map +1 -0
- package/lib/typescript/src/library/palette.d.ts +104 -0
- package/lib/typescript/src/library/palette.d.ts.map +1 -0
- package/lib/typescript/src/library/responsive.d.ts +15 -0
- package/lib/typescript/src/library/responsive.d.ts.map +1 -0
- package/lib/typescript/src/locales.d.ts +103 -0
- package/lib/typescript/src/locales.d.ts.map +1 -0
- package/lib/typescript/src/localization.d.ts +1051 -0
- package/lib/typescript/src/localization.d.ts.map +1 -0
- package/lib/typescript/src/logger.d.ts +4 -0
- package/lib/typescript/src/logger.d.ts.map +1 -0
- package/lib/typescript/src/lowMemoryCache.d.ts +64 -0
- package/lib/typescript/src/lowMemoryCache.d.ts.map +1 -0
- package/lib/typescript/src/mediaHistory.d.ts +3 -0
- package/lib/typescript/src/mediaHistory.d.ts.map +1 -0
- package/lib/typescript/src/mediaStorageService.d.ts +155 -0
- package/lib/typescript/src/mediaStorageService.d.ts.map +1 -0
- package/lib/typescript/src/memory.d.ts +21 -0
- package/lib/typescript/src/memory.d.ts.map +1 -0
- package/lib/typescript/src/nativeActions.d.ts +174 -0
- package/lib/typescript/src/nativeActions.d.ts.map +1 -0
- package/lib/typescript/src/nativeComponentSupport.d.ts +30 -0
- package/lib/typescript/src/nativeComponentSupport.d.ts.map +1 -0
- package/lib/typescript/src/networkPerformance.d.ts +12 -0
- package/lib/typescript/src/networkPerformance.d.ts.map +1 -0
- package/lib/typescript/src/performance.d.ts +32 -0
- package/lib/typescript/src/performance.d.ts.map +1 -0
- package/lib/typescript/src/performanceMonitor.d.ts +45 -0
- package/lib/typescript/src/performanceMonitor.d.ts.map +1 -0
- package/lib/typescript/src/permissions.d.ts +132 -0
- package/lib/typescript/src/permissions.d.ts.map +1 -0
- package/lib/typescript/src/rendering.d.ts +9 -0
- package/lib/typescript/src/rendering.d.ts.map +1 -0
- package/lib/typescript/src/security.d.ts +6 -0
- package/lib/typescript/src/security.d.ts.map +1 -0
- package/lib/typescript/src/state.d.ts +102 -0
- package/lib/typescript/src/state.d.ts.map +1 -0
- package/lib/typescript/src/storage.d.ts +117 -0
- package/lib/typescript/src/storage.d.ts.map +1 -0
- package/lib/typescript/src/theme.d.ts +15 -0
- package/lib/typescript/src/theme.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +802 -0
- package/lib/typescript/src/types.d.ts.map +1 -0
- package/lib/typescript/src/utils.d.ts +50 -0
- package/lib/typescript/src/utils.d.ts.map +1 -0
- package/lib/typescript/src/validation.d.ts +52 -0
- package/lib/typescript/src/validation.d.ts.map +1 -0
- package/package.json +204 -0
- package/react-native.config.js +17 -0
- package/src/AudioPlayerContext.tsx +220 -0
- package/src/CameraPreview.tsx +352 -0
- package/src/NativeAnimatedImageView.tsx +71 -0
- package/src/NativeAudioPlayerView.tsx +39 -0
- package/src/NativeBlurView.tsx +40 -0
- package/src/NativeCustomCamera.ts +305 -0
- package/src/NativeCustomCamera.web.ts +192 -0
- package/src/NativeCustomVideoView.ts +30 -0
- package/src/NativeDocumentPreviewView.tsx +196 -0
- package/src/NativeMediaThumbnailView.tsx +167 -0
- package/src/NativeSvgIcon.tsx +46 -0
- package/src/ResponsivePreviewContainer.tsx +227 -0
- package/src/accessibility.ts +80 -0
- package/src/config.ts +406 -0
- package/src/cpuOptimization.ts +365 -0
- package/src/db/DatabaseManager.ts +212 -0
- package/src/db/MediaMapper.ts +377 -0
- package/src/db/MediaRepository.ts +313 -0
- package/src/db/index.ts +73 -0
- package/src/db/migrations.ts +222 -0
- package/src/db/react-native-sqlcipher.d.ts +76 -0
- package/src/db/secureKey.ts +100 -0
- package/src/db/types.ts +123 -0
- package/src/db/useMediaDatabase.ts +191 -0
- package/src/defaultIcons.tsx +2390 -0
- package/src/deviceCapabilities.ts +438 -0
- package/src/errors.ts +350 -0
- package/src/iconConfig.tsx +962 -0
- package/src/icons.tsx +788 -0
- package/src/index.tsx +763 -0
- package/src/jest-globals.d.ts +9 -0
- package/src/library/MediaLibrary.tsx +141 -0
- package/src/library/MediaLibraryScreen.tsx +3003 -0
- package/src/library/components/AudioPreview.tsx +73 -0
- package/src/library/components/DocumentPreview.tsx +361 -0
- package/src/library/components/ExpandableFab.tsx +342 -0
- package/src/library/components/FileTiles.tsx +252 -0
- package/src/library/components/ImagePreview.tsx +72 -0
- package/src/library/components/MediaMetadataHeader.tsx +208 -0
- package/src/library/components/RecordAudioScreen.tsx +845 -0
- package/src/library/components/VideoPreview.tsx +478 -0
- package/src/library/components/ViewerCounter.tsx +71 -0
- package/src/library/compressionEstimate.ts +662 -0
- package/src/library/fileMetadata.ts +212 -0
- package/src/library/index.ts +66 -0
- package/src/library/libraryIconRegistry.tsx +86 -0
- package/src/library/libraryIcons.tsx +928 -0
- package/src/library/localizedFormat.ts +207 -0
- package/src/library/palette.ts +281 -0
- package/src/library/responsive.ts +49 -0
- package/src/locales.ts +381 -0
- package/src/localization.ts +1947 -0
- package/src/logger.ts +20 -0
- package/src/lowMemoryCache.ts +218 -0
- package/src/mediaHistory.ts +42 -0
- package/src/mediaStorageService.ts +546 -0
- package/src/memory.ts +102 -0
- package/src/nativeActions.ts +422 -0
- package/src/nativeComponentSupport.ts +60 -0
- package/src/networkPerformance.ts +30 -0
- package/src/performance.ts +137 -0
- package/src/performanceMonitor.ts +219 -0
- package/src/permissions.ts +403 -0
- package/src/rendering.ts +47 -0
- package/src/security.ts +19 -0
- package/src/state.ts +333 -0
- package/src/storage.ts +453 -0
- package/src/theme.tsx +467 -0
- package/src/types.ts +946 -0
- package/src/utils.ts +177 -0
- 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
|
+
}
|