reorganize global types to shared directory

This commit is contained in:
jeffvli 2025-05-20 18:08:51 -07:00
parent 26c02e03c5
commit 9db2e51d2d
17 changed files with 160 additions and 144 deletions

View file

@ -28,6 +28,7 @@ export default tseslint.config(
...eslintPluginReactHooks.configs.recommended.rules, ...eslintPluginReactHooks.configs.recommended.rules,
...eslintPluginReactRefresh.configs.vite.rules, ...eslintPluginReactRefresh.configs.vite.rules,
'@typescript-eslint/explicit-function-return-type': 'off', '@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/no-duplicate-enum-values': 'off',
'@typescript-eslint/no-unused-vars': 'warn', '@typescript-eslint/no-unused-vars': 'warn',
curly: ['error', 'all'], curly: ['error', 'all'],
indent: [ indent: [

View file

@ -1,34 +1,6 @@
import { AxiosHeaders } from 'axios'; import { toast } from '/@/renderer/components';
import isElectron from 'is-electron';
import semverCoerce from 'semver/functions/coerce';
import semverGte from 'semver/functions/gte';
import { z } from 'zod';
import { ServerFeature } from '/@/renderer/api/features-types';
import { ServerListItem } from '/@/renderer/api/types';
import { toast } from '/@/renderer/components/toast';
import { useAuthStore } from '/@/renderer/store'; import { useAuthStore } from '/@/renderer/store';
import { ServerListItem } from '/@/shared/types/types';
// Since ts-rest client returns a strict response type, we need to add the headers to the body object
export const resultWithHeaders = <ItemType extends z.ZodTypeAny>(itemSchema: ItemType) => {
return z.object({
data: itemSchema,
headers: z.instanceof(AxiosHeaders),
});
};
export const resultSubsonicBaseResponse = <ItemType extends z.ZodRawShape>(
itemSchema: ItemType,
) => {
return z.object({
'subsonic-response': z
.object({
status: z.string(),
version: z.string(),
})
.extend(itemSchema),
});
};
export const authenticationFailure = (currentServer: null | ServerListItem) => { export const authenticationFailure = (currentServer: null | ServerListItem) => {
toast.error({ toast.error({
@ -43,87 +15,3 @@ export const authenticationFailure = (currentServer: null | ServerListItem) => {
useAuthStore.getState().actions.setCurrentServer(null); useAuthStore.getState().actions.setCurrentServer(null);
} }
}; };
export const hasFeature = (server: null | ServerListItem, feature: ServerFeature): boolean => {
if (!server || !server.features) {
return false;
}
return (server.features[feature]?.length || 0) > 0;
};
export type VersionInfo = ReadonlyArray<[string, Record<string, readonly number[]>]>;
/**
* Returns the available server features given the version string.
* @param versionInfo a list, in DECREASING VERSION order, of the features supported by the server.
* The first version match will automatically consider the rest matched.
* @example
* ```
* // The CORRECT way to order
* const VERSION_INFO: VersionInfo = [
* ['0.49.3', { [ServerFeature.SHARING_ALBUM_SONG]: [1] }],
* ['0.48.0', { [ServerFeature.PLAYLISTS_SMART]: [1] }],
* ];
* // INCORRECT way to order
* const VERSION_INFO: VersionInfo = [
* ['0.48.0', { [ServerFeature.PLAYLISTS_SMART]: [1] }],
* ['0.49.3', { [ServerFeature.SHARING_ALBUM_SONG]: [1] }],
* ];
* ```
* @param version the version string (SemVer)
* @returns a Record containing the matched features (if any) and their versions
*/
export const getFeatures = (
versionInfo: VersionInfo,
version: string,
): Record<string, number[]> => {
const cleanVersion = semverCoerce(version);
const features: Record<string, number[]> = {};
let matched = cleanVersion === null;
for (const [version, supportedFeatures] of versionInfo) {
if (!matched) {
matched = semverGte(cleanVersion!, version);
}
if (matched) {
for (const [feature, feat] of Object.entries(supportedFeatures)) {
if (feature in features) {
features[feature].push(...feat);
} else {
features[feature] = [...feat];
}
}
}
}
return features;
};
export const getClientType = (): string => {
if (isElectron()) {
return 'Desktop Client';
}
const agent = navigator.userAgent;
switch (true) {
case agent.toLowerCase().indexOf('edge') > -1:
return 'Microsoft Edge';
case agent.toLowerCase().indexOf('edg/') > -1:
return 'Edge Chromium'; // Match also / to avoid matching for the older Edge
case agent.toLowerCase().indexOf('opr') > -1:
return 'Opera';
case agent.toLowerCase().indexOf('chrome') > -1:
return 'Chrome';
case agent.toLowerCase().indexOf('trident') > -1:
return 'Internet Explorer';
case agent.toLowerCase().indexOf('firefox') > -1:
return 'Firefox';
case agent.toLowerCase().indexOf('safari') > -1:
return 'Safari';
default:
return 'PC';
}
};
export const SEPARATOR_STRING = ' · ';

View file

@ -1,8 +1,8 @@
import { nanoid } from 'nanoid'; import { nanoid } from 'nanoid';
import { z } from 'zod'; import { z } from 'zod';
import { JFAlbum, JFGenre, JFMusicFolder, JFPlaylist } from '/@/renderer/api/jellyfin.types'; import { JFAlbum, JFGenre, JFMusicFolder, JFPlaylist } from '/@/shared/api/jellyfin.types';
import { jfType } from '/@/renderer/api/jellyfin/jellyfin-types'; import { jfType } from '/@/shared/api/jellyfin/jellyfin-types';
import { import {
Album, Album,
AlbumArtist, AlbumArtist,
@ -11,10 +11,9 @@ import {
MusicFolder, MusicFolder,
Playlist, Playlist,
RelatedArtist, RelatedArtist,
ServerListItem,
ServerType,
Song, Song,
} from '/@/renderer/api/types'; } from '/@/shared/types/domain-types';
import { ServerListItem, ServerType } from '/@/shared/types/types';
const getStreamUrl = (args: { const getStreamUrl = (args: {
container?: string; container?: string;

View file

@ -1,4 +1,4 @@
import { SSArtistInfo } from '/@/renderer/api/subsonic.types'; import { SSArtistInfo } from '/@/shared/api/subsonic.types';
export enum NDAlbumArtistListSort { export enum NDAlbumArtistListSort {
ALBUM_COUNT = 'albumCount', ALBUM_COUNT = 'albumCount',
@ -197,7 +197,7 @@ export type NDCreatePlaylistParams = {
comment?: string; comment?: string;
name: string; name: string;
public?: boolean; public?: boolean;
rules?: null | Record<string, any>; rules?: null | Record<string, unknown>;
}; };
export type NDCreatePlaylistResponse = { export type NDCreatePlaylistResponse = {
@ -247,7 +247,7 @@ export type NDPlaylist = {
ownerName: string; ownerName: string;
path: string; path: string;
public: boolean; public: boolean;
rules: null | Record<string, any>; rules: null | Record<string, unknown>;
size: number; size: number;
songCount: number; songCount: number;
sync: boolean; sync: boolean;

View file

@ -1,10 +1,9 @@
import { nanoid } from 'nanoid'; import { nanoid } from 'nanoid';
import z from 'zod'; import z from 'zod';
import { ndType } from './navidrome-types'; import { NDGenre } from '/@/shared/api/navidrome.types';
import { ndType } from '/@/shared/api/navidrome/navidrome-types';
import { NDGenre } from '/@/renderer/api/navidrome.types'; import { ssType } from '/@/shared/api/subsonic/subsonic-types';
import { ssType } from '/@/renderer/api/subsonic/subsonic-types';
import { import {
Album, Album,
AlbumArtist, AlbumArtist,
@ -12,11 +11,10 @@ import {
LibraryItem, LibraryItem,
Playlist, Playlist,
RelatedArtist, RelatedArtist,
ServerListItem,
ServerType,
Song, Song,
User, User,
} from '/@/renderer/api/types'; } from '/@/shared/types/domain-types';
import { ServerListItem, ServerType } from '/@/shared/types/types';
const getImageUrl = (args: { url: null | string }) => { const getImageUrl = (args: { url: null | string }) => {
const { url } = args; const { url } = args;

View file

@ -5,7 +5,7 @@ import {
NDAlbumListSort, NDAlbumListSort,
NDPlaylistListSort, NDPlaylistListSort,
NDSongListSort, NDSongListSort,
} from '/@/renderer/api/navidrome.types'; } from '/@/shared/api/navidrome.types';
const sortOrderValues = ['ASC', 'DESC'] as const; const sortOrderValues = ['ASC', 'DESC'] as const;

View file

@ -1,7 +1,7 @@
import { nanoid } from 'nanoid'; import { nanoid } from 'nanoid';
import { z } from 'zod'; import { z } from 'zod';
import { ssType } from '/@/renderer/api/subsonic/subsonic-types'; import { ssType } from '/@/shared/api/subsonic/subsonic-types';
import { import {
Album, Album,
AlbumArtist, AlbumArtist,
@ -12,7 +12,7 @@ import {
RelatedArtist, RelatedArtist,
ServerListItem, ServerListItem,
ServerType, ServerType,
} from '/@/renderer/api/types'; } from '/@/shared/types/domain-types';
const getCoverArtUrl = (args: { const getCoverArtUrl = (args: {
baseUrl: string | undefined; baseUrl: string | undefined;

113
src/shared/api/utils.ts Normal file
View file

@ -0,0 +1,113 @@
import { AxiosHeaders } from 'axios';
import isElectron from 'is-electron';
import semverCoerce from 'semver/functions/coerce';
import semverGte from 'semver/functions/gte';
import { z } from 'zod';
import { ServerListItem } from '/@/shared/types/domain-types';
import { ServerFeature } from '/@/shared/types/features-types';
// Since ts-rest client returns a strict response type, we need to add the headers to the body object
export const resultWithHeaders = <ItemType extends z.ZodTypeAny>(itemSchema: ItemType) => {
return z.object({
data: itemSchema,
headers: z.instanceof(AxiosHeaders),
});
};
export const resultSubsonicBaseResponse = <ItemType extends z.ZodRawShape>(
itemSchema: ItemType,
) => {
return z.object({
'subsonic-response': z
.object({
status: z.string(),
version: z.string(),
})
.extend(itemSchema),
});
};
export const hasFeature = (server: null | ServerListItem, feature: ServerFeature): boolean => {
if (!server || !server.features) {
return false;
}
return (server.features[feature]?.length || 0) > 0;
};
export type VersionInfo = ReadonlyArray<[string, Record<string, readonly number[]>]>;
/**
* Returns the available server features given the version string.
* @param versionInfo a list, in DECREASING VERSION order, of the features supported by the server.
* The first version match will automatically consider the rest matched.
* @example
* ```
* // The CORRECT way to order
* const VERSION_INFO: VersionInfo = [
* ['0.49.3', { [ServerFeature.SHARING_ALBUM_SONG]: [1] }],
* ['0.48.0', { [ServerFeature.PLAYLISTS_SMART]: [1] }],
* ];
* // INCORRECT way to order
* const VERSION_INFO: VersionInfo = [
* ['0.48.0', { [ServerFeature.PLAYLISTS_SMART]: [1] }],
* ['0.49.3', { [ServerFeature.SHARING_ALBUM_SONG]: [1] }],
* ];
* ```
* @param version the version string (SemVer)
* @returns a Record containing the matched features (if any) and their versions
*/
export const getFeatures = (
versionInfo: VersionInfo,
version: string,
): Record<string, number[]> => {
const cleanVersion = semverCoerce(version);
const features: Record<string, number[]> = {};
let matched = cleanVersion === null;
for (const [version, supportedFeatures] of versionInfo) {
if (!matched) {
matched = semverGte(cleanVersion!, version);
}
if (matched) {
for (const [feature, feat] of Object.entries(supportedFeatures)) {
if (feature in features) {
features[feature].push(...feat);
} else {
features[feature] = [...feat];
}
}
}
}
return features;
};
export const getClientType = (): string => {
if (isElectron()) {
return 'Desktop Client';
}
const agent = navigator.userAgent;
switch (true) {
case agent.toLowerCase().indexOf('edge') > -1:
return 'Microsoft Edge';
case agent.toLowerCase().indexOf('edg/') > -1:
return 'Edge Chromium'; // Match also / to avoid matching for the older Edge
case agent.toLowerCase().indexOf('opr') > -1:
return 'Opera';
case agent.toLowerCase().indexOf('chrome') > -1:
return 'Chrome';
case agent.toLowerCase().indexOf('trident') > -1:
return 'Internet Explorer';
case agent.toLowerCase().indexOf('firefox') > -1:
return 'Firefox';
case agent.toLowerCase().indexOf('safari') > -1:
return 'Safari';
default:
return 'PC';
}
};
export const SEPARATOR_STRING = ' · ';

View file

@ -3,7 +3,8 @@ import reverse from 'lodash/reverse';
import shuffle from 'lodash/shuffle'; import shuffle from 'lodash/shuffle';
import { z } from 'zod'; import { z } from 'zod';
import { ServerFeatures } from './features-types'; import { jfType } from '../../renderer/api/jellyfin/jellyfin-types';
import { ndType } from '../api/navidrome/navidrome-types';
import { import {
JFAlbumArtistListSort, JFAlbumArtistListSort,
JFAlbumListSort, JFAlbumListSort,
@ -12,8 +13,7 @@ import {
JFPlaylistListSort, JFPlaylistListSort,
JFSongListSort, JFSongListSort,
JFSortOrder, JFSortOrder,
} from './jellyfin.types'; } from '../renderer/api/jellyfin.types';
import { jfType } from './jellyfin/jellyfin-types';
import { import {
NDAlbumArtistListSort, NDAlbumArtistListSort,
NDAlbumListSort, NDAlbumListSort,
@ -23,8 +23,8 @@ import {
NDSongListSort, NDSongListSort,
NDSortOrder, NDSortOrder,
NDUserListSort, NDUserListSort,
} from './navidrome.types'; } from '../renderer/api/navidrome.types';
import { ndType } from './navidrome/navidrome-types'; import { ServerFeatures } from './features-types';
export enum LibraryItem { export enum LibraryItem {
ALBUM = 'album', ALBUM = 'album',

View file

@ -1,6 +1,7 @@
import { AppRoute } from '@ts-rest/core';
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import { Song } from 'src/main/features/core/lyrics/netease';
import { ServerFeatures } from '/@/renderer/api/features-types';
import { import {
Album, Album,
AlbumArtist, AlbumArtist,
@ -8,9 +9,8 @@ import {
LibraryItem, LibraryItem,
Playlist, Playlist,
QueueSong, QueueSong,
Song, } from '/@/shared/types/domain-types';
} from '/@/renderer/api/types'; import { ServerFeatures } from '/@/shared/types/features-types';
import { AppRoute } from '/@/renderer/router/routes';
export enum ListDisplayType { export enum ListDisplayType {
CARD = 'card', CARD = 'card',

View file

@ -1,8 +1,24 @@
{ {
"extends": "@electron-toolkit/tsconfig/tsconfig.node.json", "extends": "@electron-toolkit/tsconfig/tsconfig.node.json",
"include": ["electron.vite.config.*", "src/main/**/*", "src/preload/**/*", "src/i18n/**/*"], "include": ["electron.vite.config.*", "src/main/**/*", "src/preload/**/*", "src/i18n/**/*", "src/types/**/*", "src/shared/**/*", "src/renderer/api/jellyfin/jellyfin-controller.ts", "src/renderer/api/jellyfin/jellyfin-api.ts", "src/renderer/api/navidrome/navidrome-api.ts", "src/renderer/api/navidrome/navidrome-controller.ts", "src/renderer/api/subsonic/subsonic-api.ts", "src/renderer/api/subsonic/subsonic-controller.ts"],
"compilerOptions": { "compilerOptions": {
"composite": true, "composite": true,
"types": ["electron-vite/node"] "types": ["electron-vite/node"],
"esModuleInterop": true,
"baseUrl": ".",
"paths": {
"/@/renderer/*": [
"src/renderer/*"
],
"/@/main/*": [
"src/main/src/*"
],
"/@/preload/*": [
"src/preload/*"
],
"/@/shared/*": [
"src/shared/*"
],
}
} }
} }

View file

@ -6,6 +6,7 @@
"src/renderer/**/*.tsx", "src/renderer/**/*.tsx",
"src/preload/*.d.ts", "src/preload/*.d.ts",
"src/i18n/**/*", "src/i18n/**/*",
"src/shared/**/*",
"package.json" "package.json"
], ],
"compilerOptions": { "compilerOptions": {
@ -21,7 +22,7 @@
"src/main/src/*" "src/main/src/*"
], ],
"/@/shared/*": [ "/@/shared/*": [
"src/shared/src/*" "src/shared/*"
], ],
"/@/i18n/*": [ "/@/i18n/*": [
"src/i18n/*" "src/i18n/*"