mirror of
https://github.com/stashapp/stash.git
synced 2025-12-06 08:26:00 +01:00
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:
parent
c9ca40152f
commit
15bf28d5be
6 changed files with 186 additions and 15 deletions
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
```
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
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 ||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
51
ui/v2.5/src/hooks/Interactive/utils.ts
Normal file
51
ui/v2.5/src/hooks/Interactive/utils.ts
Normal 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;
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue