diff --git a/.github/workflows/build-apk.yml b/.github/workflows/build-apk.yml index 592c9f7..a612fde 100644 --- a/.github/workflows/build-apk.yml +++ b/.github/workflows/build-apk.yml @@ -54,7 +54,7 @@ jobs: - name: Build release APK working-directory: example/android - run: ./gradlew assembleRelease --no-daemon --stacktrace + run: ./gradlew assembleRelease --no-daemon --stacktrace -PnewArchEnabled=true - name: Upload APK uses: actions/upload-artifact@v4 diff --git a/package.json b/package.json index 798ea6b..6f23883 100644 --- a/package.json +++ b/package.json @@ -5,15 +5,7 @@ "type": "module", "main": "lib/index.js", "types": "lib/index.d.ts", - "react-native": { - "windows": { - "sourceDir": "windows", - "solutionFile": "RNViewShot.sln", - "project": { - "projectFile": "RNViewShot\\RNViewShot.csproj" - } - } - }, + "react-native": "src/index.tsx", "keywords": [ "react-native", "screenshot", diff --git a/react-native.config.js b/react-native.config.cjs similarity index 95% rename from react-native.config.js rename to react-native.config.cjs index a7453d8..5407de2 100644 --- a/react-native.config.js +++ b/react-native.config.cjs @@ -1,4 +1,4 @@ -export default { +module.exports = { dependency: { platforms: { ios: {}, diff --git a/src/index.tsx b/src/index.tsx index 7d8ccc5..db769b7 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,4 +1,13 @@ -import React, {Component, ReactNode, RefObject} from "react"; +import React, { + ReactNode, + RefObject, + useRef, + useCallback, + useEffect, + useMemo, + useImperativeHandle, + forwardRef, +} from "react"; import { View, Platform, @@ -262,115 +271,154 @@ function checkCompatibleProps(props: ViewShotProperties): void { } } -export default class ViewShot extends Component { - static captureRef = captureRef; - static releaseCapture = releaseCapture; - - root: any = null; - _raf: number | null = null; - lastCapturedURI: string | null = null; +/** + * Ref handle exposed by ViewShot component. + */ +export type ViewShotRef = { + capture: () => Promise; +}; - resolveFirstLayout: ((layout: any) => void) | null = null; - firstLayoutPromise: Promise = new Promise(resolve => { - this.resolveFirstLayout = resolve; - }); +const ViewShotComponent = forwardRef( + function ViewShot(props, ref) { + const { + children, + options, + captureMode, + onCapture, + onCaptureFailure, + onLayout, + style, + } = props; - capture = (): Promise => - this.firstLayoutPromise - .then(() => { - const {root} = this; - if (!root) return neverEndingPromise as Promise; // component is unmounted, you never want to hear back from the promise - return captureRef(root, this.props.options); - }) - .then( - uri => { - this.onCapture(uri); - return uri; - }, - e => { - this.onCaptureFailure(e); - throw e; - }, - ) as Promise; + const rootRef = useRef(null); + const rafRef = useRef(null); + const lastCapturedURIRef = useRef(null); + const resolveFirstLayoutRef = useRef<((layout: any) => void) | null>(null); - onCapture = (uri: string): void => { - if (!this.root) return; - if (this.lastCapturedURI) { - // schedule releasing the previous capture - setTimeout(releaseCapture, 500, this.lastCapturedURI); - } - this.lastCapturedURI = uri; - const {onCapture} = this.props; - if (onCapture) onCapture(uri); - }; + const firstLayoutPromise = useMemo( + () => + new Promise(resolve => { + resolveFirstLayoutRef.current = resolve; + }), + [], + ); - onCaptureFailure = (e: Error): void => { - if (!this.root) return; - const {onCaptureFailure} = this.props; - if (onCaptureFailure) onCaptureFailure(e); - }; + // Keep latest props in refs so stable callbacks always see fresh values + const onCaptureRef = useRef(onCapture); + onCaptureRef.current = onCapture; + const onCaptureFailureRef = useRef(onCaptureFailure); + onCaptureFailureRef.current = onCaptureFailure; + const optionsRef = useRef(options); + optionsRef.current = options; - syncCaptureLoop = (captureMode: string | null | undefined): void => { - cancelAnimationFrame(this._raf as number); - if (captureMode === "continuous") { - let previousCaptureURI: string | null = "-"; // needs to capture at least once at first, so we use "-" arbitrary string - const loop = (): void => { - this._raf = requestAnimationFrame(loop); - if (previousCaptureURI === this.lastCapturedURI) return; // previous capture has not finished, don't capture yet - previousCaptureURI = this.lastCapturedURI; - this.capture(); - }; - this._raf = requestAnimationFrame(loop); - } - }; + const capture = useCallback( + (): Promise => + firstLayoutPromise + .then(() => { + if (!rootRef.current) return neverEndingPromise as Promise; + return captureRef(rootRef.current, optionsRef.current); + }) + .then( + uri => { + if (!rootRef.current) return uri; + if (lastCapturedURIRef.current) { + setTimeout(releaseCapture, 500, lastCapturedURIRef.current); + } + lastCapturedURIRef.current = uri; + if (onCaptureRef.current) onCaptureRef.current(uri); + return uri; + }, + e => { + if (!rootRef.current) throw e; + if (onCaptureFailureRef.current) onCaptureFailureRef.current(e); + throw e; + }, + ) as Promise, + [firstLayoutPromise], + ); - onRef = (ref: any): void => { - this.root = ref; - }; + useImperativeHandle(ref, () => ({capture}), [capture]); - onLayout = (e: LayoutChangeEvent): void => { - const {onLayout} = this.props; - if (this.resolveFirstLayout) { - this.resolveFirstLayout(e.nativeEvent.layout); - } - if (onLayout) onLayout(e); - }; + const syncCaptureLoop = useCallback( + (mode: string | null | undefined): void => { + cancelAnimationFrame(rafRef.current as number); + if (mode === "continuous") { + let previousCaptureURI: string | null = "-"; + const loop = (): void => { + rafRef.current = requestAnimationFrame(loop); + if (previousCaptureURI === lastCapturedURIRef.current) return; + previousCaptureURI = lastCapturedURIRef.current; + capture(); + }; + rafRef.current = requestAnimationFrame(loop); + } + }, + [capture], + ); - componentDidMount(): void { - if (__DEV__) checkCompatibleProps(this.props); - if (this.props.captureMode === "mount") { - this.capture(); - } else { - this.syncCaptureLoop(this.props.captureMode); - } - } + // Mount + unmount lifecycle (equivalent to componentDidMount / componentWillUnmount) + useEffect(() => { + if (__DEV__) checkCompatibleProps(props); + if (captureMode === "mount") { + capture(); + } else { + syncCaptureLoop(captureMode); + } + return () => syncCaptureLoop(null); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); - componentDidUpdate(prevProps: ViewShotProperties): void { - if (this.props.captureMode !== undefined) { - if (this.props.captureMode !== prevProps.captureMode) { - this.syncCaptureLoop(this.props.captureMode); + // Update lifecycle (equivalent to componentDidUpdate) + const prevCaptureModeRef = useRef(captureMode); + const isFirstRender = useRef(true); + useEffect(() => { + if (isFirstRender.current) { + isFirstRender.current = false; + prevCaptureModeRef.current = captureMode; + return; } - } - if (this.props.captureMode === "update") { - this.capture(); - } - } + if ( + captureMode !== undefined && + captureMode !== prevCaptureModeRef.current + ) { + syncCaptureLoop(captureMode); + } + prevCaptureModeRef.current = captureMode; + if (captureMode === "update") { + capture(); + } + }); - componentWillUnmount(): void { - this.syncCaptureLoop(null); - } + const onLayoutHandler = useCallback( + (e: LayoutChangeEvent): void => { + if (resolveFirstLayoutRef.current) { + resolveFirstLayoutRef.current(e.nativeEvent.layout); + resolveFirstLayoutRef.current = null; + } + if (onLayout) onLayout(e); + }, + [onLayout], + ); - render(): ReactNode { - const {children} = this.props; return ( {children} ); - } -} + }, +); + +// Attach static methods for backwards compatibility (ViewShot.captureRef, ViewShot.releaseCapture) +const ViewShot = ViewShotComponent as typeof ViewShotComponent & { + captureRef: typeof captureRef; + releaseCapture: typeof releaseCapture; +}; +ViewShot.captureRef = captureRef; +ViewShot.releaseCapture = releaseCapture; + +export default ViewShot; diff --git a/src/specs/NativeRNViewShot.ts b/src/specs/NativeRNViewShot.ts index 8744906..fb2607e 100644 --- a/src/specs/NativeRNViewShot.ts +++ b/src/specs/NativeRNViewShot.ts @@ -1,5 +1,5 @@ import type {TurboModule} from "react-native"; -import {TurboModuleRegistry, NativeModules, Platform} from "react-native"; +import {TurboModuleRegistry, NativeModules} from "react-native"; import {Int32, WithDefault} from "react-native/Libraries/Types/CodegenTypes"; export interface Spec extends TurboModule {