mirror of
https://github.com/jellyfin/jellyfin-vue.git
synced 2025-03-04 19:57:34 +00:00
feat: add useApi composable
Signed-off-by: Fernando Fernández <ferferga@hotmail.com>
This commit is contained in:
parent
f80a1084c1
commit
7d2656454b
@ -4,16 +4,23 @@ import { items } from '@/store/items';
|
||||
import type { Api } from '@jellyfin/sdk';
|
||||
import { ItemFields, type BaseItemDto } from '@jellyfin/sdk/lib/generated-client';
|
||||
import type { AxiosResponse } from 'axios';
|
||||
import { computed, getCurrentScope, ref, toValue, watch, type ComputedRef, type Ref } from 'vue';
|
||||
import { computed, getCurrentScope, ref, toValue, unref, watch, type ComputedRef, type MaybeRef, type Ref } from 'vue';
|
||||
|
||||
const allFields = Object.freeze(Object.values(ItemFields));
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any, @typescript-eslint/ban-ts-comment */
|
||||
type ExtractResponseType<T> = Awaited<T> extends AxiosResponse<infer U, any> ?
|
||||
(U extends BaseItemDto ? U : (U extends BaseItemDto[] ? U : undefined))
|
||||
: undefined;
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any, @typescript-eslint/ban-ts-comment, @typescript-eslint/no-unsafe-return */
|
||||
type ParametersAsGetters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? { [K in keyof P]: () => P[K] } : never;
|
||||
|
||||
type ExtractResponseDataType<T> = Awaited<T> extends AxiosResponse<infer U, any> ? U : undefined;
|
||||
/**
|
||||
* If response.data is BaseItemDto or BaseItemDto[], returns it. Otherwise, returns undefined.
|
||||
*/
|
||||
type ExtractBaseItemDtoResponse<T> = (ExtractResponseDataType<T> extends BaseItemDto ? ExtractResponseDataType<T> : (ExtractResponseDataType<T> extends BaseItemDto[] ? ExtractResponseDataType<T> : undefined));
|
||||
/**
|
||||
* If response.data is BaseItemDto or BaseItemDto[], returns undefined. Otherwise, returns the data type.
|
||||
*/
|
||||
type ExtractResponseType<T> = (ExtractResponseDataType<T> extends BaseItemDto ? undefined : (ExtractResponseDataType<T> extends BaseItemDto[] ? undefined : ExtractResponseDataType<T>));
|
||||
|
||||
/**
|
||||
* Ensures the function is used in the given
|
||||
* @param func - The function to check.
|
||||
@ -25,6 +32,50 @@ function ensureCorrectUsage(func: any): void {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Perfoms the given request
|
||||
* @param api - Relevant API
|
||||
* @param methodName - Method to execute
|
||||
* @param isBaseItem - Whether the request is BaseItemDto based or not
|
||||
* @param requestData - Ref to hold the request data
|
||||
* @param loading - Ref to hold the loading state
|
||||
* @param args - Func args
|
||||
*/
|
||||
async function resolveAndAdd<T extends Record<K, (...args: any[]) => any>, K extends keyof T>(
|
||||
api: (api: Api) => T,
|
||||
methodName: K,
|
||||
isBaseItem: boolean,
|
||||
requestData: Ref<Awaited<ReturnType<T[K]>['data']> | undefined>,
|
||||
loading: Ref<boolean>,
|
||||
...args: ParametersAsGetters<T[K]>): Promise<void> {
|
||||
/**
|
||||
* We add all BaseItemDto's fields for consistency in what we can expect from the store.
|
||||
* toValue normalizes the getters.
|
||||
*/
|
||||
const extendedParams = [
|
||||
{ ...toValue(args[0]), fields: allFields },
|
||||
...args.slice(1).map((a) => toValue(a))
|
||||
] as Parameters<T[K]>;
|
||||
|
||||
try {
|
||||
loading.value = true;
|
||||
|
||||
const response = await remote.sdk.newUserApi(api)[methodName](...extendedParams) as Awaited<ReturnType<T[K]>>;
|
||||
|
||||
if (response.data) {
|
||||
requestData.value = response.data;
|
||||
|
||||
if (isBaseItem) {
|
||||
items.rawAdd(response.data as BaseItemDto | BaseItemDto[]);
|
||||
}
|
||||
} else {
|
||||
requestData.value = undefined;
|
||||
}
|
||||
} catch {} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reactively performs item requests to the API:
|
||||
*
|
||||
@ -59,13 +110,81 @@ function ensureCorrectUsage(func: any): void {
|
||||
export function useBaseItem<T extends Record<K, (...args: any[]) => any>, K extends keyof T>(
|
||||
api: (api: Api) => T,
|
||||
methodName: K
|
||||
): (this: any, ...args: ParametersAsGetters<T[K]>) => Promise<{ loading: Ref<boolean>, data: ComputedRef<ExtractResponseType<ReturnType<T[K]>>> }> {
|
||||
ensureCorrectUsage(remote.sdk.newUserApi(api)[methodName]);
|
||||
): (this: any, ...args: ParametersAsGetters<T[K]>) => Promise<{ loading: Ref<boolean>, data: ComputedRef<ExtractBaseItemDtoResponse<ReturnType<T[K]>>> }> {
|
||||
ensureCorrectUsage(remote.sdk.newUserApi(unref(api))[unref(methodName)]);
|
||||
|
||||
/**
|
||||
* For some reason, the watcher also fires on startup, so we need to keep track of that to avoid double requests.
|
||||
*/
|
||||
let initialFetchDone = false;
|
||||
const loading = ref(true);
|
||||
const requestData = ref<Awaited<ReturnType<T[K]>['data']>>();
|
||||
const calledFunctions = computed(() => `${api.name}.${methodName.toString()}`);
|
||||
const calledFunctions = computed(() => `${unref(api).name}.${unref(methodName).toString()}`);
|
||||
|
||||
/**
|
||||
* Returns a proxy ref from the store
|
||||
*/
|
||||
// @ts-expect-error - Typings get too complex at this point
|
||||
const data = computed<ExtractBaseItemDtoResponse<ReturnType<T[K]>>>(() => {
|
||||
if (typeof requestData.value === 'object') {
|
||||
// @ts-expect-error - We check both capitalizations just in case
|
||||
const itemArray: BaseItemDto[] | undefined = requestData.value.items ?? requestData.value.Items;
|
||||
|
||||
if (Array.isArray(itemArray)) {
|
||||
const ids = itemArray.map((i) => i.Id).filter((id): id is string => typeof id === 'string');
|
||||
|
||||
return items.getItemsById(ids).filter((item): item is BaseItemDto => typeof item === 'object');
|
||||
} else {
|
||||
return items.getItemById((requestData.value as BaseItemDto).Id);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return async function (this: any, ...args: ParametersAsGetters<T[K]>) {
|
||||
const run = async (args: ParametersAsGetters<T[K]>): Promise<void> => {
|
||||
try {
|
||||
await resolveAndAdd(unref(api), unref(methodName), true, requestData, loading, ...args);
|
||||
initialFetchDone = true;
|
||||
} catch {}
|
||||
};
|
||||
|
||||
if (getCurrentScope() !== undefined) {
|
||||
const cbk = async (): Promise<void> => {
|
||||
if (initialFetchDone) {
|
||||
await run(args);
|
||||
}
|
||||
};
|
||||
|
||||
watch(args, cbk);
|
||||
watch(calledFunctions, cbk);
|
||||
}
|
||||
|
||||
await run(args);
|
||||
|
||||
return { loading, data };
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Initial JSDoc
|
||||
*
|
||||
* @param api
|
||||
* @param methodName
|
||||
* @returns
|
||||
*/
|
||||
export function useApi<T extends Record<K, (...args: any[]) => any>, K extends keyof T>(
|
||||
api: MaybeRef<(api: Api) => T>,
|
||||
methodName: MaybeRef<K>
|
||||
): (this: any, ...args: ParametersAsGetters<T[K]>) => Promise<{ loading: Ref<boolean>, data: ComputedRef<ExtractResponseType<ReturnType<T[K]>>> }> {
|
||||
ensureCorrectUsage(remote.sdk.newUserApi(unref(api))[unref(methodName)]);
|
||||
|
||||
/**
|
||||
* For some reason, the watcher also fires on startup, so we need to keep track of that to avoid double requests.
|
||||
*/
|
||||
let initialFetchDone = false;
|
||||
const loading = ref(true);
|
||||
const requestData = ref<Awaited<ReturnType<T[K]>['data']>>();
|
||||
const calledFunctions = computed(() => `${unref(api).name}.${unref(methodName).toString()}`);
|
||||
|
||||
/**
|
||||
* Returns a proxy ref from the store
|
||||
@ -86,46 +205,30 @@ export function useBaseItem<T extends Record<K, (...args: any[]) => any>, K exte
|
||||
}
|
||||
});
|
||||
|
||||
const resolveAndAdd = async (args: Array<() => ParametersAsGetters<T[K]>>): Promise<void> => {
|
||||
/**
|
||||
* We add all BaseItemDto's fields for consistency in what we can expect from the store.
|
||||
* toValue normalizes the getters.
|
||||
*/
|
||||
const extendedParams = [
|
||||
{ ...toValue(args[0]), fields: allFields },
|
||||
...args.slice(1).map((a) => toValue(a))
|
||||
] as Parameters<T[K]>;
|
||||
|
||||
try {
|
||||
loading.value = true;
|
||||
|
||||
const response = await remote.sdk.newUserApi(api)[methodName](...extendedParams) as Awaited<ReturnType<T[K]>>;
|
||||
|
||||
if (response.data) {
|
||||
requestData.value = response.data;
|
||||
items.rawAdd(response.data as BaseItemDto | BaseItemDto[]);
|
||||
} else {
|
||||
requestData.value = undefined;
|
||||
}
|
||||
} catch {} finally {
|
||||
loading.value = false;
|
||||
initialFetchDone = true;
|
||||
}
|
||||
};
|
||||
|
||||
return async function (this: any, ...args: ParametersAsGetters<T[K]>) {
|
||||
const run = async (args: ParametersAsGetters<T[K]>): Promise<void> => {
|
||||
try {
|
||||
await resolveAndAdd(unref(api), unref(methodName), false, requestData, loading, ...args);
|
||||
initialFetchDone = true;
|
||||
} catch {}
|
||||
};
|
||||
|
||||
if (getCurrentScope() !== undefined) {
|
||||
watch([args, calledFunctions], async () => {
|
||||
// eslint-disable-next-line sonarjs/no-identical-functions
|
||||
const cbk = async (): Promise<void> => {
|
||||
if (initialFetchDone) {
|
||||
await resolveAndAdd(args);
|
||||
await run(args);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
watch(args, cbk);
|
||||
watch(calledFunctions, cbk);
|
||||
}
|
||||
|
||||
await resolveAndAdd(args);
|
||||
await run(args);
|
||||
|
||||
return { loading, data };
|
||||
};
|
||||
}
|
||||
|
||||
/* eslint-enable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any, @typescript-eslint/ban-ts-comment */
|
||||
/* eslint-enable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any, @typescript-eslint/ban-ts-comment, @typescript-eslint/no-unsafe-return */
|
||||
|
Loading…
x
Reference in New Issue
Block a user