diff --git a/ui/v2.5/src/components/ScenePlayer/util.ts b/ui/v2.5/src/components/ScenePlayer/util.ts index a63ab6a2e..8c6fb8010 100644 --- a/ui/v2.5/src/components/ScenePlayer/util.ts +++ b/ui/v2.5/src/components/ScenePlayer/util.ts @@ -2,5 +2,6 @@ import videojs from "video.js"; export const VIDEO_PLAYER_ID = "VideoJsPlayer"; -export const getPlayerPosition = () => - videojs.getPlayer(VIDEO_PLAYER_ID)?.currentTime(); +export const getPlayer = () => videojs.getPlayer(VIDEO_PLAYER_ID); + +export const getPlayerPosition = () => getPlayer()?.currentTime(); diff --git a/ui/v2.5/src/docs/en/Manual/UIPluginApi.md b/ui/v2.5/src/docs/en/Manual/UIPluginApi.md index f010deb38..23cd3fd64 100644 --- a/ui/v2.5/src/docs/en/Manual/UIPluginApi.md +++ b/ui/v2.5/src/docs/en/Manual/UIPluginApi.md @@ -66,7 +66,7 @@ This namespace contains all of the components available to plugins. These includ ### `utils` -This namespace provides access to the `NavUtils` and `StashService` namespaces. It also provides access to the `loadComponents` method. +This namespace provides access to the `NavUtils` , `StashService` and `InteractiveUtils` namespaces. It also provides access to the `loadComponents` method. #### `PluginApi.utils.loadComponents` @@ -80,6 +80,72 @@ In general, `PluginApi.hooks.useLoadComponents` hook should be used instead. Returns a `Promise` that resolves when all of the components have been loaded. +#### `PluginApi.utils.InteractiveUtils` +This namespace provides access to `interactiveClientProvider` and `getPlayer` + - `getPlayer` returns the current `videojs` player object + - `interactiveClientProvider` takes `IInteractiveClientProvider` which allows a developer to hook into the lifecycle of funscripts. +```ts + export interface IDeviceSettings { + connectionKey: string; + scriptOffset: number; + estimatedServerTimeOffset?: number; + useStashHostedFunscript?: boolean; + [key: string]: unknown; +} + +export interface IInteractiveClientProviderOptions { + handyKey: string; + scriptOffset: number; + defaultClientProvider?: IInteractiveClientProvider; + stashConfig?: GQL.ConfigDataFragment; +} +export interface IInteractiveClientProvider { + (options: IInteractiveClientProviderOptions): IInteractiveClient; +} + +/** + * Interface that is used for InteractiveProvider + */ +export interface IInteractiveClient { + connect(): Promise; + handyKey: string; + uploadScript: (funscriptPath: string, apiKey?: string) => Promise; + sync(): Promise; + configure(config: Partial): Promise; + play(position: number): Promise; + pause(): Promise; + ensurePlaying(position: number): Promise; + setLooping(looping: boolean): Promise; + readonly connected: boolean; + readonly playing: boolean; +} + +``` +##### Example +For instance say I wanted to add extra logging when `IInteractiveClient.connect()` is called. +In my plugin you would install your own client provider as seen below + +```ts +InteractiveUtils.interactiveClientProvider = ( + opts +) => { + if (!opts.defaultClientProvider) { + throw new Error('invalid setup'); + } + + const client = opts.defaultClientProvider(opts); + const connect = client.connect; + client.connect = async () => { + console.log('patching connect method'); + return connect.call(client); + }; + + return client; +}; + +``` + + ### `hooks` This namespace provides access to the following core utility hooks: @@ -251,3 +317,5 @@ Allows plugins to listen for Stash's events. ```js PluginApi.Event.addEventListener("stash:location", (e) => console.log("Page Changed", e.detail.data.location.pathname)) ``` + + diff --git a/ui/v2.5/src/hooks/Interactive/context.tsx b/ui/v2.5/src/hooks/Interactive/context.tsx index a42f0aa7b..9e7194d6a 100644 --- a/ui/v2.5/src/hooks/Interactive/context.tsx +++ b/ui/v2.5/src/hooks/Interactive/context.tsx @@ -2,6 +2,10 @@ import React, { useCallback, useContext, useEffect, useState } from "react"; import { ConfigurationContext } from "../Config"; import { useLocalForage } from "../LocalForage"; import { Interactive as InteractiveAPI } from "./interactive"; +import InteractiveUtils, { + IInteractiveClient, + IInteractiveClientProvider, +} from "./utils"; export enum ConnectionState { Missing, @@ -34,7 +38,7 @@ export function connectionStateLabel(s: ConnectionState) { } export interface IState { - interactive: InteractiveAPI; + interactive: IInteractiveClient; state: ConnectionState; serverOffset: number; initialised: boolean; @@ -69,6 +73,13 @@ interface IInteractiveState { lastSyncTime: number; } +export const defaultInteractiveClientProvider: IInteractiveClientProvider = ({ + handyKey, + scriptOffset, +}): IInteractiveClient => { + return new InteractiveAPI(handyKey, scriptOffset); +}; + export const InteractiveProvider: React.FC = ({ children }) => { const [{ data: config }, setConfig] = useLocalForage( LOCAL_FORAGE_KEY, @@ -85,7 +96,22 @@ export const InteractiveProvider: React.FC = ({ children }) => { const [scriptOffset, setScriptOffset] = useState(0); const [useStashHostedFunscript, setUseStashHostedFunscript] = useState(false); - const [interactive] = useState(new InteractiveAPI("", 0)); + + const resolveInteractiveClient = useCallback(() => { + const interactiveClientProvider = + InteractiveUtils.interactiveClientProvider ?? + defaultInteractiveClientProvider; + + return interactiveClientProvider({ + handyKey: "", + scriptOffset: 0, + defaultClientProvider: defaultInteractiveClientProvider, + stashConfig, + }); + }, [stashConfig]); + + // fetch client provider from PluginApi if not found use default provider + const [interactive] = useState(resolveInteractiveClient); const [initialised, setInitialised] = useState(false); const [error, setError] = useState(); @@ -104,7 +130,9 @@ export const InteractiveProvider: React.FC = ({ children }) => { } if (config?.serverOffset) { - interactive.setServerTimeOffset(config.serverOffset); + await interactive.configure({ + estimatedServerTimeOffset: config.serverOffset, + }); setState(ConnectionState.Connecting); try { await interactive.connect(); @@ -138,13 +166,17 @@ export const InteractiveProvider: React.FC = ({ children }) => { const oldKey = interactive.handyKey; - interactive.handyKey = handyKey ?? ""; - interactive.scriptOffset = scriptOffset; - interactive.useStashHostedFunscript = useStashHostedFunscript; - - if (oldKey !== interactive.handyKey && interactive.handyKey) { - initialise(); - } + interactive + .configure({ + connectionKey: handyKey ?? "", + offset: scriptOffset, + useStashHostedFunscript, + }) + .then(() => { + if (oldKey !== interactive.handyKey && interactive.handyKey) { + initialise(); + } + }); }, [ handyKey, scriptOffset, @@ -171,7 +203,7 @@ export const InteractiveProvider: React.FC = ({ children }) => { const uploadScript = useCallback( async (funscriptPath: string) => { - interactive.pause(); + await interactive.pause(); if ( !interactive.handyKey || !funscriptPath || diff --git a/ui/v2.5/src/hooks/Interactive/interactive.ts b/ui/v2.5/src/hooks/Interactive/interactive.ts index ef34bd2ef..4ca59b25b 100644 --- a/ui/v2.5/src/hooks/Interactive/interactive.ts +++ b/ui/v2.5/src/hooks/Interactive/interactive.ts @@ -5,6 +5,7 @@ import { CsvUploadResponse, HandyFirmwareStatus, } from "thehandy/lib/types"; +import { IDeviceSettings } from "./utils"; interface IFunscript { actions: Array; @@ -108,6 +109,13 @@ export class Interactive { this._playing = false; } + get connected() { + return this._connected; + } + get playing() { + return this._playing; + } + async connect() { const connected = await this._handy.getConnected(); if (!connected) { @@ -180,6 +188,15 @@ export class Interactive { this._handy.estimatedServerTimeOffset = offset; } + async configure(config: Partial) { + this._scriptOffset = config.scriptOffset ?? this._scriptOffset; + this.handyKey = config.connectionKey ?? this.handyKey; + this._handy.estimatedServerTimeOffset = + config.estimatedServerTimeOffset ?? this._handy.estimatedServerTimeOffset; + this.useStashHostedFunscript = + config.useStashHostedFunscript ?? this.useStashHostedFunscript; + } + async play(position: number) { if (!this._connected) { return; diff --git a/ui/v2.5/src/hooks/Interactive/utils.ts b/ui/v2.5/src/hooks/Interactive/utils.ts new file mode 100644 index 000000000..c1d066e86 --- /dev/null +++ b/ui/v2.5/src/hooks/Interactive/utils.ts @@ -0,0 +1,51 @@ +import { getPlayer } from "src/components/ScenePlayer/util"; +import type { VideoJsPlayer } from "video.js"; +import * as GQL from "src/core/generated-graphql"; + +export interface IDeviceSettings { + connectionKey: string; + scriptOffset: number; + estimatedServerTimeOffset?: number; + useStashHostedFunscript?: boolean; + [key: string]: unknown; +} + +export interface IInteractiveClientProviderOptions { + handyKey: string; + scriptOffset: number; + defaultClientProvider?: IInteractiveClientProvider; + stashConfig?: GQL.ConfigDataFragment; +} +export interface IInteractiveClientProvider { + (options: IInteractiveClientProviderOptions): IInteractiveClient; +} + +/** + * Interface that is used for InteractiveProvider + */ +export interface IInteractiveClient { + connect(): Promise; + handyKey: string; + uploadScript: (funscriptPath: string, apiKey?: string) => Promise; + sync(): Promise; + configure(config: Partial): Promise; + play(position: number): Promise; + pause(): Promise; + ensurePlaying(position: number): Promise; + setLooping(looping: boolean): Promise; + readonly connected: boolean; + readonly playing: boolean; +} + +export interface IInteractiveUtils { + getPlayer: () => VideoJsPlayer | undefined; + interactiveClientProvider: IInteractiveClientProvider | undefined; +} +const InteractiveUtils = { + // hook to allow to customize the interactive client + interactiveClientProvider: undefined, + // returns the active player + getPlayer, +}; + +export default InteractiveUtils; diff --git a/ui/v2.5/src/pluginApi.tsx b/ui/v2.5/src/pluginApi.tsx index 99d4a5992..e534dddef 100644 --- a/ui/v2.5/src/pluginApi.tsx +++ b/ui/v2.5/src/pluginApi.tsx @@ -16,9 +16,10 @@ import * as ReactSelect from "react-select"; import { useSpriteInfo } from "./hooks/sprite"; import { useToast } from "./hooks/Toast"; import Event from "./hooks/event"; -import { before, instead, after, components, RegisterComponent } from "./patch"; +import { after, before, components, instead, RegisterComponent } from "./patch"; import { useSettings } from "./components/Settings/context"; import { useInteractive } from "./hooks/Interactive/context"; +import InteractiveUtils from "./hooks/Interactive/utils"; import { useLightbox, useGalleryLightbox } from "./hooks/Lightbox/hooks"; // due to code splitting, some components may not have been loaded when a plugin @@ -152,6 +153,7 @@ export const PluginApi = { }, components, utils: { + InteractiveUtils, NavUtils, StashService, loadComponents,