From 59c470a76c13aed09f4fc76ea696942260b79417 Mon Sep 17 00:00:00 2001 From: Simon Chan <1330321+yume-chan@users.noreply.github.com> Date: Sun, 2 Apr 2023 20:50:46 +0800 Subject: [PATCH] feat(demo): allow hiding Scrcpy full screen hint fixes #514 --- apps/demo/src/hooks/add-event-listener.ts | 40 +++++++++++++++++++++++ apps/demo/src/hooks/index.ts | 4 +++ apps/demo/src/hooks/layout-effect.ts | 4 +++ apps/demo/src/hooks/local-storage.ts | 34 +++++++++++++++++++ apps/demo/src/hooks/stable-callback.ts | 35 ++++++++++++++++++++ apps/demo/src/pages/scrcpy.tsx | 38 ++++++++++++++++----- 6 files changed, 146 insertions(+), 9 deletions(-) create mode 100644 apps/demo/src/hooks/add-event-listener.ts create mode 100644 apps/demo/src/hooks/index.ts create mode 100644 apps/demo/src/hooks/layout-effect.ts create mode 100644 apps/demo/src/hooks/local-storage.ts create mode 100644 apps/demo/src/hooks/stable-callback.ts diff --git a/apps/demo/src/hooks/add-event-listener.ts b/apps/demo/src/hooks/add-event-listener.ts new file mode 100644 index 00000000..a70f313f --- /dev/null +++ b/apps/demo/src/hooks/add-event-listener.ts @@ -0,0 +1,40 @@ +import { useEffect } from "react"; + +type CommonEventMaps = T extends typeof globalThis + ? WindowEventMap + : T extends Window + ? WindowEventMap + : T extends Document + ? DocumentEventMap + : T extends HTMLElement + ? HTMLElementEventMap + : T extends SVGElement + ? SVGElementEventMap + : { [type: string]: unknown }; + +const useClientAddEventListener = < + T extends EventTarget, + U extends keyof CommonEventMaps +>( + target: T | (() => T), + type: U, + listener: (this: T, ev: CommonEventMaps[U]) => any, + options?: AddEventListenerOptions, + deps?: readonly unknown[] +) => { + useEffect(() => { + const targetValue = typeof target === "function" ? target() : target; + targetValue.addEventListener(type as any, listener as any, options); + + return () => + targetValue.removeEventListener( + type as any, + listener as any, + options + ); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, deps); +}; + +export const useAddEventListener = + typeof window !== "undefined" ? useClientAddEventListener : () => {}; diff --git a/apps/demo/src/hooks/index.ts b/apps/demo/src/hooks/index.ts new file mode 100644 index 00000000..1b72e28c --- /dev/null +++ b/apps/demo/src/hooks/index.ts @@ -0,0 +1,4 @@ +export * from "./add-event-listener"; +export * from "./layout-effect"; +export * from "./local-storage"; +export * from "./stable-callback"; diff --git a/apps/demo/src/hooks/layout-effect.ts b/apps/demo/src/hooks/layout-effect.ts new file mode 100644 index 00000000..896e22a1 --- /dev/null +++ b/apps/demo/src/hooks/layout-effect.ts @@ -0,0 +1,4 @@ +import { useLayoutEffect as useReactLayoutEffect } from "react"; + +export const useLayoutEffect = + typeof window !== "undefined" ? useReactLayoutEffect : () => {}; diff --git a/apps/demo/src/hooks/local-storage.ts b/apps/demo/src/hooks/local-storage.ts new file mode 100644 index 00000000..1b4c86dd --- /dev/null +++ b/apps/demo/src/hooks/local-storage.ts @@ -0,0 +1,34 @@ +import { useState } from "react"; + +import { useAddEventListener } from "./add-event-listener"; +import { useStableCallback } from "./stable-callback"; + +function useClientLocalStorage( + key: string, + fallbackValue: T +) { + const [value, setValue] = useState( + () => (localStorage.getItem(key) as T) || fallbackValue + ); + + useAddEventListener( + globalThis, + "storage", + () => + setValue((localStorage.getItem(key) as T | null) ?? fallbackValue), + { passive: true }, + [key, fallbackValue] + ); + + const handleChange = useStableCallback((value: T) => { + setValue(value); + localStorage.setItem(key, value); + }); + + return [value, handleChange] as const; +} + +export const useLocalStorage: typeof useClientLocalStorage = + typeof localStorage !== "undefined" + ? useClientLocalStorage + : (key, fallbackValue) => [fallbackValue, () => {}]; diff --git a/apps/demo/src/hooks/stable-callback.ts b/apps/demo/src/hooks/stable-callback.ts new file mode 100644 index 00000000..e4678693 --- /dev/null +++ b/apps/demo/src/hooks/stable-callback.ts @@ -0,0 +1,35 @@ +import { MutableRefObject, useRef } from "react"; + +import { useLayoutEffect } from "./layout-effect"; + +const UNINITIALIZED = Symbol("UNINITIALIZED"); + +export function useConstLazy(initializer: () => T): T { + const ref = useRef(UNINITIALIZED); + if (ref.current === UNINITIALIZED) { + ref.current = initializer(); + } + return ref.current; +} + +export function useConst(value: T): T { + const ref = useRef(value); + return ref.current; +} + +export function useLatestRef(value: T): MutableRefObject { + const ref = useRef(value); + useLayoutEffect(() => { + ref.current = value; + }, [value]); + return ref; +} + +export function useStableCallback void>( + callback: T +): T { + const callbackRef = useLatestRef(callback); + return useConst(function (...args) { + return callbackRef.current(...args); + } as T); +} diff --git a/apps/demo/src/pages/scrcpy.tsx b/apps/demo/src/pages/scrcpy.tsx index 6aabb21a..043b38f5 100644 --- a/apps/demo/src/pages/scrcpy.tsx +++ b/apps/demo/src/pages/scrcpy.tsx @@ -1,4 +1,10 @@ -import { Dialog, LayerHost, ProgressIndicator, Stack } from "@fluentui/react"; +import { + Dialog, + LayerHost, + Link, + ProgressIndicator, + Stack, +} from "@fluentui/react"; import { useId } from "@fluentui/react-hooks"; import { makeStyles, shorthands } from "@griffel/react"; import { WebCodecsDecoder } from "@yume-chan/scrcpy-decoder-webcodecs"; @@ -17,6 +23,7 @@ import { SettingItem, VideoContainer, } from "../components/scrcpy"; +import { useLocalStorage } from "../hooks"; import { GLOBAL_STATE } from "../state"; import { CommonStackTokens, RouteStackProps, formatSpeed } from "../utils"; @@ -176,6 +183,11 @@ const Scrcpy: NextPage = () => { }; }, []); + const [hintHidden, setHintHidden] = useLocalStorage<`${boolean}`>( + "scrcpy-hint-hidden", + "false" + ); + useEffect(() => { window.addEventListener("blur", handleBlur); @@ -200,16 +212,24 @@ const Scrcpy: NextPage = () => { onKeyDown={handleKeyEvent} onKeyUp={handleKeyEvent} > - {keyboardLockEnabled && STATE.isFullScreen && ( -
-
{GLOBAL_STATE.backend?.serial}
-
FPS: {STATE.fps}
+ {keyboardLockEnabled && + hintHidden !== "true" && + STATE.isFullScreen && ( +
+
{GLOBAL_STATE.backend?.serial}
+
FPS: {STATE.fps}
-
+
-
Press and hold ESC to exit full screen
-
- )} +
+ Press and hold ESC to exit full screen +
+ + setHintHidden("true")}> + {`Don't show again`} + +
+ )}