Adding the ability to support different Haptic Devices (#5856)

* refactored `Interactive` class to allow more HapticDevice devices
* simplified api hooks
* update creation of `interactive` to pass `stashConfig`
* updated UIPluginApi to mention `PluginApi.InteractiveUtils`
This commit is contained in:
xtc1337 2025-09-25 00:27:58 -05:00 committed by GitHub
parent c9ca40152f
commit 15bf28d5be
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 186 additions and 15 deletions

View file

@ -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();

View file

@ -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<void>` 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<void>;
handyKey: string;
uploadScript: (funscriptPath: string, apiKey?: string) => Promise<void>;
sync(): Promise<number>;
configure(config: Partial<IDeviceSettings>): Promise<void>;
play(position: number): Promise<void>;
pause(): Promise<void>;
ensurePlaying(position: number): Promise<void>;
setLooping(looping: boolean): Promise<void>;
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))
```

View file

@ -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<IInteractiveState>(
LOCAL_FORAGE_KEY,
@ -85,7 +96,22 @@ export const InteractiveProvider: React.FC = ({ children }) => {
const [scriptOffset, setScriptOffset] = useState<number>(0);
const [useStashHostedFunscript, setUseStashHostedFunscript] =
useState<boolean>(false);
const [interactive] = useState<InteractiveAPI>(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<string | undefined>();
@ -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 ||

View file

@ -5,6 +5,7 @@ import {
CsvUploadResponse,
HandyFirmwareStatus,
} from "thehandy/lib/types";
import { IDeviceSettings } from "./utils";
interface IFunscript {
actions: Array<IAction>;
@ -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<IDeviceSettings>) {
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;

View file

@ -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<void>;
handyKey: string;
uploadScript: (funscriptPath: string, apiKey?: string) => Promise<void>;
sync(): Promise<number>;
configure(config: Partial<IDeviceSettings>): Promise<void>;
play(position: number): Promise<void>;
pause(): Promise<void>;
ensurePlaying(position: number): Promise<void>;
setLooping(looping: boolean): Promise<void>;
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;

View file

@ -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,