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 VIDEO_PLAYER_ID = "VideoJsPlayer";
|
||||||
|
|
||||||
export const getPlayerPosition = () =>
|
export const getPlayer = () => videojs.getPlayer(VIDEO_PLAYER_ID);
|
||||||
videojs.getPlayer(VIDEO_PLAYER_ID)?.currentTime();
|
|
||||||
|
export const getPlayerPosition = () => getPlayer()?.currentTime();
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,7 @@ This namespace contains all of the components available to plugins. These includ
|
||||||
|
|
||||||
### `utils`
|
### `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`
|
#### `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.
|
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`
|
### `hooks`
|
||||||
|
|
||||||
This namespace provides access to the following core utility hooks:
|
This namespace provides access to the following core utility hooks:
|
||||||
|
|
@ -251,3 +317,5 @@ Allows plugins to listen for Stash's events.
|
||||||
```js
|
```js
|
||||||
PluginApi.Event.addEventListener("stash:location", (e) => console.log("Page Changed", e.detail.data.location.pathname))
|
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 { ConfigurationContext } from "../Config";
|
||||||
import { useLocalForage } from "../LocalForage";
|
import { useLocalForage } from "../LocalForage";
|
||||||
import { Interactive as InteractiveAPI } from "./interactive";
|
import { Interactive as InteractiveAPI } from "./interactive";
|
||||||
|
import InteractiveUtils, {
|
||||||
|
IInteractiveClient,
|
||||||
|
IInteractiveClientProvider,
|
||||||
|
} from "./utils";
|
||||||
|
|
||||||
export enum ConnectionState {
|
export enum ConnectionState {
|
||||||
Missing,
|
Missing,
|
||||||
|
|
@ -34,7 +38,7 @@ export function connectionStateLabel(s: ConnectionState) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IState {
|
export interface IState {
|
||||||
interactive: InteractiveAPI;
|
interactive: IInteractiveClient;
|
||||||
state: ConnectionState;
|
state: ConnectionState;
|
||||||
serverOffset: number;
|
serverOffset: number;
|
||||||
initialised: boolean;
|
initialised: boolean;
|
||||||
|
|
@ -69,6 +73,13 @@ interface IInteractiveState {
|
||||||
lastSyncTime: number;
|
lastSyncTime: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const defaultInteractiveClientProvider: IInteractiveClientProvider = ({
|
||||||
|
handyKey,
|
||||||
|
scriptOffset,
|
||||||
|
}): IInteractiveClient => {
|
||||||
|
return new InteractiveAPI(handyKey, scriptOffset);
|
||||||
|
};
|
||||||
|
|
||||||
export const InteractiveProvider: React.FC = ({ children }) => {
|
export const InteractiveProvider: React.FC = ({ children }) => {
|
||||||
const [{ data: config }, setConfig] = useLocalForage<IInteractiveState>(
|
const [{ data: config }, setConfig] = useLocalForage<IInteractiveState>(
|
||||||
LOCAL_FORAGE_KEY,
|
LOCAL_FORAGE_KEY,
|
||||||
|
|
@ -85,7 +96,22 @@ export const InteractiveProvider: React.FC = ({ children }) => {
|
||||||
const [scriptOffset, setScriptOffset] = useState<number>(0);
|
const [scriptOffset, setScriptOffset] = useState<number>(0);
|
||||||
const [useStashHostedFunscript, setUseStashHostedFunscript] =
|
const [useStashHostedFunscript, setUseStashHostedFunscript] =
|
||||||
useState<boolean>(false);
|
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 [initialised, setInitialised] = useState(false);
|
||||||
const [error, setError] = useState<string | undefined>();
|
const [error, setError] = useState<string | undefined>();
|
||||||
|
|
@ -104,7 +130,9 @@ export const InteractiveProvider: React.FC = ({ children }) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config?.serverOffset) {
|
if (config?.serverOffset) {
|
||||||
interactive.setServerTimeOffset(config.serverOffset);
|
await interactive.configure({
|
||||||
|
estimatedServerTimeOffset: config.serverOffset,
|
||||||
|
});
|
||||||
setState(ConnectionState.Connecting);
|
setState(ConnectionState.Connecting);
|
||||||
try {
|
try {
|
||||||
await interactive.connect();
|
await interactive.connect();
|
||||||
|
|
@ -138,13 +166,17 @@ export const InteractiveProvider: React.FC = ({ children }) => {
|
||||||
|
|
||||||
const oldKey = interactive.handyKey;
|
const oldKey = interactive.handyKey;
|
||||||
|
|
||||||
interactive.handyKey = handyKey ?? "";
|
interactive
|
||||||
interactive.scriptOffset = scriptOffset;
|
.configure({
|
||||||
interactive.useStashHostedFunscript = useStashHostedFunscript;
|
connectionKey: handyKey ?? "",
|
||||||
|
offset: scriptOffset,
|
||||||
if (oldKey !== interactive.handyKey && interactive.handyKey) {
|
useStashHostedFunscript,
|
||||||
initialise();
|
})
|
||||||
}
|
.then(() => {
|
||||||
|
if (oldKey !== interactive.handyKey && interactive.handyKey) {
|
||||||
|
initialise();
|
||||||
|
}
|
||||||
|
});
|
||||||
}, [
|
}, [
|
||||||
handyKey,
|
handyKey,
|
||||||
scriptOffset,
|
scriptOffset,
|
||||||
|
|
@ -171,7 +203,7 @@ export const InteractiveProvider: React.FC = ({ children }) => {
|
||||||
|
|
||||||
const uploadScript = useCallback(
|
const uploadScript = useCallback(
|
||||||
async (funscriptPath: string) => {
|
async (funscriptPath: string) => {
|
||||||
interactive.pause();
|
await interactive.pause();
|
||||||
if (
|
if (
|
||||||
!interactive.handyKey ||
|
!interactive.handyKey ||
|
||||||
!funscriptPath ||
|
!funscriptPath ||
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import {
|
||||||
CsvUploadResponse,
|
CsvUploadResponse,
|
||||||
HandyFirmwareStatus,
|
HandyFirmwareStatus,
|
||||||
} from "thehandy/lib/types";
|
} from "thehandy/lib/types";
|
||||||
|
import { IDeviceSettings } from "./utils";
|
||||||
|
|
||||||
interface IFunscript {
|
interface IFunscript {
|
||||||
actions: Array<IAction>;
|
actions: Array<IAction>;
|
||||||
|
|
@ -108,6 +109,13 @@ export class Interactive {
|
||||||
this._playing = false;
|
this._playing = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get connected() {
|
||||||
|
return this._connected;
|
||||||
|
}
|
||||||
|
get playing() {
|
||||||
|
return this._playing;
|
||||||
|
}
|
||||||
|
|
||||||
async connect() {
|
async connect() {
|
||||||
const connected = await this._handy.getConnected();
|
const connected = await this._handy.getConnected();
|
||||||
if (!connected) {
|
if (!connected) {
|
||||||
|
|
@ -180,6 +188,15 @@ export class Interactive {
|
||||||
this._handy.estimatedServerTimeOffset = offset;
|
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) {
|
async play(position: number) {
|
||||||
if (!this._connected) {
|
if (!this._connected) {
|
||||||
return;
|
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 { useSpriteInfo } from "./hooks/sprite";
|
||||||
import { useToast } from "./hooks/Toast";
|
import { useToast } from "./hooks/Toast";
|
||||||
import Event from "./hooks/event";
|
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 { useSettings } from "./components/Settings/context";
|
||||||
import { useInteractive } from "./hooks/Interactive/context";
|
import { useInteractive } from "./hooks/Interactive/context";
|
||||||
|
import InteractiveUtils from "./hooks/Interactive/utils";
|
||||||
import { useLightbox, useGalleryLightbox } from "./hooks/Lightbox/hooks";
|
import { useLightbox, useGalleryLightbox } from "./hooks/Lightbox/hooks";
|
||||||
|
|
||||||
// due to code splitting, some components may not have been loaded when a plugin
|
// due to code splitting, some components may not have been loaded when a plugin
|
||||||
|
|
@ -152,6 +153,7 @@ export const PluginApi = {
|
||||||
},
|
},
|
||||||
components,
|
components,
|
||||||
utils: {
|
utils: {
|
||||||
|
InteractiveUtils,
|
||||||
NavUtils,
|
NavUtils,
|
||||||
StashService,
|
StashService,
|
||||||
loadComponents,
|
loadComponents,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue