mirror of
https://github.com/antebudimir/feishin.git
synced 2025-12-31 18:13:31 +00:00
Merge branch 'development' into navidrome-version
This commit is contained in:
commit
cc6cad1d70
32 changed files with 531 additions and 345 deletions
|
|
@ -4,6 +4,7 @@ import {
|
|||
ReactNode,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
|
|
@ -114,9 +115,11 @@ export const SwiperGridCarousel = ({
|
|||
isLoading,
|
||||
uniqueId,
|
||||
}: SwiperGridCarouselProps) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const swiperRef = useRef<SwiperCore | any>(null);
|
||||
const playButtonBehavior = usePlayButtonBehavior();
|
||||
const handlePlayQueueAdd = usePlayQueueAdd();
|
||||
const [slideCount, setSlideCount] = useState(4);
|
||||
|
||||
useEffect(() => {
|
||||
swiperRef.current?.slideTo(0, 0);
|
||||
|
|
@ -191,23 +194,24 @@ export const SwiperGridCarousel = ({
|
|||
|
||||
const handleNext = useCallback(() => {
|
||||
const activeIndex = swiperRef?.current?.activeIndex || 0;
|
||||
const slidesPerView = Math.round(Number(swiperProps?.slidesPerView || 4));
|
||||
const slidesPerView = Math.round(Number(swiperProps?.slidesPerView || slideCount));
|
||||
swiperRef?.current?.slideTo(activeIndex + slidesPerView);
|
||||
}, [swiperProps?.slidesPerView]);
|
||||
}, [slideCount, swiperProps?.slidesPerView]);
|
||||
|
||||
const handlePrev = useCallback(() => {
|
||||
const activeIndex = swiperRef?.current?.activeIndex || 0;
|
||||
const slidesPerView = Math.round(Number(swiperProps?.slidesPerView || 4));
|
||||
const slidesPerView = Math.round(Number(swiperProps?.slidesPerView || slideCount));
|
||||
swiperRef?.current?.slideTo(activeIndex - slidesPerView);
|
||||
}, [swiperProps?.slidesPerView]);
|
||||
}, [slideCount, swiperProps?.slidesPerView]);
|
||||
|
||||
const handleOnSlideChange = useCallback((e: SwiperCore) => {
|
||||
const { slides, isEnd, isBeginning, params } = e;
|
||||
if (isEnd || isBeginning) return;
|
||||
|
||||
const slideCount = (params.slidesPerView as number | undefined) || 4;
|
||||
setPagination({
|
||||
hasNextPage: (params?.slidesPerView || 4) < slides.length,
|
||||
hasPreviousPage: (params?.slidesPerView || 4) < slides.length,
|
||||
hasNextPage: slideCount < slides.length,
|
||||
hasPreviousPage: slideCount < slides.length,
|
||||
});
|
||||
}, []);
|
||||
|
||||
|
|
@ -215,82 +219,106 @@ export const SwiperGridCarousel = ({
|
|||
const { slides, isEnd, isBeginning, params } = e;
|
||||
if (isEnd || isBeginning) return;
|
||||
|
||||
const slideCount = (params.slidesPerView as number | undefined) || 4;
|
||||
setPagination({
|
||||
hasNextPage: (params.slidesPerView || 4) < slides.length,
|
||||
hasPreviousPage: (params.slidesPerView || 4) < slides.length,
|
||||
hasNextPage: slideCount < slides.length,
|
||||
hasPreviousPage: slideCount < slides.length,
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleOnReachEnd = useCallback((e: SwiperCore) => {
|
||||
const { slides, params } = e;
|
||||
|
||||
const slideCount = (params.slidesPerView as number | undefined) || 4;
|
||||
setPagination({
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: (params.slidesPerView || 4) < slides.length,
|
||||
hasPreviousPage: slideCount < slides.length,
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleOnReachBeginning = useCallback((e: SwiperCore) => {
|
||||
const { slides, params } = e;
|
||||
|
||||
const slideCount = (params.slidesPerView as number | undefined) || 4;
|
||||
setPagination({
|
||||
hasNextPage: (params.slidesPerView || 4) < slides.length,
|
||||
hasNextPage: slideCount < slides.length,
|
||||
hasPreviousPage: false,
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleOnResize = useCallback((e: SwiperCore) => {
|
||||
if (!e) return;
|
||||
const { width } = e;
|
||||
const slidesPerView = getSlidesPerView(width);
|
||||
if (!e.params) return;
|
||||
e.params.slidesPerView = slidesPerView;
|
||||
}, []);
|
||||
useLayoutEffect(() => {
|
||||
const handleResize = () => {
|
||||
// Use the container div ref and not swiper width, as this value is more accurate
|
||||
const width = containerRef.current?.clientWidth;
|
||||
const { activeIndex, params, slides } =
|
||||
(swiperRef.current as SwiperCore | undefined) ?? {};
|
||||
|
||||
const throttledOnResize = throttle(handleOnResize, 200);
|
||||
if (width) {
|
||||
const slidesPerView = getSlidesPerView(width);
|
||||
setSlideCount(slidesPerView);
|
||||
}
|
||||
|
||||
if (activeIndex !== undefined && slides && params?.slidesPerView) {
|
||||
const slideCount = (params.slidesPerView as number | undefined) || 4;
|
||||
setPagination({
|
||||
hasNextPage: activeIndex + slideCount < slides.length,
|
||||
hasPreviousPage: activeIndex > 0,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
handleResize();
|
||||
|
||||
const throttledResize = throttle(handleResize, 200);
|
||||
window.addEventListener('resize', throttledResize);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', throttledResize);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<CarouselContainer
|
||||
className="grid-carousel"
|
||||
spacing="md"
|
||||
>
|
||||
{title ? (
|
||||
<Title
|
||||
{...title}
|
||||
handleNext={handleNext}
|
||||
handlePrev={handlePrev}
|
||||
pagination={pagination}
|
||||
/>
|
||||
) : null}
|
||||
<Swiper
|
||||
ref={swiperRef}
|
||||
resizeObserver
|
||||
modules={[Virtual]}
|
||||
slidesPerView={4}
|
||||
spaceBetween={20}
|
||||
style={{ height: '100%', width: '100%' }}
|
||||
onBeforeInit={(swiper) => {
|
||||
swiperRef.current = swiper;
|
||||
}}
|
||||
onBeforeResize={handleOnResize}
|
||||
onReachBeginning={handleOnReachBeginning}
|
||||
onReachEnd={handleOnReachEnd}
|
||||
onResize={throttledOnResize}
|
||||
onSlideChange={handleOnSlideChange}
|
||||
onZoomChange={handleOnZoomChange}
|
||||
{...swiperProps}
|
||||
>
|
||||
{slides.map((slideContent, index) => {
|
||||
return (
|
||||
<SwiperSlide
|
||||
key={`${uniqueId}-${slideContent?.props?.data?.id}-${index}`}
|
||||
virtualIndex={index}
|
||||
>
|
||||
{slideContent}
|
||||
</SwiperSlide>
|
||||
);
|
||||
})}
|
||||
</Swiper>
|
||||
<div ref={containerRef}>
|
||||
{title ? (
|
||||
<Title
|
||||
{...title}
|
||||
handleNext={handleNext}
|
||||
handlePrev={handlePrev}
|
||||
pagination={pagination}
|
||||
/>
|
||||
) : null}
|
||||
<Swiper
|
||||
ref={swiperRef}
|
||||
resizeObserver
|
||||
modules={[Virtual]}
|
||||
slidesPerView={slideCount}
|
||||
spaceBetween={20}
|
||||
style={{ height: '100%', width: '100%' }}
|
||||
onBeforeInit={(swiper) => {
|
||||
swiperRef.current = swiper;
|
||||
}}
|
||||
onReachBeginning={handleOnReachBeginning}
|
||||
onReachEnd={handleOnReachEnd}
|
||||
onSlideChange={handleOnSlideChange}
|
||||
onZoomChange={handleOnZoomChange}
|
||||
{...swiperProps}
|
||||
>
|
||||
{slides.map((slideContent, index) => {
|
||||
return (
|
||||
<SwiperSlide
|
||||
key={`${uniqueId}-${slideContent?.props?.data?.id}-${index}`}
|
||||
virtualIndex={index}
|
||||
>
|
||||
{slideContent}
|
||||
</SwiperSlide>
|
||||
);
|
||||
})}
|
||||
</Swiper>
|
||||
</div>
|
||||
</CarouselContainer>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -158,6 +158,14 @@ const tableColumns: { [key: string]: ColDef } = {
|
|||
params.data ? params.data.channels : undefined,
|
||||
width: 100,
|
||||
},
|
||||
codec: {
|
||||
cellRenderer: (params: ICellRendererParams) => GenericCell(params, { position: 'center' }),
|
||||
colId: TableColumn.CODEC,
|
||||
headerName: i18n.t('table.column.codec'),
|
||||
valueGetter: (params: ValueGetterParams) =>
|
||||
params.data ? params.data.container : undefined,
|
||||
width: 60,
|
||||
},
|
||||
comment: {
|
||||
cellRenderer: NoteCell,
|
||||
colId: TableColumn.COMMENT,
|
||||
|
|
|
|||
|
|
@ -60,6 +60,10 @@ export const SONG_TABLE_COLUMNS = [
|
|||
label: i18n.t('table.config.label.bitrate', { postProcess: 'titleCase' }),
|
||||
value: TableColumn.BIT_RATE,
|
||||
},
|
||||
{
|
||||
label: i18n.t('table.config.label.codec', { postProcess: 'titleCase' }),
|
||||
value: TableColumn.CODEC,
|
||||
},
|
||||
{
|
||||
label: i18n.t('table.config.label.lastPlayed', { postProcess: 'titleCase' }),
|
||||
value: TableColumn.LAST_PLAYED,
|
||||
|
|
|
|||
|
|
@ -522,7 +522,7 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => {
|
|||
|
||||
const handleUpdateRating = useCallback(
|
||||
(rating: number) => {
|
||||
if (!ctx.dataNodes || !ctx.data) return;
|
||||
if (!ctx.dataNodes && !ctx.data) return;
|
||||
|
||||
let uniqueServerIds: string[] = [];
|
||||
let items: AnyLibraryItems = [];
|
||||
|
|
|
|||
|
|
@ -89,8 +89,7 @@ export const PlayQueue = forwardRef(({ type }: QueueProps, ref: Ref<any>) => {
|
|||
|
||||
if (playbackType === PlaybackType.LOCAL) {
|
||||
mpvPlayer!.volume(volume);
|
||||
mpvPlayer!.setQueue(playerData);
|
||||
mpvPlayer!.play();
|
||||
mpvPlayer!.setQueue(playerData, false);
|
||||
}
|
||||
|
||||
play();
|
||||
|
|
|
|||
|
|
@ -134,7 +134,7 @@ export const FullScreenPlayerImage = () => {
|
|||
const albumArtRes = useSettingsStore((store) => store.general.albumArtRes);
|
||||
|
||||
const { queue } = usePlayerData();
|
||||
const { opacity, useImageAspectRatio } = useFullScreenPlayerStore();
|
||||
const { useImageAspectRatio } = useFullScreenPlayerStore();
|
||||
const currentSong = queue.current;
|
||||
const { color: background } = useFastAverageColor({
|
||||
algorithm: 'dominant',
|
||||
|
|
@ -250,7 +250,6 @@ export const FullScreenPlayerImage = () => {
|
|||
<MetadataContainer
|
||||
className="full-screen-player-image-metadata"
|
||||
maw="100%"
|
||||
opacity={opacity}
|
||||
spacing="xs"
|
||||
>
|
||||
<TextTitle
|
||||
|
|
@ -278,7 +277,6 @@ export const FullScreenPlayerImage = () => {
|
|||
to={generatePath(AppRoute.LIBRARY_ALBUMS_DETAIL, {
|
||||
albumId: currentSong?.albumId || '',
|
||||
})}
|
||||
transform="uppercase"
|
||||
w="100%"
|
||||
weight={600}
|
||||
>
|
||||
|
|
@ -292,7 +290,6 @@ export const FullScreenPlayerImage = () => {
|
|||
style={{
|
||||
textShadow: 'var(--fullscreen-player-text-shadow)',
|
||||
}}
|
||||
transform="uppercase"
|
||||
>
|
||||
{index > 0 && (
|
||||
<Text
|
||||
|
|
@ -313,7 +310,6 @@ export const FullScreenPlayerImage = () => {
|
|||
to={generatePath(AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL, {
|
||||
albumArtistId: artist.id,
|
||||
})}
|
||||
transform="uppercase"
|
||||
weight={600}
|
||||
>
|
||||
{artist.name}
|
||||
|
|
|
|||
|
|
@ -42,11 +42,11 @@ const HeaderItemWrapper = styled.div`
|
|||
z-index: 2;
|
||||
`;
|
||||
|
||||
interface TransparendGridContainerProps {
|
||||
interface TransparentGridContainerProps {
|
||||
opacity: number;
|
||||
}
|
||||
|
||||
const GridContainer = styled.div<TransparendGridContainerProps>`
|
||||
const GridContainer = styled.div<TransparentGridContainerProps>`
|
||||
display: grid;
|
||||
grid-template-rows: auto minmax(0, 1fr);
|
||||
grid-template-columns: 1fr;
|
||||
|
|
@ -82,8 +82,6 @@ export const FullScreenPlayerQueue = () => {
|
|||
},
|
||||
];
|
||||
|
||||
console.log('opacity', opacity);
|
||||
|
||||
return (
|
||||
<GridContainer
|
||||
className="full-screen-player-queue-container"
|
||||
|
|
|
|||
|
|
@ -153,7 +153,7 @@ const Controls = () => {
|
|||
defaultValue={opacity}
|
||||
label={(e) => `${e} %`}
|
||||
max={100}
|
||||
min={1}
|
||||
min={0}
|
||||
w="100%"
|
||||
onChangeEnd={(e) => setStore({ opacity: Number(e) })}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -175,8 +175,7 @@ export const useHandlePlayQueueAdd = () => {
|
|||
|
||||
if (playType === Play.NOW || !hadSong) {
|
||||
mpvPlayer!.pause();
|
||||
mpvPlayer!.setQueue(playerData);
|
||||
mpvPlayer!.play();
|
||||
mpvPlayer!.setQueue(playerData, false);
|
||||
} else {
|
||||
mpvPlayer!.setQueueNext(playerData);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import isElectron from 'is-electron';
|
|||
import { nanoid } from 'nanoid/non-secure';
|
||||
import { AuthenticationResponse, ServerType } from '/@/renderer/api/types';
|
||||
import { useAuthStoreActions } from '/@/renderer/store';
|
||||
import { ServerType, toServerType } from '/@/renderer/types';
|
||||
import { api } from '/@/renderer/api';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
|
|
@ -32,15 +33,27 @@ export const AddServerForm = ({ onCancel }: AddServerFormProps) => {
|
|||
const form = useForm({
|
||||
initialValues: {
|
||||
legacyAuth: false,
|
||||
name: '',
|
||||
name: (localSettings ? localSettings.env.SERVER_NAME : window.SERVER_NAME) ?? '',
|
||||
password: '',
|
||||
savePassword: false,
|
||||
type: ServerType.JELLYFIN,
|
||||
url: 'http://',
|
||||
type:
|
||||
(localSettings
|
||||
? localSettings.env.SERVER_TYPE
|
||||
: toServerType(window.SERVER_TYPE)) ?? ServerType.JELLYFIN,
|
||||
url: (localSettings ? localSettings.env.SERVER_URL : window.SERVER_URL) ?? 'https://',
|
||||
username: '',
|
||||
},
|
||||
});
|
||||
|
||||
// server lock for web is only true if lock is true *and* all other properties are set
|
||||
const serverLock =
|
||||
(localSettings
|
||||
? !!localSettings.env.SERVER_LOCK
|
||||
: !!window.SERVER_LOCK &&
|
||||
window.SERVER_TYPE &&
|
||||
window.SERVER_NAME &&
|
||||
window.SERVER_URL) || false;
|
||||
|
||||
const isSubmitDisabled = !form.values.name || !form.values.url || !form.values.username;
|
||||
|
||||
const handleSubmit = form.onSubmit(async (values) => {
|
||||
|
|
@ -61,7 +74,7 @@ export const AddServerForm = ({ onCancel }: AddServerFormProps) => {
|
|||
password: values.password,
|
||||
username: values.username,
|
||||
},
|
||||
values.type,
|
||||
values.type as ServerType,
|
||||
);
|
||||
|
||||
if (!data) {
|
||||
|
|
@ -75,7 +88,7 @@ export const AddServerForm = ({ onCancel }: AddServerFormProps) => {
|
|||
id: nanoid(),
|
||||
name: values.name,
|
||||
ndCredential: data.ndCredential,
|
||||
type: values.type,
|
||||
type: values.type as ServerType,
|
||||
url: values.url.replace(/\/$/, ''),
|
||||
userId: data.userId,
|
||||
username: data.username,
|
||||
|
|
@ -116,11 +129,13 @@ export const AddServerForm = ({ onCancel }: AddServerFormProps) => {
|
|||
>
|
||||
<SegmentedControl
|
||||
data={SERVER_TYPES}
|
||||
disabled={serverLock}
|
||||
{...form.getInputProps('type')}
|
||||
/>
|
||||
<Group grow>
|
||||
<TextInput
|
||||
data-autofocus
|
||||
disabled={serverLock}
|
||||
label={t('form.addServer.input', {
|
||||
context: 'name',
|
||||
postProcess: 'titleCase',
|
||||
|
|
@ -128,6 +143,7 @@ export const AddServerForm = ({ onCancel }: AddServerFormProps) => {
|
|||
{...form.getInputProps('name')}
|
||||
/>
|
||||
<TextInput
|
||||
disabled={serverLock}
|
||||
label={t('form.addServer.input', {
|
||||
context: 'url',
|
||||
postProcess: 'titleCase',
|
||||
|
|
|
|||
|
|
@ -131,6 +131,31 @@ export const WindowSettings = () => {
|
|||
isHidden: !isElectron(),
|
||||
title: t('setting.exitToTray', { postProcess: 'sentenceCase' }),
|
||||
},
|
||||
{
|
||||
control: (
|
||||
<Switch
|
||||
aria-label="Toggle start in tray"
|
||||
defaultChecked={settings.startMinimized}
|
||||
disabled={!isElectron()}
|
||||
onChange={(e) => {
|
||||
if (!e) return;
|
||||
localSettings?.set('window_start_minimized', e.currentTarget.checked);
|
||||
setSettings({
|
||||
window: {
|
||||
...settings,
|
||||
startMinimized: e.currentTarget.checked,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
),
|
||||
description: t('setting.startMinimized', {
|
||||
context: 'description',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
isHidden: !isElectron(),
|
||||
title: t('setting.startMinimized', { postProcess: 'sentenceCase' }),
|
||||
},
|
||||
];
|
||||
|
||||
return <SettingsSection options={windowOptions} />;
|
||||
|
|
|
|||
|
|
@ -38,6 +38,9 @@ export const SidebarIcon = ({ active, route, size }: SidebarIconProps) => {
|
|||
case AppRoute.LIBRARY_ALBUMS:
|
||||
if (active) return <RiAlbumFill size={size} />;
|
||||
return <RiAlbumLine size={size} />;
|
||||
case AppRoute.LIBRARY_ALBUM_ARTISTS:
|
||||
if (active) return <RiUserVoiceFill size={size} />;
|
||||
return <RiUserVoiceLine size={size} />;
|
||||
case AppRoute.LIBRARY_ARTISTS:
|
||||
if (active) return <RiUserVoiceFill size={size} />;
|
||||
return <RiUserVoiceLine size={size} />;
|
||||
|
|
|
|||
|
|
@ -6,6 +6,9 @@
|
|||
<meta http-equiv="Content-Security-Policy" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Feishin</title>
|
||||
<% if (web) { %>
|
||||
<script src="settings.js"></script>
|
||||
<% } %>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
|
|
|||
4
src/renderer/preload.d.ts
vendored
4
src/renderer/preload.d.ts
vendored
|
|
@ -13,6 +13,10 @@ import { Browser } from '/@/main/preload/browser';
|
|||
|
||||
declare global {
|
||||
interface Window {
|
||||
SERVER_LOCK?: boolean;
|
||||
SERVER_NAME?: string;
|
||||
SERVER_TYPE?: string;
|
||||
SERVER_URL?: string;
|
||||
electron: {
|
||||
browser: Browser;
|
||||
discordRpc: DiscordRpc;
|
||||
|
|
|
|||
|
|
@ -267,6 +267,7 @@ export interface SettingsState {
|
|||
disableAutoUpdate: boolean;
|
||||
exitToTray: boolean;
|
||||
minimizeToTray: boolean;
|
||||
startMinimized: boolean;
|
||||
windowBarStyle: Platform;
|
||||
};
|
||||
}
|
||||
|
|
@ -575,6 +576,7 @@ const initialState: SettingsState = {
|
|||
disableAutoUpdate: false,
|
||||
exitToTray: false,
|
||||
minimizeToTray: false,
|
||||
startMinimized: false,
|
||||
windowBarStyle: platformDefaultWindowBarStyle,
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -54,6 +54,37 @@ export enum Platform {
|
|||
WINDOWS = 'windows',
|
||||
}
|
||||
|
||||
export enum ServerType {
|
||||
JELLYFIN = 'jellyfin',
|
||||
NAVIDROME = 'navidrome',
|
||||
SUBSONIC = 'subsonic',
|
||||
}
|
||||
|
||||
export const toServerType = (value?: string): ServerType | null => {
|
||||
switch (value?.toLowerCase()) {
|
||||
case ServerType.JELLYFIN:
|
||||
return ServerType.JELLYFIN;
|
||||
case ServerType.NAVIDROME:
|
||||
return ServerType.NAVIDROME;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export type ServerListItem = {
|
||||
credential: string;
|
||||
features?: Record<string, number[]>;
|
||||
id: string;
|
||||
name: string;
|
||||
ndCredential?: string;
|
||||
savePassword?: boolean;
|
||||
type: ServerType;
|
||||
url: string;
|
||||
userId: string | null;
|
||||
username: string;
|
||||
version?: string;
|
||||
};
|
||||
|
||||
export enum PlayerStatus {
|
||||
PAUSED = 'paused',
|
||||
PLAYING = 'playing',
|
||||
|
|
@ -124,6 +155,7 @@ export enum TableColumn {
|
|||
BIT_RATE = 'bitRate',
|
||||
BPM = 'bpm',
|
||||
CHANNELS = 'channels',
|
||||
CODEC = 'codec',
|
||||
COMMENT = 'comment',
|
||||
DATE_ADDED = 'dateAdded',
|
||||
DISC_NUMBER = 'discNumber',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue