diff --git a/.claude/rules/karpathy-guidelines.md b/.claude/rules/karpathy-guidelines.md new file mode 100644 index 00000000..f456e1f2 --- /dev/null +++ b/.claude/rules/karpathy-guidelines.md @@ -0,0 +1,68 @@ +# CLAUDE.md + +Behavioral guidelines to reduce common LLM coding mistakes. Merge with project-specific instructions as needed. + +**Tradeoff:** These guidelines bias toward caution over speed. For trivial tasks, use judgment. + +## 1. Think Before Coding + +**Don't assume. Don't hide confusion. Surface tradeoffs.** + +Before implementing: +- State your assumptions explicitly. If uncertain, ask. +- If multiple interpretations exist, present them - don't pick silently. +- If a simpler approach exists, say so. Push back when warranted. +- If something is unclear, stop. Name what's confusing. Ask. +- **Blast radius.** List what depends on this — callers, consumers, migrations, contracts, flags, cached state. What you can't enumerate, you can't reason about. +- **Guardrails inventory.** Know what already exists: validation, auth, rate limits, error handling. Don't duplicate. Don't silently bypass. If one needs to change, change it explicitly. + +## 2. Simplicity First + +**Minimum code that solves the problem. Nothing speculative.** + +- Write the code carefully. All code will be reviewed by Codex. +- No features beyond what was asked. +- No abstractions for single-use code. +- No "flexibility" or "configurability" that wasn't requested. +- No error handling for impossible scenarios. +- If you write 200 lines and it could be 50, rewrite it. + +Ask yourself: "Would a senior engineer say this is overcomplicated?" If yes, simplify. + +## 3. Surgical Changes + +**Touch only what you must. Clean up only your own mess.** + +When editing existing code: +- Don't "improve" adjacent code, comments, or formatting. +- Don't refactor things that aren't broken. +- Match existing style, even if you'd do it differently. +- If you notice unrelated dead code, mention it - don't delete it. + +When your changes create orphans: +- Remove imports/variables/functions that YOUR changes made unused. +- Don't remove pre-existing dead code unless asked. + +The test: Every changed line should trace directly to the user's request. + +## 4. Goal-Driven Execution + +**Define success criteria. Loop until verified.** + +Transform tasks into verifiable goals: +- "Add validation" → "Write tests for invalid inputs, then make them pass" +- "Fix the bug" → "Write a test that reproduces it, then make it pass" +- "Refactor X" → "Ensure tests pass before and after" + +For multi-step tasks, state a brief plan: +``` +1. [Step] → verify: [check] +2. [Step] → verify: [check] +3. [Step] → verify: [check] +``` + +Strong success criteria let you loop independently. Weak criteria ("make it work") require constant clarification. + +--- + +**These guidelines are working if:** fewer unnecessary changes in diffs, fewer rewrites due to overcomplication, and clarifying questions come before implementation rather than after mistakes. diff --git a/.nvmrc b/.nvmrc index 3f430af8..db49bb14 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v18 +22.22.2 diff --git a/CLAUDE.md b/CLAUDE.md index b1131839..9d0a2a90 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,84 +1,97 @@ -# CLAUDE.md +# CLAUDE.md — react-native-compressor -Guidance for AI agents (Claude, Copilot, etc.) and new contributors working in -this repository. Keep this file up to date when project structure, conventions, -or workflows change. +## Project Overview -## What this project is +`react-native-compressor` is a React Native library that compresses Image, Video, and Audio (WhatsApp-style auto compression, plus manual mode), with background upload/download and video thumbnail helpers. The TypeScript/JS layer is a thin wrapper over a single native module named `Compressor` implemented natively on Android (Kotlin) and iOS (Swift). It is published to npm as a library — the `examples/` apps exist only to develop and test against it. -`react-native-compressor` is a React Native library that compresses **video**, -**image**, and **audio** files (and provides background upload/download helpers) -with results comparable to WhatsApp-style compression. It ships native code for -both **iOS** (Swift/Obj-C) and **Android** (Kotlin), exposed to JavaScript through -a TurboModule-capable spec, and supports both the old and new React Native -architectures plus an Expo config plugin. +## Tech Stack & Architecture -- Package name: `react-native-compressor` -- Package manager: **Yarn 4** (`packageManager: yarn@4.14.1`); Yarn workspaces with `examples/*`. -- Upstream: https://github.com/numandev1/react-native-compressor — this is a fork - (`XChikuX/react-native-compressor`) that triages and fixes upstream issues. +**JS layer:** TypeScript · single native module `Compressor` exposed as a **Nitro HybridObject** (`react-native-nitro-modules`), resolved in `src/Main.tsx` via `NitroModules.createHybridObject` +**Android native:** Kotlin · hand-rolled MediaCodec/MediaMuxer video transcoder +**iOS native:** Swift (C++/Swift interop) · AVFoundation via vendored `NextLevelSessionExporter.swift` +**Codegen:** Nitrogen (`yarn nitrogen`) generates the native bindings from the `*.nitro.ts` spec into `nitrogen/generated/` (committed to git) +**Tooling:** Yarn 4 (Berry) workspace (`examples/*`) · Node `>= 22.11` · Jest (native mocked) · react-native-builder-bob · Expo config plugin +**Requirements (Nitro):** RN ≥ 0.75 · iOS ≥ 13.4 / Xcode ≥ 16.4 · Android compileSdk ≥ 34 · C++20. Works on both old & new architecture (Nitro handles its own linking) -## Repository layout +### JS wrapper → single native module -``` -src/ JavaScript/TypeScript public API (the npm entry point) - index.tsx Re-exports the public surface - Main.tsx Aggregates the default export object - Spec/NativeCompressor.ts TurboModule spec (single source of truth for native methods) - Video/ Image/ Audio/ Per-domain JS wrappers (compress(), options, events) - utils/ Uploader/Downloader/helpers (uuid, path normalization) - expo-plugin/ Expo config plugin -ios/ Native iOS implementation (Swift + Obj-C bridge) - Video/VideoMain.swift Video compression entry (auto/manual helpers) - Video/NextLevelSessionExporter.swift AVAssetReader/Writer export engine - Image/ Audio/ Utils/ Image, audio, upload/download, thumbnails - Compressor.mm / Compressor.h Obj-C bridge to the Swift module -android/ Native Android implementation (Kotlin) - src/main/java/com/reactnativecompressor/ - Video/ Video compression (MediaCodec transcode pipeline) - VideoCompressor/compressor/Compressor.kt Core encode/decode loop - VideoCompressor/utils/CompressorUtils.kt Format/codec helpers - Image/ Audio/ Utils/ Image, audio, upload/download, helpers - src/oldarch / src/newarch Architecture-specific TurboModule specs -__tests__/ Jest unit tests for the JS wrapper (native is mocked) -harness/ react-native-harness on-device smoke test definitions -examples/bare Bare React Native example app (build + harness target) -examples/expo Expo example app -TRIAGE.md Running triage of upstream issues and fixes in this fork +All native functionality is exposed through one Nitro HybridObject named `Compressor`. `src/Main.tsx` resolves it once via `NitroModules.createHybridObject('Compressor')`, typed by the spec `src/specs/Compressor.nitro.ts`, and re-throws a friendly linking error if Nitro can't find it. Options are passed as Nitro `AnyMap` (untyped maps), parsed natively as before. + +The public API is assembled in `src/index.tsx` from four domain modules plus utils: + +- `src/Image/index.tsx` — `Image.compress` (strips base64 data-URI headers before calling native) +- `src/Video/index.tsx` — `Video.compress`, `cancelCompression`, `activate/deactivateBackgroundTask` +- `src/Audio/index.tsx` — `Audio.compress` +- `src/utils/` — `Uploader.tsx` (`backgroundUpload`, `cancelUpload`), `Downloader.tsx` (`download`), `helpers.ts`, and metadata/path helpers (`getRealPath`, `getVideoMetaData`, `getImageMetaData`, `generateFilePath`, `createVideoThumbnail`, `clearCache`, `getFileSize`) + +### Progress is delivered via callbacks, not events + +Nitro has no `NativeEventEmitter`. Progress is delivered through **callback functions passed as method parameters** (`onProgress`, `onDownloadProgress`, `onExpired`) — first-class, reference-counted, auto-scheduled onto the JS thread. Callbacks can't live inside an `AnyMap`, so any callback that used to be nested in the options object is lifted to a top-level method parameter (the JS layer strips functions/`undefined` from option maps via `toNativeOptions` in `src/utils/helpers.ts`, since Nitro's AnyMap throws on those). + +A `uuid` (`uuidv4`) is still generated in JS and threaded inside the options map, but now only for (a) cancellation (`cancelCompression`, `cancelUpload`, `AbortController`) and (b) routing native progress emissions to the correct callback. Natively, the per-domain code still calls `EventEmitterHandler.emit*`, but that class is now a **uuid → callback registry** (not a bridge emitter): the binding registers the JS callback under the uuid before invoking the domain method and unregisters when the Promise settles. Keep the uuid threading consistent across JS and both native sides. + +### Native code organization (mirrors the JS domains) + +The thin Nitro binding lives separately from the heavy domain logic: + +- **Android binding** `android/src/main/java/com/margelo/nitro/compressor/HybridCompressor.kt` (extends the generated `HybridCompressorSpec`) converts `AnyMap` → `ReadableMap`, bridges the Nitro `Promise` to the domain layer's `com.facebook.react.bridge.Promise` via `NitroPromiseAdapter.kt`, and runs domain work on a background executor. `NitroCompressorPackage.kt` (a `BaseReactPackage`) exists only so RN autolinking registers the Gradle project and its `companion init` loads `libNitroCompressor.so`. +- **iOS binding** `ios/HybridCompressor.swift` (implements `HybridCompressorSpec`) converts `AnyMap` → `NSDictionary`, synthesizes `RCTPromiseResolveBlock`/`RejectBlock` to drive the Nitro `Promise`. + +Heavy domain logic (unchanged, mirrors the JS domains): + +- **Android** `android/src/main/java/com/reactnativecompressor/` → `Image/`, `Video/`, `Audio/`, `Utils/`. The video transcoder is hand-rolled under `Video/VideoCompressor/` (MediaCodec/MediaMuxer pipeline: `Compressor.kt`, `MP4Builder.kt`, surfaces/renderer, `utils/`). `VideoMain.compress` routes to auto vs manual via `VideoCompressorHelper`. `StreamableVideo.kt` moves the `moov` atom to the front of the output by default — preserve this behavior. +- **iOS** `ios/` → `Image/`, `Audio/` (with `FormatConverter/`), `Video/`, `Utils/`. Video uses `NextLevelSessionExporter.swift` (a vendored AVFoundation exporter) driven by `VideoMain.swift`. Domain Swift files `import React` for `RCTPromise*` (bridging headers are unsupported under RN 0.85 framework linkage), and `FormatConverter`/`AudioFileFormat` are `internal` to keep them out of the Swift↔C++ interop surface. +- On both platforms `EventEmitterHandler` is the uuid→callback registry described above. + +### Expo support + +`src/expo-plugin/compressor.ts` is an Expo config plugin; `app.plugin.js` loads its built output from `lib/commonjs/expo-plugin/compressor` (so the plugin only works after `yarn prepack`). + +--- + +## Commands + +Uses **Yarn 4** (Berry) and a Yarn workspace (`examples/*`). Node `>= 22.11`. + +```sh +yarn # install deps for root + example workspaces +yarn test # Jest unit tests (native is mocked — fast, no device) +yarn test path/to/file # run a single test file +yarn test -t "pattern" # run tests matching a name pattern +yarn typecheck # tsc --noEmit +yarn lint # eslint over **/*.{js,ts,tsx} +yarn lint --fix # auto-fix lint/prettier +yarn test:pr # full PR gate: test --runInBand + typecheck + lint +yarn nitrogen # regenerate Nitro native bindings into nitrogen/generated/ (run after editing the *.nitro.ts spec) +yarn prepack # nitrogen + build the publishable lib/ via react-native-builder-bob +yarn build:android # assemble the bare example (arm64-v8a) — native Android compile check +yarn build:ios # build the bare example for the iOS simulator — native iOS compile check +yarn clean # delete android/ios build dirs ``` -## Public API surface +Example apps (workspace shortcuts): -The default export aggregates these modules/functions (see -`__tests__/compressor.test.ts` for the authoritative list): -`Audio`, `Image`, `Video`, `UploadType`, `UploaderHttpMethod`, `backgroundUpload`, -`cancelUpload`, `clearCache`, `createVideoThumbnail`, `download`, -`generateFilePath`, `getDetails`, `getFileSize`, `getImageMetaData`, -`getRealPath`, `getVideoMetaData`, `uuidv4`. +```sh +yarn example:bare start # Metro for the bare RN example +yarn example:bare android # run bare example on Android +yarn example:bare ios # run bare example on iOS +yarn example:expo start # Expo example +``` -Video compression supports `compressionMethod: 'auto' | 'manual'`, `maxSize`, -`bitrate`, `progressDivider`, `minimumFileSizeForCompress`, and `stripAudio`. +### On-device integration tests (react-native-harness) -## Build, test, and validate +The Jest unit tests mock the native module, so they validate only the JS contract — **real media decoding/encoding is only exercised by the harness tests**, which run inside the bare example app on a booted simulator/emulator: -Run JS-level checks from the repo root: +```sh +yarn test:harness:android # requires an Android emulator (default Pixel_8_API_35) +yarn test:harness:ios # requires an iOS simulator (default iPhone 17 Pro, iOS 26.4) +``` -| Command | Purpose | -| --- | --- | -| `yarn install` | Install dependencies (Yarn 4) | -| `yarn jest` / `yarn test` | Run the JS wrapper unit tests | -| `yarn typecheck` | `tsc --noEmit` | -| `yarn lint` | ESLint over `**/*.{js,ts,tsx}` | -| `yarn test:pr` | `test --runInBand && typecheck && lint` (run before opening a PR) | -| `yarn build:android` | Assemble the bare example (`arm64-v8a`) | -| `yarn build:ios` | Build the bare example for the iOS simulator | -| `yarn test:harness:android` / `yarn test:harness:ios` | On-device/simulator smoke tests | +Device/version overrides live in `examples/bare/rn-harness.config.mjs` (env vars `RN_HARNESS_ANDROID_DEVICE`, `RN_HARNESS_IOS_DEVICE`, `RN_HARNESS_IOS_VERSION`, etc.). The harness spec is `harness/native-compressor.harness.ts` (the copy under `examples/bare/harness/` re-exports it). -**Important:** The Jest tests mock the native module, so they validate only the -JS contract. Real media decoding/encoding **cannot** be verified by unit tests — -it must be smoke-tested in the example app on a simulator or device. When you -change native Swift/Kotlin code, state clearly that it was not runtime-verified -in CI and, where possible, validate via the example app or harness. +When you change native Swift/Kotlin code and can't run it on a device/simulator, state clearly that it was not runtime-verified. + +--- ## Native video pipeline notes (high-signal, easy to get wrong) @@ -109,18 +122,23 @@ in CI and, where possible, validate via the example app or harness. `MediaCodec.createEncoderByType("video/avc")`; QTI AVC encoders can produce MP4s that do not play on Mac/iPhone, so avoid switching this without testing. -## Conventions +--- + +## Gotchas +- **One Nitro spec — regenerate after editing.** The spec lives in ONE place: `src/specs/Compressor.nitro.ts` (config in `nitro.json`). After adding/renaming/changing a method, run `yarn nitrogen` and commit the updated `nitrogen/generated/`, then update the two implementations: `ios/HybridCompressor.swift` and `android/.../com/margelo/nitro/compressor/HybridCompressor.kt`. (No more old/new-arch specs, `Compressor.mm`, or `RCTEventEmitter` — those were deleted.) +- **Nitrogen Swift keyword gotcha:** don't name a spec parameter after a Swift keyword (e.g. `extension`) — nitrogen emits it unescaped and the generated Swift won't compile. `generateFilePath` uses `fileExtension` for this reason. +- **iOS framework linkage:** RN 0.85 builds pods as frameworks, where bridging headers are unsupported. Swift files needing React types must `import React` (and `import UIKit` for UIKit types). Public Swift value types with nested types (e.g. `FormatConverter`) must stay `internal` or they break the Swift↔C++ interop link. +- **Android autolink/.so:** `NitroCompressorPackage` must exist (RN CLI keys autolinking off a `ReactPackage`, and its `init` loads `libNitroCompressor.so`). After changing it, clear `examples/bare/android/build/generated/autolinking` if the project stops being found. +- **AnyMap is strict:** option maps must contain only JSON-like values (no functions, no `undefined`) — use `toNativeOptions`. Numbers arrive natively as `Double`; the binding round-trips through `ReadableMap`/`NSNumber` so the domain parsers' `getInt`/`as? Int` keep working. +- **Streamable:** `StreamableVideo.kt` moves `moov` atom to front by default — preserve. +- **uuid threading:** keep `uuid` consistent across JS + both native sides for cancellation + progress-callback routing. +- **Unsupported media:** prefer graceful, descriptive failures over cryptic native crashes — clear error messages that tell the user what happened. +- **Commits follow Conventional Commits** (`fix:`, `feat:`, `refactor:`, `docs:`, `test:`, `chore:`). `commit-msg` hook runs commitlint; `pre-commit` hook (lefthook) runs eslint + `tsc --noEmit` on staged files. Don't bypass. +- **Build output:** `lib/` and example workspaces excluded from lint/tsc/jest — don't edit `lib/` by hand. +- **Releases:** cut with `yarn release` (release-it + conventional-changelog). -- Keep changes surgical and aligned with surrounding style. Native helper objects - (e.g. `CompressorUtils`) use member imports and unqualified calls in - `Compressor.kt` — match that. -- When fixing an upstream issue, record it in `TRIAGE.md` (triage row + the - "Minor fixes made in this branch" list) referencing the issue number. -- Prefer graceful, descriptive failures over cryptic native crashes for - unsupported media (clear error messages that tell the user what happened). +`TRIAGE.md` tracks the running triage of GitHub issues and the fixes made for them — when fixing an issue, record it there (triage row + the "Minor fixes made in this branch" list) referencing the issue number. -## Merging back upstream +## Coding Guidelines -This fork accumulates many incremental commits. When contributing back to -`numandev1/react-native-compressor`, use a **squash merge** so the history lands -as a single, well-described commit rather than the full incremental series. +@.claude/rules/karpathy-guidelines.md diff --git a/__tests__/compressor.test.ts b/__tests__/compressor.test.ts index 0ec037a2..9d9a4ddf 100644 --- a/__tests__/compressor.test.ts +++ b/__tests__/compressor.test.ts @@ -1,6 +1,3 @@ -const mockListeners: Record void>> = {}; -const mockSubscriptions: Array<{ remove: jest.Mock }> = []; - // These unit tests validate the JavaScript wrapper contract. The native // Compressor module is mocked, so real media decoding must be smoke-tested // in an example app on a simulator or device. @@ -27,41 +24,26 @@ const localVideoUri = 'file:///tmp/react-native-compressor/input-video.mp4'; const localImageUri = 'file:///tmp/react-native-compressor/input-image.jpg'; const localAudioUri = 'file:///tmp/react-native-compressor/input-audio.wav'; -jest.mock('react-native', () => ({ - NativeModules: { - Compressor: mockCompressor, +// The wrapper resolves the native module through Nitro instead of NativeModules. +jest.mock('react-native-nitro-modules', () => ({ + NitroModules: { + createHybridObject: jest.fn(() => mockCompressor), }, - NativeEventEmitter: jest.fn().mockImplementation(() => ({ - addListener: jest.fn((eventName: string, callback: (event: unknown) => void) => { - const subscription = { remove: jest.fn() }; - mockListeners[eventName] = [...(mockListeners[eventName] ?? []), callback]; - mockSubscriptions.push(subscription); - return subscription; - }), - removeAllListeners: jest.fn((eventName: string) => { - mockListeners[eventName] = []; - }), - })), +})); + +jest.mock('react-native', () => ({ Platform: { OS: 'ios', select: jest.fn((options: Record) => options.ios ?? options.default), }, })); -const emitNativeEvent = (eventName: string, event: unknown) => { - (mockListeners[eventName] ?? []).forEach((callback) => callback(event)); -}; - describe('react-native-compressor JS wrapper API', () => { let api: typeof import('../src'); let reactNative: typeof import('react-native'); beforeEach(() => { jest.clearAllMocks(); - Object.keys(mockListeners).forEach((eventName) => { - mockListeners[eventName] = []; - }); - mockSubscriptions.length = 0; reactNative = require('react-native'); reactNative.Platform.OS = 'ios'; api = require('../src'); @@ -96,16 +78,9 @@ describe('react-native-compressor JS wrapper API', () => { expect(api.UploaderHttpMethod.PATCH).toBe('PATCH'); }); - it('compresses images, strips base64 headers, forwards progress, and removes listeners', async () => { - mockCompressor.image_compress.mockImplementation(async (_value, options) => { - emitNativeEvent('downloadProgress', { - uuid: options.uuid, - data: { progress: 55 }, - }); - emitNativeEvent('downloadProgress', { - uuid: 'other-id', - data: { progress: 99 }, - }); + it('compresses images, strips base64 headers, and forwards download progress', async () => { + mockCompressor.image_compress.mockImplementation(async (_value, _options, onDownloadProgress) => { + onDownloadProgress?.(55); return 'file://compressed-image.jpg'; }); const downloadProgress = jest.fn(); @@ -117,16 +92,9 @@ describe('react-native-compressor JS wrapper API', () => { }), ).resolves.toBe('file://compressed-image.jpg'); - expect(mockCompressor.image_compress).toHaveBeenCalledWith( - 'abc123', - expect.objectContaining({ - quality: 0.7, - uuid: expect.any(String), - }), - ); + expect(mockCompressor.image_compress).toHaveBeenCalledWith('abc123', expect.objectContaining({ quality: 0.7 }), expect.any(Function)); expect(downloadProgress).toHaveBeenCalledWith(55); expect(downloadProgress).toHaveBeenCalledTimes(1); - expect(mockSubscriptions.at(-1)?.remove).toHaveBeenCalledTimes(1); }); it('rejects empty image compression input before calling native code', async () => { @@ -134,16 +102,10 @@ describe('react-native-compressor JS wrapper API', () => { expect(mockCompressor.image_compress).not.toHaveBeenCalled(); }); - it('compresses videos with defaults, cancellation id, progress callbacks, and listener cleanup', async () => { - mockCompressor.compress.mockImplementation(async (_fileUrl, options) => { - emitNativeEvent('videoCompressProgress', { - uuid: options.uuid, - data: { progress: 22 }, - }); - emitNativeEvent('downloadProgress', { - uuid: options.uuid, - data: { progress: 33 }, - }); + it('compresses videos with defaults, cancellation id, and progress callbacks', async () => { + mockCompressor.compress.mockImplementation(async (_fileUrl, _options, onProgress, onDownloadProgress) => { + onProgress?.(22); + onDownloadProgress?.(33); return 'file://compressed-video.mp4'; }); const onProgress = jest.fn(); @@ -172,13 +134,12 @@ describe('react-native-compressor JS wrapper API', () => { maxSize: 640, stripAudio: true, }), + expect.any(Function), + expect.any(Function), ); expect(getCancellationId).toHaveBeenCalledWith(expect.any(String)); expect(onProgress).toHaveBeenCalledWith(22); expect(downloadProgress).toHaveBeenCalledWith(33); - mockSubscriptions.slice(-2).forEach((subscription) => { - expect(subscription.remove).toHaveBeenCalledTimes(1); - }); }); it('forwards manual video compression options and minimum file size', async () => { @@ -199,12 +160,14 @@ describe('react-native-compressor JS wrapper API', () => { minimumFileSizeForCompress: 10, progressDivider: 5, }), + undefined, + undefined, ); }); it('proxies video cancellation and background task lifecycle', async () => { - mockCompressor.activateBackgroundTask.mockImplementation(async () => { - emitNativeEvent('backgroundTaskExpired', { expired: true }); + mockCompressor.activateBackgroundTask.mockImplementation(async (_options, onExpired) => { + onExpired?.(); return 'activated'; }); mockCompressor.deactivateBackgroundTask.mockResolvedValue('deactivated'); @@ -215,9 +178,9 @@ describe('react-native-compressor JS wrapper API', () => { await expect(api.Video.deactivateBackgroundTask()).resolves.toBe('deactivated'); expect(mockCompressor.cancelCompression).toHaveBeenCalledWith('video-id'); - expect(mockCompressor.activateBackgroundTask).toHaveBeenCalledWith({}); + expect(mockCompressor.activateBackgroundTask).toHaveBeenCalledWith({}, expect.any(Function)); expect(mockCompressor.deactivateBackgroundTask).toHaveBeenCalledWith({}); - expect(onExpired).toHaveBeenCalledWith({ expired: true }); + expect(onExpired).toHaveBeenCalledWith(undefined); }); it('compresses audio with defaults and custom options', async () => { @@ -291,34 +254,31 @@ describe('react-native-compressor JS wrapper API', () => { }); }); - it('downloads files, strips Android file prefixes, reports progress, and removes listeners', async () => { + it('downloads files, strips Android file prefixes, and reports progress', async () => { reactNative.Platform.OS = 'android'; - mockCompressor.download.mockImplementation(async (_fileUrl, options) => { - emitNativeEvent('downloadProgress', { - uuid: options.uuid, - data: { progress: 88 }, - }); + mockCompressor.download.mockImplementation(async (_fileUrl, _options, onProgress) => { + onProgress?.(88); return '/downloads/file.mp4'; }); const downloadProgress = jest.fn(); await expect(api.download('file:///storage/input.mp4', downloadProgress, 10)).resolves.toBe('/downloads/file.mp4'); - expect(mockCompressor.download).toHaveBeenCalledWith('/storage/input.mp4', { - uuid: expect.any(String), - progressDivider: 10, - }); + expect(mockCompressor.download).toHaveBeenCalledWith( + '/storage/input.mp4', + { + uuid: expect.any(String), + progressDivider: 10, + }, + expect.any(Function), + ); expect(downloadProgress).toHaveBeenCalledWith(88); - expect(mockSubscriptions.at(-1)?.remove).toHaveBeenCalledTimes(1); }); it('uploads files with options, progress, cancellation id, abort handling, and Android path normalization', async () => { reactNative.Platform.OS = 'android'; - mockCompressor.upload.mockImplementation(async (_fileUrl, options) => { - emitNativeEvent('uploadProgress', { - uuid: options.uuid, - data: { written: 4, total: 10 }, - }); + mockCompressor.upload.mockImplementation(async (_fileUrl, _options, onProgress) => { + onProgress?.(4, 10); return { status: 200 }; }); const onProgress = jest.fn(); @@ -356,11 +316,11 @@ describe('react-native-compressor JS wrapper API', () => { headers: { Authorization: 'token' }, parameters: { album: 'demo' }, }), + expect.any(Function), ); expect(getCancellationId).toHaveBeenCalledWith(uploadOptions.uuid); expect(onProgress).toHaveBeenCalledWith(4, 10); expect(mockCompressor.cancelUpload).toHaveBeenCalledWith(uploadOptions.uuid, false); - expect(mockSubscriptions.at(-1)?.remove).toHaveBeenCalledTimes(1); }); it('cancels one upload or all uploads through the native module', () => { diff --git a/android/CMakeLists.txt b/android/CMakeLists.txt new file mode 100644 index 00000000..9f3019fa --- /dev/null +++ b/android/CMakeLists.txt @@ -0,0 +1,27 @@ +project(NitroCompressor) +cmake_minimum_required(VERSION 3.9.0) + +set(PACKAGE_NAME NitroCompressor) +set(CMAKE_VERBOSE_MAKEFILE ON) +set(CMAKE_CXX_STANDARD 20) + +# Define the C++ library and add all of our own sources. +add_library(${PACKAGE_NAME} SHARED + src/main/cpp/cpp-adapter.cpp +) + +# Add all files generated by Nitrogen (sources, headers, prefab links). +include(${CMAKE_SOURCE_DIR}/../nitrogen/generated/android/NitroCompressor+autolinking.cmake) + +# Local includes +include_directories( + "src/main/cpp" +) + +find_library(LOG_LIB log) + +target_link_libraries( + ${PACKAGE_NAME} + ${LOG_LIB} + android +) diff --git a/android/build.gradle b/android/build.gradle index bb2e287a..81cf0c64 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,5 +1,5 @@ buildscript { - // Buildscript is evaluated before everything else so we can't use getExtOrDefault + // Buildscript is evaluated before everything else so we can't use getExtOrDefault def kotlin_version = rootProject.ext.has("kotlinVersion") ? rootProject.ext.get("kotlinVersion") : project.properties["Compressor_kotlinVersion"] repositories { @@ -8,25 +8,21 @@ buildscript { } dependencies { - classpath "com.android.tools.build:gradle:7.2.1" + classpath "com.android.tools.build:gradle:9.2.1" // noinspection DifferentKotlinGradleVersion classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } -def isNewArchitectureEnabled() { - return rootProject.hasProperty("newArchEnabled") && rootProject.getProperty("newArchEnabled") == "true" +def reactNativeArchitectures() { + def value = rootProject.getProperties().get("reactNativeArchitectures") + return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"] } apply plugin: "com.android.library" apply plugin: "kotlin-android" - - -def appProject = rootProject.allprojects.find { it.plugins.hasPlugin('com.android.application') } - -if (isNewArchitectureEnabled()) { - apply plugin: "com.facebook.react" -} +// Nitrogen: registers the generated Kotlin sources with this Gradle project. +apply from: '../nitrogen/generated/android/NitroCompressor+autolinking.gradle' def getExtOrDefault(name) { return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties["Compressor_" + name] @@ -36,40 +32,62 @@ def getExtOrIntegerDefault(name) { return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["Compressor_" + name]).toInteger() } -def supportsNamespace() { - def parsed = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION.tokenize('.') - def major = parsed[0].toInteger() - def minor = parsed[1].toInteger() - - // Namespace support was added in 7.3.0 - if (major == 7 && minor >= 3) { - return true - } - - return major >= 8 -} - android { - if (supportsNamespace()) { - namespace "com.reactnativecompressor" + namespace "com.reactnativecompressor" - sourceSets { - main { - manifest.srcFile "src/main/AndroidManifestNew.xml" - } + ndkVersion getExtOrDefault("ndkVersion") + compileSdkVersion getExtOrIntegerDefault("compileSdkVersion") + + sourceSets { + main { + manifest.srcFile "src/main/AndroidManifestNew.xml" } } - compileSdkVersion getExtOrIntegerDefault("compileSdkVersion") - defaultConfig { minSdkVersion getExtOrIntegerDefault("minSdkVersion") targetSdkVersion getExtOrIntegerDefault("targetSdkVersion") - buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString() + + externalNativeBuild { + cmake { + cppFlags "-frtti -fexceptions -Wall -Wextra -fstack-protector-all -std=c++20" + arguments "-DANDROID_STL=c++_shared", "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON" + abiFilters(*reactNativeArchitectures()) + } + } } + + externalNativeBuild { + cmake { + path "CMakeLists.txt" + } + } + buildFeatures { - buildConfig true + prefab true } + + packagingOptions { + excludes = [ + "META-INF", + "META-INF/**", + "**/libc++_shared.so", + "**/libfbjni.so", + "**/libjsi.so", + "**/libfolly_json.so", + "**/libfolly_runtime.so", + "**/libglog.so", + "**/libhermes.so", + "**/libhermes-executor-debug.so", + "**/libhermes_executor.so", + "**/libreactnative.so", + "**/libreactnativejni.so", + "**/libturbomodulejsijni.so", + "**/libreact_nativemodule_core.so", + "**/libjscexecutor.so" + ] + } + buildTypes { release { minifyEnabled false @@ -81,22 +99,8 @@ android { } compileOptions { - sourceCompatibility JavaVersion.VERSION_11 - targetCompatibility JavaVersion.VERSION_11 - } - - sourceSets { - main { - if (isNewArchitectureEnabled()) { - java.srcDirs += [ - "src/newarch", - // This is needed to build Kotlin project with NewArch enabled - "${project.buildDir}/generated/source/codegen/java" - ] - } else { - java.srcDirs += ["src/oldarch"] - } - } + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 } } @@ -114,6 +118,8 @@ dependencies { //noinspection GradleDynamicVersion implementation "com.facebook.react:react-native:+" implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" + // Nitro core (provided as a Gradle subproject by autolinking). + implementation project(":react-native-nitro-modules") implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4" @@ -121,11 +127,3 @@ dependencies { implementation 'com.github.kaushik-naik:TAndroidLame:277c2ab4b0' implementation 'javazoom:jlayer:1.0.1' } - -if (isNewArchitectureEnabled()) { - react { - jsRootDir = file("../src/") - libraryName = "Compressor" - codegenJavaPackageName = "com.reactnativecompressor" - } -} diff --git a/android/gradle.properties b/android/gradle.properties index 7d25f089..02d59bc1 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1,5 +1,5 @@ -Compressor_kotlinVersion=1.7.0 -Compressor_minSdkVersion=21 -Compressor_targetSdkVersion=31 -Compressor_compileSdkVersion=31 -Compressor_ndkversion=21.4.7075529 +Compressor_kotlinVersion=2.1.20 +Compressor_minSdkVersion=24 +Compressor_targetSdkVersion=36 +Compressor_compileSdkVersion=36 +Compressor_ndkVersion=27.1.12297006 diff --git a/android/src/main/AndroidManifestNew.xml b/android/src/main/AndroidManifestNew.xml index a2f47b60..5bd7ed88 100644 --- a/android/src/main/AndroidManifestNew.xml +++ b/android/src/main/AndroidManifestNew.xml @@ -1,2 +1,5 @@ + + diff --git a/android/src/main/cpp/cpp-adapter.cpp b/android/src/main/cpp/cpp-adapter.cpp new file mode 100644 index 00000000..e89cc8c7 --- /dev/null +++ b/android/src/main/cpp/cpp-adapter.cpp @@ -0,0 +1,8 @@ +#include +#include + +#include "NitroCompressorOnLoad.hpp" + +JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void*) { + return facebook::jni::initialize(vm, [] { margelo::nitro::compressor::registerAllNatives(); }); +} diff --git a/android/src/main/java/com/margelo/nitro/compressor/HybridCompressor.kt b/android/src/main/java/com/margelo/nitro/compressor/HybridCompressor.kt new file mode 100644 index 00000000..22e430c5 --- /dev/null +++ b/android/src/main/java/com/margelo/nitro/compressor/HybridCompressor.kt @@ -0,0 +1,213 @@ +package com.margelo.nitro.compressor + +import com.facebook.react.bridge.Arguments +import com.facebook.react.bridge.ReadableMap +import com.facebook.react.bridge.WritableMap +import com.margelo.nitro.NitroModules +import com.margelo.nitro.core.AnyMap +import com.margelo.nitro.core.Promise +import com.reactnativecompressor.Audio.AudioMain +import com.reactnativecompressor.Image.ImageMain +import com.reactnativecompressor.NitroPromiseAdapter +import com.reactnativecompressor.Utils.CreateVideoThumbnailClass +import com.reactnativecompressor.Utils.Downloader +import com.reactnativecompressor.Utils.EventEmitterHandler +import com.reactnativecompressor.Utils.Uploader +import com.reactnativecompressor.Utils.Utils +import com.reactnativecompressor.Video.VideoMain +import java.util.UUID +import java.util.concurrent.Executors +import com.facebook.react.bridge.Promise as RNPromise + +/** + * Nitro HybridObject implementation of the single `Compressor` native module. + * + * Thin binding layer: it converts Nitro's `AnyMap` options into the + * `ReadableMap` the existing domain code already consumes, bridges the Nitro + * `Promise` to the domain layer's `com.facebook.react.bridge.Promise` via + * [NitroPromiseAdapter], and registers progress callbacks (keyed by `uuid`) + * with [EventEmitterHandler]. All heavy logic stays in the domain classes, + * which run on a background executor so the JS thread is never blocked. + */ +class HybridCompressor : HybridCompressorSpec() { + private val reactContext + get() = NitroModules.applicationContext + ?: throw IllegalStateException("react-native-compressor: ReactApplicationContext is not available") + + private val executor = Executors.newCachedThreadPool() + + private val imageMain by lazy { ImageMain(reactContext) } + private val videoMain by lazy { VideoMain(reactContext) } + private val audioMain by lazy { AudioMain(reactContext) } + private val uploader by lazy { Uploader(reactContext) } + private val videoThumbnail by lazy { CreateVideoThumbnailClass(reactContext) } + + // region Converters + + private val toStringResult: (Any?) -> String = { it as? String ?: "" } + + private val toAnyMapResult: (Any?) -> AnyMap = { value -> + val hashMap = (value as? ReadableMap)?.toHashMap() ?: HashMap() + AnyMap.fromMap(hashMap, true) + } + + private val toThumbnailResult: (Any?) -> VideoThumbnailResult = { value -> + val map = value as ReadableMap + VideoThumbnailResult( + if (map.hasKey("path")) map.getString("path") ?: "" else "", + if (map.hasKey("size")) map.getDouble("size") else 0.0, + if (map.hasKey("mime")) map.getString("mime") ?: "" else "", + if (map.hasKey("width")) map.getDouble("width") else 0.0, + if (map.hasKey("height")) map.getDouble("height") else 0.0, + ) + } + + // endregion + + // region Image + + override fun image_compress(imagePath: String, optionMap: AnyMap, onDownloadProgress: ((progress: Double) -> Unit)?): Promise { + val map = toWritableMap(optionMap) + // Remote-image download progress is keyed by `uuid` inside the Downloader. The JS + // layer no longer sends one (the callback is passed directly), so mint one here. + val uuid = if (map.hasKey("uuid")) map.getString("uuid") ?: UUID.randomUUID().toString() else UUID.randomUUID().toString() + map.putString("uuid", uuid) + EventEmitterHandler.registerDownloadProgress(uuid, onDownloadProgress) + return runOnExecutor(toStringResult, { EventEmitterHandler.unregister(uuid) }) { promise -> + imageMain.image_compress(imagePath, map, promise) + } + } + + override fun getImageMetaData(filePath: String): Promise { + return runOnExecutor(toAnyMapResult) { promise -> imageMain.getImageMetaData(filePath, promise) } + } + + // endregion + + // region Video + + override fun compress( + fileUrl: String, + optionMap: AnyMap, + onProgress: ((progress: Double) -> Unit)?, + onDownloadProgress: ((progress: Double) -> Unit)?, + ): Promise { + val map = toWritableMap(optionMap) + val uuid = if (map.hasKey("uuid")) map.getString("uuid") ?: "" else "" + EventEmitterHandler.registerVideoCompressProgress(uuid, onProgress) + EventEmitterHandler.registerDownloadProgress(uuid, onDownloadProgress) + return runOnExecutor(toStringResult, { EventEmitterHandler.unregister(uuid) }) { promise -> + videoMain.compress(fileUrl, map, promise) + } + } + + override fun cancelCompression(uuid: String) { + videoMain.cancelCompression(uuid) + } + + override fun getVideoMetaData(filePath: String): Promise { + return runOnExecutor(toAnyMapResult) { promise -> videoMain.getVideoMetaData(filePath, promise) } + } + + override fun activateBackgroundTask(options: AnyMap, onExpired: (() -> Unit)?): Promise { + EventEmitterHandler.setBackgroundTaskExpiredCallback(onExpired) + val map = toWritableMap(options) + return runOnExecutor(toStringResult) { promise -> videoMain.activateBackgroundTask(map, promise) } + } + + override fun deactivateBackgroundTask(options: AnyMap): Promise { + EventEmitterHandler.setBackgroundTaskExpiredCallback(null) + val map = toWritableMap(options) + return runOnExecutor(toStringResult) { promise -> videoMain.deactivateBackgroundTask(map, promise) } + } + + // endregion + + // region Audio + + override fun compress_audio(fileUrl: String, optionMap: AnyMap): Promise { + val map = toWritableMap(optionMap) + return runOnExecutor(toStringResult) { promise -> audioMain.compress_audio(fileUrl, map, promise) } + } + + // endregion + + // region Upload / Download + + override fun upload(fileUrl: String, options: AnyMap, onProgress: ((written: Double, total: Double) -> Unit)?): Promise { + val map = toWritableMap(options) + val uuid = if (map.hasKey("uuid")) map.getString("uuid") ?: "" else "" + EventEmitterHandler.registerUploadProgress(uuid, onProgress) + return runOnExecutor(toAnyMapResult, { EventEmitterHandler.unregister(uuid) }) { promise -> + uploader.upload(fileUrl, map, reactContext, promise) + } + } + + override fun cancelUpload(uuid: String, shouldCancelAll: Boolean) { + uploader.cancelUpload(uuid, shouldCancelAll) + } + + override fun download(fileUrl: String, options: AnyMap, onProgress: ((progress: Double) -> Unit)?): Promise { + val uuid = if (options.contains("uuid") && options.isString("uuid")) options.getString("uuid") else "" + val progressDivider = if (options.contains("progressDivider") && options.isDouble("progressDivider")) options.getDouble("progressDivider").toInt() else 0 + EventEmitterHandler.registerDownloadProgress(uuid, onProgress) + return runOnExecutor(toStringResult, { EventEmitterHandler.unregister(uuid) }) { promise -> + val downloadedFilePath = Downloader.downloadMediaWithProgress(fileUrl, uuid, progressDivider, reactContext) + if (downloadedFilePath != null) promise.resolve(downloadedFilePath) else promise.reject("Unable to download", "Unable to download") + } + } + + // endregion + + // region Others + + override fun generateFilePath(fileExtension: String): Promise { + return runOnExecutor(toStringResult) { promise -> promise.resolve(Utils.generateCacheFilePath(fileExtension, reactContext)) } + } + + override fun getRealPath(path: String, type: String): Promise { + // Utils.getRealPath already returns a `file://`-prefixed path via slashifyFilePath, + // so it must not be prefixed again (that produced a malformed `file://file:///…`). + return runOnExecutor(toStringResult) { promise -> promise.resolve(Utils.getRealPath(path, reactContext)) } + } + + override fun getFileSize(filePath: String): Promise { + return runOnExecutor(toStringResult) { promise -> Utils.getFileSize(filePath, promise, reactContext) } + } + + override fun createVideoThumbnail(fileUrl: String, options: AnyMap): Promise { + val map = toWritableMap(options) + return runOnExecutor(toThumbnailResult) { promise -> videoThumbnail.create(fileUrl, map, promise) } + } + + override fun clearCache(cacheDir: String?): Promise { + return runOnExecutor(toStringResult) { promise -> CreateVideoThumbnailClass.clearCache(cacheDir, promise, reactContext) } + } + + // endregion + + // region Helpers + + /** Convert a Nitro [AnyMap] to a React Native [WritableMap] that the domain parsers consume. */ + private fun toWritableMap(map: AnyMap): WritableMap = Arguments.makeNativeMap(map.toHashMap()) + + /** + * Runs [block] on a background thread, handing it a bridge [RNPromise] that + * resolves/rejects the returned Nitro [Promise]. Synchronous throws are routed + * to the same adapter so the Promise is never double-settled. + */ + private fun runOnExecutor(convert: (Any?) -> T, onSettle: () -> Unit = {}, block: (RNPromise) -> Unit): Promise { + val promise = Promise() + val adapter = NitroPromiseAdapter(promise, convert, onSettle) + executor.execute { + try { + block(adapter) + } catch (e: Throwable) { + adapter.reject(e) + } + } + return promise + } + + // endregion +} diff --git a/android/src/main/java/com/reactnativecompressor/CompressorModule.kt b/android/src/main/java/com/reactnativecompressor/CompressorModule.kt deleted file mode 100644 index f758458b..00000000 --- a/android/src/main/java/com/reactnativecompressor/CompressorModule.kt +++ /dev/null @@ -1,180 +0,0 @@ -package com.reactnativecompressor - -import android.os.Build -import androidx.annotation.RequiresApi -import com.facebook.react.bridge.Promise -import com.facebook.react.bridge.ReactApplicationContext -import com.facebook.react.bridge.ReactMethod -import com.facebook.react.bridge.ReadableMap -import com.reactnativecompressor.Audio.AudioMain -import com.reactnativecompressor.Image.ImageMain -import com.reactnativecompressor.Utils.CreateVideoThumbnailClass -import com.reactnativecompressor.Utils.Downloader -import com.reactnativecompressor.Utils.EventEmitterHandler -import com.reactnativecompressor.Utils.Uploader -import com.reactnativecompressor.Utils.Utils -import com.reactnativecompressor.Utils.Utils.generateCacheFilePath -import com.reactnativecompressor.Utils.Utils.getRealPath -import com.reactnativecompressor.Video.VideoMain - -class CompressorModule(private val reactContext: ReactApplicationContext) : CompressorSpec(reactContext) { - private val imageMain: ImageMain = ImageMain(reactContext) - private val videoMain: VideoMain = VideoMain(reactContext) - private val audioMain: AudioMain = AudioMain(reactContext) - private val uploader: Uploader = Uploader(reactContext) - private val videoThumbnail: CreateVideoThumbnailClass = CreateVideoThumbnailClass(reactContext) - - override fun initialize() { - super.initialize() - EventEmitterHandler.reactContext=reactContext; - } - - - companion object { - const val NAME = "Compressor" - } - override fun getName(): String { - return NAME - } - - //Image - @ReactMethod - override fun image_compress( - imagePath: String, - optionMap: ReadableMap, - promise: Promise) { - imageMain.image_compress(imagePath,optionMap,promise) - } - - @ReactMethod - override fun getImageMetaData(filePath: String, promise: Promise) { - imageMain.getImageMetaData(filePath,promise) - } - - // VIdeo - @ReactMethod - override fun compress( - fileUrl: String, - optionMap: ReadableMap, - promise: Promise) { - videoMain.compress(fileUrl,optionMap,promise) - } - - @ReactMethod - override fun cancelCompression( - uuid: String) { - videoMain.cancelCompression(uuid) - } - - @ReactMethod - override fun activateBackgroundTask( - options: ReadableMap, - promise: Promise) { - videoMain.activateBackgroundTask(options,promise) - } - - @ReactMethod - override fun deactivateBackgroundTask( - options: ReadableMap, - promise: Promise) { - videoMain.deactivateBackgroundTask(options,promise) - } - - // Audio - @ReactMethod - @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2) - override fun compress_audio( - fileUrl: String, - optionMap: ReadableMap, - promise: Promise) { - audioMain.compress_audio(fileUrl,optionMap,promise) - } - - // Others - @ReactMethod - override fun generateFilePath(_extension: String, promise: Promise) { - try { - val outputUri = generateCacheFilePath(_extension, reactContext) - promise.resolve(outputUri) - } catch (e: Exception) { - promise.reject(e) - } - } - - @ReactMethod - override fun getRealPath(path: String, type: String, promise: Promise) { - try { - val realPath = getRealPath(path, reactContext) - promise.resolve("file://$realPath") - } catch (e: Exception) { - promise.reject(e) - } - } - - @ReactMethod - override fun getVideoMetaData(filePath: String, promise: Promise) { - videoMain.getVideoMetaData(filePath,promise) - } - - @ReactMethod - override fun getFileSize(filePath: String, promise: Promise) { - Utils.getFileSize(filePath,promise,reactContext) - } - - @ReactMethod - override fun upload( - fileUrl: String, - options: ReadableMap, - promise: Promise) { - uploader.upload(fileUrl, options, reactContext, promise) - } - - @ReactMethod - override fun cancelUpload(uuid: String,shouldCancelAll:Boolean) { - uploader.cancelUpload(uuid,shouldCancelAll) - } - - @ReactMethod - override fun download( - fileUrl: String, - options: ReadableMap, - promise: Promise) { - var uuid: String = "" - var progressDivider=0; - if(options.hasKey("uuid")) - { - uuid= options.getString("uuid") as String - } - if(options.hasKey("progressDivider")) - { - progressDivider= options.getString("progressDivider") as Int - } - val downloadedFilePath:String?=Downloader.downloadMediaWithProgress(fileUrl, uuid,progressDivider,reactContext) - if(downloadedFilePath!=null) - { - promise.resolve(downloadedFilePath) - } - else - { - promise.reject("Unable to download") - } - } - - @ReactMethod - override fun createVideoThumbnail(fileUrl:String, options:ReadableMap, promise:Promise) { - videoThumbnail.create(fileUrl,options,promise) - } - - @ReactMethod - override fun clearCache(cacheDir:String?, promise:Promise) { - CreateVideoThumbnailClass.clearCache(cacheDir, promise, reactContext) - } - - @ReactMethod - override fun addListener(eventName: String) { - } - - @ReactMethod - override fun removeListeners(count: Double) { - } -} diff --git a/android/src/main/java/com/reactnativecompressor/CompressorPackage.kt b/android/src/main/java/com/reactnativecompressor/CompressorPackage.kt deleted file mode 100644 index 85f08ef8..00000000 --- a/android/src/main/java/com/reactnativecompressor/CompressorPackage.kt +++ /dev/null @@ -1,34 +0,0 @@ -package com.reactnativecompressor - -import com.facebook.react.TurboReactPackage -import com.facebook.react.bridge.NativeModule -import com.facebook.react.bridge.ReactApplicationContext -import com.facebook.react.module.model.ReactModuleInfo -import com.facebook.react.module.model.ReactModuleInfoProvider - -class CompressorPackage : TurboReactPackage() { - override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? { - return if (name == CompressorModule.NAME) { - CompressorModule(reactContext) - } else { - null - } - } - - override fun getReactModuleInfoProvider(): ReactModuleInfoProvider { - return ReactModuleInfoProvider { - val moduleInfos: MutableMap = HashMap() - val isTurboModule = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED - moduleInfos[CompressorModule.NAME] = ReactModuleInfo( - CompressorModule.NAME, - CompressorModule.NAME, - false, // canOverrideExistingModule - false, // needsEagerInit - true, // hasConstants - false, // isCxxModule - isTurboModule // isTurboModule - ) - moduleInfos - } - } -} diff --git a/android/src/main/java/com/reactnativecompressor/NitroCompressorPackage.kt b/android/src/main/java/com/reactnativecompressor/NitroCompressorPackage.kt new file mode 100644 index 00000000..07973fb1 --- /dev/null +++ b/android/src/main/java/com/reactnativecompressor/NitroCompressorPackage.kt @@ -0,0 +1,30 @@ +package com.reactnativecompressor + +import com.facebook.react.BaseReactPackage +import com.facebook.react.bridge.NativeModule +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.module.model.ReactModuleInfoProvider +import com.margelo.nitro.compressor.NitroCompressorOnLoad + +/** + * Empty React package whose sole jobs are: + * 1. Make this library discoverable by React Native autolinking (which keys off a + * `ReactPackage`), so the Gradle project + native `.so` get wired into the app. + * 2. Load the Nitro C++ library (`libNitroCompressor.so`) on first class-load so the + * `Compressor` HybridObject is registered. The module itself is served by Nitro, + * not by `getModule`, so no native modules are returned here. + * + * Lives in the `com.reactnativecompressor` namespace (not `com.margelo.nitro.compressor`) + * because the React Native CLI derives the autolink import path from the Android namespace. + */ +class NitroCompressorPackage : BaseReactPackage() { + override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? = null + + override fun getReactModuleInfoProvider(): ReactModuleInfoProvider = ReactModuleInfoProvider { HashMap() } + + companion object { + init { + NitroCompressorOnLoad.initializeNative() + } + } +} diff --git a/android/src/main/java/com/reactnativecompressor/NitroPromiseAdapter.kt b/android/src/main/java/com/reactnativecompressor/NitroPromiseAdapter.kt new file mode 100644 index 00000000..a6c142a9 --- /dev/null +++ b/android/src/main/java/com/reactnativecompressor/NitroPromiseAdapter.kt @@ -0,0 +1,55 @@ +package com.reactnativecompressor + +import com.facebook.react.bridge.Promise +import com.facebook.react.bridge.WritableMap +import java.util.concurrent.atomic.AtomicBoolean +import com.margelo.nitro.core.Promise as NitroPromise + +/** + * Adapts the React Native bridge [Promise] to a Nitro [NitroPromise] so the existing + * domain methods — which speak the bridge `Promise` contract (`resolve`/`reject`) — can + * drive a Nitro Promise unchanged. + * + * [convert] maps the resolved bridge value to the Nitro result type [T]; + * [onSettle] runs once on resolve/reject (used to unregister progress callbacks). + */ +class NitroPromiseAdapter( + private val promise: NitroPromise, + private val convert: (Any?) -> T, + private val onSettle: () -> Unit = {}, +) : Promise { + private val settled = AtomicBoolean(false) + + override fun resolve(value: Any?) { + if (!settled.compareAndSet(false, true)) return + onSettle() + promise.resolve(convert(value)) + } + + private fun rejectInternal(message: String?, throwable: Throwable?) { + if (!settled.compareAndSet(false, true)) return + onSettle() + promise.reject(throwable ?: Throwable(message ?: "react-native-compressor error")) + } + + override fun reject(code: String?, message: String?) = rejectInternal(message ?: code, null) + + override fun reject(code: String?, throwable: Throwable?) = rejectInternal(code, throwable) + + override fun reject(code: String?, message: String?, throwable: Throwable?) = rejectInternal(message ?: code, throwable) + + override fun reject(throwable: Throwable) = rejectInternal(throwable.message, throwable) + + override fun reject(throwable: Throwable, userInfo: WritableMap) = rejectInternal(throwable.message, throwable) + + override fun reject(code: String?, userInfo: WritableMap) = rejectInternal(code, null) + + override fun reject(code: String?, throwable: Throwable?, userInfo: WritableMap) = rejectInternal(code, throwable) + + override fun reject(code: String?, message: String?, userInfo: WritableMap) = rejectInternal(message ?: code, null) + + override fun reject(code: String?, message: String?, throwable: Throwable?, userInfo: WritableMap?) = rejectInternal(message ?: code, throwable) + + @Deprecated("Prefer passing a module-specific error code to JS.", ReplaceWith("reject(code, message)")) + override fun reject(message: String) = rejectInternal(message, null) +} diff --git a/android/src/main/java/com/reactnativecompressor/Utils/EventEmitterHandler.kt b/android/src/main/java/com/reactnativecompressor/Utils/EventEmitterHandler.kt index e093aa1e..27ae0982 100644 --- a/android/src/main/java/com/reactnativecompressor/Utils/EventEmitterHandler.kt +++ b/android/src/main/java/com/reactnativecompressor/Utils/EventEmitterHandler.kt @@ -1,66 +1,70 @@ package com.reactnativecompressor.Utils -import com.facebook.react.bridge.Arguments -import com.facebook.react.bridge.ReactApplicationContext -import com.facebook.react.modules.core.DeviceEventManagerModule +import java.util.concurrent.ConcurrentHashMap +/** + * Routes native progress emissions to the per-call JS callbacks that + * `HybridCompressor` registers. This replaces the old `RCTDeviceEventEmitter` + * bridge: Nitro delivers progress through callback parameters, so the domain + * layer's `emit*` calls (unchanged) are dispatched to the callback registered + * under the same `uuid` that the JS layer threads through the options map. + */ class EventEmitterHandler { companion object { - public var reactContext: ReactApplicationContext?=null + private val videoCompressProgressCallbacks = ConcurrentHashMap Unit>() + private val downloadProgressCallbacks = ConcurrentHashMap Unit>() + private val uploadProgressCallbacks = ConcurrentHashMap Unit>() - private fun sendEvent(eventName: String, - params: Any?) { - reactContext - ?.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) - ?.emit(eventName, params) + @Volatile + private var backgroundTaskExpiredCallback: (() -> Unit)? = null + + // Registration (called by HybridCompressor) + + fun registerVideoCompressProgress(uuid: String, callback: ((Double) -> Unit)?) { + if (callback != null) videoCompressProgressCallbacks[uuid] = callback } - fun emitBackgroundTaskExpired(backgroundId: String?){ - sendEvent("backgroundTaskExpired",backgroundId) + fun registerDownloadProgress(uuid: String, callback: ((Double) -> Unit)?) { + if (callback != null) downloadProgressCallbacks[uuid] = callback } - fun emitVideoCompressProgress(progress:Double,uuid:String){ - val params = Arguments.createMap() - val data = Arguments.createMap() - params.putString("uuid", uuid) - data.putDouble("progress", progress) - params.putMap("data", data) - sendEvent("videoCompressProgress", params) + fun registerUploadProgress(uuid: String, callback: ((Double, Double) -> Unit)?) { + if (callback != null) uploadProgressCallbacks[uuid] = callback } - fun emitDownloadProgress(progress:Double,uuid:String){ - val params = Arguments.createMap() - val data = Arguments.createMap() - params.putString("uuid", uuid) - data.putDouble("progress", progress) - params.putMap("data", data) - sendEvent("downloadProgress", params) + fun unregister(uuid: String) { + videoCompressProgressCallbacks.remove(uuid) + downloadProgressCallbacks.remove(uuid) + uploadProgressCallbacks.remove(uuid) } - fun emitDownloadProgressError(uuid:String?, error:String?){ - if (uuid != null && error != null) { - val params = Arguments.createMap() - val data = Arguments.createMap() - params.putString("uuid", uuid) - params.putString("error", error) - params.putMap("data", data) - sendEvent("downloadProgressError", params) - } + fun setBackgroundTaskExpiredCallback(callback: (() -> Unit)?) { + backgroundTaskExpiredCallback = callback } - fun sendUploadProgressEvent(numBytes: Long, totalBytes: Long, uuid:String?) { - if(uuid!=null) { - val _params = Arguments.createMap() - val _data = Arguments.createMap() - _params.putString("uuid", uuid) - _data.putDouble("written", numBytes.toDouble()) - _data.putDouble("total", totalBytes.toDouble()) - _params.putMap("data", _data) - sendEvent("uploadProgress", _params) - } + // Emission (called by the domain layer — method names preserved) + + fun emitBackgroundTaskExpired(backgroundId: String?) { + backgroundTaskExpiredCallback?.invoke() } + fun emitVideoCompressProgress(progress: Double, uuid: String) { + videoCompressProgressCallbacks[uuid]?.invoke(progress) + } - } + fun emitDownloadProgress(progress: Double, uuid: String) { + downloadProgressCallbacks[uuid]?.invoke(progress) + } + + fun emitDownloadProgressError(uuid: String?, error: String?) { + // No JS consumer for `downloadProgressError`; download failures surface + // through the rejected Promise instead. Kept so domain call sites compile. + } + fun sendUploadProgressEvent(numBytes: Long, totalBytes: Long, uuid: String?) { + if (uuid != null) { + uploadProgressCallbacks[uuid]?.invoke(numBytes.toDouble(), totalBytes.toDouble()) + } + } + } } diff --git a/android/src/main/java/com/reactnativecompressor/Utils/Utils.kt b/android/src/main/java/com/reactnativecompressor/Utils/Utils.kt index 2c7c1fde..db130931 100644 --- a/android/src/main/java/com/reactnativecompressor/Utils/Utils.kt +++ b/android/src/main/java/com/reactnativecompressor/Utils/Utils.kt @@ -134,7 +134,7 @@ object Utils { if (fileSize >= 0) { promise.resolve(fileSize.toString()) } else { - promise.resolve("") + promise.reject("FILE_SIZE_ERROR", "Failed to get file size for path: $filePath") } } } diff --git a/android/src/main/java/com/reactnativecompressor/Video/VideoCompressorHelper.kt b/android/src/main/java/com/reactnativecompressor/Video/VideoCompressorHelper.kt index 6669a27b..d4c46299 100644 --- a/android/src/main/java/com/reactnativecompressor/Video/VideoCompressorHelper.kt +++ b/android/src/main/java/com/reactnativecompressor/Video/VideoCompressorHelper.kt @@ -5,6 +5,7 @@ import android.content.Context import android.media.MediaMetadataRetriever import android.net.Uri import android.os.Handler +import android.os.Looper import android.os.PowerManager import android.os.PowerManager.WakeLock import com.facebook.react.bridge.LifecycleEventListener @@ -58,7 +59,9 @@ class VideoCompressorHelper { if (!wakeLock!!.isHeld()) { wakeLock!!.acquire() } - handler = Handler() + // Bind to the main Looper: under Nitro this runs on a background executor + // thread (no Looper), so the no-arg Handler() constructor would throw. + handler = Handler(Looper.getMainLooper()) runnable = Runnable { } handler!!.post(runnable!!) return "" diff --git a/android/src/newarch/CompressorSpec.kt b/android/src/newarch/CompressorSpec.kt deleted file mode 100644 index a9570499..00000000 --- a/android/src/newarch/CompressorSpec.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.reactnativecompressor - -import com.facebook.react.bridge.ReactApplicationContext - -abstract class CompressorSpec(context: ReactApplicationContext?) : NativeCompressorSpec(context) diff --git a/android/src/oldarch/CompressorSpec.kt b/android/src/oldarch/CompressorSpec.kt deleted file mode 100644 index b0fa83d2..00000000 --- a/android/src/oldarch/CompressorSpec.kt +++ /dev/null @@ -1,36 +0,0 @@ -package com.reactnativecompressor - -import com.facebook.react.bridge.Promise -import com.facebook.react.bridge.ReactApplicationContext -import com.facebook.react.bridge.ReactContextBaseJavaModule -import com.facebook.react.bridge.ReadableMap - -abstract class CompressorSpec(context: ReactApplicationContext?) : ReactContextBaseJavaModule(context) { - abstract fun image_compress( - imagePath: String, - optionMap: ReadableMap, - promise: Promise) - - abstract fun compress_audio( - fileUrl: String, - optionMap: ReadableMap, - promise: Promise) - - abstract fun generateFilePath(_extension: String, promise: Promise) - abstract fun getRealPath(path: String, type: String, promise: Promise) - abstract fun getVideoMetaData(filePath: String, promise: Promise) - abstract fun getImageMetaData(filePath: String, promise: Promise); - abstract fun getFileSize(filePath: String, promise: Promise) - abstract fun compress(fileUrl: String, optionMap: ReadableMap, promise: Promise) - abstract fun cancelCompression(uuid: String) - abstract fun upload(fileUrl: String, options: ReadableMap, promise: Promise) - abstract fun cancelUpload(uuid: String, shouldCancelAll:Boolean) - - abstract fun download(fileUrl: String, options: ReadableMap, promise: Promise) - abstract fun activateBackgroundTask(options: ReadableMap, promise: Promise) - abstract fun deactivateBackgroundTask(options: ReadableMap, promise: Promise) - abstract fun createVideoThumbnail(fileUrl: String, options: ReadableMap, promise: Promise) - abstract fun clearCache(cacheDir: String?, promise: Promise) - abstract fun addListener(eventName: String) - abstract fun removeListeners(count: Double) -} diff --git a/examples/bare/.ruby-version b/examples/bare/.ruby-version new file mode 100644 index 00000000..0aec50e6 --- /dev/null +++ b/examples/bare/.ruby-version @@ -0,0 +1 @@ +3.1.4 diff --git a/examples/bare/android/app/build.gradle b/examples/bare/android/app/build.gradle index e134059e..f84330d1 100644 --- a/examples/bare/android/app/build.gradle +++ b/examples/bare/android/app/build.gradle @@ -105,6 +105,15 @@ android { proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" } } + + lint { + // The example app merges manifests from third-party deps (react-native-video's + // VideoPlaybackService, media/camera permissions from the image & video pickers), + // which trip these checks. They are not our library's code; silence just these so + // the "Validate Android" lint job isn't blocked by dependency manifests. + disable.add("NotificationPermission") + disable.add("PermissionImpliesUnsupportedChromeOsHardware") + } } dependencies { diff --git a/examples/bare/android/settings.gradle b/examples/bare/android/settings.gradle index e2448249..d004245c 100644 --- a/examples/bare/android/settings.gradle +++ b/examples/bare/android/settings.gradle @@ -4,3 +4,15 @@ extensions.configure(com.facebook.react.ReactSettingsExtension){ ex -> ex.autoli rootProject.name = 'BareExample' include ':app' includeBuild('../node_modules/@react-native/gradle-plugin') + +// The "Validate Android" CI job runs `./gradlew lint`, which lints every +// autolinked subproject — including third-party deps. react-native-video trips +// the `WrongThread` check (ReactExoplayerView.getVideoTrackInfoFromManifest), +// which is not our code and aborts the lint build. Disable just that one check +// on Android library subprojects. Registered here (before any project is +// evaluated) so AGP hasn't locked the lint DSL by the time we add to it. +gradle.beforeProject { project -> + project.plugins.withId("com.android.library") { + project.android.lint.disable.add("WrongThread") + } +} diff --git a/examples/bare/ios/Podfile.lock b/examples/bare/ios/Podfile.lock index 5c762697..d3fe82ea 100644 --- a/examples/bare/ios/Podfile.lock +++ b/examples/bare/ios/Podfile.lock @@ -3,6 +3,29 @@ PODS: - hermes-engine (250829098.0.10): - hermes-engine/Pre-built (= 250829098.0.10) - hermes-engine/Pre-built (250829098.0.10) + - NitroModules (0.35.9): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-callinvoker + - React-Core + - React-Core-prebuilt + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga - RCTDeprecation (0.85.2) - RCTRequired (0.85.2) - RCTSwiftUI (0.85.2) @@ -1384,8 +1407,9 @@ PODS: - ReactCommon/turbomodule/core - ReactNativeDependencies - Yoga - - react-native-compressor (1.18.1): + - react-native-compressor (1.19.0): - hermes-engine + - NitroModules - RCTRequired - RCTTypeSafety - React-Core @@ -2280,6 +2304,7 @@ PODS: DEPENDENCIES: - FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`) - hermes-engine (from `../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec`) + - NitroModules (from `../node_modules/react-native-nitro-modules`) - RCTDeprecation (from `../node_modules/react-native/ReactApple/Libraries/RCTFoundation/RCTDeprecation`) - RCTRequired (from `../node_modules/react-native/Libraries/Required`) - RCTSwiftUI (from `../node_modules/react-native/ReactApple/RCTSwiftUI`) @@ -2372,6 +2397,8 @@ EXTERNAL SOURCES: hermes-engine: :podspec: "../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec" :tag: hermes-v250829098.0.10 + NitroModules: + :path: "../node_modules/react-native-nitro-modules" RCTDeprecation: :path: "../node_modules/react-native/ReactApple/Libraries/RCTFoundation/RCTDeprecation" RCTRequired: @@ -2544,6 +2571,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: FBLazyVector: 26fd21c75314e101f280d401e97f27d54f3f7064 hermes-engine: eaa65d42895b52c6d680c0aebfccfa50baccce3d + NitroModules: 16bc17a076b12304d608f7c915b9d321f56dfc19 RCTDeprecation: c7a2768f905d76ca6d2cfefb26e4349eacbdfca3 RCTRequired: 5e502c3553cfbed090a991c444448da452fb752e RCTSwiftUI: 5ce3ccbdc58b78cc4ebbaace01709ec22d58e131 @@ -2581,7 +2609,7 @@ SPEC CHECKSUMS: React-Mapbuffer: 1fc10d873f00aa895836c316a9737ce7d43875ce React-microtasksnativemodule: 85ac7286ff84e8fc8e1956233748b8d4b2a6dcea react-native-cameraroll: 4d4fcff8a057235ce7a9f57d4566409207216a86 - react-native-compressor: baa9ad85164b535ecbb2a930e89fb8172cd19575 + react-native-compressor: de8424f653a4d0ff70ebf0a74dc718a277c7b103 react-native-document-picker: 15cca2d1a6bfb6d0d3ff0283bd9b903e57cb028b react-native-get-random-values: 68792987aef40e8aa72dc448d97e009d1f440c88 react-native-image-picker: 23540feacc79c63c60857f318fdfa8477c26e70a diff --git a/examples/bare/package.json b/examples/bare/package.json index de94642b..27c45019 100644 --- a/examples/bare/package.json +++ b/examples/bare/package.json @@ -3,10 +3,10 @@ "version": "0.0.1", "private": true, "scripts": { - "android": "react-native run-android", + "android": "react-native run-android --port 8082", "ios": "react-native run-ios --simulator \"${IOS_SIMULATOR:-iPhone 17 Pro}\"", "lint": "eslint .", - "start": "react-native start", + "start": "react-native start --port 8082", "test": "jest" }, "dependencies": { @@ -22,6 +22,7 @@ "react-native": "0.85.2", "react-native-get-random-values": "^2.0.0", "react-native-image-picker": "^8.2.1", + "react-native-nitro-modules": "^0.35.9", "react-native-progress": "^5.0.1", "react-native-reanimated": "^4.3.0", "react-native-safe-area-context": "^5.7.0", diff --git a/examples/bare/rn-harness.config.mjs b/examples/bare/rn-harness.config.mjs index 4abc7889..cb713a4d 100644 --- a/examples/bare/rn-harness.config.mjs +++ b/examples/bare/rn-harness.config.mjs @@ -1,7 +1,7 @@ import { androidEmulator, androidPlatform } from '@react-native-harness/platform-android'; import { applePlatform, appleSimulator } from '@react-native-harness/platform-apple'; -const androidDevice = process.env.RN_HARNESS_ANDROID_DEVICE ?? 'Pixel_8_API_35'; +const androidDevice = process.env.RN_HARNESS_ANDROID_DEVICE ?? 'Pixel_9a'; const iosDevice = process.env.RN_HARNESS_IOS_DEVICE ?? 'iPhone 17 Pro'; const iosVersion = process.env.RN_HARNESS_IOS_VERSION ?? '26.4'; diff --git a/examples/bare/src/Screens/Video/index.tsx b/examples/bare/src/Screens/Video/index.tsx index 10fab2e9..2c5f75c2 100644 --- a/examples/bare/src/Screens/Video/index.tsx +++ b/examples/bare/src/Screens/Video/index.tsx @@ -48,7 +48,7 @@ export default function App() { (async () => { const detail: any = await getFileInfo(sourceVideo); setSourceSize(prettyBytes(parseInt(detail.size, 10))); - })(); + })().catch((error) => console.log({ sourceSizeError: error })); }, [sourceVideo]); useEffect(() => { diff --git a/examples/bare/src/Utils/index.tsx b/examples/bare/src/Utils/index.tsx index cc224d86..2702b25f 100644 --- a/examples/bare/src/Utils/index.tsx +++ b/examples/bare/src/Utils/index.tsx @@ -12,4 +12,15 @@ export const getFullFilename = (path: string | null) => { return ''; }; -export const getFileInfo = stat; +// RNFS `stat` expects a plain filesystem path. Picker URIs arrive as +// percent-encoded `file://` URLs (e.g. spaces become %20), which stat can't +// resolve. Strip the scheme and decode before delegating. +export const getFileInfo = (path: string) => { + let _path = path.startsWith('file://') ? path.replace('file://', '') : path; + try { + _path = decodeURIComponent(_path); + } catch { + // leave path as-is if it isn't valid percent-encoding + } + return stat(_path); +}; diff --git a/examples/expo/package.json b/examples/expo/package.json index 0565366b..576f977d 100644 --- a/examples/expo/package.json +++ b/examples/expo/package.json @@ -24,6 +24,7 @@ "react-dom": "19.1.0", "react-native": "0.81.5", "react-native-get-random-values": "^1.9.0", + "react-native-nitro-modules": "^0.35.9", "react-native-progress": "^5.0.0", "react-native-reanimated": "^4.3.0", "react-native-safe-area-context": "~5.6.0", diff --git a/harness/native-compressor.harness.ts b/harness/native-compressor.harness.ts index e4fddf02..4cd89a27 100644 --- a/harness/native-compressor.harness.ts +++ b/harness/native-compressor.harness.ts @@ -155,7 +155,9 @@ describe('react-native-compressor native harness', () => { expect(thumbnail.mime).toBe('image/jpeg'); expect(thumbnail.width).toBeGreaterThan(0); expect(thumbnail.height).toBeGreaterThan(0); - await expectFileOutput(`file://${thumbnail.path}`); + // iOS returns a raw path; Android returns a `file://`-prefixed path. Normalize so the + // existence check receives a single `file://` scheme on both platforms. + await expectFileOutput(thumbnail.path.startsWith('file://') ? thumbnail.path : `file://${thumbnail.path}`); await expect(clearCache('thumbnails/')).resolves.toBe('done'); }); @@ -169,10 +171,10 @@ describe('react-native-compressor native harness', () => { it('manages exported background task and cancellation APIs without crashing', async () => { const taskId = await Video.activateBackgroundTask(); - expect(taskId === null || typeof taskId === 'number').toBe(true); + expect(typeof taskId).toBe('string'); const deactivatedTask = await Video.deactivateBackgroundTask(); - expect(deactivatedTask == null).toBe(true); + expect(typeof deactivatedTask).toBe('string'); expect(() => Video.cancelCompression('missing-compression-id')).not.toThrow(); expect(() => cancelUpload('', true)).not.toThrow(); }); diff --git a/ios/Audio/FormatConverter/FormatConverter+Compressed.swift b/ios/Audio/FormatConverter/FormatConverter+Compressed.swift index a437963d..b0a0668e 100644 --- a/ios/Audio/FormatConverter/FormatConverter+Compressed.swift +++ b/ios/Audio/FormatConverter/FormatConverter+Compressed.swift @@ -5,7 +5,7 @@ import AVFoundation // MARK: - internal helper functions -public extension AVURLAsset { +extension AVURLAsset { /// Audio format for the file in the URL asset var audioFormat: AVAudioFormat? { // pull the input format out of the audio file... diff --git a/ios/Audio/FormatConverter/FormatConverter+Utilities.swift b/ios/Audio/FormatConverter/FormatConverter+Utilities.swift index 574aa6c6..47dd6c1d 100644 --- a/ios/Audio/FormatConverter/FormatConverter+Utilities.swift +++ b/ios/Audio/FormatConverter/FormatConverter+Utilities.swift @@ -11,7 +11,7 @@ extension FormatConverter { } } -public extension FormatConverter { +extension FormatConverter { /// Is this file a PCM file? /// - Parameters: /// - url: The URL to parse diff --git a/ios/Audio/FormatConverter/FormatConverter.swift b/ios/Audio/FormatConverter/FormatConverter.swift index 665ab423..df9a2330 100644 --- a/ios/Audio/FormatConverter/FormatConverter.swift +++ b/ios/Audio/FormatConverter/FormatConverter.swift @@ -19,7 +19,9 @@ import AVFoundation } ``` */ -public class FormatConverter { +// `internal` (not `public`): keeps these types out of the Swift↔C++ interop surface +// that Nitro enables module-wide, which otherwise fails to link the nested `Options` type. +class FormatConverter { // MARK: - properties /// The source audio file @@ -122,7 +124,7 @@ public class FormatConverter { // MARK: - Definitions -public enum AudioFileFormat: String { +enum AudioFileFormat: String { case aac case aif case aifc @@ -141,7 +143,7 @@ public enum AudioFileFormat: String { case wav } -public extension FormatConverter { +extension FormatConverter { /// FormatConverterCallback is the callback format for start() /// - Parameter: error This will contain one parameter of type Error which is nil if the conversion was successful. diff --git a/ios/Compressor-Bridging-Header.h b/ios/Compressor-Bridging-Header.h deleted file mode 100644 index b85cffd7..00000000 --- a/ios/Compressor-Bridging-Header.h +++ /dev/null @@ -1,6 +0,0 @@ -// -// Use this file to import your target's public headers that you would like to expose to Swift. -// -#import -#import -#import diff --git a/ios/Compressor.h b/ios/Compressor.h deleted file mode 100644 index ed02c9ba..00000000 --- a/ios/Compressor.h +++ /dev/null @@ -1,5 +0,0 @@ -#import - -#ifdef RCT_NEW_ARCH_ENABLED -#import "RNCompressorSpec.h" -#endif diff --git a/ios/Compressor.mm b/ios/Compressor.mm deleted file mode 100644 index 38e63b74..00000000 --- a/ios/Compressor.mm +++ /dev/null @@ -1,82 +0,0 @@ -#import "Compressor.h" - -@interface RCT_EXTERN_MODULE(Compressor, RCTEventEmitter) - -RCT_EXTERN_METHOD(image_compress: (NSString*) imagePath - withOptions: (NSDictionary*) optionMap - withResolver: (RCTPromiseResolveBlock) resolve - withRejecter: (RCTPromiseRejectBlock) reject) - -RCT_EXTERN_METHOD(getImageMetaData: (NSString*) filePath - withResolver: (RCTPromiseResolveBlock) resolve - withRejecter: (RCTPromiseRejectBlock) reject) - -RCT_EXTERN_METHOD(compress_audio: (NSString*) filePath - withOptions: (NSDictionary*) optionsDict - withResolver: (RCTPromiseResolveBlock) resolve - withRejecter: (RCTPromiseRejectBlock) reject) - -RCT_EXTERN_METHOD(generateFilePath: (NSString*) _extension - withResolver: (RCTPromiseResolveBlock) resolve - withRejecter: (RCTPromiseRejectBlock) reject) - -RCT_EXTERN_METHOD(getRealPath: (NSString*) path - withType: (NSString*) type - withResolver: (RCTPromiseResolveBlock) resolve - withRejecter: (RCTPromiseRejectBlock) reject) - -RCT_EXTERN_METHOD(getFileSize: (NSString*) filePath - withResolver: (RCTPromiseResolveBlock) resolve - withRejecter: (RCTPromiseRejectBlock) reject) - -RCT_EXTERN_METHOD(getVideoMetaData: (NSString*) filePath - withResolver: (RCTPromiseResolveBlock) resolve - withRejecter: (RCTPromiseRejectBlock) reject) - -RCT_EXTERN_METHOD(compress:(NSString *)fileUrl - withOptions:(NSDictionary *)options - withResolver:(RCTPromiseResolveBlock)resolve - withRejecter:(RCTPromiseRejectBlock)reject) - -RCT_EXTERN_METHOD(upload:(NSString *)fileUrl - withOptions:(NSDictionary *)options - withResolver:(RCTPromiseResolveBlock)resolve - withRejecter:(RCTPromiseRejectBlock)reject) - -RCT_EXTERN_METHOD(cancelUpload:(NSString *)uuid - withShouldCancelAll:(BOOL*)shouldCancelAll) - -RCT_EXTERN_METHOD(download:(NSString *)fileUrlu - withOptions:(NSDictionary *)options - withResolver:(RCTPromiseResolveBlock)resolve - withRejecter:(RCTPromiseRejectBlock)reject) - -RCT_EXTERN_METHOD(activateBackgroundTask: (NSDictionary *)options - withResolver:(RCTPromiseResolveBlock)resolve - withRejecter:(RCTPromiseRejectBlock)reject) - -RCT_EXTERN_METHOD(deactivateBackgroundTask: (NSDictionary *)options - withResolver:(RCTPromiseResolveBlock)resolve - withRejecter:(RCTPromiseRejectBlock)reject) - -RCT_EXTERN_METHOD(createVideoThumbnail:(NSString *)fileUrl - withOptions:(NSDictionary *)options - withResolver:(RCTPromiseResolveBlock)resolve - withRejecter:(RCTPromiseRejectBlock)reject) - -RCT_EXTERN_METHOD(clearCache:(NSString *)cacheDir - withResolver:(RCTPromiseResolveBlock)resolve - withRejecter:(RCTPromiseRejectBlock)reject) - -RCT_EXTERN_METHOD(cancelCompression:(NSString *)uuid) - -// Don't compile this code when we build for the old architecture. -#ifdef RCT_NEW_ARCH_ENABLED -- (std::shared_ptr)getTurboModule: - (const facebook::react::ObjCTurboModule::InitParams &)params -{ - return std::make_shared(params); -} -#endif - -@end diff --git a/ios/CompressorManager.swift b/ios/CompressorManager.swift deleted file mode 100644 index 9d4fb6dc..00000000 --- a/ios/CompressorManager.swift +++ /dev/null @@ -1,117 +0,0 @@ -import Foundation -import AVFoundation - -let videoCompressor = VideoCompressor() -let uploader=Uploader() -@objc(Compressor) -class Compressor: RCTEventEmitter { - override static func moduleName() -> String { - return "Compressor" - } - - override init() { - super.init() - EventEmitterHandler.initCompressorInstance(self) - } - - override func stopObserving() -> Void { - EventEmitterHandler.stopObserving() - } - - override func startObserving() -> Void { - EventEmitterHandler.startObserving() - } - - override static func requiresMainQueueSetup() -> Bool { - return false - } - - override func supportedEvents() -> [String] { - return ["downloadProgress", "videoCompressProgress", "uploadProgress", "backgroundTaskExpired"] - } - - @objc(image_compress:withOptions:withResolver:withRejecter:) - func image_compress(_ imagePath: String, optionMap: NSDictionary, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { - ImageMain.image_compress(imagePath, optionMap: optionMap, resolve: resolve, reject: reject) - } - - @objc(getImageMetaData:withResolver:withRejecter:) - func getImageMetaData(_ filePath: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { - ImageMain.getImageMetaData(filePath,resolve: resolve,reject: reject) - } - - @objc(compress_audio:withOptions:withResolver:withRejecter:) - func compress_audio(_ fileUrl: String, optionMap: NSDictionary, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { - AudioMain.compress_audio(fileUrl, optionMap: optionMap, resolve: resolve, reject: reject) - } - - @objc(generateFilePath:withResolver:withRejecter:) - func generateFilePath(_ _extension: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { - Utils.generateFilePath(_extension, resolve: resolve, reject: reject) - } - - @objc(getRealPath:withType:withResolver:withRejecter:) - func getRealPath(_ path: String, type: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { - Utils.getRealPath(path, type: type, resolve: resolve, reject: reject) - } - - @objc(getFileSize:withResolver:withRejecter:) - func getFileSize(_ filePath: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { - Utils.getFileSize(filePath, resolve: resolve, reject: reject) - } - - @objc(getVideoMetaData:withResolver:withRejecter:) - func getVideoMetaData(_ filePath: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { - videoCompressor.getVideoMetaData(filePath,resolve: resolve,reject: reject) - } - - - @objc(activateBackgroundTask:withResolver:withRejecter:) - func activateBackgroundTask(options: [String: Any], resolve:@escaping RCTPromiseResolveBlock, reject:@escaping RCTPromiseRejectBlock) -> Void { - videoCompressor.activateBackgroundTask(options: options,resolve: resolve,reject: reject) - } - - @objc(deactivateBackgroundTask:withResolver:withRejecter:) - func deactivateBackgroundTask(options: [String: Any], resolve:@escaping RCTPromiseResolveBlock, reject:@escaping RCTPromiseRejectBlock) -> Void { - videoCompressor.deactivateBackgroundTask(options: options,resolve: resolve,reject: reject) - } - - @objc(compress:withOptions:withResolver:withRejecter:) - func compress(fileUrl: String, options: [String: Any], resolve:@escaping RCTPromiseResolveBlock, reject:@escaping RCTPromiseRejectBlock) -> Void { - videoCompressor.compress(fileUrl: fileUrl, options: options, resolve: resolve, reject: reject) - } - - @objc(cancelCompression:) - func cancelCompression(uuid: String) -> Void { - videoCompressor.cancelCompression(uuid: uuid) - } - - @objc(upload:withOptions:withResolver:withRejecter:) - func upload(filePath: String, options: [String: Any], resolve:@escaping RCTPromiseResolveBlock, reject:@escaping RCTPromiseRejectBlock) -> Void { - uploader.upload(filePath: filePath, options: options, resolve: resolve, reject: reject) - } - - @objc(cancelUpload:withShouldCancelAll:) - func cancelUpload(uuid: String,shouldCancelAll:Bool) -> Void { - uploader.cancelUpload(uuid: uuid,shouldCancelAll: shouldCancelAll) - } - - @objc(download:withOptions:withResolver:withRejecter:) - func download(filePath: String, options: [String: Any], resolve:@escaping RCTPromiseResolveBlock, reject:@escaping RCTPromiseRejectBlock) -> Void { - Downloader.downloadFileAndSaveToCache(filePath, uuid: options["uuid"] as! String,progressDivider: options["progressDivider"] as? Int ?? 0) { downloadedPath in - resolve(downloadedPath) - } - } - - @objc(createVideoThumbnail:withOptions:withResolver:withRejecter:) - func createVideoThumbnail(fileUrl: String, options: NSDictionary, resolve:@escaping RCTPromiseResolveBlock, reject:@escaping RCTPromiseRejectBlock) -> Void { - let videoThumbnail=CreateVideoThumbnail() - videoThumbnail.create(fileUrl,options: options, resolve: resolve, rejecter: reject) - } - - @objc(clearCache:withResolver:withRejecter:) - func clearCache(cacheDir: String, resolve:@escaping RCTPromiseResolveBlock, reject:@escaping RCTPromiseRejectBlock) -> Void { - CreateVideoThumbnail.cleanCacheDir(cacheDir: cacheDir,resolve: resolve,rejecter: reject) - } - -} diff --git a/ios/HybridCompressor.swift b/ios/HybridCompressor.swift new file mode 100644 index 00000000..d76ed7f2 --- /dev/null +++ b/ios/HybridCompressor.swift @@ -0,0 +1,247 @@ +// +// HybridCompressor.swift +// react-native-compressor +// +// Nitro HybridObject implementation of the single `Compressor` native module. +// It is a thin binding layer: it converts Nitro's `AnyMap` options into the +// `NSDictionary`/`[String: Any]` the existing domain code already consumes, +// bridges the Nitro `Promise` to the domain layer's `RCTPromiseResolveBlock`/ +// `RCTPromiseRejectBlock`, and registers progress callbacks (keyed by `uuid`) +// with `EventEmitterHandler`. All heavy logic stays in the domain classes. +// + +import Foundation +import NitroModules + +// Previously imported from React. We define them locally so this Swift module no +// longer needs to `import React`: under Nitro's Swift↔C++ interop, importing +// React pulls move-only C++ types (e.g. jsinspector's RuntimeSamplingProfile) +// into Swift's importer and fails to compile. These blocks are created here and +// consumed by the domain layer entirely in Swift, so plain closures suffice. +typealias RCTPromiseResolveBlock = (Any?) -> Void +typealias RCTPromiseRejectBlock = (String?, String?, (any Error)?) -> Void + +private let videoCompressor = VideoCompressor() +private let uploader = Uploader() + +final class HybridCompressor: HybridCompressorSpec { + // MARK: - Image + + func image_compress(imagePath: String, optionMap: AnyMap, onDownloadProgress: ((Double) -> Void)?) throws -> Promise { + let promise = Promise() + var options = dictionary(from: optionMap) + // Remote-image download progress is keyed by `uuid` inside the Downloader. The JS + // layer no longer sends one (the callback is passed directly), so mint one here. + let uuid = (options["uuid"] as? String) ?? UUID().uuidString + options["uuid"] = uuid + EventEmitterHandler.registerDownloadProgress(uuid: uuid, onDownloadProgress) + ImageMain.image_compress(imagePath, optionMap: options as NSDictionary, resolve: resolveString(promise, uuid), reject: reject(promise, uuid)) + return promise + } + + func getImageMetaData(filePath: String) throws -> Promise { + let promise = Promise() + ImageMain.getImageMetaData(filePath, resolve: resolveAnyMap(promise, nil), reject: reject(promise, nil)) + return promise + } + + // MARK: - Video + + func compress(fileUrl: String, optionMap: AnyMap, onProgress: ((Double) -> Void)?, onDownloadProgress: ((Double) -> Void)?) throws -> Promise { + let promise = Promise() + let options = dictionary(from: optionMap) + let uuid = (options["uuid"] as? String) ?? "" + EventEmitterHandler.registerVideoCompressProgress(uuid: uuid, onProgress) + EventEmitterHandler.registerDownloadProgress(uuid: uuid, onDownloadProgress) + videoCompressor.compress(fileUrl: fileUrl, options: options, resolve: resolveString(promise, uuid), reject: reject(promise, uuid)) + return promise + } + + func cancelCompression(uuid: String) throws { + videoCompressor.cancelCompression(uuid: uuid) + } + + func getVideoMetaData(filePath: String) throws -> Promise { + let promise = Promise() + videoCompressor.getVideoMetaData(filePath, resolve: resolveAnyMap(promise, nil), reject: reject(promise, nil)) + return promise + } + + func activateBackgroundTask(options: AnyMap, onExpired: (() -> Void)?) throws -> Promise { + let promise = Promise() + EventEmitterHandler.setBackgroundTaskExpiredCallback(onExpired) + videoCompressor.activateBackgroundTask(options: dictionary(from: options), resolve: resolveString(promise, nil), reject: reject(promise, nil)) + return promise + } + + func deactivateBackgroundTask(options: AnyMap) throws -> Promise { + let promise = Promise() + EventEmitterHandler.setBackgroundTaskExpiredCallback(nil) + videoCompressor.deactivateBackgroundTask(options: dictionary(from: options), resolve: resolveString(promise, nil), reject: reject(promise, nil)) + return promise + } + + // MARK: - Audio + + func compress_audio(fileUrl: String, optionMap: AnyMap) throws -> Promise { + let promise = Promise() + AudioMain.compress_audio(fileUrl, optionMap: dictionary(from: optionMap) as NSDictionary, resolve: resolveString(promise, nil), reject: reject(promise, nil)) + return promise + } + + // MARK: - Upload / Download + + func upload(fileUrl: String, options: AnyMap, onProgress: ((Double, Double) -> Void)?) throws -> Promise { + let promise = Promise() + let dict = dictionary(from: options) + let uuid = (dict["uuid"] as? String) ?? "" + EventEmitterHandler.registerUploadProgress(uuid: uuid, onProgress) + uploader.upload(filePath: fileUrl, options: dict, resolve: resolveAnyMap(promise, uuid), reject: reject(promise, uuid)) + return promise + } + + func cancelUpload(uuid: String, shouldCancelAll: Bool) throws { + uploader.cancelUpload(uuid: uuid, shouldCancelAll: shouldCancelAll) + } + + func download(fileUrl: String, options: AnyMap, onProgress: ((Double) -> Void)?) throws -> Promise { + let promise = Promise() + let dict = dictionary(from: options) + let uuid = (dict["uuid"] as? String) ?? "" + let progressDivider = (dict["progressDivider"] as? NSNumber)?.intValue ?? 0 + EventEmitterHandler.registerDownloadProgress(uuid: uuid, onProgress) + Downloader.downloadFileAndSaveToCache(fileUrl, uuid: uuid, progressDivider: progressDivider) { downloadedPath in + EventEmitterHandler.unregister(uuid: uuid) + promise.resolve(withResult: downloadedPath) + } + return promise + } + + // MARK: - Others + + func generateFilePath(fileExtension: String) throws -> Promise { + let promise = Promise() + Utils.generateFilePath(fileExtension, resolve: resolveString(promise, nil), reject: reject(promise, nil)) + return promise + } + + func getRealPath(path: String, type: String) throws -> Promise { + let promise = Promise() + Utils.getRealPath(path, type: type, resolve: resolveString(promise, nil), reject: reject(promise, nil)) + return promise + } + + func getFileSize(filePath: String) throws -> Promise { + let promise = Promise() + Utils.getFileSize(filePath, resolve: resolveString(promise, nil), reject: reject(promise, nil)) + return promise + } + + func createVideoThumbnail(fileUrl: String, options: AnyMap) throws -> Promise { + let promise = Promise() + let thumbnail = CreateVideoThumbnail() + thumbnail.create(fileUrl, options: dictionary(from: options) as NSDictionary, resolve: { value in + let dict = HybridCompressor.stringKeyedDictionary(value) + let result = VideoThumbnailResult( + path: dict["path"] as? String ?? "", + size: HybridCompressor.doubleValue(dict["size"]), + mime: dict["mime"] as? String ?? "", + width: HybridCompressor.doubleValue(dict["width"]), + height: HybridCompressor.doubleValue(dict["height"]) + ) + promise.resolve(withResult: result) + }, rejecter: reject(promise, nil)) + return promise + } + + func clearCache(cacheDir: String?) throws -> Promise { + let promise = Promise() + CreateVideoThumbnail.cleanCacheDir(cacheDir: cacheDir ?? "", resolve: resolveString(promise, nil), rejecter: reject(promise, nil)) + return promise + } + + // MARK: - Helpers + + /// Convert a Nitro `AnyMap` to a `[String: Any]`, round-tripping through + /// `NSDictionary` so numbers are boxed as `NSNumber` (the domain parsers rely + /// on `as? Int` / `as? Bool`, which fail on a bare Swift `Double`). + private func dictionary(from map: AnyMap) -> [String: Any] { + let normalized = (HybridCompressor.normalize(map.toDictionary()) as? [String: Any]) ?? [:] + return ((normalized as NSDictionary) as? [String: Any]) ?? normalized + } + + private func resolveString(_ promise: Promise, _ uuid: String?) -> RCTPromiseResolveBlock { + return { value in + if let uuid = uuid { EventEmitterHandler.unregister(uuid: uuid) } + promise.resolve(withResult: value as? String ?? "") + } + } + + private func resolveAnyMap(_ promise: Promise, _ uuid: String?) -> RCTPromiseResolveBlock { + return { value in + if let uuid = uuid { EventEmitterHandler.unregister(uuid: uuid) } + promise.resolve(withResult: AnyMap.fromDictionaryIgnoreIncompatible(HybridCompressor.stringKeyedOptionalDictionary(value))) + } + } + + private func reject(_ promise: Promise, _ uuid: String?) -> RCTPromiseRejectBlock { + return { code, message, error in + if let uuid = uuid { EventEmitterHandler.unregister(uuid: uuid) } + let nsError = error ?? RuntimeError.error(withMessage: message ?? code ?? "react-native-compressor error") + promise.reject(withError: nsError) + } + } + + /// Recursively strip optionals so a `[String: Any?]` (what `AnyMap.toDictionary()` + /// returns, including nested objects/arrays) becomes a plain `[String: Any]`. + private static func normalize(_ value: Any?) -> Any? { + switch value { + case let dict as [String: Any?]: + var out = [String: Any]() + for (key, nested) in dict { + if let nestedValue = normalize(nested) { out[key] = nestedValue } + } + return out + case let array as [Any?]: + return array.compactMap { normalize($0) } + default: + return value + } + } + + private static func stringKeyedDictionary(_ value: Any?) -> [String: Any] { + if let dict = value as? [String: Any] { return dict } + if let ns = value as? NSDictionary { + var out = [String: Any]() + for (key, val) in ns where key is String { + out[key as! String] = val + } + return out + } + return [:] + } + + private static func stringKeyedOptionalDictionary(_ value: Any?) -> [String: Any?] { + if let ns = value as? NSDictionary { + var out = [String: Any?]() + for (key, val) in ns where key is String { + out[key as! String] = val + } + return out + } + if let dict = value as? [String: Any] { + var out = [String: Any?]() + for (key, val) in dict { out[key] = val } + return out + } + return [:] + } + + private static func doubleValue(_ value: Any?) -> Double { + if let number = value as? NSNumber { return number.doubleValue } + if let double = value as? Double { return double } + if let float = value as? Float { return Double(float) } + if let int = value as? Int { return Double(int) } + return 0 + } +} diff --git a/ios/Image/ImageCompressor.swift b/ios/Image/ImageCompressor.swift index b7cb642a..2bcc790f 100644 --- a/ios/Image/ImageCompressor.swift +++ b/ios/Image/ImageCompressor.swift @@ -1,11 +1,17 @@ import Accelerate import CoreGraphics import Photos -import React import Foundation import MobileCoreServices +// Local stand-in for the single `RCTResizeMode` case this file relies on, so the +// module no longer needs to `import React` (see HybridCompressor for the reason). +private enum CompressorResizeMode { + case contain +} + + class ImageCompressor { static func findTargetSize(_ image: UIImage, maxWidth: Int, maxHeight: Int) -> CGSize { let width = image.size.width @@ -392,7 +398,7 @@ class ImageCompressor { let imageURL = URL(string: imagePath.replacingOccurrences(of: " ", with: "%20")) var size = CGSize.zero var scale: CGFloat = 1 - var resizeMode = RCTResizeMode.contain + var resizeMode = CompressorResizeMode.contain let assetID = imagePath.replacingOccurrences(of: "ph://", with: "") let results = PHAsset.fetchAssets(withLocalIdentifiers: [assetID], options: nil) diff --git a/ios/Utils/EventEmitterHandler.swift b/ios/Utils/EventEmitterHandler.swift index 281c7de0..c0ab872f 100644 --- a/ios/Utils/EventEmitterHandler.swift +++ b/ios/Utils/EventEmitterHandler.swift @@ -6,49 +6,71 @@ // import Foundation +import UIKit +/// Routes native progress emissions to the per-call JS callbacks that +/// `HybridCompressor` registers. This replaces the old `RCTEventEmitter` bridge: +/// Nitro delivers progress through callback parameters, so the domain layer's +/// `emit*` calls (unchanged) are dispatched to the callback registered under the +/// same `uuid` that the JS layer threads through the options map. class EventEmitterHandler { - static var sharedCompressorObject: Any! - static var hasListener: Bool=false - - static func initCompressorInstance(_ object: Any) { - sharedCompressorObject = object + private static let lock = NSLock() + private static var videoCompressProgressCallbacks = [String: (Double) -> Void]() + private static var downloadProgressCallbacks = [String: (Double) -> Void]() + private static var uploadProgressCallbacks = [String: (Double, Double) -> Void]() + private static var backgroundTaskExpiredCallback: (() -> Void)? + + // MARK: - Registration (called by HybridCompressor) + + static func registerVideoCompressProgress(uuid: String, _ callback: ((Double) -> Void)?) { + guard let callback = callback else { return } + lock.lock(); defer { lock.unlock() } + videoCompressProgressCallbacks[uuid] = callback + } + + static func registerDownloadProgress(uuid: String, _ callback: ((Double) -> Void)?) { + guard let callback = callback else { return } + lock.lock(); defer { lock.unlock() } + downloadProgressCallbacks[uuid] = callback } - - - static func stopObserving() -> Void { - hasListener = false + + static func registerUploadProgress(uuid: String, _ callback: ((Double, Double) -> Void)?) { + guard let callback = callback else { return } + lock.lock(); defer { lock.unlock() } + uploadProgressCallbacks[uuid] = callback + } + + static func unregister(uuid: String) { + lock.lock(); defer { lock.unlock() } + videoCompressProgressCallbacks[uuid] = nil + downloadProgressCallbacks[uuid] = nil + uploadProgressCallbacks[uuid] = nil } - static func startObserving() -> Void { - hasListener = true + static func setBackgroundTaskExpiredCallback(_ callback: (() -> Void)?) { + lock.lock(); defer { lock.unlock() } + backgroundTaskExpiredCallback = callback } - + + // MARK: - Emission (called by the domain layer — method names preserved) + static func emitDownloadProgress(_ progress: NSNumber, uuid: String) { - var params = [String: Any]() - var data = [String: Any]() - params["uuid"] = uuid - data["progress"] = progress - params["data"] = data - - (sharedCompressorObject as AnyObject).sendEvent(withName: "downloadProgress", body: params) - } - + lock.lock(); let callback = downloadProgressCallbacks[uuid]; lock.unlock() + callback?(progress.doubleValue) + } + static func emitVideoCompressProgress(_ progress: Float, uuid: String) { - if(self.hasListener){ - (sharedCompressorObject as AnyObject).sendEvent(withName: "videoCompressProgress", body: ["uuid": uuid, "data": ["progress": progress]]) - } + lock.lock(); let callback = videoCompressProgressCallbacks[uuid]; lock.unlock() + callback?(Double(progress)) } - + static func emituploadProgress(_ uuid: String, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) { - if(self.hasListener){ - (sharedCompressorObject as AnyObject).sendEvent(withName: "uploadProgress", body: ["uuid": uuid, "data": ["written": totalBytesSent, "total": totalBytesExpectedToSend]]) - } - } - - static func emitBackgroundTaskExpired(_ backgroundTaskId:UIBackgroundTaskIdentifier) { - if(self.hasListener){ - (sharedCompressorObject as AnyObject).sendEvent(withName: "backgroundTaskExpired", body: ["backgroundTaskId": backgroundTaskId]) - } + lock.lock(); let callback = uploadProgressCallbacks[uuid]; lock.unlock() + callback?(Double(totalBytesSent), Double(totalBytesExpectedToSend)) + } + + static func emitBackgroundTaskExpired(_ backgroundTaskId: UIBackgroundTaskIdentifier) { + lock.lock(); let callback = backgroundTaskExpiredCallback; lock.unlock() + callback?() } } diff --git a/ios/Video/VideoMain.swift b/ios/Video/VideoMain.swift index 7d702f7e..4c6dd1a7 100644 --- a/ios/Video/VideoMain.swift +++ b/ios/Video/VideoMain.swift @@ -2,6 +2,7 @@ import Foundation import AVFoundation import Photos import MobileCoreServices +import UIKit struct CompressionError: Error { private let message: String diff --git a/nitro.json b/nitro.json new file mode 100644 index 00000000..d7515c6f --- /dev/null +++ b/nitro.json @@ -0,0 +1,24 @@ +{ + "$schema": "https://nitro.margelo.com/nitro.schema.json", + "cxxNamespace": ["compressor"], + "ios": { + "iosModuleName": "react_native_compressor" + }, + "android": { + "androidNamespace": ["compressor"], + "androidCxxLibName": "NitroCompressor" + }, + "autolinking": { + "Compressor": { + "ios": { + "language": "swift", + "implementationClassName": "HybridCompressor" + }, + "android": { + "language": "kotlin", + "implementationClassName": "HybridCompressor" + } + } + }, + "ignorePaths": ["**/node_modules"] +} diff --git a/nitrogen/generated/.gitattributes b/nitrogen/generated/.gitattributes new file mode 100644 index 00000000..fb7a0d5a --- /dev/null +++ b/nitrogen/generated/.gitattributes @@ -0,0 +1 @@ +** linguist-generated=true diff --git a/nitrogen/generated/android/NitroCompressor+autolinking.cmake b/nitrogen/generated/android/NitroCompressor+autolinking.cmake new file mode 100644 index 00000000..c5af8c26 --- /dev/null +++ b/nitrogen/generated/android/NitroCompressor+autolinking.cmake @@ -0,0 +1,81 @@ +# +# NitroCompressor+autolinking.cmake +# This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +# https://github.com/mrousavy/nitro +# Copyright © Marc Rousavy @ Margelo +# + +# This is a CMake file that adds all files generated by Nitrogen +# to the current CMake project. +# +# To use it, add this to your CMakeLists.txt: +# ```cmake +# include(${CMAKE_SOURCE_DIR}/../nitrogen/generated/android/NitroCompressor+autolinking.cmake) +# ``` + +# Define a flag to check if we are building properly +add_definitions(-DBUILDING_NITROCOMPRESSOR_WITH_GENERATED_CMAKE_PROJECT) + +# Enable Raw Props parsing in react-native (for Nitro Views) +add_definitions(-DRN_SERIALIZABLE_STATE) + +# Add all headers that were generated by Nitrogen +include_directories( + "../nitrogen/generated/shared/c++" + "../nitrogen/generated/android/c++" + "../nitrogen/generated/android/" +) + +# Add all .cpp sources that were generated by Nitrogen +target_sources( + # CMake project name (Android C++ library name) + NitroCompressor PRIVATE + # Autolinking Setup + ../nitrogen/generated/android/NitroCompressorOnLoad.cpp + # Shared Nitrogen C++ sources + ../nitrogen/generated/shared/c++/HybridCompressorSpec.cpp + # Android-specific Nitrogen C++ sources + ../nitrogen/generated/android/c++/JHybridCompressorSpec.cpp +) + +# From node_modules/react-native/ReactAndroid/cmake-utils/folly-flags.cmake +# Used in node_modules/react-native/ReactAndroid/cmake-utils/ReactNative-application.cmake +target_compile_definitions( + NitroCompressor PRIVATE + -DFOLLY_NO_CONFIG=1 + -DFOLLY_HAVE_CLOCK_GETTIME=1 + -DFOLLY_USE_LIBCPP=1 + -DFOLLY_CFG_NO_COROUTINES=1 + -DFOLLY_MOBILE=1 + -DFOLLY_HAVE_RECVMMSG=1 + -DFOLLY_HAVE_PTHREAD=1 + # Once we target android-23 above, we can comment + # the following line. NDK uses GNU style stderror_r() after API 23. + -DFOLLY_HAVE_XSI_STRERROR_R=1 +) + +# Add all libraries required by the generated specs +find_package(fbjni REQUIRED) # <-- Used for communication between Java <-> C++ +find_package(ReactAndroid REQUIRED) # <-- Used to set up React Native bindings (e.g. CallInvoker/TurboModule) +find_package(react-native-nitro-modules REQUIRED) # <-- Used to create all HybridObjects and use the Nitro core library + +# Link all libraries together +target_link_libraries( + NitroCompressor + fbjni::fbjni # <-- Facebook C++ JNI helpers + ReactAndroid::jsi # <-- RN: JSI + react-native-nitro-modules::NitroModules # <-- NitroModules Core :) +) + +# Link react-native (different prefab between RN 0.75 and RN 0.76) +if(ReactAndroid_VERSION_MINOR GREATER_EQUAL 76) + target_link_libraries( + NitroCompressor + ReactAndroid::reactnative # <-- RN: Native Modules umbrella prefab + ) +else() + target_link_libraries( + NitroCompressor + ReactAndroid::react_nativemodule_core # <-- RN: TurboModules Core + ) +endif() diff --git a/nitrogen/generated/android/NitroCompressor+autolinking.gradle b/nitrogen/generated/android/NitroCompressor+autolinking.gradle new file mode 100644 index 00000000..25ce25da --- /dev/null +++ b/nitrogen/generated/android/NitroCompressor+autolinking.gradle @@ -0,0 +1,27 @@ +/// +/// NitroCompressor+autolinking.gradle +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © Marc Rousavy @ Margelo +/// + +/// This is a Gradle file that adds all files generated by Nitrogen +/// to the current Gradle project. +/// +/// To use it, add this to your build.gradle: +/// ```gradle +/// apply from: '../nitrogen/generated/android/NitroCompressor+autolinking.gradle' +/// ``` + +logger.warn("[NitroModules] 🔥 NitroCompressor is boosted by nitro!") + +android { + sourceSets { + main { + java.srcDirs += [ + // Nitrogen files + "${project.projectDir}/../nitrogen/generated/android/kotlin" + ] + } + } +} diff --git a/nitrogen/generated/android/NitroCompressorOnLoad.cpp b/nitrogen/generated/android/NitroCompressorOnLoad.cpp new file mode 100644 index 00000000..474ef98a --- /dev/null +++ b/nitrogen/generated/android/NitroCompressorOnLoad.cpp @@ -0,0 +1,60 @@ +/// +/// NitroCompressorOnLoad.cpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © Marc Rousavy @ Margelo +/// + +#ifndef BUILDING_NITROCOMPRESSOR_WITH_GENERATED_CMAKE_PROJECT +#error NitroCompressorOnLoad.cpp is not being built with the autogenerated CMakeLists.txt project. Is a different CMakeLists.txt building this? +#endif + +#include "NitroCompressorOnLoad.hpp" + +#include +#include +#include + +#include "JHybridCompressorSpec.hpp" +#include "JFunc_void_double.hpp" +#include "JFunc_void.hpp" +#include "JFunc_void_double_double.hpp" +#include + +namespace margelo::nitro::compressor { + +int initialize(JavaVM* vm) { + return facebook::jni::initialize(vm, []() { + ::margelo::nitro::compressor::registerAllNatives(); + }); +} + +struct JHybridCompressorSpecImpl: public jni::JavaClass { + static constexpr auto kJavaDescriptor = "Lcom/margelo/nitro/compressor/HybridCompressor;"; + static std::shared_ptr create() { + static const auto constructorFn = javaClassStatic()->getConstructor(); + jni::local_ref javaPart = javaClassStatic()->newObject(constructorFn); + return javaPart->getJHybridCompressorSpec(); + } +}; + +void registerAllNatives() { + using namespace margelo::nitro; + using namespace margelo::nitro::compressor; + + // Register native JNI methods + margelo::nitro::compressor::JHybridCompressorSpec::CxxPart::registerNatives(); + margelo::nitro::compressor::JFunc_void_double_cxx::registerNatives(); + margelo::nitro::compressor::JFunc_void_cxx::registerNatives(); + margelo::nitro::compressor::JFunc_void_double_double_cxx::registerNatives(); + + // Register Nitro Hybrid Objects + HybridObjectRegistry::registerHybridObjectConstructor( + "Compressor", + []() -> std::shared_ptr { + return JHybridCompressorSpecImpl::create(); + } + ); +} + +} // namespace margelo::nitro::compressor diff --git a/nitrogen/generated/android/NitroCompressorOnLoad.hpp b/nitrogen/generated/android/NitroCompressorOnLoad.hpp new file mode 100644 index 00000000..a8415f5f --- /dev/null +++ b/nitrogen/generated/android/NitroCompressorOnLoad.hpp @@ -0,0 +1,34 @@ +/// +/// NitroCompressorOnLoad.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © Marc Rousavy @ Margelo +/// + +#include +#include +#include + +namespace margelo::nitro::compressor { + + [[deprecated("Use registerNatives() instead.")]] + int initialize(JavaVM* vm); + + /** + * Register the native (C++) part of NitroCompressor, and autolinks all Hybrid Objects. + * Call this in your `JNI_OnLoad` function (probably inside `cpp-adapter.cpp`), + * inside a `facebook::jni::initialize(vm, ...)` call. + * Example: + * ```cpp (cpp-adapter.cpp) + * JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void*) { + * return facebook::jni::initialize(vm, []() { + * // register all NitroCompressor HybridObjects + * margelo::nitro::compressor::registerNatives(); + * // any other custom registrations go here. + * }); + * } + * ``` + */ + void registerAllNatives(); + +} // namespace margelo::nitro::compressor diff --git a/nitrogen/generated/android/c++/JFunc_void.hpp b/nitrogen/generated/android/c++/JFunc_void.hpp new file mode 100644 index 00000000..ad9bc2d6 --- /dev/null +++ b/nitrogen/generated/android/c++/JFunc_void.hpp @@ -0,0 +1,75 @@ +/// +/// JFunc_void.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © Marc Rousavy @ Margelo +/// + +#pragma once + +#include +#include + +#include +#include + +namespace margelo::nitro::compressor { + + using namespace facebook; + + /** + * Represents the Java/Kotlin callback `() -> Unit`. + * This can be passed around between C++ and Java/Kotlin. + */ + struct JFunc_void: public jni::JavaClass { + public: + static constexpr auto kJavaDescriptor = "Lcom/margelo/nitro/compressor/Func_void;"; + + public: + /** + * Invokes the function this `JFunc_void` instance holds through JNI. + */ + void invoke() const { + static const auto method = javaClassStatic()->getMethod("invoke"); + method(self()); + } + }; + + /** + * An implementation of Func_void that is backed by a C++ implementation (using `std::function<...>`) + */ + class JFunc_void_cxx final: public jni::HybridClass { + public: + static jni::local_ref fromCpp(const std::function& func) { + return JFunc_void_cxx::newObjectCxxArgs(func); + } + + public: + /** + * Invokes the C++ `std::function<...>` this `JFunc_void_cxx` instance holds. + */ + void invoke_cxx() { + _func(); + } + + public: + [[nodiscard]] + inline const std::function& getFunction() const { + return _func; + } + + public: + static constexpr auto kJavaDescriptor = "Lcom/margelo/nitro/compressor/Func_void_cxx;"; + static void registerNatives() { + registerHybrid({makeNativeMethod("invoke_cxx", JFunc_void_cxx::invoke_cxx)}); + } + + private: + explicit JFunc_void_cxx(const std::function& func): _func(func) { } + + private: + friend HybridBase; + std::function _func; + }; + +} // namespace margelo::nitro::compressor diff --git a/nitrogen/generated/android/c++/JFunc_void_double.hpp b/nitrogen/generated/android/c++/JFunc_void_double.hpp new file mode 100644 index 00000000..8f29be79 --- /dev/null +++ b/nitrogen/generated/android/c++/JFunc_void_double.hpp @@ -0,0 +1,75 @@ +/// +/// JFunc_void_double.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © Marc Rousavy @ Margelo +/// + +#pragma once + +#include +#include + +#include +#include + +namespace margelo::nitro::compressor { + + using namespace facebook; + + /** + * Represents the Java/Kotlin callback `(progress: Double) -> Unit`. + * This can be passed around between C++ and Java/Kotlin. + */ + struct JFunc_void_double: public jni::JavaClass { + public: + static constexpr auto kJavaDescriptor = "Lcom/margelo/nitro/compressor/Func_void_double;"; + + public: + /** + * Invokes the function this `JFunc_void_double` instance holds through JNI. + */ + void invoke(double progress) const { + static const auto method = javaClassStatic()->getMethod("invoke"); + method(self(), progress); + } + }; + + /** + * An implementation of Func_void_double that is backed by a C++ implementation (using `std::function<...>`) + */ + class JFunc_void_double_cxx final: public jni::HybridClass { + public: + static jni::local_ref fromCpp(const std::function& func) { + return JFunc_void_double_cxx::newObjectCxxArgs(func); + } + + public: + /** + * Invokes the C++ `std::function<...>` this `JFunc_void_double_cxx` instance holds. + */ + void invoke_cxx(double progress) { + _func(progress); + } + + public: + [[nodiscard]] + inline const std::function& getFunction() const { + return _func; + } + + public: + static constexpr auto kJavaDescriptor = "Lcom/margelo/nitro/compressor/Func_void_double_cxx;"; + static void registerNatives() { + registerHybrid({makeNativeMethod("invoke_cxx", JFunc_void_double_cxx::invoke_cxx)}); + } + + private: + explicit JFunc_void_double_cxx(const std::function& func): _func(func) { } + + private: + friend HybridBase; + std::function _func; + }; + +} // namespace margelo::nitro::compressor diff --git a/nitrogen/generated/android/c++/JFunc_void_double_double.hpp b/nitrogen/generated/android/c++/JFunc_void_double_double.hpp new file mode 100644 index 00000000..ff060b6c --- /dev/null +++ b/nitrogen/generated/android/c++/JFunc_void_double_double.hpp @@ -0,0 +1,75 @@ +/// +/// JFunc_void_double_double.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © Marc Rousavy @ Margelo +/// + +#pragma once + +#include +#include + +#include +#include + +namespace margelo::nitro::compressor { + + using namespace facebook; + + /** + * Represents the Java/Kotlin callback `(written: Double, total: Double) -> Unit`. + * This can be passed around between C++ and Java/Kotlin. + */ + struct JFunc_void_double_double: public jni::JavaClass { + public: + static constexpr auto kJavaDescriptor = "Lcom/margelo/nitro/compressor/Func_void_double_double;"; + + public: + /** + * Invokes the function this `JFunc_void_double_double` instance holds through JNI. + */ + void invoke(double written, double total) const { + static const auto method = javaClassStatic()->getMethod("invoke"); + method(self(), written, total); + } + }; + + /** + * An implementation of Func_void_double_double that is backed by a C++ implementation (using `std::function<...>`) + */ + class JFunc_void_double_double_cxx final: public jni::HybridClass { + public: + static jni::local_ref fromCpp(const std::function& func) { + return JFunc_void_double_double_cxx::newObjectCxxArgs(func); + } + + public: + /** + * Invokes the C++ `std::function<...>` this `JFunc_void_double_double_cxx` instance holds. + */ + void invoke_cxx(double written, double total) { + _func(written, total); + } + + public: + [[nodiscard]] + inline const std::function& getFunction() const { + return _func; + } + + public: + static constexpr auto kJavaDescriptor = "Lcom/margelo/nitro/compressor/Func_void_double_double_cxx;"; + static void registerNatives() { + registerHybrid({makeNativeMethod("invoke_cxx", JFunc_void_double_double_cxx::invoke_cxx)}); + } + + private: + explicit JFunc_void_double_double_cxx(const std::function& func): _func(func) { } + + private: + friend HybridBase; + std::function _func; + }; + +} // namespace margelo::nitro::compressor diff --git a/nitrogen/generated/android/c++/JHybridCompressorSpec.cpp b/nitrogen/generated/android/c++/JHybridCompressorSpec.cpp new file mode 100644 index 00000000..c7553bc1 --- /dev/null +++ b/nitrogen/generated/android/c++/JHybridCompressorSpec.cpp @@ -0,0 +1,293 @@ +/// +/// JHybridCompressorSpec.cpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © Marc Rousavy @ Margelo +/// + +#include "JHybridCompressorSpec.hpp" + +// Forward declaration of `VideoThumbnailResult` to properly resolve imports. +namespace margelo::nitro::compressor { struct VideoThumbnailResult; } + +#include +#include +#include +#include +#include +#include "VideoThumbnailResult.hpp" +#include "JVideoThumbnailResult.hpp" +#include +#include +#include "JFunc_void_double.hpp" +#include +#include "JFunc_void.hpp" +#include "JFunc_void_double_double.hpp" + +namespace margelo::nitro::compressor { + + std::shared_ptr JHybridCompressorSpec::JavaPart::getJHybridCompressorSpec() { + auto hybridObject = JHybridObject::JavaPart::getJHybridObject(); + auto castHybridObject = std::dynamic_pointer_cast(hybridObject); + if (castHybridObject == nullptr) [[unlikely]] { + throw std::runtime_error("Failed to downcast JHybridObject to JHybridCompressorSpec!"); + } + return castHybridObject; + } + + jni::local_ref JHybridCompressorSpec::CxxPart::initHybrid(jni::alias_ref jThis) { + return makeCxxInstance(jThis); + } + + std::shared_ptr JHybridCompressorSpec::CxxPart::createHybridObject(const jni::local_ref& javaPart) { + auto castJavaPart = jni::dynamic_ref_cast(javaPart); + if (castJavaPart == nullptr) [[unlikely]] { + throw std::runtime_error("Failed to cast JHybridObject::JavaPart to JHybridCompressorSpec::JavaPart!"); + } + return std::make_shared(castJavaPart); + } + + void JHybridCompressorSpec::CxxPart::registerNatives() { + registerHybrid({ + makeNativeMethod("initHybrid", JHybridCompressorSpec::CxxPart::initHybrid), + }); + } + + // Properties + + + // Methods + std::shared_ptr> JHybridCompressorSpec::image_compress(const std::string& imagePath, const std::shared_ptr& optionMap, const std::optional>& onDownloadProgress) { + static const auto method = _javaPart->javaClassStatic()->getMethod(jni::alias_ref /* imagePath */, jni::alias_ref /* optionMap */, jni::alias_ref /* onDownloadProgress */)>("image_compress_cxx"); + auto __result = method(_javaPart, jni::make_jstring(imagePath), JAnyMap::create(optionMap), onDownloadProgress.has_value() ? JFunc_void_double_cxx::fromCpp(onDownloadProgress.value()) : nullptr); + return [&]() { + auto __promise = Promise::create(); + __result->cthis()->addOnResolvedListener([=](const jni::alias_ref& __boxedResult) { + auto __result = jni::static_ref_cast(__boxedResult); + __promise->resolve(__result->toStdString()); + }); + __result->cthis()->addOnRejectedListener([=](const jni::alias_ref& __throwable) { + jni::JniException __jniError(__throwable); + __promise->reject(std::make_exception_ptr(__jniError)); + }); + return __promise; + }(); + } + std::shared_ptr>> JHybridCompressorSpec::getImageMetaData(const std::string& filePath) { + static const auto method = _javaPart->javaClassStatic()->getMethod(jni::alias_ref /* filePath */)>("getImageMetaData"); + auto __result = method(_javaPart, jni::make_jstring(filePath)); + return [&]() { + auto __promise = Promise>::create(); + __result->cthis()->addOnResolvedListener([=](const jni::alias_ref& __boxedResult) { + auto __result = jni::static_ref_cast(__boxedResult); + __promise->resolve(__result->cthis()->getMap()); + }); + __result->cthis()->addOnRejectedListener([=](const jni::alias_ref& __throwable) { + jni::JniException __jniError(__throwable); + __promise->reject(std::make_exception_ptr(__jniError)); + }); + return __promise; + }(); + } + std::shared_ptr> JHybridCompressorSpec::compress(const std::string& fileUrl, const std::shared_ptr& optionMap, const std::optional>& onProgress, const std::optional>& onDownloadProgress) { + static const auto method = _javaPart->javaClassStatic()->getMethod(jni::alias_ref /* fileUrl */, jni::alias_ref /* optionMap */, jni::alias_ref /* onProgress */, jni::alias_ref /* onDownloadProgress */)>("compress_cxx"); + auto __result = method(_javaPart, jni::make_jstring(fileUrl), JAnyMap::create(optionMap), onProgress.has_value() ? JFunc_void_double_cxx::fromCpp(onProgress.value()) : nullptr, onDownloadProgress.has_value() ? JFunc_void_double_cxx::fromCpp(onDownloadProgress.value()) : nullptr); + return [&]() { + auto __promise = Promise::create(); + __result->cthis()->addOnResolvedListener([=](const jni::alias_ref& __boxedResult) { + auto __result = jni::static_ref_cast(__boxedResult); + __promise->resolve(__result->toStdString()); + }); + __result->cthis()->addOnRejectedListener([=](const jni::alias_ref& __throwable) { + jni::JniException __jniError(__throwable); + __promise->reject(std::make_exception_ptr(__jniError)); + }); + return __promise; + }(); + } + void JHybridCompressorSpec::cancelCompression(const std::string& uuid) { + static const auto method = _javaPart->javaClassStatic()->getMethod /* uuid */)>("cancelCompression"); + method(_javaPart, jni::make_jstring(uuid)); + } + std::shared_ptr>> JHybridCompressorSpec::getVideoMetaData(const std::string& filePath) { + static const auto method = _javaPart->javaClassStatic()->getMethod(jni::alias_ref /* filePath */)>("getVideoMetaData"); + auto __result = method(_javaPart, jni::make_jstring(filePath)); + return [&]() { + auto __promise = Promise>::create(); + __result->cthis()->addOnResolvedListener([=](const jni::alias_ref& __boxedResult) { + auto __result = jni::static_ref_cast(__boxedResult); + __promise->resolve(__result->cthis()->getMap()); + }); + __result->cthis()->addOnRejectedListener([=](const jni::alias_ref& __throwable) { + jni::JniException __jniError(__throwable); + __promise->reject(std::make_exception_ptr(__jniError)); + }); + return __promise; + }(); + } + std::shared_ptr> JHybridCompressorSpec::activateBackgroundTask(const std::shared_ptr& options, const std::optional>& onExpired) { + static const auto method = _javaPart->javaClassStatic()->getMethod(jni::alias_ref /* options */, jni::alias_ref /* onExpired */)>("activateBackgroundTask_cxx"); + auto __result = method(_javaPart, JAnyMap::create(options), onExpired.has_value() ? JFunc_void_cxx::fromCpp(onExpired.value()) : nullptr); + return [&]() { + auto __promise = Promise::create(); + __result->cthis()->addOnResolvedListener([=](const jni::alias_ref& __boxedResult) { + auto __result = jni::static_ref_cast(__boxedResult); + __promise->resolve(__result->toStdString()); + }); + __result->cthis()->addOnRejectedListener([=](const jni::alias_ref& __throwable) { + jni::JniException __jniError(__throwable); + __promise->reject(std::make_exception_ptr(__jniError)); + }); + return __promise; + }(); + } + std::shared_ptr> JHybridCompressorSpec::deactivateBackgroundTask(const std::shared_ptr& options) { + static const auto method = _javaPart->javaClassStatic()->getMethod(jni::alias_ref /* options */)>("deactivateBackgroundTask"); + auto __result = method(_javaPart, JAnyMap::create(options)); + return [&]() { + auto __promise = Promise::create(); + __result->cthis()->addOnResolvedListener([=](const jni::alias_ref& __boxedResult) { + auto __result = jni::static_ref_cast(__boxedResult); + __promise->resolve(__result->toStdString()); + }); + __result->cthis()->addOnRejectedListener([=](const jni::alias_ref& __throwable) { + jni::JniException __jniError(__throwable); + __promise->reject(std::make_exception_ptr(__jniError)); + }); + return __promise; + }(); + } + std::shared_ptr> JHybridCompressorSpec::compress_audio(const std::string& fileUrl, const std::shared_ptr& optionMap) { + static const auto method = _javaPart->javaClassStatic()->getMethod(jni::alias_ref /* fileUrl */, jni::alias_ref /* optionMap */)>("compress_audio"); + auto __result = method(_javaPart, jni::make_jstring(fileUrl), JAnyMap::create(optionMap)); + return [&]() { + auto __promise = Promise::create(); + __result->cthis()->addOnResolvedListener([=](const jni::alias_ref& __boxedResult) { + auto __result = jni::static_ref_cast(__boxedResult); + __promise->resolve(__result->toStdString()); + }); + __result->cthis()->addOnRejectedListener([=](const jni::alias_ref& __throwable) { + jni::JniException __jniError(__throwable); + __promise->reject(std::make_exception_ptr(__jniError)); + }); + return __promise; + }(); + } + std::shared_ptr>> JHybridCompressorSpec::upload(const std::string& fileUrl, const std::shared_ptr& options, const std::optional>& onProgress) { + static const auto method = _javaPart->javaClassStatic()->getMethod(jni::alias_ref /* fileUrl */, jni::alias_ref /* options */, jni::alias_ref /* onProgress */)>("upload_cxx"); + auto __result = method(_javaPart, jni::make_jstring(fileUrl), JAnyMap::create(options), onProgress.has_value() ? JFunc_void_double_double_cxx::fromCpp(onProgress.value()) : nullptr); + return [&]() { + auto __promise = Promise>::create(); + __result->cthis()->addOnResolvedListener([=](const jni::alias_ref& __boxedResult) { + auto __result = jni::static_ref_cast(__boxedResult); + __promise->resolve(__result->cthis()->getMap()); + }); + __result->cthis()->addOnRejectedListener([=](const jni::alias_ref& __throwable) { + jni::JniException __jniError(__throwable); + __promise->reject(std::make_exception_ptr(__jniError)); + }); + return __promise; + }(); + } + void JHybridCompressorSpec::cancelUpload(const std::string& uuid, bool shouldCancelAll) { + static const auto method = _javaPart->javaClassStatic()->getMethod /* uuid */, jboolean /* shouldCancelAll */)>("cancelUpload"); + method(_javaPart, jni::make_jstring(uuid), shouldCancelAll); + } + std::shared_ptr> JHybridCompressorSpec::download(const std::string& fileUrl, const std::shared_ptr& options, const std::optional>& onProgress) { + static const auto method = _javaPart->javaClassStatic()->getMethod(jni::alias_ref /* fileUrl */, jni::alias_ref /* options */, jni::alias_ref /* onProgress */)>("download_cxx"); + auto __result = method(_javaPart, jni::make_jstring(fileUrl), JAnyMap::create(options), onProgress.has_value() ? JFunc_void_double_cxx::fromCpp(onProgress.value()) : nullptr); + return [&]() { + auto __promise = Promise::create(); + __result->cthis()->addOnResolvedListener([=](const jni::alias_ref& __boxedResult) { + auto __result = jni::static_ref_cast(__boxedResult); + __promise->resolve(__result->toStdString()); + }); + __result->cthis()->addOnRejectedListener([=](const jni::alias_ref& __throwable) { + jni::JniException __jniError(__throwable); + __promise->reject(std::make_exception_ptr(__jniError)); + }); + return __promise; + }(); + } + std::shared_ptr> JHybridCompressorSpec::generateFilePath(const std::string& fileExtension) { + static const auto method = _javaPart->javaClassStatic()->getMethod(jni::alias_ref /* fileExtension */)>("generateFilePath"); + auto __result = method(_javaPart, jni::make_jstring(fileExtension)); + return [&]() { + auto __promise = Promise::create(); + __result->cthis()->addOnResolvedListener([=](const jni::alias_ref& __boxedResult) { + auto __result = jni::static_ref_cast(__boxedResult); + __promise->resolve(__result->toStdString()); + }); + __result->cthis()->addOnRejectedListener([=](const jni::alias_ref& __throwable) { + jni::JniException __jniError(__throwable); + __promise->reject(std::make_exception_ptr(__jniError)); + }); + return __promise; + }(); + } + std::shared_ptr> JHybridCompressorSpec::getRealPath(const std::string& path, const std::string& type) { + static const auto method = _javaPart->javaClassStatic()->getMethod(jni::alias_ref /* path */, jni::alias_ref /* type */)>("getRealPath"); + auto __result = method(_javaPart, jni::make_jstring(path), jni::make_jstring(type)); + return [&]() { + auto __promise = Promise::create(); + __result->cthis()->addOnResolvedListener([=](const jni::alias_ref& __boxedResult) { + auto __result = jni::static_ref_cast(__boxedResult); + __promise->resolve(__result->toStdString()); + }); + __result->cthis()->addOnRejectedListener([=](const jni::alias_ref& __throwable) { + jni::JniException __jniError(__throwable); + __promise->reject(std::make_exception_ptr(__jniError)); + }); + return __promise; + }(); + } + std::shared_ptr> JHybridCompressorSpec::getFileSize(const std::string& filePath) { + static const auto method = _javaPart->javaClassStatic()->getMethod(jni::alias_ref /* filePath */)>("getFileSize"); + auto __result = method(_javaPart, jni::make_jstring(filePath)); + return [&]() { + auto __promise = Promise::create(); + __result->cthis()->addOnResolvedListener([=](const jni::alias_ref& __boxedResult) { + auto __result = jni::static_ref_cast(__boxedResult); + __promise->resolve(__result->toStdString()); + }); + __result->cthis()->addOnRejectedListener([=](const jni::alias_ref& __throwable) { + jni::JniException __jniError(__throwable); + __promise->reject(std::make_exception_ptr(__jniError)); + }); + return __promise; + }(); + } + std::shared_ptr> JHybridCompressorSpec::createVideoThumbnail(const std::string& fileUrl, const std::shared_ptr& options) { + static const auto method = _javaPart->javaClassStatic()->getMethod(jni::alias_ref /* fileUrl */, jni::alias_ref /* options */)>("createVideoThumbnail"); + auto __result = method(_javaPart, jni::make_jstring(fileUrl), JAnyMap::create(options)); + return [&]() { + auto __promise = Promise::create(); + __result->cthis()->addOnResolvedListener([=](const jni::alias_ref& __boxedResult) { + auto __result = jni::static_ref_cast(__boxedResult); + __promise->resolve(__result->toCpp()); + }); + __result->cthis()->addOnRejectedListener([=](const jni::alias_ref& __throwable) { + jni::JniException __jniError(__throwable); + __promise->reject(std::make_exception_ptr(__jniError)); + }); + return __promise; + }(); + } + std::shared_ptr> JHybridCompressorSpec::clearCache(const std::optional& cacheDir) { + static const auto method = _javaPart->javaClassStatic()->getMethod(jni::alias_ref /* cacheDir */)>("clearCache"); + auto __result = method(_javaPart, cacheDir.has_value() ? jni::make_jstring(cacheDir.value()) : nullptr); + return [&]() { + auto __promise = Promise::create(); + __result->cthis()->addOnResolvedListener([=](const jni::alias_ref& __boxedResult) { + auto __result = jni::static_ref_cast(__boxedResult); + __promise->resolve(__result->toStdString()); + }); + __result->cthis()->addOnRejectedListener([=](const jni::alias_ref& __throwable) { + jni::JniException __jniError(__throwable); + __promise->reject(std::make_exception_ptr(__jniError)); + }); + return __promise; + }(); + } + +} // namespace margelo::nitro::compressor diff --git a/nitrogen/generated/android/c++/JHybridCompressorSpec.hpp b/nitrogen/generated/android/c++/JHybridCompressorSpec.hpp new file mode 100644 index 00000000..edeb1c31 --- /dev/null +++ b/nitrogen/generated/android/c++/JHybridCompressorSpec.hpp @@ -0,0 +1,78 @@ +/// +/// HybridCompressorSpec.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © Marc Rousavy @ Margelo +/// + +#pragma once + +#include +#include +#include "HybridCompressorSpec.hpp" + + + + +namespace margelo::nitro::compressor { + + using namespace facebook; + + class JHybridCompressorSpec: public virtual HybridCompressorSpec, public virtual JHybridObject { + public: + struct JavaPart: public jni::JavaClass { + static constexpr auto kJavaDescriptor = "Lcom/margelo/nitro/compressor/HybridCompressorSpec;"; + std::shared_ptr getJHybridCompressorSpec(); + }; + struct CxxPart: public jni::HybridClass { + static constexpr auto kJavaDescriptor = "Lcom/margelo/nitro/compressor/HybridCompressorSpec$CxxPart;"; + static jni::local_ref initHybrid(jni::alias_ref jThis); + static void registerNatives(); + using HybridBase::HybridBase; + protected: + std::shared_ptr createHybridObject(const jni::local_ref& javaPart) override; + }; + + public: + explicit JHybridCompressorSpec(const jni::local_ref& javaPart): + HybridObject(HybridCompressorSpec::TAG), + JHybridObject(javaPart), + _javaPart(jni::make_global(javaPart)) {} + ~JHybridCompressorSpec() override { + // Hermes GC can destroy JS objects on a non-JNI Thread. + jni::ThreadScope::WithClassLoader([&] { _javaPart.reset(); }); + } + + public: + inline const jni::global_ref& getJavaPart() const noexcept { + return _javaPart; + } + + public: + // Properties + + + public: + // Methods + std::shared_ptr> image_compress(const std::string& imagePath, const std::shared_ptr& optionMap, const std::optional>& onDownloadProgress) override; + std::shared_ptr>> getImageMetaData(const std::string& filePath) override; + std::shared_ptr> compress(const std::string& fileUrl, const std::shared_ptr& optionMap, const std::optional>& onProgress, const std::optional>& onDownloadProgress) override; + void cancelCompression(const std::string& uuid) override; + std::shared_ptr>> getVideoMetaData(const std::string& filePath) override; + std::shared_ptr> activateBackgroundTask(const std::shared_ptr& options, const std::optional>& onExpired) override; + std::shared_ptr> deactivateBackgroundTask(const std::shared_ptr& options) override; + std::shared_ptr> compress_audio(const std::string& fileUrl, const std::shared_ptr& optionMap) override; + std::shared_ptr>> upload(const std::string& fileUrl, const std::shared_ptr& options, const std::optional>& onProgress) override; + void cancelUpload(const std::string& uuid, bool shouldCancelAll) override; + std::shared_ptr> download(const std::string& fileUrl, const std::shared_ptr& options, const std::optional>& onProgress) override; + std::shared_ptr> generateFilePath(const std::string& fileExtension) override; + std::shared_ptr> getRealPath(const std::string& path, const std::string& type) override; + std::shared_ptr> getFileSize(const std::string& filePath) override; + std::shared_ptr> createVideoThumbnail(const std::string& fileUrl, const std::shared_ptr& options) override; + std::shared_ptr> clearCache(const std::optional& cacheDir) override; + + private: + jni::global_ref _javaPart; + }; + +} // namespace margelo::nitro::compressor diff --git a/nitrogen/generated/android/c++/JVideoThumbnailResult.hpp b/nitrogen/generated/android/c++/JVideoThumbnailResult.hpp new file mode 100644 index 00000000..b9cbea90 --- /dev/null +++ b/nitrogen/generated/android/c++/JVideoThumbnailResult.hpp @@ -0,0 +1,73 @@ +/// +/// JVideoThumbnailResult.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © Marc Rousavy @ Margelo +/// + +#pragma once + +#include +#include "VideoThumbnailResult.hpp" + +#include + +namespace margelo::nitro::compressor { + + using namespace facebook; + + /** + * The C++ JNI bridge between the C++ struct "VideoThumbnailResult" and the the Kotlin data class "VideoThumbnailResult". + */ + struct JVideoThumbnailResult final: public jni::JavaClass { + public: + static constexpr auto kJavaDescriptor = "Lcom/margelo/nitro/compressor/VideoThumbnailResult;"; + + public: + /** + * Convert this Java/Kotlin-based struct to the C++ struct VideoThumbnailResult by copying all values to C++. + */ + [[maybe_unused]] + [[nodiscard]] + VideoThumbnailResult toCpp() const { + static const auto clazz = javaClassStatic(); + static const auto fieldPath = clazz->getField("path"); + jni::local_ref path = this->getFieldValue(fieldPath); + static const auto fieldSize = clazz->getField("size"); + double size = this->getFieldValue(fieldSize); + static const auto fieldMime = clazz->getField("mime"); + jni::local_ref mime = this->getFieldValue(fieldMime); + static const auto fieldWidth = clazz->getField("width"); + double width = this->getFieldValue(fieldWidth); + static const auto fieldHeight = clazz->getField("height"); + double height = this->getFieldValue(fieldHeight); + return VideoThumbnailResult( + path->toStdString(), + size, + mime->toStdString(), + width, + height + ); + } + + public: + /** + * Create a Java/Kotlin-based struct by copying all values from the given C++ struct to Java. + */ + [[maybe_unused]] + static jni::local_ref fromCpp(const VideoThumbnailResult& value) { + using JSignature = JVideoThumbnailResult(jni::alias_ref, double, jni::alias_ref, double, double); + static const auto clazz = javaClassStatic(); + static const auto create = clazz->getStaticMethod("fromCpp"); + return create( + clazz, + jni::make_jstring(value.path), + value.size, + jni::make_jstring(value.mime), + value.width, + value.height + ); + } + }; + +} // namespace margelo::nitro::compressor diff --git a/nitrogen/generated/android/kotlin/com/margelo/nitro/compressor/Func_void.kt b/nitrogen/generated/android/kotlin/com/margelo/nitro/compressor/Func_void.kt new file mode 100644 index 00000000..3b733e87 --- /dev/null +++ b/nitrogen/generated/android/kotlin/com/margelo/nitro/compressor/Func_void.kt @@ -0,0 +1,80 @@ +/// +/// Func_void.kt +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © Marc Rousavy @ Margelo +/// + +package com.margelo.nitro.compressor + +import androidx.annotation.Keep +import com.facebook.jni.HybridData +import com.facebook.proguard.annotations.DoNotStrip +import dalvik.annotation.optimization.FastNative + + +/** + * Represents the JavaScript callback `() => void`. + * This can be either implemented in C++ (in which case it might be a callback coming from JS), + * or in Kotlin/Java (in which case it is a native callback). + */ +@DoNotStrip +@Keep +@Suppress("ClassName", "RedundantUnitReturnType") +fun interface Func_void: () -> Unit { + /** + * Call the given JS callback. + * @throws Throwable if the JS function itself throws an error, or if the JS function/runtime has already been deleted. + */ + @DoNotStrip + @Keep + override fun invoke(): Unit +} + +/** + * Represents the JavaScript callback `() => void`. + * This is implemented in C++, via a `std::function<...>`. + * The callback might be coming from JS. + */ +@DoNotStrip +@Keep +@Suppress( + "KotlinJniMissingFunction", "unused", + "RedundantSuppression", "RedundantUnitReturnType", "FunctionName", + "ConvertSecondaryConstructorToPrimary", "ClassName", "LocalVariableName", +) +class Func_void_cxx: Func_void { + @DoNotStrip + @Keep + private val mHybridData: HybridData + + @DoNotStrip + @Keep + private constructor(hybridData: HybridData) { + mHybridData = hybridData + } + + @DoNotStrip + @Keep + override fun invoke(): Unit + = invoke_cxx() + + @FastNative + private external fun invoke_cxx(): Unit +} + +/** + * Represents the JavaScript callback `() => void`. + * This is implemented in Java/Kotlin, via a `() -> Unit`. + * The callback is always coming from native. + */ +@DoNotStrip +@Keep +@Suppress("ClassName", "RedundantUnitReturnType", "unused") +class Func_void_java(private val function: () -> Unit): Func_void { + @DoNotStrip + @Keep + override fun invoke(): Unit { + return this.function() + } +} diff --git a/nitrogen/generated/android/kotlin/com/margelo/nitro/compressor/Func_void_double.kt b/nitrogen/generated/android/kotlin/com/margelo/nitro/compressor/Func_void_double.kt new file mode 100644 index 00000000..7699a88f --- /dev/null +++ b/nitrogen/generated/android/kotlin/com/margelo/nitro/compressor/Func_void_double.kt @@ -0,0 +1,80 @@ +/// +/// Func_void_double.kt +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © Marc Rousavy @ Margelo +/// + +package com.margelo.nitro.compressor + +import androidx.annotation.Keep +import com.facebook.jni.HybridData +import com.facebook.proguard.annotations.DoNotStrip +import dalvik.annotation.optimization.FastNative + + +/** + * Represents the JavaScript callback `(progress: number) => void`. + * This can be either implemented in C++ (in which case it might be a callback coming from JS), + * or in Kotlin/Java (in which case it is a native callback). + */ +@DoNotStrip +@Keep +@Suppress("ClassName", "RedundantUnitReturnType") +fun interface Func_void_double: (Double) -> Unit { + /** + * Call the given JS callback. + * @throws Throwable if the JS function itself throws an error, or if the JS function/runtime has already been deleted. + */ + @DoNotStrip + @Keep + override fun invoke(progress: Double): Unit +} + +/** + * Represents the JavaScript callback `(progress: number) => void`. + * This is implemented in C++, via a `std::function<...>`. + * The callback might be coming from JS. + */ +@DoNotStrip +@Keep +@Suppress( + "KotlinJniMissingFunction", "unused", + "RedundantSuppression", "RedundantUnitReturnType", "FunctionName", + "ConvertSecondaryConstructorToPrimary", "ClassName", "LocalVariableName", +) +class Func_void_double_cxx: Func_void_double { + @DoNotStrip + @Keep + private val mHybridData: HybridData + + @DoNotStrip + @Keep + private constructor(hybridData: HybridData) { + mHybridData = hybridData + } + + @DoNotStrip + @Keep + override fun invoke(progress: Double): Unit + = invoke_cxx(progress) + + @FastNative + private external fun invoke_cxx(progress: Double): Unit +} + +/** + * Represents the JavaScript callback `(progress: number) => void`. + * This is implemented in Java/Kotlin, via a `(Double) -> Unit`. + * The callback is always coming from native. + */ +@DoNotStrip +@Keep +@Suppress("ClassName", "RedundantUnitReturnType", "unused") +class Func_void_double_java(private val function: (Double) -> Unit): Func_void_double { + @DoNotStrip + @Keep + override fun invoke(progress: Double): Unit { + return this.function(progress) + } +} diff --git a/nitrogen/generated/android/kotlin/com/margelo/nitro/compressor/Func_void_double_double.kt b/nitrogen/generated/android/kotlin/com/margelo/nitro/compressor/Func_void_double_double.kt new file mode 100644 index 00000000..d6966322 --- /dev/null +++ b/nitrogen/generated/android/kotlin/com/margelo/nitro/compressor/Func_void_double_double.kt @@ -0,0 +1,80 @@ +/// +/// Func_void_double_double.kt +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © Marc Rousavy @ Margelo +/// + +package com.margelo.nitro.compressor + +import androidx.annotation.Keep +import com.facebook.jni.HybridData +import com.facebook.proguard.annotations.DoNotStrip +import dalvik.annotation.optimization.FastNative + + +/** + * Represents the JavaScript callback `(written: number, total: number) => void`. + * This can be either implemented in C++ (in which case it might be a callback coming from JS), + * or in Kotlin/Java (in which case it is a native callback). + */ +@DoNotStrip +@Keep +@Suppress("ClassName", "RedundantUnitReturnType") +fun interface Func_void_double_double: (Double, Double) -> Unit { + /** + * Call the given JS callback. + * @throws Throwable if the JS function itself throws an error, or if the JS function/runtime has already been deleted. + */ + @DoNotStrip + @Keep + override fun invoke(written: Double, total: Double): Unit +} + +/** + * Represents the JavaScript callback `(written: number, total: number) => void`. + * This is implemented in C++, via a `std::function<...>`. + * The callback might be coming from JS. + */ +@DoNotStrip +@Keep +@Suppress( + "KotlinJniMissingFunction", "unused", + "RedundantSuppression", "RedundantUnitReturnType", "FunctionName", + "ConvertSecondaryConstructorToPrimary", "ClassName", "LocalVariableName", +) +class Func_void_double_double_cxx: Func_void_double_double { + @DoNotStrip + @Keep + private val mHybridData: HybridData + + @DoNotStrip + @Keep + private constructor(hybridData: HybridData) { + mHybridData = hybridData + } + + @DoNotStrip + @Keep + override fun invoke(written: Double, total: Double): Unit + = invoke_cxx(written,total) + + @FastNative + private external fun invoke_cxx(written: Double, total: Double): Unit +} + +/** + * Represents the JavaScript callback `(written: number, total: number) => void`. + * This is implemented in Java/Kotlin, via a `(Double, Double) -> Unit`. + * The callback is always coming from native. + */ +@DoNotStrip +@Keep +@Suppress("ClassName", "RedundantUnitReturnType", "unused") +class Func_void_double_double_java(private val function: (Double, Double) -> Unit): Func_void_double_double { + @DoNotStrip + @Keep + override fun invoke(written: Double, total: Double): Unit { + return this.function(written, total) + } +} diff --git a/nitrogen/generated/android/kotlin/com/margelo/nitro/compressor/HybridCompressorSpec.kt b/nitrogen/generated/android/kotlin/com/margelo/nitro/compressor/HybridCompressorSpec.kt new file mode 100644 index 00000000..954456a3 --- /dev/null +++ b/nitrogen/generated/android/kotlin/com/margelo/nitro/compressor/HybridCompressorSpec.kt @@ -0,0 +1,141 @@ +/// +/// HybridCompressorSpec.kt +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © Marc Rousavy @ Margelo +/// + +package com.margelo.nitro.compressor + +import androidx.annotation.Keep +import com.facebook.jni.HybridData +import com.facebook.proguard.annotations.DoNotStrip +import com.margelo.nitro.core.Promise +import com.margelo.nitro.core.AnyMap +import com.margelo.nitro.core.HybridObject + +/** + * A Kotlin class representing the Compressor HybridObject. + * Implement this abstract class to create Kotlin-based instances of Compressor. + */ +@DoNotStrip +@Keep +@Suppress( + "KotlinJniMissingFunction", "unused", + "RedundantSuppression", "RedundantUnitReturnType", "SimpleRedundantLet", + "LocalVariableName", "PropertyName", "PrivatePropertyName", "FunctionName" +) +abstract class HybridCompressorSpec: HybridObject() { + // Properties + + + // Methods + abstract fun image_compress(imagePath: String, optionMap: AnyMap, onDownloadProgress: ((progress: Double) -> Unit)?): Promise + + @DoNotStrip + @Keep + private fun image_compress_cxx(imagePath: String, optionMap: AnyMap, onDownloadProgress: Func_void_double?): Promise { + val __result = image_compress(imagePath, optionMap, onDownloadProgress?.let { it }) + return __result + } + + @DoNotStrip + @Keep + abstract fun getImageMetaData(filePath: String): Promise + + abstract fun compress(fileUrl: String, optionMap: AnyMap, onProgress: ((progress: Double) -> Unit)?, onDownloadProgress: ((progress: Double) -> Unit)?): Promise + + @DoNotStrip + @Keep + private fun compress_cxx(fileUrl: String, optionMap: AnyMap, onProgress: Func_void_double?, onDownloadProgress: Func_void_double?): Promise { + val __result = compress(fileUrl, optionMap, onProgress?.let { it }, onDownloadProgress?.let { it }) + return __result + } + + @DoNotStrip + @Keep + abstract fun cancelCompression(uuid: String): Unit + + @DoNotStrip + @Keep + abstract fun getVideoMetaData(filePath: String): Promise + + abstract fun activateBackgroundTask(options: AnyMap, onExpired: (() -> Unit)?): Promise + + @DoNotStrip + @Keep + private fun activateBackgroundTask_cxx(options: AnyMap, onExpired: Func_void?): Promise { + val __result = activateBackgroundTask(options, onExpired?.let { it }) + return __result + } + + @DoNotStrip + @Keep + abstract fun deactivateBackgroundTask(options: AnyMap): Promise + + @DoNotStrip + @Keep + abstract fun compress_audio(fileUrl: String, optionMap: AnyMap): Promise + + abstract fun upload(fileUrl: String, options: AnyMap, onProgress: ((written: Double, total: Double) -> Unit)?): Promise + + @DoNotStrip + @Keep + private fun upload_cxx(fileUrl: String, options: AnyMap, onProgress: Func_void_double_double?): Promise { + val __result = upload(fileUrl, options, onProgress?.let { it }) + return __result + } + + @DoNotStrip + @Keep + abstract fun cancelUpload(uuid: String, shouldCancelAll: Boolean): Unit + + abstract fun download(fileUrl: String, options: AnyMap, onProgress: ((progress: Double) -> Unit)?): Promise + + @DoNotStrip + @Keep + private fun download_cxx(fileUrl: String, options: AnyMap, onProgress: Func_void_double?): Promise { + val __result = download(fileUrl, options, onProgress?.let { it }) + return __result + } + + @DoNotStrip + @Keep + abstract fun generateFilePath(fileExtension: String): Promise + + @DoNotStrip + @Keep + abstract fun getRealPath(path: String, type: String): Promise + + @DoNotStrip + @Keep + abstract fun getFileSize(filePath: String): Promise + + @DoNotStrip + @Keep + abstract fun createVideoThumbnail(fileUrl: String, options: AnyMap): Promise + + @DoNotStrip + @Keep + abstract fun clearCache(cacheDir: String?): Promise + + // Default implementation of `HybridObject.toString()` + override fun toString(): String { + return "[HybridObject Compressor]" + } + + // C++ backing class + @DoNotStrip + @Keep + protected open class CxxPart(javaPart: HybridCompressorSpec): HybridObject.CxxPart(javaPart) { + // C++ JHybridCompressorSpec::CxxPart::initHybrid(...) + external override fun initHybrid(): HybridData + } + override fun createCxxPart(): CxxPart { + return CxxPart(this) + } + + companion object { + protected const val TAG = "HybridCompressorSpec" + } +} diff --git a/nitrogen/generated/android/kotlin/com/margelo/nitro/compressor/NitroCompressorOnLoad.kt b/nitrogen/generated/android/kotlin/com/margelo/nitro/compressor/NitroCompressorOnLoad.kt new file mode 100644 index 00000000..04251646 --- /dev/null +++ b/nitrogen/generated/android/kotlin/com/margelo/nitro/compressor/NitroCompressorOnLoad.kt @@ -0,0 +1,35 @@ +/// +/// NitroCompressorOnLoad.kt +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © Marc Rousavy @ Margelo +/// + +package com.margelo.nitro.compressor + +import android.util.Log + +internal class NitroCompressorOnLoad { + companion object { + private const val TAG = "NitroCompressorOnLoad" + private var didLoad = false + /** + * Initializes the native part of "NitroCompressor". + * This method is idempotent and can be called more than once. + */ + @JvmStatic + fun initializeNative() { + if (didLoad) return + try { + Log.i(TAG, "Loading NitroCompressor C++ library...") + System.loadLibrary("NitroCompressor") + Log.i(TAG, "Successfully loaded NitroCompressor C++ library!") + didLoad = true + } catch (e: Error) { + Log.e(TAG, "Failed to load NitroCompressor C++ library! Is it properly installed and linked? " + + "Is the name correct? (see `CMakeLists.txt`, at `add_library(...)`)", e) + throw e + } + } + } +} diff --git a/nitrogen/generated/android/kotlin/com/margelo/nitro/compressor/VideoThumbnailResult.kt b/nitrogen/generated/android/kotlin/com/margelo/nitro/compressor/VideoThumbnailResult.kt new file mode 100644 index 00000000..54a09434 --- /dev/null +++ b/nitrogen/generated/android/kotlin/com/margelo/nitro/compressor/VideoThumbnailResult.kt @@ -0,0 +1,71 @@ +/// +/// VideoThumbnailResult.kt +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © Marc Rousavy @ Margelo +/// + +package com.margelo.nitro.compressor + +import androidx.annotation.Keep +import com.facebook.proguard.annotations.DoNotStrip +import java.util.Objects + + +/** + * Represents the JavaScript object/struct "VideoThumbnailResult". + */ +@DoNotStrip +@Keep +data class VideoThumbnailResult( + @DoNotStrip + @Keep + val path: String, + @DoNotStrip + @Keep + val size: Double, + @DoNotStrip + @Keep + val mime: String, + @DoNotStrip + @Keep + val width: Double, + @DoNotStrip + @Keep + val height: Double +) { + /* primary constructor */ + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is VideoThumbnailResult) return false + return Objects.deepEquals(this.path, other.path) + && Objects.deepEquals(this.size, other.size) + && Objects.deepEquals(this.mime, other.mime) + && Objects.deepEquals(this.width, other.width) + && Objects.deepEquals(this.height, other.height) + } + + override fun hashCode(): Int { + return arrayOf( + path, + size, + mime, + width, + height + ).contentDeepHashCode() + } + + companion object { + /** + * Constructor called from C++ + */ + @DoNotStrip + @Keep + @Suppress("unused") + @JvmStatic + private fun fromCpp(path: String, size: Double, mime: String, width: Double, height: Double): VideoThumbnailResult { + return VideoThumbnailResult(path, size, mime, width, height) + } + } +} diff --git a/nitrogen/generated/ios/c++/HybridCompressorSpecSwift.cpp b/nitrogen/generated/ios/c++/HybridCompressorSpecSwift.cpp new file mode 100644 index 00000000..41763435 --- /dev/null +++ b/nitrogen/generated/ios/c++/HybridCompressorSpecSwift.cpp @@ -0,0 +1,11 @@ +/// +/// HybridCompressorSpecSwift.cpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © Marc Rousavy @ Margelo +/// + +#include "HybridCompressorSpecSwift.hpp" + +namespace margelo::nitro::compressor { +} // namespace margelo::nitro::compressor diff --git a/nitrogen/generated/ios/c++/HybridCompressorSpecSwift.hpp b/nitrogen/generated/ios/c++/HybridCompressorSpecSwift.hpp new file mode 100644 index 00000000..3d0140b4 --- /dev/null +++ b/nitrogen/generated/ios/c++/HybridCompressorSpecSwift.hpp @@ -0,0 +1,204 @@ +/// +/// HybridCompressorSpecSwift.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © Marc Rousavy @ Margelo +/// + +#pragma once + +#include "HybridCompressorSpec.hpp" + +// Forward declaration of `HybridCompressorSpec_cxx` to properly resolve imports. +namespace react_native_compressor { class HybridCompressorSpec_cxx; } + +// Forward declaration of `VideoThumbnailResult` to properly resolve imports. +namespace margelo::nitro::compressor { struct VideoThumbnailResult; } + +#include +#include +#include +#include +#include +#include "VideoThumbnailResult.hpp" + +#include "react_native_compressor-Swift-Cxx-Umbrella.hpp" + +namespace margelo::nitro::compressor { + + /** + * The C++ part of HybridCompressorSpec_cxx.swift. + * + * HybridCompressorSpecSwift (C++) accesses HybridCompressorSpec_cxx (Swift), and might + * contain some additional bridging code for C++ <> Swift interop. + * + * Since this obviously introduces an overhead, I hope at some point in + * the future, HybridCompressorSpec_cxx can directly inherit from the C++ class HybridCompressorSpec + * to simplify the whole structure and memory management. + */ + class HybridCompressorSpecSwift: public virtual HybridCompressorSpec { + public: + // Constructor from a Swift instance + explicit HybridCompressorSpecSwift(const react_native_compressor::HybridCompressorSpec_cxx& swiftPart): + HybridObject(HybridCompressorSpec::TAG), + _swiftPart(swiftPart) { } + + public: + // Get the Swift part + inline react_native_compressor::HybridCompressorSpec_cxx& getSwiftPart() noexcept { + return _swiftPart; + } + + public: + inline size_t getExternalMemorySize() noexcept override { + return _swiftPart.getMemorySize(); + } + bool equals(const std::shared_ptr& other) override { + if (auto otherCast = std::dynamic_pointer_cast(other)) { + return _swiftPart.equals(otherCast->_swiftPart); + } + return false; + } + void dispose() noexcept override { + _swiftPart.dispose(); + } + std::string toString() override { + return _swiftPart.toString(); + } + + public: + // Properties + + + public: + // Methods + inline std::shared_ptr> image_compress(const std::string& imagePath, const std::shared_ptr& optionMap, const std::optional>& onDownloadProgress) override { + auto __result = _swiftPart.image_compress(imagePath, optionMap, onDownloadProgress); + if (__result.hasError()) [[unlikely]] { + std::rethrow_exception(__result.error()); + } + auto __value = std::move(__result.value()); + return __value; + } + inline std::shared_ptr>> getImageMetaData(const std::string& filePath) override { + auto __result = _swiftPart.getImageMetaData(filePath); + if (__result.hasError()) [[unlikely]] { + std::rethrow_exception(__result.error()); + } + auto __value = std::move(__result.value()); + return __value; + } + inline std::shared_ptr> compress(const std::string& fileUrl, const std::shared_ptr& optionMap, const std::optional>& onProgress, const std::optional>& onDownloadProgress) override { + auto __result = _swiftPart.compress(fileUrl, optionMap, onProgress, onDownloadProgress); + if (__result.hasError()) [[unlikely]] { + std::rethrow_exception(__result.error()); + } + auto __value = std::move(__result.value()); + return __value; + } + inline void cancelCompression(const std::string& uuid) override { + auto __result = _swiftPart.cancelCompression(uuid); + if (__result.hasError()) [[unlikely]] { + std::rethrow_exception(__result.error()); + } + } + inline std::shared_ptr>> getVideoMetaData(const std::string& filePath) override { + auto __result = _swiftPart.getVideoMetaData(filePath); + if (__result.hasError()) [[unlikely]] { + std::rethrow_exception(__result.error()); + } + auto __value = std::move(__result.value()); + return __value; + } + inline std::shared_ptr> activateBackgroundTask(const std::shared_ptr& options, const std::optional>& onExpired) override { + auto __result = _swiftPart.activateBackgroundTask(options, onExpired); + if (__result.hasError()) [[unlikely]] { + std::rethrow_exception(__result.error()); + } + auto __value = std::move(__result.value()); + return __value; + } + inline std::shared_ptr> deactivateBackgroundTask(const std::shared_ptr& options) override { + auto __result = _swiftPart.deactivateBackgroundTask(options); + if (__result.hasError()) [[unlikely]] { + std::rethrow_exception(__result.error()); + } + auto __value = std::move(__result.value()); + return __value; + } + inline std::shared_ptr> compress_audio(const std::string& fileUrl, const std::shared_ptr& optionMap) override { + auto __result = _swiftPart.compress_audio(fileUrl, optionMap); + if (__result.hasError()) [[unlikely]] { + std::rethrow_exception(__result.error()); + } + auto __value = std::move(__result.value()); + return __value; + } + inline std::shared_ptr>> upload(const std::string& fileUrl, const std::shared_ptr& options, const std::optional>& onProgress) override { + auto __result = _swiftPart.upload(fileUrl, options, onProgress); + if (__result.hasError()) [[unlikely]] { + std::rethrow_exception(__result.error()); + } + auto __value = std::move(__result.value()); + return __value; + } + inline void cancelUpload(const std::string& uuid, bool shouldCancelAll) override { + auto __result = _swiftPart.cancelUpload(uuid, std::forward(shouldCancelAll)); + if (__result.hasError()) [[unlikely]] { + std::rethrow_exception(__result.error()); + } + } + inline std::shared_ptr> download(const std::string& fileUrl, const std::shared_ptr& options, const std::optional>& onProgress) override { + auto __result = _swiftPart.download(fileUrl, options, onProgress); + if (__result.hasError()) [[unlikely]] { + std::rethrow_exception(__result.error()); + } + auto __value = std::move(__result.value()); + return __value; + } + inline std::shared_ptr> generateFilePath(const std::string& fileExtension) override { + auto __result = _swiftPart.generateFilePath(fileExtension); + if (__result.hasError()) [[unlikely]] { + std::rethrow_exception(__result.error()); + } + auto __value = std::move(__result.value()); + return __value; + } + inline std::shared_ptr> getRealPath(const std::string& path, const std::string& type) override { + auto __result = _swiftPart.getRealPath(path, type); + if (__result.hasError()) [[unlikely]] { + std::rethrow_exception(__result.error()); + } + auto __value = std::move(__result.value()); + return __value; + } + inline std::shared_ptr> getFileSize(const std::string& filePath) override { + auto __result = _swiftPart.getFileSize(filePath); + if (__result.hasError()) [[unlikely]] { + std::rethrow_exception(__result.error()); + } + auto __value = std::move(__result.value()); + return __value; + } + inline std::shared_ptr> createVideoThumbnail(const std::string& fileUrl, const std::shared_ptr& options) override { + auto __result = _swiftPart.createVideoThumbnail(fileUrl, options); + if (__result.hasError()) [[unlikely]] { + std::rethrow_exception(__result.error()); + } + auto __value = std::move(__result.value()); + return __value; + } + inline std::shared_ptr> clearCache(const std::optional& cacheDir) override { + auto __result = _swiftPart.clearCache(cacheDir); + if (__result.hasError()) [[unlikely]] { + std::rethrow_exception(__result.error()); + } + auto __value = std::move(__result.value()); + return __value; + } + + private: + react_native_compressor::HybridCompressorSpec_cxx _swiftPart; + }; + +} // namespace margelo::nitro::compressor diff --git a/nitrogen/generated/ios/react_native_compressor+autolinking.rb b/nitrogen/generated/ios/react_native_compressor+autolinking.rb new file mode 100644 index 00000000..41a61c9f --- /dev/null +++ b/nitrogen/generated/ios/react_native_compressor+autolinking.rb @@ -0,0 +1,62 @@ +# +# react_native_compressor+autolinking.rb +# This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +# https://github.com/mrousavy/nitro +# Copyright © Marc Rousavy @ Margelo +# + +# This is a Ruby script that adds all files generated by Nitrogen +# to the given podspec. +# +# To use it, add this to your .podspec: +# ```ruby +# Pod::Spec.new do |spec| +# # ... +# +# # Add all files generated by Nitrogen +# load 'nitrogen/generated/ios/react_native_compressor+autolinking.rb' +# add_nitrogen_files(spec) +# end +# ``` + +def add_nitrogen_files(spec) + Pod::UI.puts "[NitroModules] 🔥 react_native_compressor is boosted by nitro!" + + spec.dependency "NitroModules" + + current_source_files = Array(spec.attributes_hash['source_files']) + spec.source_files = current_source_files + [ + # Generated cross-platform specs + "nitrogen/generated/shared/**/*.{h,hpp,c,cpp,swift}", + # Generated bridges for the cross-platform specs + "nitrogen/generated/ios/**/*.{h,hpp,c,cpp,mm,swift}", + ] + + current_public_header_files = Array(spec.attributes_hash['public_header_files']) + spec.public_header_files = current_public_header_files + [ + # Generated specs + "nitrogen/generated/shared/**/*.{h,hpp}", + # Swift to C++ bridging helpers + "nitrogen/generated/ios/react_native_compressor-Swift-Cxx-Bridge.hpp" + ] + + current_private_header_files = Array(spec.attributes_hash['private_header_files']) + spec.private_header_files = current_private_header_files + [ + # iOS specific specs + "nitrogen/generated/ios/c++/**/*.{h,hpp}", + # Views are framework-specific and should be private + "nitrogen/generated/shared/**/views/**/*" + ] + + current_pod_target_xcconfig = spec.attributes_hash['pod_target_xcconfig'] || {} + spec.pod_target_xcconfig = current_pod_target_xcconfig.merge({ + # Use C++ 20 + "CLANG_CXX_LANGUAGE_STANDARD" => "c++20", + # Enables C++ <-> Swift interop (by default it's only ObjC) + "SWIFT_OBJC_INTEROP_MODE" => "objcxx", + # Enables stricter modular headers + "DEFINES_MODULE" => "YES", + # Disable auto-generated ObjC header for Swift (Static linkage on Xcode 26.4 breaks here) + "SWIFT_INSTALL_OBJC_HEADER" => "NO", + }) +end diff --git a/nitrogen/generated/ios/react_native_compressor-Swift-Cxx-Bridge.cpp b/nitrogen/generated/ios/react_native_compressor-Swift-Cxx-Bridge.cpp new file mode 100644 index 00000000..36121c3c --- /dev/null +++ b/nitrogen/generated/ios/react_native_compressor-Swift-Cxx-Bridge.cpp @@ -0,0 +1,89 @@ +/// +/// react_native_compressor-Swift-Cxx-Bridge.cpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © Marc Rousavy @ Margelo +/// + +#include "react_native_compressor-Swift-Cxx-Bridge.hpp" + +// Include C++ implementation defined types +#include "HybridCompressorSpecSwift.hpp" +#include "react_native_compressor-Swift-Cxx-Umbrella.hpp" +#include + +namespace margelo::nitro::compressor::bridge::swift { + + // pragma MARK: std::function + Func_void_std__string create_Func_void_std__string(void* NON_NULL swiftClosureWrapper) noexcept { + auto swiftClosure = react_native_compressor::Func_void_std__string::fromUnsafe(swiftClosureWrapper); + return [swiftClosure = std::move(swiftClosure)](const std::string& result) mutable -> void { + swiftClosure.call(result); + }; + } + + // pragma MARK: std::function + Func_void_std__exception_ptr create_Func_void_std__exception_ptr(void* NON_NULL swiftClosureWrapper) noexcept { + auto swiftClosure = react_native_compressor::Func_void_std__exception_ptr::fromUnsafe(swiftClosureWrapper); + return [swiftClosure = std::move(swiftClosure)](const std::exception_ptr& error) mutable -> void { + swiftClosure.call(error); + }; + } + + // pragma MARK: std::function + Func_void_double create_Func_void_double(void* NON_NULL swiftClosureWrapper) noexcept { + auto swiftClosure = react_native_compressor::Func_void_double::fromUnsafe(swiftClosureWrapper); + return [swiftClosure = std::move(swiftClosure)](double progress) mutable -> void { + swiftClosure.call(progress); + }; + } + + // pragma MARK: std::function& /* result */)> + Func_void_std__shared_ptr_AnyMap_ create_Func_void_std__shared_ptr_AnyMap_(void* NON_NULL swiftClosureWrapper) noexcept { + auto swiftClosure = react_native_compressor::Func_void_std__shared_ptr_AnyMap_::fromUnsafe(swiftClosureWrapper); + return [swiftClosure = std::move(swiftClosure)](const std::shared_ptr& result) mutable -> void { + swiftClosure.call(result); + }; + } + + // pragma MARK: std::function + Func_void create_Func_void(void* NON_NULL swiftClosureWrapper) noexcept { + auto swiftClosure = react_native_compressor::Func_void::fromUnsafe(swiftClosureWrapper); + return [swiftClosure = std::move(swiftClosure)]() mutable -> void { + swiftClosure.call(); + }; + } + + // pragma MARK: std::function + Func_void_double_double create_Func_void_double_double(void* NON_NULL swiftClosureWrapper) noexcept { + auto swiftClosure = react_native_compressor::Func_void_double_double::fromUnsafe(swiftClosureWrapper); + return [swiftClosure = std::move(swiftClosure)](double written, double total) mutable -> void { + swiftClosure.call(written, total); + }; + } + + // pragma MARK: std::function + Func_void_VideoThumbnailResult create_Func_void_VideoThumbnailResult(void* NON_NULL swiftClosureWrapper) noexcept { + auto swiftClosure = react_native_compressor::Func_void_VideoThumbnailResult::fromUnsafe(swiftClosureWrapper); + return [swiftClosure = std::move(swiftClosure)](const VideoThumbnailResult& result) mutable -> void { + swiftClosure.call(result); + }; + } + + // pragma MARK: std::shared_ptr + std::shared_ptr create_std__shared_ptr_HybridCompressorSpec_(void* NON_NULL swiftUnsafePointer) noexcept { + react_native_compressor::HybridCompressorSpec_cxx swiftPart = react_native_compressor::HybridCompressorSpec_cxx::fromUnsafe(swiftUnsafePointer); + return std::make_shared(swiftPart); + } + void* NON_NULL get_std__shared_ptr_HybridCompressorSpec_(std__shared_ptr_HybridCompressorSpec_ cppType) { + std::shared_ptr swiftWrapper = std::dynamic_pointer_cast(cppType); + #ifdef NITRO_DEBUG + if (swiftWrapper == nullptr) [[unlikely]] { + throw std::runtime_error("Class \"HybridCompressorSpec\" is not implemented in Swift!"); + } + #endif + react_native_compressor::HybridCompressorSpec_cxx& swiftPart = swiftWrapper->getSwiftPart(); + return swiftPart.toUnsafe(); + } + +} // namespace margelo::nitro::compressor::bridge::swift diff --git a/nitrogen/generated/ios/react_native_compressor-Swift-Cxx-Bridge.hpp b/nitrogen/generated/ios/react_native_compressor-Swift-Cxx-Bridge.hpp new file mode 100644 index 00000000..7c9d0013 --- /dev/null +++ b/nitrogen/generated/ios/react_native_compressor-Swift-Cxx-Bridge.hpp @@ -0,0 +1,337 @@ +/// +/// react_native_compressor-Swift-Cxx-Bridge.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © Marc Rousavy @ Margelo +/// + +#pragma once + +// Forward declarations of C++ defined types +// Forward declaration of `HybridCompressorSpec` to properly resolve imports. +namespace margelo::nitro::compressor { class HybridCompressorSpec; } +// Forward declaration of `VideoThumbnailResult` to properly resolve imports. +namespace margelo::nitro::compressor { struct VideoThumbnailResult; } + +// Forward declarations of Swift defined types +// Forward declaration of `HybridCompressorSpec_cxx` to properly resolve imports. +namespace react_native_compressor { class HybridCompressorSpec_cxx; } + +// Include C++ defined types +#include "HybridCompressorSpec.hpp" +#include "VideoThumbnailResult.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/** + * Contains specialized versions of C++ templated types so they can be accessed from Swift, + * as well as helper functions to interact with those C++ types from Swift. + */ +namespace margelo::nitro::compressor::bridge::swift { + + // pragma MARK: std::shared_ptr> + /** + * Specialized version of `std::shared_ptr>`. + */ + using std__shared_ptr_Promise_std__string__ = std::shared_ptr>; + inline std::shared_ptr> create_std__shared_ptr_Promise_std__string__() noexcept { + return Promise::create(); + } + inline PromiseHolder wrap_std__shared_ptr_Promise_std__string__(std::shared_ptr> promise) noexcept { + return PromiseHolder(std::move(promise)); + } + + // pragma MARK: std::function + /** + * Specialized version of `std::function`. + */ + using Func_void_std__string = std::function; + /** + * Wrapper class for a `std::function`, this can be used from Swift. + */ + class Func_void_std__string_Wrapper final { + public: + explicit Func_void_std__string_Wrapper(std::function&& func): _function(std::make_unique>(std::move(func))) {} + inline void call(std::string result) const noexcept { + _function->operator()(result); + } + private: + std::unique_ptr> _function; + } SWIFT_NONCOPYABLE; + Func_void_std__string create_Func_void_std__string(void* NON_NULL swiftClosureWrapper) noexcept; + inline Func_void_std__string_Wrapper wrap_Func_void_std__string(Func_void_std__string value) noexcept { + return Func_void_std__string_Wrapper(std::move(value)); + } + + // pragma MARK: std::function + /** + * Specialized version of `std::function`. + */ + using Func_void_std__exception_ptr = std::function; + /** + * Wrapper class for a `std::function`, this can be used from Swift. + */ + class Func_void_std__exception_ptr_Wrapper final { + public: + explicit Func_void_std__exception_ptr_Wrapper(std::function&& func): _function(std::make_unique>(std::move(func))) {} + inline void call(std::exception_ptr error) const noexcept { + _function->operator()(error); + } + private: + std::unique_ptr> _function; + } SWIFT_NONCOPYABLE; + Func_void_std__exception_ptr create_Func_void_std__exception_ptr(void* NON_NULL swiftClosureWrapper) noexcept; + inline Func_void_std__exception_ptr_Wrapper wrap_Func_void_std__exception_ptr(Func_void_std__exception_ptr value) noexcept { + return Func_void_std__exception_ptr_Wrapper(std::move(value)); + } + + // pragma MARK: std::function + /** + * Specialized version of `std::function`. + */ + using Func_void_double = std::function; + /** + * Wrapper class for a `std::function`, this can be used from Swift. + */ + class Func_void_double_Wrapper final { + public: + explicit Func_void_double_Wrapper(std::function&& func): _function(std::make_unique>(std::move(func))) {} + inline void call(double progress) const noexcept { + _function->operator()(progress); + } + private: + std::unique_ptr> _function; + } SWIFT_NONCOPYABLE; + Func_void_double create_Func_void_double(void* NON_NULL swiftClosureWrapper) noexcept; + inline Func_void_double_Wrapper wrap_Func_void_double(Func_void_double value) noexcept { + return Func_void_double_Wrapper(std::move(value)); + } + + // pragma MARK: std::optional> + /** + * Specialized version of `std::optional>`. + */ + using std__optional_std__function_void_double____progress______ = std::optional>; + inline std::optional> create_std__optional_std__function_void_double____progress______(const std::function& value) noexcept { + return std::optional>(value); + } + inline bool has_value_std__optional_std__function_void_double____progress______(const std::optional>& optional) noexcept { + return optional.has_value(); + } + inline std::function get_std__optional_std__function_void_double____progress______(const std::optional>& optional) noexcept { + return optional.value(); + } + + // pragma MARK: std::shared_ptr>> + /** + * Specialized version of `std::shared_ptr>>`. + */ + using std__shared_ptr_Promise_std__shared_ptr_AnyMap___ = std::shared_ptr>>; + inline std::shared_ptr>> create_std__shared_ptr_Promise_std__shared_ptr_AnyMap___() noexcept { + return Promise>::create(); + } + inline PromiseHolder> wrap_std__shared_ptr_Promise_std__shared_ptr_AnyMap___(std::shared_ptr>> promise) noexcept { + return PromiseHolder>(std::move(promise)); + } + + // pragma MARK: std::function& /* result */)> + /** + * Specialized version of `std::function&)>`. + */ + using Func_void_std__shared_ptr_AnyMap_ = std::function& /* result */)>; + /** + * Wrapper class for a `std::function& / * result * /)>`, this can be used from Swift. + */ + class Func_void_std__shared_ptr_AnyMap__Wrapper final { + public: + explicit Func_void_std__shared_ptr_AnyMap__Wrapper(std::function& /* result */)>&& func): _function(std::make_unique& /* result */)>>(std::move(func))) {} + inline void call(std::shared_ptr result) const noexcept { + _function->operator()(result); + } + private: + std::unique_ptr& /* result */)>> _function; + } SWIFT_NONCOPYABLE; + Func_void_std__shared_ptr_AnyMap_ create_Func_void_std__shared_ptr_AnyMap_(void* NON_NULL swiftClosureWrapper) noexcept; + inline Func_void_std__shared_ptr_AnyMap__Wrapper wrap_Func_void_std__shared_ptr_AnyMap_(Func_void_std__shared_ptr_AnyMap_ value) noexcept { + return Func_void_std__shared_ptr_AnyMap__Wrapper(std::move(value)); + } + + // pragma MARK: std::function + /** + * Specialized version of `std::function`. + */ + using Func_void = std::function; + /** + * Wrapper class for a `std::function`, this can be used from Swift. + */ + class Func_void_Wrapper final { + public: + explicit Func_void_Wrapper(std::function&& func): _function(std::make_unique>(std::move(func))) {} + inline void call() const noexcept { + _function->operator()(); + } + private: + std::unique_ptr> _function; + } SWIFT_NONCOPYABLE; + Func_void create_Func_void(void* NON_NULL swiftClosureWrapper) noexcept; + inline Func_void_Wrapper wrap_Func_void(Func_void value) noexcept { + return Func_void_Wrapper(std::move(value)); + } + + // pragma MARK: std::optional> + /** + * Specialized version of `std::optional>`. + */ + using std__optional_std__function_void____ = std::optional>; + inline std::optional> create_std__optional_std__function_void____(const std::function& value) noexcept { + return std::optional>(value); + } + inline bool has_value_std__optional_std__function_void____(const std::optional>& optional) noexcept { + return optional.has_value(); + } + inline std::function get_std__optional_std__function_void____(const std::optional>& optional) noexcept { + return optional.value(); + } + + // pragma MARK: std::function + /** + * Specialized version of `std::function`. + */ + using Func_void_double_double = std::function; + /** + * Wrapper class for a `std::function`, this can be used from Swift. + */ + class Func_void_double_double_Wrapper final { + public: + explicit Func_void_double_double_Wrapper(std::function&& func): _function(std::make_unique>(std::move(func))) {} + inline void call(double written, double total) const noexcept { + _function->operator()(written, total); + } + private: + std::unique_ptr> _function; + } SWIFT_NONCOPYABLE; + Func_void_double_double create_Func_void_double_double(void* NON_NULL swiftClosureWrapper) noexcept; + inline Func_void_double_double_Wrapper wrap_Func_void_double_double(Func_void_double_double value) noexcept { + return Func_void_double_double_Wrapper(std::move(value)); + } + + // pragma MARK: std::optional> + /** + * Specialized version of `std::optional>`. + */ + using std__optional_std__function_void_double____written_____double____total______ = std::optional>; + inline std::optional> create_std__optional_std__function_void_double____written_____double____total______(const std::function& value) noexcept { + return std::optional>(value); + } + inline bool has_value_std__optional_std__function_void_double____written_____double____total______(const std::optional>& optional) noexcept { + return optional.has_value(); + } + inline std::function get_std__optional_std__function_void_double____written_____double____total______(const std::optional>& optional) noexcept { + return optional.value(); + } + + // pragma MARK: std::shared_ptr> + /** + * Specialized version of `std::shared_ptr>`. + */ + using std__shared_ptr_Promise_VideoThumbnailResult__ = std::shared_ptr>; + inline std::shared_ptr> create_std__shared_ptr_Promise_VideoThumbnailResult__() noexcept { + return Promise::create(); + } + inline PromiseHolder wrap_std__shared_ptr_Promise_VideoThumbnailResult__(std::shared_ptr> promise) noexcept { + return PromiseHolder(std::move(promise)); + } + + // pragma MARK: std::function + /** + * Specialized version of `std::function`. + */ + using Func_void_VideoThumbnailResult = std::function; + /** + * Wrapper class for a `std::function`, this can be used from Swift. + */ + class Func_void_VideoThumbnailResult_Wrapper final { + public: + explicit Func_void_VideoThumbnailResult_Wrapper(std::function&& func): _function(std::make_unique>(std::move(func))) {} + inline void call(VideoThumbnailResult result) const noexcept { + _function->operator()(result); + } + private: + std::unique_ptr> _function; + } SWIFT_NONCOPYABLE; + Func_void_VideoThumbnailResult create_Func_void_VideoThumbnailResult(void* NON_NULL swiftClosureWrapper) noexcept; + inline Func_void_VideoThumbnailResult_Wrapper wrap_Func_void_VideoThumbnailResult(Func_void_VideoThumbnailResult value) noexcept { + return Func_void_VideoThumbnailResult_Wrapper(std::move(value)); + } + + // pragma MARK: std::optional + /** + * Specialized version of `std::optional`. + */ + using std__optional_std__string_ = std::optional; + inline std::optional create_std__optional_std__string_(const std::string& value) noexcept { + return std::optional(value); + } + inline bool has_value_std__optional_std__string_(const std::optional& optional) noexcept { + return optional.has_value(); + } + inline std::string get_std__optional_std__string_(const std::optional& optional) noexcept { + return optional.value(); + } + + // pragma MARK: std::shared_ptr + /** + * Specialized version of `std::shared_ptr`. + */ + using std__shared_ptr_HybridCompressorSpec_ = std::shared_ptr; + std::shared_ptr create_std__shared_ptr_HybridCompressorSpec_(void* NON_NULL swiftUnsafePointer) noexcept; + void* NON_NULL get_std__shared_ptr_HybridCompressorSpec_(std__shared_ptr_HybridCompressorSpec_ cppType); + + // pragma MARK: std::weak_ptr + using std__weak_ptr_HybridCompressorSpec_ = std::weak_ptr; + inline std__weak_ptr_HybridCompressorSpec_ weakify_std__shared_ptr_HybridCompressorSpec_(const std::shared_ptr& strong) noexcept { return strong; } + + // pragma MARK: Result>> + using Result_std__shared_ptr_Promise_std__string___ = Result>>; + inline Result_std__shared_ptr_Promise_std__string___ create_Result_std__shared_ptr_Promise_std__string___(const std::shared_ptr>& value) noexcept { + return Result>>::withValue(value); + } + inline Result_std__shared_ptr_Promise_std__string___ create_Result_std__shared_ptr_Promise_std__string___(const std::exception_ptr& error) noexcept { + return Result>>::withError(error); + } + + // pragma MARK: Result>>> + using Result_std__shared_ptr_Promise_std__shared_ptr_AnyMap____ = Result>>>; + inline Result_std__shared_ptr_Promise_std__shared_ptr_AnyMap____ create_Result_std__shared_ptr_Promise_std__shared_ptr_AnyMap____(const std::shared_ptr>>& value) noexcept { + return Result>>>::withValue(value); + } + inline Result_std__shared_ptr_Promise_std__shared_ptr_AnyMap____ create_Result_std__shared_ptr_Promise_std__shared_ptr_AnyMap____(const std::exception_ptr& error) noexcept { + return Result>>>::withError(error); + } + + // pragma MARK: Result + using Result_void_ = Result; + inline Result_void_ create_Result_void_() noexcept { + return Result::withValue(); + } + inline Result_void_ create_Result_void_(const std::exception_ptr& error) noexcept { + return Result::withError(error); + } + + // pragma MARK: Result>> + using Result_std__shared_ptr_Promise_VideoThumbnailResult___ = Result>>; + inline Result_std__shared_ptr_Promise_VideoThumbnailResult___ create_Result_std__shared_ptr_Promise_VideoThumbnailResult___(const std::shared_ptr>& value) noexcept { + return Result>>::withValue(value); + } + inline Result_std__shared_ptr_Promise_VideoThumbnailResult___ create_Result_std__shared_ptr_Promise_VideoThumbnailResult___(const std::exception_ptr& error) noexcept { + return Result>>::withError(error); + } + +} // namespace margelo::nitro::compressor::bridge::swift diff --git a/nitrogen/generated/ios/react_native_compressor-Swift-Cxx-Umbrella.hpp b/nitrogen/generated/ios/react_native_compressor-Swift-Cxx-Umbrella.hpp new file mode 100644 index 00000000..7a634ba1 --- /dev/null +++ b/nitrogen/generated/ios/react_native_compressor-Swift-Cxx-Umbrella.hpp @@ -0,0 +1,51 @@ +/// +/// react_native_compressor-Swift-Cxx-Umbrella.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © Marc Rousavy @ Margelo +/// + +#pragma once + +// Forward declarations of C++ defined types +// Forward declaration of `HybridCompressorSpec` to properly resolve imports. +namespace margelo::nitro::compressor { class HybridCompressorSpec; } +// Forward declaration of `VideoThumbnailResult` to properly resolve imports. +namespace margelo::nitro::compressor { struct VideoThumbnailResult; } + +// Include C++ defined types +#include "HybridCompressorSpec.hpp" +#include "VideoThumbnailResult.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +// C++ helpers for Swift +#include "react_native_compressor-Swift-Cxx-Bridge.hpp" + +// Common C++ types used in Swift +#include +#include +#include +#include + +// Forward declarations of Swift defined types +// Forward declaration of `HybridCompressorSpec_cxx` to properly resolve imports. +namespace react_native_compressor { class HybridCompressorSpec_cxx; } + +// Include Swift defined types +#if __has_include("react_native_compressor-Swift.h") +// This header is generated by Xcode/Swift on every app build. +// If it cannot be found, make sure the Swift module's name (= podspec name) is actually "react_native_compressor". +#include "react_native_compressor-Swift.h" +// Same as above, but used when building with frameworks (`use_frameworks`) +#elif __has_include() +#include +#else +#error react_native_compressor's autogenerated Swift header cannot be found! Make sure the Swift module's name (= podspec name) is actually "react_native_compressor", and try building the app first. +#endif diff --git a/nitrogen/generated/ios/react_native_compressorAutolinking.mm b/nitrogen/generated/ios/react_native_compressorAutolinking.mm new file mode 100644 index 00000000..f9fe8094 --- /dev/null +++ b/nitrogen/generated/ios/react_native_compressorAutolinking.mm @@ -0,0 +1,33 @@ +/// +/// react_native_compressorAutolinking.mm +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © Marc Rousavy @ Margelo +/// + +#import +#import +#import "react_native_compressor-Swift-Cxx-Umbrella.hpp" +#import + +#include "HybridCompressorSpecSwift.hpp" + +@interface react_native_compressorAutolinking : NSObject +@end + +@implementation react_native_compressorAutolinking + ++ (void) load { + using namespace margelo::nitro; + using namespace margelo::nitro::compressor; + + HybridObjectRegistry::registerHybridObjectConstructor( + "Compressor", + []() -> std::shared_ptr { + std::shared_ptr hybridObject = react_native_compressor::react_native_compressorAutolinking::createCompressor(); + return hybridObject; + } + ); +} + +@end diff --git a/nitrogen/generated/ios/react_native_compressorAutolinking.swift b/nitrogen/generated/ios/react_native_compressorAutolinking.swift new file mode 100644 index 00000000..5cc310bb --- /dev/null +++ b/nitrogen/generated/ios/react_native_compressorAutolinking.swift @@ -0,0 +1,26 @@ +/// +/// react_native_compressorAutolinking.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © Marc Rousavy @ Margelo +/// + +import NitroModules + +// TODO: Use empty enums once Swift supports exporting them as namespaces +// See: https://github.com/swiftlang/swift/pull/83616 +public final class react_native_compressorAutolinking { + public typealias bridge = margelo.nitro.compressor.bridge.swift + + public static func createCompressor() -> bridge.std__shared_ptr_HybridCompressorSpec_ { + let hybridObject = HybridCompressor() + return { () -> bridge.std__shared_ptr_HybridCompressorSpec_ in + let __cxxWrapped = hybridObject.getCxxWrapper() + return __cxxWrapped.getCxxPart() + }() + } + + public static func isCompressorRecyclable() -> Bool { + return HybridCompressor.self is any RecyclableView.Type + } +} diff --git a/nitrogen/generated/ios/swift/Func_void.swift b/nitrogen/generated/ios/swift/Func_void.swift new file mode 100644 index 00000000..e017ff5b --- /dev/null +++ b/nitrogen/generated/ios/swift/Func_void.swift @@ -0,0 +1,46 @@ +/// +/// Func_void.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © Marc Rousavy @ Margelo +/// + +import NitroModules + +/** + * Wraps a Swift `() -> Void` as a class. + * This class can be used from C++, e.g. to wrap the Swift closure as a `std::function`. + */ +public final class Func_void { + public typealias bridge = margelo.nitro.compressor.bridge.swift + + private let closure: () -> Void + + public init(_ closure: @escaping () -> Void) { + self.closure = closure + } + + @inline(__always) + public func call() -> Void { + self.closure() + } + + /** + * Casts this instance to a retained unsafe raw pointer. + * This acquires one additional strong reference on the object! + */ + @inline(__always) + public func toUnsafe() -> UnsafeMutableRawPointer { + return Unmanaged.passRetained(self).toOpaque() + } + + /** + * Casts an unsafe pointer to a `Func_void`. + * The pointer has to be a retained opaque `Unmanaged`. + * This removes one strong reference from the object! + */ + @inline(__always) + public static func fromUnsafe(_ pointer: UnsafeMutableRawPointer) -> Func_void { + return Unmanaged.fromOpaque(pointer).takeRetainedValue() + } +} diff --git a/nitrogen/generated/ios/swift/Func_void_VideoThumbnailResult.swift b/nitrogen/generated/ios/swift/Func_void_VideoThumbnailResult.swift new file mode 100644 index 00000000..2f465de7 --- /dev/null +++ b/nitrogen/generated/ios/swift/Func_void_VideoThumbnailResult.swift @@ -0,0 +1,46 @@ +/// +/// Func_void_VideoThumbnailResult.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © Marc Rousavy @ Margelo +/// + +import NitroModules + +/** + * Wraps a Swift `(_ value: VideoThumbnailResult) -> Void` as a class. + * This class can be used from C++, e.g. to wrap the Swift closure as a `std::function`. + */ +public final class Func_void_VideoThumbnailResult { + public typealias bridge = margelo.nitro.compressor.bridge.swift + + private let closure: (_ value: VideoThumbnailResult) -> Void + + public init(_ closure: @escaping (_ value: VideoThumbnailResult) -> Void) { + self.closure = closure + } + + @inline(__always) + public func call(value: VideoThumbnailResult) -> Void { + self.closure(value) + } + + /** + * Casts this instance to a retained unsafe raw pointer. + * This acquires one additional strong reference on the object! + */ + @inline(__always) + public func toUnsafe() -> UnsafeMutableRawPointer { + return Unmanaged.passRetained(self).toOpaque() + } + + /** + * Casts an unsafe pointer to a `Func_void_VideoThumbnailResult`. + * The pointer has to be a retained opaque `Unmanaged`. + * This removes one strong reference from the object! + */ + @inline(__always) + public static func fromUnsafe(_ pointer: UnsafeMutableRawPointer) -> Func_void_VideoThumbnailResult { + return Unmanaged.fromOpaque(pointer).takeRetainedValue() + } +} diff --git a/nitrogen/generated/ios/swift/Func_void_double.swift b/nitrogen/generated/ios/swift/Func_void_double.swift new file mode 100644 index 00000000..493b6e21 --- /dev/null +++ b/nitrogen/generated/ios/swift/Func_void_double.swift @@ -0,0 +1,46 @@ +/// +/// Func_void_double.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © Marc Rousavy @ Margelo +/// + +import NitroModules + +/** + * Wraps a Swift `(_ progress: Double) -> Void` as a class. + * This class can be used from C++, e.g. to wrap the Swift closure as a `std::function`. + */ +public final class Func_void_double { + public typealias bridge = margelo.nitro.compressor.bridge.swift + + private let closure: (_ progress: Double) -> Void + + public init(_ closure: @escaping (_ progress: Double) -> Void) { + self.closure = closure + } + + @inline(__always) + public func call(progress: Double) -> Void { + self.closure(progress) + } + + /** + * Casts this instance to a retained unsafe raw pointer. + * This acquires one additional strong reference on the object! + */ + @inline(__always) + public func toUnsafe() -> UnsafeMutableRawPointer { + return Unmanaged.passRetained(self).toOpaque() + } + + /** + * Casts an unsafe pointer to a `Func_void_double`. + * The pointer has to be a retained opaque `Unmanaged`. + * This removes one strong reference from the object! + */ + @inline(__always) + public static func fromUnsafe(_ pointer: UnsafeMutableRawPointer) -> Func_void_double { + return Unmanaged.fromOpaque(pointer).takeRetainedValue() + } +} diff --git a/nitrogen/generated/ios/swift/Func_void_double_double.swift b/nitrogen/generated/ios/swift/Func_void_double_double.swift new file mode 100644 index 00000000..acf0c2f0 --- /dev/null +++ b/nitrogen/generated/ios/swift/Func_void_double_double.swift @@ -0,0 +1,46 @@ +/// +/// Func_void_double_double.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © Marc Rousavy @ Margelo +/// + +import NitroModules + +/** + * Wraps a Swift `(_ written: Double, _ total: Double) -> Void` as a class. + * This class can be used from C++, e.g. to wrap the Swift closure as a `std::function`. + */ +public final class Func_void_double_double { + public typealias bridge = margelo.nitro.compressor.bridge.swift + + private let closure: (_ written: Double, _ total: Double) -> Void + + public init(_ closure: @escaping (_ written: Double, _ total: Double) -> Void) { + self.closure = closure + } + + @inline(__always) + public func call(written: Double, total: Double) -> Void { + self.closure(written, total) + } + + /** + * Casts this instance to a retained unsafe raw pointer. + * This acquires one additional strong reference on the object! + */ + @inline(__always) + public func toUnsafe() -> UnsafeMutableRawPointer { + return Unmanaged.passRetained(self).toOpaque() + } + + /** + * Casts an unsafe pointer to a `Func_void_double_double`. + * The pointer has to be a retained opaque `Unmanaged`. + * This removes one strong reference from the object! + */ + @inline(__always) + public static func fromUnsafe(_ pointer: UnsafeMutableRawPointer) -> Func_void_double_double { + return Unmanaged.fromOpaque(pointer).takeRetainedValue() + } +} diff --git a/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift b/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift new file mode 100644 index 00000000..a575b39d --- /dev/null +++ b/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift @@ -0,0 +1,46 @@ +/// +/// Func_void_std__exception_ptr.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © Marc Rousavy @ Margelo +/// + +import NitroModules + +/** + * Wraps a Swift `(_ error: Error) -> Void` as a class. + * This class can be used from C++, e.g. to wrap the Swift closure as a `std::function`. + */ +public final class Func_void_std__exception_ptr { + public typealias bridge = margelo.nitro.compressor.bridge.swift + + private let closure: (_ error: Error) -> Void + + public init(_ closure: @escaping (_ error: Error) -> Void) { + self.closure = closure + } + + @inline(__always) + public func call(error: std.exception_ptr) -> Void { + self.closure(RuntimeError.from(cppError: error)) + } + + /** + * Casts this instance to a retained unsafe raw pointer. + * This acquires one additional strong reference on the object! + */ + @inline(__always) + public func toUnsafe() -> UnsafeMutableRawPointer { + return Unmanaged.passRetained(self).toOpaque() + } + + /** + * Casts an unsafe pointer to a `Func_void_std__exception_ptr`. + * The pointer has to be a retained opaque `Unmanaged`. + * This removes one strong reference from the object! + */ + @inline(__always) + public static func fromUnsafe(_ pointer: UnsafeMutableRawPointer) -> Func_void_std__exception_ptr { + return Unmanaged.fromOpaque(pointer).takeRetainedValue() + } +} diff --git a/nitrogen/generated/ios/swift/Func_void_std__shared_ptr_AnyMap_.swift b/nitrogen/generated/ios/swift/Func_void_std__shared_ptr_AnyMap_.swift new file mode 100644 index 00000000..2d5a37c5 --- /dev/null +++ b/nitrogen/generated/ios/swift/Func_void_std__shared_ptr_AnyMap_.swift @@ -0,0 +1,46 @@ +/// +/// Func_void_std__shared_ptr_AnyMap_.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © Marc Rousavy @ Margelo +/// + +import NitroModules + +/** + * Wraps a Swift `(_ value: AnyMap) -> Void` as a class. + * This class can be used from C++, e.g. to wrap the Swift closure as a `std::function`. + */ +public final class Func_void_std__shared_ptr_AnyMap_ { + public typealias bridge = margelo.nitro.compressor.bridge.swift + + private let closure: (_ value: AnyMap) -> Void + + public init(_ closure: @escaping (_ value: AnyMap) -> Void) { + self.closure = closure + } + + @inline(__always) + public func call(value: margelo.nitro.SharedAnyMap) -> Void { + self.closure(AnyMap(withCppPart: value)) + } + + /** + * Casts this instance to a retained unsafe raw pointer. + * This acquires one additional strong reference on the object! + */ + @inline(__always) + public func toUnsafe() -> UnsafeMutableRawPointer { + return Unmanaged.passRetained(self).toOpaque() + } + + /** + * Casts an unsafe pointer to a `Func_void_std__shared_ptr_AnyMap_`. + * The pointer has to be a retained opaque `Unmanaged`. + * This removes one strong reference from the object! + */ + @inline(__always) + public static func fromUnsafe(_ pointer: UnsafeMutableRawPointer) -> Func_void_std__shared_ptr_AnyMap_ { + return Unmanaged.fromOpaque(pointer).takeRetainedValue() + } +} diff --git a/nitrogen/generated/ios/swift/Func_void_std__string.swift b/nitrogen/generated/ios/swift/Func_void_std__string.swift new file mode 100644 index 00000000..c13cb0d6 --- /dev/null +++ b/nitrogen/generated/ios/swift/Func_void_std__string.swift @@ -0,0 +1,46 @@ +/// +/// Func_void_std__string.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © Marc Rousavy @ Margelo +/// + +import NitroModules + +/** + * Wraps a Swift `(_ value: String) -> Void` as a class. + * This class can be used from C++, e.g. to wrap the Swift closure as a `std::function`. + */ +public final class Func_void_std__string { + public typealias bridge = margelo.nitro.compressor.bridge.swift + + private let closure: (_ value: String) -> Void + + public init(_ closure: @escaping (_ value: String) -> Void) { + self.closure = closure + } + + @inline(__always) + public func call(value: std.string) -> Void { + self.closure(String(value)) + } + + /** + * Casts this instance to a retained unsafe raw pointer. + * This acquires one additional strong reference on the object! + */ + @inline(__always) + public func toUnsafe() -> UnsafeMutableRawPointer { + return Unmanaged.passRetained(self).toOpaque() + } + + /** + * Casts an unsafe pointer to a `Func_void_std__string`. + * The pointer has to be a retained opaque `Unmanaged`. + * This removes one strong reference from the object! + */ + @inline(__always) + public static func fromUnsafe(_ pointer: UnsafeMutableRawPointer) -> Func_void_std__string { + return Unmanaged.fromOpaque(pointer).takeRetainedValue() + } +} diff --git a/nitrogen/generated/ios/swift/HybridCompressorSpec.swift b/nitrogen/generated/ios/swift/HybridCompressorSpec.swift new file mode 100644 index 00000000..86deb91e --- /dev/null +++ b/nitrogen/generated/ios/swift/HybridCompressorSpec.swift @@ -0,0 +1,70 @@ +/// +/// HybridCompressorSpec.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © Marc Rousavy @ Margelo +/// + +import NitroModules + +/// See ``HybridCompressorSpec`` +public protocol HybridCompressorSpec_protocol: HybridObject { + // Properties + + + // Methods + func image_compress(imagePath: String, optionMap: AnyMap, onDownloadProgress: ((_ progress: Double) -> Void)?) throws -> Promise + func getImageMetaData(filePath: String) throws -> Promise + func compress(fileUrl: String, optionMap: AnyMap, onProgress: ((_ progress: Double) -> Void)?, onDownloadProgress: ((_ progress: Double) -> Void)?) throws -> Promise + func cancelCompression(uuid: String) throws -> Void + func getVideoMetaData(filePath: String) throws -> Promise + func activateBackgroundTask(options: AnyMap, onExpired: (() -> Void)?) throws -> Promise + func deactivateBackgroundTask(options: AnyMap) throws -> Promise + func compress_audio(fileUrl: String, optionMap: AnyMap) throws -> Promise + func upload(fileUrl: String, options: AnyMap, onProgress: ((_ written: Double, _ total: Double) -> Void)?) throws -> Promise + func cancelUpload(uuid: String, shouldCancelAll: Bool) throws -> Void + func download(fileUrl: String, options: AnyMap, onProgress: ((_ progress: Double) -> Void)?) throws -> Promise + func generateFilePath(fileExtension: String) throws -> Promise + func getRealPath(path: String, type: String) throws -> Promise + func getFileSize(filePath: String) throws -> Promise + func createVideoThumbnail(fileUrl: String, options: AnyMap) throws -> Promise + func clearCache(cacheDir: String?) throws -> Promise +} + +public extension HybridCompressorSpec_protocol { + /// Default implementation of ``HybridObject.toString`` + func toString() -> String { + return "[HybridObject Compressor]" + } +} + +/// See ``HybridCompressorSpec`` +open class HybridCompressorSpec_base { + private weak var cxxWrapper: HybridCompressorSpec_cxx? = nil + public init() { } + public func getCxxWrapper() -> HybridCompressorSpec_cxx { + #if DEBUG + guard self is any HybridCompressorSpec else { + fatalError("`self` is not a `HybridCompressorSpec`! Did you accidentally inherit from `HybridCompressorSpec_base` instead of `HybridCompressorSpec`?") + } + #endif + if let cxxWrapper = self.cxxWrapper { + return cxxWrapper + } else { + let cxxWrapper = HybridCompressorSpec_cxx(self as! any HybridCompressorSpec) + self.cxxWrapper = cxxWrapper + return cxxWrapper + } + } +} + +/** + * A Swift base-protocol representing the Compressor HybridObject. + * Implement this protocol to create Swift-based instances of Compressor. + * ```swift + * class HybridCompressor : HybridCompressorSpec { + * // ... + * } + * ``` + */ +public typealias HybridCompressorSpec = HybridCompressorSpec_protocol & HybridCompressorSpec_base diff --git a/nitrogen/generated/ios/swift/HybridCompressorSpec_cxx.swift b/nitrogen/generated/ios/swift/HybridCompressorSpec_cxx.swift new file mode 100644 index 00000000..070fe99c --- /dev/null +++ b/nitrogen/generated/ios/swift/HybridCompressorSpec_cxx.swift @@ -0,0 +1,493 @@ +/// +/// HybridCompressorSpec_cxx.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © Marc Rousavy @ Margelo +/// + +import NitroModules + +/** + * A class implementation that bridges HybridCompressorSpec over to C++. + * In C++, we cannot use Swift protocols - so we need to wrap it in a class to make it strongly defined. + * + * Also, some Swift types need to be bridged with special handling: + * - Enums need to be wrapped in Structs, otherwise they cannot be accessed bi-directionally (Swift bug: https://github.com/swiftlang/swift/issues/75330) + * - Other HybridObjects need to be wrapped/unwrapped from the Swift TCxx wrapper + * - Throwing methods need to be wrapped with a Result type, as exceptions cannot be propagated to C++ + */ +open class HybridCompressorSpec_cxx { + /** + * The Swift <> C++ bridge's namespace (`margelo::nitro::compressor::bridge::swift`) + * from `react_native_compressor-Swift-Cxx-Bridge.hpp`. + * This contains specialized C++ templates, and C++ helper functions that can be accessed from Swift. + */ + public typealias bridge = margelo.nitro.compressor.bridge.swift + + /** + * Holds an instance of the `HybridCompressorSpec` Swift protocol. + */ + private var __implementation: any HybridCompressorSpec + + /** + * Holds a weak pointer to the C++ class that wraps the Swift class. + */ + private var __cxxPart: bridge.std__weak_ptr_HybridCompressorSpec_ + + /** + * Create a new `HybridCompressorSpec_cxx` that wraps the given `HybridCompressorSpec`. + * All properties and methods bridge to C++ types. + */ + public init(_ implementation: any HybridCompressorSpec) { + self.__implementation = implementation + self.__cxxPart = .init() + /* no base class */ + } + + /** + * Get the actual `HybridCompressorSpec` instance this class wraps. + */ + @inline(__always) + public func getHybridCompressorSpec() -> any HybridCompressorSpec { + return __implementation + } + + /** + * Casts this instance to a retained unsafe raw pointer. + * This acquires one additional strong reference on the object! + */ + public func toUnsafe() -> UnsafeMutableRawPointer { + return Unmanaged.passRetained(self).toOpaque() + } + + /** + * Casts an unsafe pointer to a `HybridCompressorSpec_cxx`. + * The pointer has to be a retained opaque `Unmanaged`. + * This removes one strong reference from the object! + */ + public class func fromUnsafe(_ pointer: UnsafeMutableRawPointer) -> HybridCompressorSpec_cxx { + return Unmanaged.fromOpaque(pointer).takeRetainedValue() + } + + /** + * Gets (or creates) the C++ part of this Hybrid Object. + * The C++ part is a `std::shared_ptr`. + */ + public func getCxxPart() -> bridge.std__shared_ptr_HybridCompressorSpec_ { + let cachedCxxPart = self.__cxxPart.lock() + if Bool(fromCxx: cachedCxxPart) { + return cachedCxxPart + } else { + let newCxxPart = bridge.create_std__shared_ptr_HybridCompressorSpec_(self.toUnsafe()) + __cxxPart = bridge.weakify_std__shared_ptr_HybridCompressorSpec_(newCxxPart) + return newCxxPart + } + } + + + + /** + * Get the memory size of the Swift class (plus size of any other allocations) + * so the JS VM can properly track it and garbage-collect the JS object if needed. + */ + @inline(__always) + public var memorySize: Int { + return MemoryHelper.getSizeOf(self.__implementation) + self.__implementation.memorySize + } + + /** + * Compares this object with the given [other] object for reference equality. + */ + @inline(__always) + public func equals(other: HybridCompressorSpec_cxx) -> Bool { + return self.__implementation === other.__implementation + } + + /** + * Call dispose() on the Swift class. + * This _may_ be called manually from JS. + */ + @inline(__always) + public func dispose() { + self.__implementation.dispose() + } + + /** + * Call toString() on the Swift class. + */ + @inline(__always) + public func toString() -> String { + return self.__implementation.toString() + } + + // Properties + + + // Methods + @inline(__always) + public final func image_compress(imagePath: std.string, optionMap: margelo.nitro.SharedAnyMap, onDownloadProgress: bridge.std__optional_std__function_void_double____progress______) -> bridge.Result_std__shared_ptr_Promise_std__string___ { + do { + let __result = try self.__implementation.image_compress(imagePath: String(imagePath), optionMap: AnyMap(withCppPart: optionMap), onDownloadProgress: { () -> ((_ progress: Double) -> Void)? in + if bridge.has_value_std__optional_std__function_void_double____progress______(onDownloadProgress) { + let __unwrapped = bridge.get_std__optional_std__function_void_double____progress______(onDownloadProgress) + return { () -> (Double) -> Void in + let __wrappedFunction = bridge.wrap_Func_void_double(__unwrapped) + return { (__progress: Double) -> Void in + __wrappedFunction.call(__progress) + } + }() + } else { + return nil + } + }()) + let __resultCpp = { () -> bridge.std__shared_ptr_Promise_std__string__ in + let __promise = bridge.create_std__shared_ptr_Promise_std__string__() + let __promiseHolder = bridge.wrap_std__shared_ptr_Promise_std__string__(__promise) + __result + .then({ __result in __promiseHolder.resolve(std.string(__result)) }) + .catch({ __error in __promiseHolder.reject(__error.toCpp()) }) + return __promise + }() + return bridge.create_Result_std__shared_ptr_Promise_std__string___(__resultCpp) + } catch (let __error) { + let __exceptionPtr = __error.toCpp() + return bridge.create_Result_std__shared_ptr_Promise_std__string___(__exceptionPtr) + } + } + + @inline(__always) + public final func getImageMetaData(filePath: std.string) -> bridge.Result_std__shared_ptr_Promise_std__shared_ptr_AnyMap____ { + do { + let __result = try self.__implementation.getImageMetaData(filePath: String(filePath)) + let __resultCpp = { () -> bridge.std__shared_ptr_Promise_std__shared_ptr_AnyMap___ in + let __promise = bridge.create_std__shared_ptr_Promise_std__shared_ptr_AnyMap___() + let __promiseHolder = bridge.wrap_std__shared_ptr_Promise_std__shared_ptr_AnyMap___(__promise) + __result + .then({ __result in __promiseHolder.resolve(__result.cppPart) }) + .catch({ __error in __promiseHolder.reject(__error.toCpp()) }) + return __promise + }() + return bridge.create_Result_std__shared_ptr_Promise_std__shared_ptr_AnyMap____(__resultCpp) + } catch (let __error) { + let __exceptionPtr = __error.toCpp() + return bridge.create_Result_std__shared_ptr_Promise_std__shared_ptr_AnyMap____(__exceptionPtr) + } + } + + @inline(__always) + public final func compress(fileUrl: std.string, optionMap: margelo.nitro.SharedAnyMap, onProgress: bridge.std__optional_std__function_void_double____progress______, onDownloadProgress: bridge.std__optional_std__function_void_double____progress______) -> bridge.Result_std__shared_ptr_Promise_std__string___ { + do { + let __result = try self.__implementation.compress(fileUrl: String(fileUrl), optionMap: AnyMap(withCppPart: optionMap), onProgress: { () -> ((_ progress: Double) -> Void)? in + if bridge.has_value_std__optional_std__function_void_double____progress______(onProgress) { + let __unwrapped = bridge.get_std__optional_std__function_void_double____progress______(onProgress) + return { () -> (Double) -> Void in + let __wrappedFunction = bridge.wrap_Func_void_double(__unwrapped) + return { (__progress: Double) -> Void in + __wrappedFunction.call(__progress) + } + }() + } else { + return nil + } + }(), onDownloadProgress: { () -> ((_ progress: Double) -> Void)? in + if bridge.has_value_std__optional_std__function_void_double____progress______(onDownloadProgress) { + let __unwrapped = bridge.get_std__optional_std__function_void_double____progress______(onDownloadProgress) + return { () -> (Double) -> Void in + let __wrappedFunction = bridge.wrap_Func_void_double(__unwrapped) + return { (__progress: Double) -> Void in + __wrappedFunction.call(__progress) + } + }() + } else { + return nil + } + }()) + let __resultCpp = { () -> bridge.std__shared_ptr_Promise_std__string__ in + let __promise = bridge.create_std__shared_ptr_Promise_std__string__() + let __promiseHolder = bridge.wrap_std__shared_ptr_Promise_std__string__(__promise) + __result + .then({ __result in __promiseHolder.resolve(std.string(__result)) }) + .catch({ __error in __promiseHolder.reject(__error.toCpp()) }) + return __promise + }() + return bridge.create_Result_std__shared_ptr_Promise_std__string___(__resultCpp) + } catch (let __error) { + let __exceptionPtr = __error.toCpp() + return bridge.create_Result_std__shared_ptr_Promise_std__string___(__exceptionPtr) + } + } + + @inline(__always) + public final func cancelCompression(uuid: std.string) -> bridge.Result_void_ { + do { + try self.__implementation.cancelCompression(uuid: String(uuid)) + return bridge.create_Result_void_() + } catch (let __error) { + let __exceptionPtr = __error.toCpp() + return bridge.create_Result_void_(__exceptionPtr) + } + } + + @inline(__always) + public final func getVideoMetaData(filePath: std.string) -> bridge.Result_std__shared_ptr_Promise_std__shared_ptr_AnyMap____ { + do { + let __result = try self.__implementation.getVideoMetaData(filePath: String(filePath)) + let __resultCpp = { () -> bridge.std__shared_ptr_Promise_std__shared_ptr_AnyMap___ in + let __promise = bridge.create_std__shared_ptr_Promise_std__shared_ptr_AnyMap___() + let __promiseHolder = bridge.wrap_std__shared_ptr_Promise_std__shared_ptr_AnyMap___(__promise) + __result + .then({ __result in __promiseHolder.resolve(__result.cppPart) }) + .catch({ __error in __promiseHolder.reject(__error.toCpp()) }) + return __promise + }() + return bridge.create_Result_std__shared_ptr_Promise_std__shared_ptr_AnyMap____(__resultCpp) + } catch (let __error) { + let __exceptionPtr = __error.toCpp() + return bridge.create_Result_std__shared_ptr_Promise_std__shared_ptr_AnyMap____(__exceptionPtr) + } + } + + @inline(__always) + public final func activateBackgroundTask(options: margelo.nitro.SharedAnyMap, onExpired: bridge.std__optional_std__function_void____) -> bridge.Result_std__shared_ptr_Promise_std__string___ { + do { + let __result = try self.__implementation.activateBackgroundTask(options: AnyMap(withCppPart: options), onExpired: { () -> (() -> Void)? in + if bridge.has_value_std__optional_std__function_void____(onExpired) { + let __unwrapped = bridge.get_std__optional_std__function_void____(onExpired) + return { () -> () -> Void in + let __wrappedFunction = bridge.wrap_Func_void(__unwrapped) + return { () -> Void in + __wrappedFunction.call() + } + }() + } else { + return nil + } + }()) + let __resultCpp = { () -> bridge.std__shared_ptr_Promise_std__string__ in + let __promise = bridge.create_std__shared_ptr_Promise_std__string__() + let __promiseHolder = bridge.wrap_std__shared_ptr_Promise_std__string__(__promise) + __result + .then({ __result in __promiseHolder.resolve(std.string(__result)) }) + .catch({ __error in __promiseHolder.reject(__error.toCpp()) }) + return __promise + }() + return bridge.create_Result_std__shared_ptr_Promise_std__string___(__resultCpp) + } catch (let __error) { + let __exceptionPtr = __error.toCpp() + return bridge.create_Result_std__shared_ptr_Promise_std__string___(__exceptionPtr) + } + } + + @inline(__always) + public final func deactivateBackgroundTask(options: margelo.nitro.SharedAnyMap) -> bridge.Result_std__shared_ptr_Promise_std__string___ { + do { + let __result = try self.__implementation.deactivateBackgroundTask(options: AnyMap(withCppPart: options)) + let __resultCpp = { () -> bridge.std__shared_ptr_Promise_std__string__ in + let __promise = bridge.create_std__shared_ptr_Promise_std__string__() + let __promiseHolder = bridge.wrap_std__shared_ptr_Promise_std__string__(__promise) + __result + .then({ __result in __promiseHolder.resolve(std.string(__result)) }) + .catch({ __error in __promiseHolder.reject(__error.toCpp()) }) + return __promise + }() + return bridge.create_Result_std__shared_ptr_Promise_std__string___(__resultCpp) + } catch (let __error) { + let __exceptionPtr = __error.toCpp() + return bridge.create_Result_std__shared_ptr_Promise_std__string___(__exceptionPtr) + } + } + + @inline(__always) + public final func compress_audio(fileUrl: std.string, optionMap: margelo.nitro.SharedAnyMap) -> bridge.Result_std__shared_ptr_Promise_std__string___ { + do { + let __result = try self.__implementation.compress_audio(fileUrl: String(fileUrl), optionMap: AnyMap(withCppPart: optionMap)) + let __resultCpp = { () -> bridge.std__shared_ptr_Promise_std__string__ in + let __promise = bridge.create_std__shared_ptr_Promise_std__string__() + let __promiseHolder = bridge.wrap_std__shared_ptr_Promise_std__string__(__promise) + __result + .then({ __result in __promiseHolder.resolve(std.string(__result)) }) + .catch({ __error in __promiseHolder.reject(__error.toCpp()) }) + return __promise + }() + return bridge.create_Result_std__shared_ptr_Promise_std__string___(__resultCpp) + } catch (let __error) { + let __exceptionPtr = __error.toCpp() + return bridge.create_Result_std__shared_ptr_Promise_std__string___(__exceptionPtr) + } + } + + @inline(__always) + public final func upload(fileUrl: std.string, options: margelo.nitro.SharedAnyMap, onProgress: bridge.std__optional_std__function_void_double____written_____double____total______) -> bridge.Result_std__shared_ptr_Promise_std__shared_ptr_AnyMap____ { + do { + let __result = try self.__implementation.upload(fileUrl: String(fileUrl), options: AnyMap(withCppPart: options), onProgress: { () -> ((_ written: Double, _ total: Double) -> Void)? in + if bridge.has_value_std__optional_std__function_void_double____written_____double____total______(onProgress) { + let __unwrapped = bridge.get_std__optional_std__function_void_double____written_____double____total______(onProgress) + return { () -> (Double, Double) -> Void in + let __wrappedFunction = bridge.wrap_Func_void_double_double(__unwrapped) + return { (__written: Double, __total: Double) -> Void in + __wrappedFunction.call(__written, __total) + } + }() + } else { + return nil + } + }()) + let __resultCpp = { () -> bridge.std__shared_ptr_Promise_std__shared_ptr_AnyMap___ in + let __promise = bridge.create_std__shared_ptr_Promise_std__shared_ptr_AnyMap___() + let __promiseHolder = bridge.wrap_std__shared_ptr_Promise_std__shared_ptr_AnyMap___(__promise) + __result + .then({ __result in __promiseHolder.resolve(__result.cppPart) }) + .catch({ __error in __promiseHolder.reject(__error.toCpp()) }) + return __promise + }() + return bridge.create_Result_std__shared_ptr_Promise_std__shared_ptr_AnyMap____(__resultCpp) + } catch (let __error) { + let __exceptionPtr = __error.toCpp() + return bridge.create_Result_std__shared_ptr_Promise_std__shared_ptr_AnyMap____(__exceptionPtr) + } + } + + @inline(__always) + public final func cancelUpload(uuid: std.string, shouldCancelAll: Bool) -> bridge.Result_void_ { + do { + try self.__implementation.cancelUpload(uuid: String(uuid), shouldCancelAll: shouldCancelAll) + return bridge.create_Result_void_() + } catch (let __error) { + let __exceptionPtr = __error.toCpp() + return bridge.create_Result_void_(__exceptionPtr) + } + } + + @inline(__always) + public final func download(fileUrl: std.string, options: margelo.nitro.SharedAnyMap, onProgress: bridge.std__optional_std__function_void_double____progress______) -> bridge.Result_std__shared_ptr_Promise_std__string___ { + do { + let __result = try self.__implementation.download(fileUrl: String(fileUrl), options: AnyMap(withCppPart: options), onProgress: { () -> ((_ progress: Double) -> Void)? in + if bridge.has_value_std__optional_std__function_void_double____progress______(onProgress) { + let __unwrapped = bridge.get_std__optional_std__function_void_double____progress______(onProgress) + return { () -> (Double) -> Void in + let __wrappedFunction = bridge.wrap_Func_void_double(__unwrapped) + return { (__progress: Double) -> Void in + __wrappedFunction.call(__progress) + } + }() + } else { + return nil + } + }()) + let __resultCpp = { () -> bridge.std__shared_ptr_Promise_std__string__ in + let __promise = bridge.create_std__shared_ptr_Promise_std__string__() + let __promiseHolder = bridge.wrap_std__shared_ptr_Promise_std__string__(__promise) + __result + .then({ __result in __promiseHolder.resolve(std.string(__result)) }) + .catch({ __error in __promiseHolder.reject(__error.toCpp()) }) + return __promise + }() + return bridge.create_Result_std__shared_ptr_Promise_std__string___(__resultCpp) + } catch (let __error) { + let __exceptionPtr = __error.toCpp() + return bridge.create_Result_std__shared_ptr_Promise_std__string___(__exceptionPtr) + } + } + + @inline(__always) + public final func generateFilePath(fileExtension: std.string) -> bridge.Result_std__shared_ptr_Promise_std__string___ { + do { + let __result = try self.__implementation.generateFilePath(fileExtension: String(fileExtension)) + let __resultCpp = { () -> bridge.std__shared_ptr_Promise_std__string__ in + let __promise = bridge.create_std__shared_ptr_Promise_std__string__() + let __promiseHolder = bridge.wrap_std__shared_ptr_Promise_std__string__(__promise) + __result + .then({ __result in __promiseHolder.resolve(std.string(__result)) }) + .catch({ __error in __promiseHolder.reject(__error.toCpp()) }) + return __promise + }() + return bridge.create_Result_std__shared_ptr_Promise_std__string___(__resultCpp) + } catch (let __error) { + let __exceptionPtr = __error.toCpp() + return bridge.create_Result_std__shared_ptr_Promise_std__string___(__exceptionPtr) + } + } + + @inline(__always) + public final func getRealPath(path: std.string, type: std.string) -> bridge.Result_std__shared_ptr_Promise_std__string___ { + do { + let __result = try self.__implementation.getRealPath(path: String(path), type: String(type)) + let __resultCpp = { () -> bridge.std__shared_ptr_Promise_std__string__ in + let __promise = bridge.create_std__shared_ptr_Promise_std__string__() + let __promiseHolder = bridge.wrap_std__shared_ptr_Promise_std__string__(__promise) + __result + .then({ __result in __promiseHolder.resolve(std.string(__result)) }) + .catch({ __error in __promiseHolder.reject(__error.toCpp()) }) + return __promise + }() + return bridge.create_Result_std__shared_ptr_Promise_std__string___(__resultCpp) + } catch (let __error) { + let __exceptionPtr = __error.toCpp() + return bridge.create_Result_std__shared_ptr_Promise_std__string___(__exceptionPtr) + } + } + + @inline(__always) + public final func getFileSize(filePath: std.string) -> bridge.Result_std__shared_ptr_Promise_std__string___ { + do { + let __result = try self.__implementation.getFileSize(filePath: String(filePath)) + let __resultCpp = { () -> bridge.std__shared_ptr_Promise_std__string__ in + let __promise = bridge.create_std__shared_ptr_Promise_std__string__() + let __promiseHolder = bridge.wrap_std__shared_ptr_Promise_std__string__(__promise) + __result + .then({ __result in __promiseHolder.resolve(std.string(__result)) }) + .catch({ __error in __promiseHolder.reject(__error.toCpp()) }) + return __promise + }() + return bridge.create_Result_std__shared_ptr_Promise_std__string___(__resultCpp) + } catch (let __error) { + let __exceptionPtr = __error.toCpp() + return bridge.create_Result_std__shared_ptr_Promise_std__string___(__exceptionPtr) + } + } + + @inline(__always) + public final func createVideoThumbnail(fileUrl: std.string, options: margelo.nitro.SharedAnyMap) -> bridge.Result_std__shared_ptr_Promise_VideoThumbnailResult___ { + do { + let __result = try self.__implementation.createVideoThumbnail(fileUrl: String(fileUrl), options: AnyMap(withCppPart: options)) + let __resultCpp = { () -> bridge.std__shared_ptr_Promise_VideoThumbnailResult__ in + let __promise = bridge.create_std__shared_ptr_Promise_VideoThumbnailResult__() + let __promiseHolder = bridge.wrap_std__shared_ptr_Promise_VideoThumbnailResult__(__promise) + __result + .then({ __result in __promiseHolder.resolve(__result) }) + .catch({ __error in __promiseHolder.reject(__error.toCpp()) }) + return __promise + }() + return bridge.create_Result_std__shared_ptr_Promise_VideoThumbnailResult___(__resultCpp) + } catch (let __error) { + let __exceptionPtr = __error.toCpp() + return bridge.create_Result_std__shared_ptr_Promise_VideoThumbnailResult___(__exceptionPtr) + } + } + + @inline(__always) + public final func clearCache(cacheDir: bridge.std__optional_std__string_) -> bridge.Result_std__shared_ptr_Promise_std__string___ { + do { + let __result = try self.__implementation.clearCache(cacheDir: { () -> String? in + if bridge.has_value_std__optional_std__string_(cacheDir) { + let __unwrapped = bridge.get_std__optional_std__string_(cacheDir) + return String(__unwrapped) + } else { + return nil + } + }()) + let __resultCpp = { () -> bridge.std__shared_ptr_Promise_std__string__ in + let __promise = bridge.create_std__shared_ptr_Promise_std__string__() + let __promiseHolder = bridge.wrap_std__shared_ptr_Promise_std__string__(__promise) + __result + .then({ __result in __promiseHolder.resolve(std.string(__result)) }) + .catch({ __error in __promiseHolder.reject(__error.toCpp()) }) + return __promise + }() + return bridge.create_Result_std__shared_ptr_Promise_std__string___(__resultCpp) + } catch (let __error) { + let __exceptionPtr = __error.toCpp() + return bridge.create_Result_std__shared_ptr_Promise_std__string___(__exceptionPtr) + } + } +} diff --git a/nitrogen/generated/ios/swift/VideoThumbnailResult.swift b/nitrogen/generated/ios/swift/VideoThumbnailResult.swift new file mode 100644 index 00000000..b20832c6 --- /dev/null +++ b/nitrogen/generated/ios/swift/VideoThumbnailResult.swift @@ -0,0 +1,49 @@ +/// +/// VideoThumbnailResult.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © Marc Rousavy @ Margelo +/// + +import NitroModules + +/** + * Represents an instance of `VideoThumbnailResult`, backed by a C++ struct. + */ +public typealias VideoThumbnailResult = margelo.nitro.compressor.VideoThumbnailResult + +public extension VideoThumbnailResult { + private typealias bridge = margelo.nitro.compressor.bridge.swift + + /** + * Create a new instance of `VideoThumbnailResult`. + */ + init(path: String, size: Double, mime: String, width: Double, height: Double) { + self.init(std.string(path), size, std.string(mime), width, height) + } + + @inline(__always) + var path: String { + return String(self.__path) + } + + @inline(__always) + var size: Double { + return self.__size + } + + @inline(__always) + var mime: String { + return String(self.__mime) + } + + @inline(__always) + var width: Double { + return self.__width + } + + @inline(__always) + var height: Double { + return self.__height + } +} diff --git a/nitrogen/generated/shared/c++/HybridCompressorSpec.cpp b/nitrogen/generated/shared/c++/HybridCompressorSpec.cpp new file mode 100644 index 00000000..5a6cf931 --- /dev/null +++ b/nitrogen/generated/shared/c++/HybridCompressorSpec.cpp @@ -0,0 +1,36 @@ +/// +/// HybridCompressorSpec.cpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © Marc Rousavy @ Margelo +/// + +#include "HybridCompressorSpec.hpp" + +namespace margelo::nitro::compressor { + + void HybridCompressorSpec::loadHybridMethods() { + // load base methods/properties + HybridObject::loadHybridMethods(); + // load custom methods/properties + registerHybrids(this, [](Prototype& prototype) { + prototype.registerHybridMethod("image_compress", &HybridCompressorSpec::image_compress); + prototype.registerHybridMethod("getImageMetaData", &HybridCompressorSpec::getImageMetaData); + prototype.registerHybridMethod("compress", &HybridCompressorSpec::compress); + prototype.registerHybridMethod("cancelCompression", &HybridCompressorSpec::cancelCompression); + prototype.registerHybridMethod("getVideoMetaData", &HybridCompressorSpec::getVideoMetaData); + prototype.registerHybridMethod("activateBackgroundTask", &HybridCompressorSpec::activateBackgroundTask); + prototype.registerHybridMethod("deactivateBackgroundTask", &HybridCompressorSpec::deactivateBackgroundTask); + prototype.registerHybridMethod("compress_audio", &HybridCompressorSpec::compress_audio); + prototype.registerHybridMethod("upload", &HybridCompressorSpec::upload); + prototype.registerHybridMethod("cancelUpload", &HybridCompressorSpec::cancelUpload); + prototype.registerHybridMethod("download", &HybridCompressorSpec::download); + prototype.registerHybridMethod("generateFilePath", &HybridCompressorSpec::generateFilePath); + prototype.registerHybridMethod("getRealPath", &HybridCompressorSpec::getRealPath); + prototype.registerHybridMethod("getFileSize", &HybridCompressorSpec::getFileSize); + prototype.registerHybridMethod("createVideoThumbnail", &HybridCompressorSpec::createVideoThumbnail); + prototype.registerHybridMethod("clearCache", &HybridCompressorSpec::clearCache); + }); + } + +} // namespace margelo::nitro::compressor diff --git a/nitrogen/generated/shared/c++/HybridCompressorSpec.hpp b/nitrogen/generated/shared/c++/HybridCompressorSpec.hpp new file mode 100644 index 00000000..b7029b7f --- /dev/null +++ b/nitrogen/generated/shared/c++/HybridCompressorSpec.hpp @@ -0,0 +1,83 @@ +/// +/// HybridCompressorSpec.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © Marc Rousavy @ Margelo +/// + +#pragma once + +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif + +// Forward declaration of `VideoThumbnailResult` to properly resolve imports. +namespace margelo::nitro::compressor { struct VideoThumbnailResult; } + +#include +#include +#include +#include +#include +#include "VideoThumbnailResult.hpp" + +namespace margelo::nitro::compressor { + + using namespace margelo::nitro; + + /** + * An abstract base class for `Compressor` + * Inherit this class to create instances of `HybridCompressorSpec` in C++. + * You must explicitly call `HybridObject`'s constructor yourself, because it is virtual. + * @example + * ```cpp + * class HybridCompressor: public HybridCompressorSpec { + * public: + * HybridCompressor(...): HybridObject(TAG) { ... } + * // ... + * }; + * ``` + */ + class HybridCompressorSpec: public virtual HybridObject { + public: + // Constructor + explicit HybridCompressorSpec(): HybridObject(TAG) { } + + // Destructor + ~HybridCompressorSpec() override = default; + + public: + // Properties + + + public: + // Methods + virtual std::shared_ptr> image_compress(const std::string& imagePath, const std::shared_ptr& optionMap, const std::optional>& onDownloadProgress) = 0; + virtual std::shared_ptr>> getImageMetaData(const std::string& filePath) = 0; + virtual std::shared_ptr> compress(const std::string& fileUrl, const std::shared_ptr& optionMap, const std::optional>& onProgress, const std::optional>& onDownloadProgress) = 0; + virtual void cancelCompression(const std::string& uuid) = 0; + virtual std::shared_ptr>> getVideoMetaData(const std::string& filePath) = 0; + virtual std::shared_ptr> activateBackgroundTask(const std::shared_ptr& options, const std::optional>& onExpired) = 0; + virtual std::shared_ptr> deactivateBackgroundTask(const std::shared_ptr& options) = 0; + virtual std::shared_ptr> compress_audio(const std::string& fileUrl, const std::shared_ptr& optionMap) = 0; + virtual std::shared_ptr>> upload(const std::string& fileUrl, const std::shared_ptr& options, const std::optional>& onProgress) = 0; + virtual void cancelUpload(const std::string& uuid, bool shouldCancelAll) = 0; + virtual std::shared_ptr> download(const std::string& fileUrl, const std::shared_ptr& options, const std::optional>& onProgress) = 0; + virtual std::shared_ptr> generateFilePath(const std::string& fileExtension) = 0; + virtual std::shared_ptr> getRealPath(const std::string& path, const std::string& type) = 0; + virtual std::shared_ptr> getFileSize(const std::string& filePath) = 0; + virtual std::shared_ptr> createVideoThumbnail(const std::string& fileUrl, const std::shared_ptr& options) = 0; + virtual std::shared_ptr> clearCache(const std::optional& cacheDir) = 0; + + protected: + // Hybrid Setup + void loadHybridMethods() override; + + protected: + // Tag for logging + static constexpr auto TAG = "Compressor"; + }; + +} // namespace margelo::nitro::compressor diff --git a/nitrogen/generated/shared/c++/VideoThumbnailResult.hpp b/nitrogen/generated/shared/c++/VideoThumbnailResult.hpp new file mode 100644 index 00000000..43dd3481 --- /dev/null +++ b/nitrogen/generated/shared/c++/VideoThumbnailResult.hpp @@ -0,0 +1,99 @@ +/// +/// VideoThumbnailResult.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © Marc Rousavy @ Margelo +/// + +#pragma once + +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif + + + +#include + +namespace margelo::nitro::compressor { + + /** + * A struct which can be represented as a JavaScript object (VideoThumbnailResult). + */ + struct VideoThumbnailResult final { + public: + std::string path SWIFT_PRIVATE; + double size SWIFT_PRIVATE; + std::string mime SWIFT_PRIVATE; + double width SWIFT_PRIVATE; + double height SWIFT_PRIVATE; + + public: + VideoThumbnailResult() = default; + explicit VideoThumbnailResult(std::string path, double size, std::string mime, double width, double height): path(path), size(size), mime(mime), width(width), height(height) {} + + public: + friend bool operator==(const VideoThumbnailResult& lhs, const VideoThumbnailResult& rhs) = default; + }; + +} // namespace margelo::nitro::compressor + +namespace margelo::nitro { + + // C++ VideoThumbnailResult <> JS VideoThumbnailResult (object) + template <> + struct JSIConverter final { + static inline margelo::nitro::compressor::VideoThumbnailResult fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) { + jsi::Object obj = arg.asObject(runtime); + return margelo::nitro::compressor::VideoThumbnailResult( + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "path"))), + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "size"))), + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "mime"))), + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "width"))), + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "height"))) + ); + } + static inline jsi::Value toJSI(jsi::Runtime& runtime, const margelo::nitro::compressor::VideoThumbnailResult& arg) { + jsi::Object obj(runtime); + obj.setProperty(runtime, PropNameIDCache::get(runtime, "path"), JSIConverter::toJSI(runtime, arg.path)); + obj.setProperty(runtime, PropNameIDCache::get(runtime, "size"), JSIConverter::toJSI(runtime, arg.size)); + obj.setProperty(runtime, PropNameIDCache::get(runtime, "mime"), JSIConverter::toJSI(runtime, arg.mime)); + obj.setProperty(runtime, PropNameIDCache::get(runtime, "width"), JSIConverter::toJSI(runtime, arg.width)); + obj.setProperty(runtime, PropNameIDCache::get(runtime, "height"), JSIConverter::toJSI(runtime, arg.height)); + return obj; + } + static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) { + if (!value.isObject()) { + return false; + } + jsi::Object obj = value.getObject(runtime); + if (!nitro::isPlainObject(runtime, obj)) { + return false; + } + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "path")))) return false; + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "size")))) return false; + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "mime")))) return false; + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "width")))) return false; + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "height")))) return false; + return true; + } + }; + +} // namespace margelo::nitro diff --git a/package.json b/package.json index e0db75a9..c5483525 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "android", "ios", "cpp", + "nitrogen", "app.plugin.js", "*.podspec", "!lib/typescript/example", @@ -34,7 +35,8 @@ "test:harness:ios": "cd examples/bare && react-native-harness --config ./jest.harness.config.mjs --harnessRunner ios --verbose", "typecheck": "tsc --noEmit", "lint": "eslint \"**/*.{js,ts,tsx}\"", - "prepack": "bob build", + "nitrogen": "nitrogen", + "prepack": "nitrogen && bob build", "release": "release-it", "example:bare": "yarn workspace react-native-compressor-bare-example", "example:expo": "yarn workspace react-native-compressor-expo-example", @@ -106,18 +108,21 @@ "eslint-plugin-prettier": "^5.5.5", "jest": "^30.3.0", "lefthook": "^2.1.4", + "nitrogen": "^0.35.9", "prettier": "^3.8.1", "react": "19.2.3", "react-native": "0.85.0", "react-native-builder-bob": "^0.41.0", "react-native-harness": "1.1.0", + "react-native-nitro-modules": "^0.35.9", "release-it": "^19.2.4", "turbo": "^2.8.21", "typescript": "^6.0.2" }, "peerDependencies": { "react": "*", - "react-native": "*" + "react-native": "*", + "react-native-nitro-modules": ">=0.35.0" }, "engines": { "node": ">= 22.11.0" @@ -136,15 +141,6 @@ ] ] }, - "codegenConfig": { - "libraries": [ - { - "name": "RNCompressorSpec", - "type": "modules", - "jsSrcsDir": "src" - } - ] - }, "workspaces": [ "examples/*" ], diff --git a/react-native-compressor.podspec b/react-native-compressor.podspec index 921e7399..ac3cd711 100644 --- a/react-native-compressor.podspec +++ b/react-native-compressor.podspec @@ -1,7 +1,6 @@ require "json" package = JSON.parse(File.read(File.join(__dir__, "package.json"))) -folly_compiler_flags = '-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -Wno-comma -Wno-shorten-64-to-32' Pod::Spec.new do |s| s.name = "react-native-compressor" @@ -11,32 +10,15 @@ Pod::Spec.new do |s| s.license = package["license"] s.authors = package["author"] - s.platforms = { :ios => "11.0" } + s.platforms = { :ios => "13.4" } s.source = { :git => "https://github.com/numandev1/react-native-compressor.git", :tag => "#{s.version}" } s.source_files = "ios/**/*.{h,m,mm,swift}" - s.dependency "React-Core" - # Use install_modules_dependencies helper to install the dependencies if React Native version >=0.71.0. - # See https://github.com/facebook/react-native/blob/febf6b7f33fdb4904669f99d795eba4c0f95d7bf/scripts/cocoapods/new_architecture.rb#L79. - if respond_to?(:install_modules_dependencies, true) - install_modules_dependencies(s) - else - s.dependency "React-Core" + # Add all files generated by Nitrogen + the NitroModules dependency. + load File.join(__dir__, 'nitrogen', 'generated', 'ios', 'react_native_compressor+autolinking.rb') + add_nitrogen_files(s) - # Don't install the dependencies when we run `pod install` in the old architecture. - if ENV['RCT_NEW_ARCH_ENABLED'] == '1' then - s.compiler_flags = folly_compiler_flags + " -DRCT_NEW_ARCH_ENABLED=1" - s.pod_target_xcconfig = { - "HEADER_SEARCH_PATHS" => "\"$(PODS_ROOT)/boost\"", - "OTHER_CPLUSPLUSFLAGS" => "-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1", - "CLANG_CXX_LANGUAGE_STANDARD" => "c++17" - } - s.dependency "React-Codegen" - s.dependency "RCT-Folly" - s.dependency "RCTRequired" - s.dependency "RCTTypeSafety" - s.dependency "ReactCommon/turbomodule/core" - end - end + s.dependency "React-Core" + install_modules_dependencies(s) end diff --git a/src/Audio/index.tsx b/src/Audio/index.tsx index e2cf0312..7a1dbcce 100644 --- a/src/Audio/index.tsx +++ b/src/Audio/index.tsx @@ -1,13 +1,13 @@ import { Compressor } from '../Main'; -import { DEFAULT_COMPRESS_AUDIO_OPTIONS } from '../utils'; +import { DEFAULT_COMPRESS_AUDIO_OPTIONS, toNativeOptions } from '../utils'; import type { AudioType } from '../utils'; const NativeAudio = Compressor; const Audio: AudioType = { compress: async (url, options = DEFAULT_COMPRESS_AUDIO_OPTIONS) => { try { - return NativeAudio.compress_audio(url, options); + return NativeAudio.compress_audio(url, toNativeOptions(options)); } catch (error: any) { throw error.message; } diff --git a/src/Image/index.tsx b/src/Image/index.tsx index 53ab2f37..09bcd35c 100644 --- a/src/Image/index.tsx +++ b/src/Image/index.tsx @@ -1,8 +1,6 @@ import { Compressor } from '../Main'; -import { uuidv4 } from '../utils'; +import { toNativeOptions } from '../utils'; const base64UrlRegex = /^data:image\/.*;(?:charset=.{3,5};)?base64,/; -import type { NativeEventSubscription } from 'react-native'; -import { NativeEventEmitter } from 'react-native'; export type InputType = 'base64' | 'uri'; @@ -55,8 +53,6 @@ export type CompressorOptions = { progressDivider?: number; }; -const ImageCompressEventEmitter = new NativeEventEmitter(Compressor); - const NativeImage = Compressor; type ImageType = { @@ -69,27 +65,8 @@ const Image: ImageType = { throw new Error('Compression value is empty, please provide a value for compression.'); } - let subscription: NativeEventSubscription; - try { - if (options?.downloadProgress) { - const uuid = uuidv4(); - //@ts-ignore - options.uuid = uuid; - subscription = ImageCompressEventEmitter.addListener('downloadProgress', (event: any) => { - if (event.uuid === uuid) { - options.downloadProgress && options.downloadProgress(event.data.progress); - } - }); - } - - const cleanData = value.replace(base64UrlRegex, ''); - return await NativeImage.image_compress(cleanData, options); - } finally { - // @ts-ignore - if (subscription) { - subscription.remove(); - } - } + const cleanData = value.replace(base64UrlRegex, ''); + return await NativeImage.image_compress(cleanData, toNativeOptions(options), options.downloadProgress); }, }; diff --git a/src/Main.tsx b/src/Main.tsx index f881323c..2eb5c48b 100644 --- a/src/Main.tsx +++ b/src/Main.tsx @@ -1,25 +1,23 @@ -import { NativeModules, Platform } from 'react-native'; +import { Platform } from 'react-native'; +import { NitroModules } from 'react-native-nitro-modules'; + +import type { Compressor as CompressorSpec } from './specs/Compressor.nitro'; const LINKING_ERROR = `The package 'react-native-compressor' doesn't seem to be linked. Make sure: \n\n` + Platform.select({ ios: "- You have run 'pod install'\n", default: '' }) + '- You rebuilt the app after installing the package\n' + - '- You are not using Expo Go\n'; - -// @ts-expect-error -const isTurboModuleEnabled = global.__turboModuleProxy != null; + '- You are not using Expo Go\n' + + '- You have installed `react-native-nitro-modules`\n'; -const CompressorModule = isTurboModuleEnabled ? require('./Spec/NativeCompressor').default : NativeModules.Compressor; +const createCompressor = (): CompressorSpec => { + try { + return NitroModules.createHybridObject('Compressor'); + } catch { + throw new Error(LINKING_ERROR); + } +}; -const Compressor = CompressorModule - ? CompressorModule - : new Proxy( - {}, - { - get() { - throw new Error(LINKING_ERROR); - }, - }, - ); +const Compressor = createCompressor(); export { Compressor }; diff --git a/src/Spec/NativeCompressor.ts b/src/Spec/NativeCompressor.ts deleted file mode 100644 index 8684414e..00000000 --- a/src/Spec/NativeCompressor.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type { TurboModule } from 'react-native'; -import { TurboModuleRegistry } from 'react-native'; - -export interface Spec extends TurboModule { - // Image - image_compress(imagePath: string, optionMap: Object): Promise; - getImageMetaData(filePath: string): Promise; - // Video - compress(fileUrl: string, optionMap: Object): Promise; - cancelCompression(uuid: string): void; - getVideoMetaData(filePath: string): Promise; - activateBackgroundTask(options: Object): Promise; - deactivateBackgroundTask(options: Object): Promise; - //Audio - compress_audio(fileUrl: string, optionMap: Object): Promise; - // Upload - upload(fileUrl: string, options: Object): Promise; - // Cancel upload - cancelUpload(uuid: string, shouldCancelAll: boolean): void; - // Download - download(fileUrl: string, options: Object): Promise; - // Others - generateFilePath(_extension: string): Promise; - getRealPath(path: string, type: string): Promise; - getFileSize(filePath: string): Promise; - addListener(eventName: string): void; - removeListeners(count: number): void; - createVideoThumbnail( - fileUrl: string, - options: Object, - ): Promise<{ - path: string; - size: number; - mime: string; - width: number; - height: number; - }>; - clearCache(cacheDir: string | null): Promise; -} - -export default TurboModuleRegistry.getEnforcing('Compressor'); diff --git a/src/Video/index.tsx b/src/Video/index.tsx index 7ec49284..563a0157 100644 --- a/src/Video/index.tsx +++ b/src/Video/index.tsx @@ -1,7 +1,5 @@ -import { NativeEventEmitter } from 'react-native'; -import type { NativeEventSubscription } from 'react-native'; import { Compressor } from '../Main'; -import { uuidv4 } from '../utils'; +import { toNativeOptions, uuidv4 } from '../utils'; export type compressionMethod = 'auto' | 'manual'; type videoCompresssionType = { @@ -29,8 +27,6 @@ export type VideoCompressorType = { deactivateBackgroundTask(): Promise; }; -const VideoCompressEventEmitter = new NativeEventEmitter(Compressor); - const NativeVideoCompressor = Compressor; export const cancelCompression = (cancellationId: string) => { @@ -40,85 +36,37 @@ export const cancelCompression = (cancellationId: string) => { const Video: VideoCompressorType = { compress: async (fileUrl: string, options?: videoCompresssionType, onProgress?: (progress: number) => void) => { const uuid = uuidv4(); - let subscription: NativeEventSubscription; - let subscription2: NativeEventSubscription; - - try { - if (onProgress) { - subscription = VideoCompressEventEmitter.addListener('videoCompressProgress', (event: any) => { - if (event.uuid === uuid) { - onProgress(event.data.progress); - } - }); - } - - if (options?.downloadProgress) { - //@ts-ignore - subscription2 = VideoCompressEventEmitter.addListener('downloadProgress', (event: any) => { - if (event.uuid === uuid) { - options.downloadProgress && options.downloadProgress(event.data.progress); - } - }); - } - const modifiedOptions: { - uuid: string; - bitrate?: number; - compressionMethod?: compressionMethod; - maxSize?: number; - minimumFileSizeForCompress?: number; - progressDivider?: number; - stripAudio?: boolean; - } = { uuid }; - if (options?.progressDivider) modifiedOptions.progressDivider = options?.progressDivider; - if (options?.bitrate) modifiedOptions.bitrate = options?.bitrate; - if (options?.compressionMethod) { - modifiedOptions.compressionMethod = options?.compressionMethod; - } else { - modifiedOptions.compressionMethod = 'auto'; - } - if (options?.maxSize) { - modifiedOptions.maxSize = options?.maxSize; - } else { - modifiedOptions.maxSize = 640; - } - if (options?.minimumFileSizeForCompress !== undefined) { - modifiedOptions.minimumFileSizeForCompress = options?.minimumFileSizeForCompress; - } - if (options?.stripAudio) { - modifiedOptions.stripAudio = options.stripAudio; - } - if (options?.getCancellationId) { - options?.getCancellationId(uuid); - } - - const result = await NativeVideoCompressor.compress(fileUrl, modifiedOptions); - return result; - } finally { - // @ts-ignore - if (subscription) { - subscription.remove(); - } - //@ts-ignore - if (subscription2) { - subscription2.remove(); - } + const modifiedOptions: Record = { uuid }; + if (options?.progressDivider) modifiedOptions.progressDivider = options?.progressDivider; + if (options?.bitrate) modifiedOptions.bitrate = options?.bitrate; + if (options?.compressionMethod) { + modifiedOptions.compressionMethod = options?.compressionMethod; + } else { + modifiedOptions.compressionMethod = 'auto'; + } + if (options?.maxSize) { + modifiedOptions.maxSize = options?.maxSize; + } else { + modifiedOptions.maxSize = 640; } + if (options?.minimumFileSizeForCompress !== undefined) { + modifiedOptions.minimumFileSizeForCompress = options?.minimumFileSizeForCompress; + } + if (options?.stripAudio) { + modifiedOptions.stripAudio = options.stripAudio; + } + if (options?.getCancellationId) { + options?.getCancellationId(uuid); + } + + return NativeVideoCompressor.compress(fileUrl, toNativeOptions(modifiedOptions), onProgress, options?.downloadProgress); }, cancelCompression, activateBackgroundTask(onExpired?) { - if (onExpired) { - const subscription: NativeEventSubscription = VideoCompressEventEmitter.addListener('backgroundTaskExpired', (event: any) => { - onExpired(event); - if (subscription) { - subscription.remove(); - } - }); - } - return NativeVideoCompressor.activateBackgroundTask({}); + return NativeVideoCompressor.activateBackgroundTask({}, onExpired ? () => onExpired(undefined) : undefined); }, deactivateBackgroundTask() { - VideoCompressEventEmitter.removeAllListeners('backgroundTaskExpired'); return NativeVideoCompressor.deactivateBackgroundTask({}); }, } as VideoCompressorType; diff --git a/src/specs/Compressor.nitro.ts b/src/specs/Compressor.nitro.ts new file mode 100644 index 00000000..71936267 --- /dev/null +++ b/src/specs/Compressor.nitro.ts @@ -0,0 +1,56 @@ +import type { AnyMap, HybridObject } from 'react-native-nitro-modules'; + +export interface VideoThumbnailResult { + path: string; + size: number; + mime: string; + width: number; + height: number; +} + +/** + * Nitro HybridObject spec for the single native `Compressor` module. + * + * Notes on the migration from the old TurboModule spec: + * - Progress is delivered through callback parameters (`onProgress` / `onDownloadProgress` / + * `onExpired`) instead of `NativeEventEmitter` events. Callbacks can't live inside an `AnyMap`, + * so any progress callback that used to be nested in the options object is now a top-level param. + * - `optionMap`/`options` stay untyped (`AnyMap`) and are parsed natively, exactly as before. + * - `getImageMetaData` / `getVideoMetaData` / `upload` resolve objects at runtime (EXIF map / + * metadata map / `{status,headers,body}`), so they return `AnyMap` — the old `Promise` + * typing was lossy. + * - `uuid` is still threaded inside the options map for cancellation (`cancelCompression` / + * `cancelUpload`) and for routing native progress emissions to the registered callback. + */ +export interface Compressor extends HybridObject<{ ios: 'swift'; android: 'kotlin' }> { + // Image + image_compress(imagePath: string, optionMap: AnyMap, onDownloadProgress?: (progress: number) => void): Promise; + getImageMetaData(filePath: string): Promise; + + // Video + compress( + fileUrl: string, + optionMap: AnyMap, + onProgress?: (progress: number) => void, + onDownloadProgress?: (progress: number) => void, + ): Promise; + cancelCompression(uuid: string): void; + getVideoMetaData(filePath: string): Promise; + activateBackgroundTask(options: AnyMap, onExpired?: () => void): Promise; + deactivateBackgroundTask(options: AnyMap): Promise; + + // Audio + compress_audio(fileUrl: string, optionMap: AnyMap): Promise; + + // Upload / Download + upload(fileUrl: string, options: AnyMap, onProgress?: (written: number, total: number) => void): Promise; + cancelUpload(uuid: string, shouldCancelAll: boolean): void; + download(fileUrl: string, options: AnyMap, onProgress?: (progress: number) => void): Promise; + + // Others + generateFilePath(fileExtension: string): Promise; + getRealPath(path: string, type: string): Promise; + getFileSize(filePath: string): Promise; + createVideoThumbnail(fileUrl: string, options: AnyMap): Promise; + clearCache(cacheDir?: string): Promise; +} diff --git a/src/utils/Downloader.tsx b/src/utils/Downloader.tsx index 2f0814ac..dd1766d7 100644 --- a/src/utils/Downloader.tsx +++ b/src/utils/Downloader.tsx @@ -1,31 +1,12 @@ -import { NativeEventEmitter, Platform } from 'react-native'; -import type { NativeEventSubscription } from 'react-native'; +import { Platform } from 'react-native'; import { Compressor } from '../Main'; -const CompressEventEmitter = new NativeEventEmitter(Compressor); -import { uuidv4 } from './helpers'; +import { toNativeOptions, uuidv4 } from './helpers'; + export const download = async (fileUrl: string, downloadProgress?: (progress: number) => void, progressDivider?: number): Promise => { - let subscription: NativeEventSubscription; - try { - const uuid = uuidv4(); - if (downloadProgress) { - subscription = CompressEventEmitter.addListener('downloadProgress', (event: any) => { - if (event.uuid === uuid) { - downloadProgress(event.data.progress); - } - }); - } - if (Platform.OS === 'android' && fileUrl.includes('file://')) { - fileUrl = fileUrl.replace('file://', ''); - } - const result = await Compressor.download(fileUrl, { - uuid, - progressDivider, - }); - return result; - } finally { - // @ts-ignore - if (subscription) { - subscription.remove(); - } + const uuid = uuidv4(); + if (Platform.OS === 'android' && fileUrl.includes('file://')) { + fileUrl = fileUrl.replace('file://', ''); } + const result = await Compressor.download(fileUrl, toNativeOptions({ uuid, progressDivider }), downloadProgress); + return result; }; diff --git a/src/utils/Uploader.tsx b/src/utils/Uploader.tsx index 3c5025ae..64e7316c 100644 --- a/src/utils/Uploader.tsx +++ b/src/utils/Uploader.tsx @@ -1,8 +1,6 @@ -import { NativeEventEmitter, Platform } from 'react-native'; -import type { NativeEventSubscription } from 'react-native'; +import { Platform } from 'react-native'; import { Compressor } from '../Main'; -const CompressEventEmitter = new NativeEventEmitter(Compressor); -import { uuidv4 } from './helpers'; +import { toNativeOptions, uuidv4 } from './helpers'; export enum UploadType { BINARY_CONTENT = 0, MULTIPART = 1, @@ -51,15 +49,7 @@ export const backgroundUpload = async ( abortSignal?: AbortSignal, ): Promise => { const uuid = uuidv4(); - let subscription: NativeEventSubscription; try { - if (onProgress) { - subscription = CompressEventEmitter.addListener('uploadProgress', (event: any) => { - if (event.uuid === uuid) { - onProgress(event.data.written, event.data.total); - } - }); - } if (Platform.OS === 'android' && fileUrl.includes('file://')) { fileUrl = fileUrl.replace('file://', ''); } @@ -70,20 +60,18 @@ export const backgroundUpload = async ( abortSignal?.addEventListener('abort', () => cancelUpload(uuid)); - const result = await Compressor.upload(fileUrl, { - uuid, - method: options.httpMethod, - headers: options.headers, - uploadType: options.uploadType, - ...options, - url, - }); + const result = await Compressor.upload( + fileUrl, + toNativeOptions({ + ...options, + uuid, + method: options.httpMethod, + url, + }), + onProgress, + ); return result; } finally { - // @ts-ignore - if (subscription) { - subscription.remove(); - } abortSignal?.removeEventListener('abort', () => cancelUpload(uuid)); } }; diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts index fb22aa74..de87c9a5 100644 --- a/src/utils/helpers.ts +++ b/src/utils/helpers.ts @@ -1,3 +1,5 @@ +import type { AnyMap } from 'react-native-nitro-modules'; + export const uuidv4 = () => { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { const r = (parseFloat('0.' + Math.random().toString().replace('0.', '') + new Date().getTime()) * 16) | 0, @@ -6,3 +8,19 @@ export const uuidv4 = () => { return v.toString(16); }); }; + +/** + * Build a Nitro-safe options map. Nitro's `AnyMap` converter throws when it + * encounters a `undefined` or function-valued property, so we drop both — + * progress callbacks are passed as separate native method parameters now. + */ +export const toNativeOptions = (options: Record): AnyMap => { + const result: Record = {}; + for (const key of Object.keys(options)) { + const value = options[key]; + if (value !== undefined && typeof value !== 'function') { + result[key] = value; + } + } + return result as AnyMap; +}; diff --git a/src/utils/index.tsx b/src/utils/index.tsx index 56482f72..ad10a2d2 100644 --- a/src/utils/index.tsx +++ b/src/utils/index.tsx @@ -1,5 +1,6 @@ import { Compressor } from '../Main'; import { Platform } from 'react-native'; +import { toNativeOptions } from './helpers'; type qualityType = 'low' | 'medium' | 'high'; const INCORRECT_INPUT_PATH = 'Incorrect input path. Please provide a valid one'; @@ -72,7 +73,7 @@ export const getRealPath: getRealPathType = (path, type = 'video') => { }; export const getVideoMetaData: getVideoMetaDataType = (path: string) => { - return Compressor.getVideoMetaData(path); + return Compressor.getVideoMetaData(path) as ReturnType; }; const unifyMetaData = (exifResult: any) => { @@ -96,7 +97,7 @@ export const getImageMetaData: getImageMetaDataType = async (path: string) => { }; export const createVideoThumbnail: createVideoThumbnailType = (fileUrl, options = {}) => { - return Compressor.createVideoThumbnail(fileUrl, options); + return Compressor.createVideoThumbnail(fileUrl, toNativeOptions(options)); }; export const clearCache: clearCacheType = (cacheDir?: string) => { diff --git a/yarn.lock b/yarn.lock index 47879266..97c3ca6b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5601,6 +5601,17 @@ __metadata: languageName: node linkType: hard +"@ts-morph/common@npm:~0.29.0": + version: 0.29.0 + resolution: "@ts-morph/common@npm:0.29.0" + dependencies: + minimatch: "npm:^10.0.1" + path-browserify: "npm:^1.0.1" + tinyglobby: "npm:^0.2.14" + checksum: 10c0/97ab8ca66558b817e5475a8893ee1476ab760a3ac71fa9868413d95ddbaaf909eda81716e3a8d14291cbf449e624af66de81bcddd3ec2e36525728b4edf5e8ee + languageName: node + linkType: hard + "@turbo/darwin-64@npm:2.9.6": version: 2.9.6 resolution: "@turbo/darwin-64@npm:2.9.6" @@ -7473,7 +7484,7 @@ __metadata: languageName: node linkType: hard -"chalk@npm:^5.6.2": +"chalk@npm:^5.3.0, chalk@npm:^5.6.2": version: 5.6.2 resolution: "chalk@npm:5.6.2" checksum: 10c0/99a4b0f0e7991796b1e7e3f52dceb9137cae2a9dfc8fc0784a550dc4c558e15ab32ed70b14b21b52beb2679b4892b41a0aa44249bcb996f01e125d58477c6976 @@ -7725,6 +7736,13 @@ __metadata: languageName: node linkType: hard +"code-block-writer@npm:^13.0.3": + version: 13.0.3 + resolution: "code-block-writer@npm:13.0.3" + checksum: 10c0/87db97b37583f71cfd7eced8bf3f0a0a0ca53af912751a734372b36c08cd27f3e8a4878ec05591c0cd9ae11bea8add1423e132d660edd86aab952656dd41fd66 + languageName: node + linkType: hard + "collect-v8-coverage@npm:^1.0.2": version: 1.0.3 resolution: "collect-v8-coverage@npm:1.0.3" @@ -14128,7 +14146,7 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:^10.2.2": +"minimatch@npm:^10.0.1, minimatch@npm:^10.2.2": version: 10.2.5 resolution: "minimatch@npm:10.2.5" dependencies: @@ -14441,6 +14459,21 @@ __metadata: languageName: node linkType: hard +"nitrogen@npm:^0.35.9": + version: 0.35.9 + resolution: "nitrogen@npm:0.35.9" + dependencies: + chalk: "npm:^5.3.0" + react-native-nitro-modules: "npm:^0.35.9" + ts-morph: "npm:^28.0.0" + yargs: "npm:^18.0.0" + zod: "npm:^4.0.5" + bin: + nitrogen: lib/index.js + checksum: 10c0/1138573077003a27498902065318afb8928976df488c352105036977b46e9b99a4c3adfb246adb8a005fa793c8f8a4fbb1f2923211d16352cf7f9a5b30423fbe + languageName: node + linkType: hard + "nocache@npm:^3.0.1": version: 3.0.4 resolution: "nocache@npm:3.0.4" @@ -15125,6 +15158,13 @@ __metadata: languageName: node linkType: hard +"path-browserify@npm:^1.0.1": + version: 1.0.1 + resolution: "path-browserify@npm:1.0.1" + checksum: 10c0/8b8c3fd5c66bd340272180590ae4ff139769e9ab79522e2eb82e3d571a89b8117c04147f65ad066dccfb42fcad902e5b7d794b3d35e0fd840491a8ddbedf8c66 + languageName: node + linkType: hard + "path-exists@npm:^4.0.0": version: 4.0.0 resolution: "path-exists@npm:4.0.0" @@ -15752,6 +15792,7 @@ __metadata: react-native-get-random-values: "npm:^2.0.0" react-native-image-picker: "npm:^8.2.1" react-native-monorepo-config: "npm:^0.3.3" + react-native-nitro-modules: "npm:^0.35.9" react-native-progress: "npm:^5.0.1" react-native-reanimated: "npm:^4.3.0" react-native-safe-area-context: "npm:^5.7.0" @@ -15783,6 +15824,7 @@ __metadata: react-dom: "npm:19.1.0" react-native: "npm:0.81.5" react-native-get-random-values: "npm:^1.9.0" + react-native-nitro-modules: "npm:^0.35.9" react-native-progress: "npm:^5.0.0" react-native-reanimated: "npm:^4.3.0" react-native-safe-area-context: "npm:~5.6.0" @@ -15819,17 +15861,20 @@ __metadata: eslint-plugin-prettier: "npm:^5.5.5" jest: "npm:^30.3.0" lefthook: "npm:^2.1.4" + nitrogen: "npm:^0.35.9" prettier: "npm:^3.8.1" react: "npm:19.2.3" react-native: "npm:0.85.0" react-native-builder-bob: "npm:^0.41.0" react-native-harness: "npm:1.1.0" + react-native-nitro-modules: "npm:^0.35.9" release-it: "npm:^19.2.4" turbo: "npm:^2.8.21" typescript: "npm:^6.0.2" peerDependencies: react: "*" react-native: "*" + react-native-nitro-modules: ">=0.35.0" languageName: unknown linkType: soft @@ -15902,6 +15947,16 @@ __metadata: languageName: node linkType: hard +"react-native-nitro-modules@npm:^0.35.9": + version: 0.35.9 + resolution: "react-native-nitro-modules@npm:0.35.9" + peerDependencies: + react: "*" + react-native: "*" + checksum: 10c0/2e5c1b3eed1d187e7c2bbbf661e6405cb69c6a20b01427ea4c8a3bfe07a4baf2efbeb170a21f8c17fb0e18f022244f4b1fe1ce69a7ec824a7a4cf72441893cf1 + languageName: node + linkType: hard + "react-native-progress@npm:^5.0.0, react-native-progress@npm:^5.0.1": version: 5.0.1 resolution: "react-native-progress@npm:5.0.1" @@ -17854,6 +17909,16 @@ __metadata: languageName: node linkType: hard +"tinyglobby@npm:^0.2.14": + version: 0.2.17 + resolution: "tinyglobby@npm:0.2.17" + dependencies: + fdir: "npm:^6.5.0" + picomatch: "npm:^4.0.4" + checksum: 10c0/7f7bb0f197c88bc4b20c231e0deca4240ca3bf313a88f5a7fee93a872b84966a4d50220947c0455ad07a60b3b360961c5b7fd979222aeb716a9f99b412002e4c + languageName: node + linkType: hard + "tinyrainbow@npm:^3.0.3": version: 3.1.0 resolution: "tinyrainbow@npm:3.1.0" @@ -17928,6 +17993,16 @@ __metadata: languageName: node linkType: hard +"ts-morph@npm:^28.0.0": + version: 28.0.0 + resolution: "ts-morph@npm:28.0.0" + dependencies: + "@ts-morph/common": "npm:~0.29.0" + code-block-writer: "npm:^13.0.3" + checksum: 10c0/969570a5e983b4193735fff43878ce7b78787b4860bf3d23f330c86d7de707286969e5a82d486dce333eea27bf9d373a46f9de58a80c0bf86d2a462620563a55 + languageName: node + linkType: hard + "tsconfig-paths@npm:^3.15.0": version: 3.15.0 resolution: "tsconfig-paths@npm:3.15.0" @@ -19077,6 +19152,13 @@ __metadata: languageName: node linkType: hard +"zod@npm:^4.0.5": + version: 4.4.3 + resolution: "zod@npm:4.4.3" + checksum: 10c0/7ea31b558e88f9faf44f31dd185e2e1cbf51fed3081787fb96cc2534749b50c0acfc6da7f0922a7353ed092dd358c7d50c28ea96c94d04af64191bd33152eca3 + languageName: node + linkType: hard + "zustand@npm:^5.0.5": version: 5.0.12 resolution: "zustand@npm:5.0.12"