Compare commits

...

12 Commits

Author SHA1 Message Date
jeffvli 7243ed7f15 fix timestamp restore when song switched before playback start (#2094) 2026-05-31 23:09:33 -07:00
York 7e9a78898f fix: punctuation hotkeys not captured (#2091) 2026-05-31 22:43:58 -07:00
Kendall Garner 6aab8d4121 fix(scrobble): use proper pause/unpause for seek, use ms for subsonic 2026-05-31 21:13:11 -07:00
Kendall Garner 70594a696b chore(context menu): show go to only for albums/tracks, prefer artist over album artist where possible 2026-05-31 19:44:58 -07:00
York 08b4c620f2 Merge pull request #2082 from york9675/fix/spacebar
Fix playback hotkeys by preventing browser default
2026-05-31 14:36:53 +00:00
Hosted Weblate 7a20cf3853 Translated using Weblate
Currently translated at 100.0% (1244 of 1244 strings) (Chinese (Traditional Han script))
Translation: feishin/Translation
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/zh_Hant/

Co-authored-by: York <goog10216922@gmail.com>
2026-05-31 10:51:08 +02:00
Hosted Weblate dd186c570f Translated using Weblate
Currently translated at 0.8% (10 of 1244 strings) (Tagalog)
Translation: feishin/Translation
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/tl/

Co-authored-by: Redd <reddtheeditor@gmail.com>
2026-05-31 08:04:07 +02:00
Hosted Weblate d5e9d491b6 Added translation using Weblate (Tagalog)
Co-authored-by: Redd <reddtheeditor@gmail.com>
2026-05-31 06:08:43 +02:00
Hosted Weblate 28dc822e4f Translated using Weblate
Currently translated at 100.0% (1244 of 1244 strings) (Spanish)
Translation: feishin/Translation
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/es/

Translated using Weblate

Currently translated at 100.0% (1244 of 1244 strings) (Polish)
Translation: feishin/Translation
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/pl/

Translated using Weblate

Currently translated at 100.0% (1244 of 1244 strings) (Czech)
Translation: feishin/Translation
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/cs/

Translated using Weblate

Currently translated at 84.1% (1047 of 1244 strings) (English)
Translation: feishin/Translation
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/en/

Co-authored-by: Fjuro <fjuro@users.noreply.hosted.weblate.org>
Co-authored-by: Fordas <fordas15@gmail.com>
Co-authored-by: Lucas Winther <lucasw89@live.dk>
Co-authored-by: skajmer <skajmer@protonmail.com>
2026-05-30 19:01:12 +00:00
Kendall Garner def1b1e710 fix(butterchurn): allow unsafe wasm for butterchurn 2026-05-30 07:01:53 -07:00
Kendall Garner 2ff9e4b0a2 fix(subsonic): fix favoriting/unfavoriting on playlist songs 2026-05-29 09:19:39 -07:00
jeffvli 49bfc907cd bump lock-threads to v6 2026-05-28 23:07:47 -07:00
17 changed files with 142 additions and 69 deletions
+2 -1
View File
@@ -12,7 +12,8 @@ jobs:
pull-requests: write pull-requests: write
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: dessant/lock-threads@v5 - uses: dessant/lock-threads@v6
with: with:
process-only: 'issues, prs' process-only: 'issues, prs'
issue-inactive-days: 120 issue-inactive-days: 120
+1 -1
View File
@@ -296,7 +296,7 @@
"releaseChannel": "Kanál vydání", "releaseChannel": "Kanál vydání",
"releaseChannel_description": "Vyberte si mezi stabilními, beta nebo alpha (nočními) vydáními pro automatické aktualizace", "releaseChannel_description": "Vyberte si mezi stabilními, beta nebo alpha (nočními) vydáními pro automatické aktualizace",
"mediaSession": "Povolit relaci médií", "mediaSession": "Povolit relaci médií",
"mediaSession_description": "Povolí integraci do služby Media Session, což zobrazí ovládání a metadata médií v překrytí systémové hlasitosti a na zamykací obrazovce", "mediaSession_description": "Povolí integraci do služby Media Session, což zobrazí ovládání a metadata médií v překrytí systémové hlasitosti a na zamykací obrazovce. Vyžaduje webový přehrávač zvuku.",
"exportImportSettings_control_description": "Exportovat a importovat nastavení pomocí souboru JSON", "exportImportSettings_control_description": "Exportovat a importovat nastavení pomocí souboru JSON",
"exportImportSettings_control_exportText": "Exportovat nastavení", "exportImportSettings_control_exportText": "Exportovat nastavení",
"exportImportSettings_control_importText": "Importovat nastavení", "exportImportSettings_control_importText": "Importovat nastavení",
+1 -1
View File
@@ -1093,7 +1093,7 @@
"sidePlayQueueLayout_description": "Sets the layout of the attached side play queue", "sidePlayQueueLayout_description": "Sets the layout of the attached side play queue",
"sidePlayQueueLayout_optionHorizontal": "Horizontal", "sidePlayQueueLayout_optionHorizontal": "Horizontal",
"sidePlayQueueLayout_optionVertical": "Vertical", "sidePlayQueueLayout_optionVertical": "Vertical",
"mediaSession_description": "Enables media session integration, displaying media controls and metadata in the system volume overlay and lock screen", "mediaSession_description": "Enables media session integration, displaying media controls and metadata in the system volume overlay and lock screen. Requires the Web Audio Player.",
"mediaSession": "Enable media session", "mediaSession": "Enable media session",
"sidePlayQueueStyle": "Side play queue style", "sidePlayQueueStyle": "Side play queue style",
"skipDuration_description": "Sets the duration to skip when using the skip buttons on the player bar", "skipDuration_description": "Sets the duration to skip when using the skip buttons on the player bar",
+1 -1
View File
@@ -296,7 +296,7 @@
"releaseChannel_description": "Elige entre lanzamientos estables, beta, o alpha (nightly) para las actualizaciones automáticas", "releaseChannel_description": "Elige entre lanzamientos estables, beta, o alpha (nightly) para las actualizaciones automáticas",
"artistBackground_description": "Añade una imagen de fondo para las páginas de artistas que contienen el arte de los artistas", "artistBackground_description": "Añade una imagen de fondo para las páginas de artistas que contienen el arte de los artistas",
"mediaSession": "Activar sesión de medios", "mediaSession": "Activar sesión de medios",
"mediaSession_description": "Activa la integración de la sesión de medios, mostrando los controles de medios y los metadatos en la superposición del volumen del sistema y en la pantalla de bloqueo", "mediaSession_description": "Activa la integración de la sesión de medios, mostrando los controles de medios y los metadatos en la superposición del volumen del sistema y en la pantalla de bloqueo. Requiere el Reproductor Web de Audio.",
"exportImportSettings_control_description": "Exporta e importa la configuración a través de JSON", "exportImportSettings_control_description": "Exporta e importa la configuración a través de JSON",
"exportImportSettings_control_exportText": "Exportar configuración", "exportImportSettings_control_exportText": "Exportar configuración",
"exportImportSettings_control_importText": "Importar configuración", "exportImportSettings_control_importText": "Importar configuración",
+1 -1
View File
@@ -979,7 +979,7 @@
"preservePitch": "Utrzymuj ton", "preservePitch": "Utrzymuj ton",
"preventSleepOnPlayback_description": "Powstrzymuje ekran przed uśpieniem, gdy muzyka jest odtwarzana", "preventSleepOnPlayback_description": "Powstrzymuje ekran przed uśpieniem, gdy muzyka jest odtwarzana",
"preventSleepOnPlayback": "Powstrzymuj uśpienie podczas odtwarzania", "preventSleepOnPlayback": "Powstrzymuj uśpienie podczas odtwarzania",
"mediaSession_description": "Włącza integrację z Media Session, wyświetlając sterowanie mediami i metadane w systemowym oknie zmiany głośności i na ekranie blokady", "mediaSession_description": "Włącza integrację z Media Session, wyświetlając sterowanie mediami i metadane w systemowym oknie zmiany głośności i na ekranie blokad. Wymaga odtwarzacza web audio.",
"mediaSession": "Włącz media session", "mediaSession": "Włącz media session",
"transcode": "Włącz transkodowanie", "transcode": "Włącz transkodowanie",
"queryBuilder": "Kreator zaptań", "queryBuilder": "Kreator zaptań",
+14
View File
@@ -0,0 +1,14 @@
{
"action": {
"addToFavorites": "Idagdag sa $t(entity.favorite, {\"count\": 2})",
"addToPlaylist": "Idagdag sa $t(entity.playlist, {\"count\": 1})",
"addOrRemoveFromSelection": "Idagdag o alisin sa pinili",
"collapseAllFolders": "Isara lahat ng mga folder",
"expandAllFolders": "Buksan lahat ng mga folder",
"createPlaylist": "Gumawa $t(entity.playlist, {\"count\": 1})",
"createRadioStation": "Gumawa $t(entity.radioStation, {\"count\": 1})",
"selectAll": "Piliin lahat",
"deselectAll": "Huwag piliin lahat",
"downloadStarted": "Nagsimulang mag-dowload ng {{count}} (mga) aytem"
}
}
+1 -1
View File
@@ -666,7 +666,7 @@
"preventSleepOnPlayback": "防止播放時進入睡眠狀態", "preventSleepOnPlayback": "防止播放時進入睡眠狀態",
"preventSleepOnPlayback_description": "在音樂播放時防止螢幕進入睡眠狀態", "preventSleepOnPlayback_description": "在音樂播放時防止螢幕進入睡眠狀態",
"mediaSession": "啟用 Media Session", "mediaSession": "啟用 Media Session",
"mediaSession_description": "啟用 Media Session 整合功能,系統音量 Overlay 和鎖定畫面顯示媒體資料與控制面板", "mediaSession_description": "啟用 Media Session 整合功能,系統音量疊加層和鎖定畫面顯示媒體控制項與中繼資料。此功能需要使用網頁播放器。",
"releaseChannel": "發佈通道", "releaseChannel": "發佈通道",
"analyticsDisable": "選擇退出使用情況分析", "analyticsDisable": "選擇退出使用情況分析",
"analyticsDisable_description": "經過匿名處理的使用情況資料將傳送給開發者,以協助改進應用程式", "analyticsDisable_description": "經過匿名處理的使用情況資料將傳送給開發者,以協助改進應用程式",
+1 -1
View File
@@ -1039,7 +1039,7 @@ if (!singleInstance) {
responseHeaders: { responseHeaders: {
...details.responseHeaders, ...details.responseHeaders,
'Content-Security-Policy': [ 'Content-Security-Policy': [
"script-src 'self' 'unsafe-inline' https://umami.jeffvli.org; style-src 'self' 'unsafe-inline'; media-src 'self' http: https: data: blob:; img-src 'self' http: https: data: blob:; connect-src 'self' http: https: ws: wss:; default-src 'self';", "script-src 'self' 'wasm-unsafe-eval' 'unsafe-inline' https://umami.jeffvli.org; style-src 'self' 'unsafe-inline'; media-src 'self' http: https: data: blob:; img-src 'self' http: https: data: blob:; connect-src 'self' http: https: ws: wss:; default-src 'self';",
], ],
}, },
}); });
@@ -366,7 +366,10 @@ export const SubsonicController: InternalControllerEndpoint = {
query.type === LibraryItem.ALBUM_ARTIST || query.type === LibraryItem.ARTIST query.type === LibraryItem.ALBUM_ARTIST || query.type === LibraryItem.ARTIST
? query.id ? query.id
: undefined, : undefined,
id: query.type === LibraryItem.SONG ? query.id : undefined, id:
query.type === LibraryItem.SONG || query.type === LibraryItem.PLAYLIST_SONG
? query.id
: undefined,
}, },
}); });
@@ -419,7 +422,10 @@ export const SubsonicController: InternalControllerEndpoint = {
query.type === LibraryItem.ALBUM_ARTIST || query.type === LibraryItem.ARTIST query.type === LibraryItem.ALBUM_ARTIST || query.type === LibraryItem.ARTIST
? query.id ? query.id
: undefined, : undefined,
id: query.type === LibraryItem.SONG ? query.id : undefined, id:
query.type === LibraryItem.SONG || query.type === LibraryItem.PLAYLIST_SONG
? query.id
: undefined,
}, },
}); });
@@ -2327,7 +2333,6 @@ export const SubsonicController: InternalControllerEndpoint = {
case 'start': case 'start':
state = 'starting'; state = 'starting';
break; break;
case 'timeupdate':
case 'unpause': case 'unpause':
state = 'playing'; state = 'playing';
break; break;
@@ -4,50 +4,38 @@ import { generatePath, useNavigate } from 'react-router';
import { AppRoute } from '/@/renderer/router/routes'; import { AppRoute } from '/@/renderer/router/routes';
import { ContextMenu } from '/@/shared/components/context-menu/context-menu'; import { ContextMenu } from '/@/shared/components/context-menu/context-menu';
import { import { Album, LibraryItem, QueueSong, Song } from '/@/shared/types/domain-types';
Album,
AlbumArtist,
Artist,
LibraryItem,
QueueSong,
Song,
} from '/@/shared/types/domain-types';
interface GoToActionProps { interface GoToActionProps {
items: Album[] | AlbumArtist[] | Artist[] | QueueSong[] | Song[]; items: Album[] | QueueSong[] | Song[];
} }
export const GoToAction = ({ items }: GoToActionProps) => { export const GoToAction = ({ items }: GoToActionProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const navigate = useNavigate(); const navigate = useNavigate();
const { albumArtists, albumId } = useMemo(() => { const { albumId, artists } = useMemo(() => {
const firstItem = items[0]; const firstItem = items[0];
if (firstItem._itemType === LibraryItem.ALBUM) { switch (firstItem._itemType) {
case LibraryItem.ALBUM:
return { return {
albumArtists: firstItem.albumArtists || [],
albumId: firstItem.id, albumId: firstItem.id,
artists: firstItem.albumArtists || [],
}; };
} else if (firstItem._itemType === LibraryItem.SONG) { case LibraryItem.SONG:
return { return {
albumArtists: firstItem.albumArtists || [],
albumId: firstItem.albumId, albumId: firstItem.albumId,
artists:
(firstItem.artists?.length ? firstItem.artists : firstItem.albumArtists) ||
[],
}; };
} else if ( default:
firstItem._itemType === LibraryItem.ARTIST ||
firstItem._itemType === LibraryItem.ALBUM_ARTIST
) {
return { return {
albumArtists: [{ id: firstItem.id, name: firstItem.name }],
albumId: null, albumId: null,
artists: [],
}; };
} }
return {
albumArtists: [],
albumId: null,
};
}, [items]); }, [items]);
const handleGoToAlbum = useCallback(() => { const handleGoToAlbum = useCallback(() => {
@@ -55,7 +43,7 @@ export const GoToAction = ({ items }: GoToActionProps) => {
navigate(generatePath(AppRoute.LIBRARY_ALBUMS_DETAIL, { albumId })); navigate(generatePath(AppRoute.LIBRARY_ALBUMS_DETAIL, { albumId }));
}, [albumId, navigate]); }, [albumId, navigate]);
const handleGoToAlbumArtist = useCallback( const handleGoToArtist = useCallback(
(albumArtistId: string) => { (albumArtistId: string) => {
navigate(generatePath(AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL, { albumArtistId })); navigate(generatePath(AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL, { albumArtistId }));
}, },
@@ -81,13 +69,13 @@ export const GoToAction = ({ items }: GoToActionProps) => {
{t('page.contextMenu.goToAlbum')} {t('page.contextMenu.goToAlbum')}
</ContextMenu.Item> </ContextMenu.Item>
)} )}
{albumArtists.map((albumArtist) => ( {artists.map((artist) => (
<ContextMenu.Item <ContextMenu.Item
key={albumArtist.id} key={artist.id}
leftIcon="artist" leftIcon="artist"
onSelect={() => handleGoToAlbumArtist(albumArtist.id)} onSelect={() => handleGoToArtist(artist.id)}
> >
{`${t('page.contextMenu.goTo')} ${albumArtist.name}`} {`${t('page.contextMenu.goTo')} ${artist.name}`}
</ContextMenu.Item> </ContextMenu.Item>
))} ))}
</ContextMenu.SubmenuContent> </ContextMenu.SubmenuContent>
@@ -3,7 +3,6 @@ import { useMemo } from 'react';
import { AddToPlaylistAction } from '/@/renderer/features/context-menu/actions/add-to-playlist-action'; import { AddToPlaylistAction } from '/@/renderer/features/context-menu/actions/add-to-playlist-action';
import { DownloadAction } from '/@/renderer/features/context-menu/actions/download-action'; import { DownloadAction } from '/@/renderer/features/context-menu/actions/download-action';
import { GetInfoAction } from '/@/renderer/features/context-menu/actions/get-info-action'; import { GetInfoAction } from '/@/renderer/features/context-menu/actions/get-info-action';
import { GoToAction } from '/@/renderer/features/context-menu/actions/go-to-action';
import { PlayAction } from '/@/renderer/features/context-menu/actions/play-action'; import { PlayAction } from '/@/renderer/features/context-menu/actions/play-action';
import { PlayArtistRadioAction } from '/@/renderer/features/context-menu/actions/play-artist-radio-action'; import { PlayArtistRadioAction } from '/@/renderer/features/context-menu/actions/play-artist-radio-action';
import { SetFavoriteAction } from '/@/renderer/features/context-menu/actions/set-favorite-action'; import { SetFavoriteAction } from '/@/renderer/features/context-menu/actions/set-favorite-action';
@@ -39,8 +38,6 @@ export const AlbumArtistContextMenu = ({ items, type }: AlbumArtistContextMenuPr
<DownloadAction ids={ids} /> <DownloadAction ids={ids} />
<ShareAction ids={ids} itemType={LibraryItem.ALBUM_ARTIST} /> <ShareAction ids={ids} itemType={LibraryItem.ALBUM_ARTIST} />
<ContextMenu.Divider /> <ContextMenu.Divider />
<GoToAction items={items} />
<ContextMenu.Divider />
<GetInfoAction disabled={items.length === 0} items={items} /> <GetInfoAction disabled={items.length === 0} items={items} />
</ContextMenu.Content> </ContextMenu.Content>
); );
@@ -3,7 +3,6 @@ import { useMemo } from 'react';
import { AddToPlaylistAction } from '/@/renderer/features/context-menu/actions/add-to-playlist-action'; import { AddToPlaylistAction } from '/@/renderer/features/context-menu/actions/add-to-playlist-action';
import { DownloadAction } from '/@/renderer/features/context-menu/actions/download-action'; import { DownloadAction } from '/@/renderer/features/context-menu/actions/download-action';
import { GetInfoAction } from '/@/renderer/features/context-menu/actions/get-info-action'; import { GetInfoAction } from '/@/renderer/features/context-menu/actions/get-info-action';
import { GoToAction } from '/@/renderer/features/context-menu/actions/go-to-action';
import { PlayAction } from '/@/renderer/features/context-menu/actions/play-action'; import { PlayAction } from '/@/renderer/features/context-menu/actions/play-action';
import { PlayArtistRadioAction } from '/@/renderer/features/context-menu/actions/play-artist-radio-action'; import { PlayArtistRadioAction } from '/@/renderer/features/context-menu/actions/play-artist-radio-action';
import { SetFavoriteAction } from '/@/renderer/features/context-menu/actions/set-favorite-action'; import { SetFavoriteAction } from '/@/renderer/features/context-menu/actions/set-favorite-action';
@@ -39,8 +38,6 @@ export const ArtistContextMenu = ({ items, type }: ArtistContextMenuProps) => {
<DownloadAction ids={ids} /> <DownloadAction ids={ids} />
<ShareAction ids={ids} itemType={LibraryItem.ARTIST} /> <ShareAction ids={ids} itemType={LibraryItem.ARTIST} />
<ContextMenu.Divider /> <ContextMenu.Divider />
<GoToAction items={items} />
<ContextMenu.Divider />
<GetInfoAction disabled={items.length === 0} items={items} /> <GetInfoAction disabled={items.length === 0} items={items} />
</ContextMenu.Content> </ContextMenu.Content>
); );
@@ -19,6 +19,8 @@ import {
import { toast } from '/@/shared/components/toast/toast'; import { toast } from '/@/shared/components/toast/toast';
import { PlayerStatus } from '/@/shared/types/types'; import { PlayerStatus } from '/@/shared/types/types';
let startupRestoreSessionHandled = false;
export const useQueueRestoreTimestamp = () => { export const useQueueRestoreTimestamp = () => {
const { mediaSeekToTimestamp } = usePlayerActions(); const { mediaSeekToTimestamp } = usePlayerActions();
@@ -51,28 +53,65 @@ export const useInitialTimestampRestore = () => {
const startupRestoreInitializedRef = useRef(false); const startupRestoreInitializedRef = useRef(false);
const startupSeekArmedRef = useRef<null | number>(null); const startupSeekArmedRef = useRef<null | number>(null);
const startupSeekTargetUniqueIdRef = useRef<null | string>(null);
const startupSeekAppliedRef = useRef(false); const startupSeekAppliedRef = useRef(false);
const applyStartupSeek = useCallback(() => { const cancelStartupSeek = useCallback(() => {
if (startupSeekAppliedRef.current) { if (startupSeekAppliedRef.current) {
return; return;
} }
const seekTimestamp = startupSeekArmedRef.current;
if (!seekTimestamp || seekTimestamp <= 0) {
return;
}
startupSeekAppliedRef.current = true; startupSeekAppliedRef.current = true;
startupSeekArmedRef.current = null; startupSeekArmedRef.current = null;
startupSeekTargetUniqueIdRef.current = null;
}, []);
const applyStartupSeek = useCallback(() => {
const seekTimestamp = startupSeekArmedRef.current;
if (startupSeekAppliedRef.current) {
return;
}
if (!seekTimestamp || seekTimestamp <= 0) {
return;
}
const targetUniqueId = startupSeekTargetUniqueIdRef.current;
const currentUniqueId = usePlayerStore.getState().getQueue().items[
usePlayerStore.getState().player.index
]?._uniqueId;
if (targetUniqueId && currentUniqueId !== targetUniqueId) {
cancelStartupSeek();
return;
}
startupSeekAppliedRef.current = true;
startupSeekArmedRef.current = null;
startupSeekTargetUniqueIdRef.current = null;
setTimeout(() => { setTimeout(() => {
mediaSeekToTimestamp(seekTimestamp); mediaSeekToTimestamp(seekTimestamp);
}, 100); }, 100);
}, [mediaSeekToTimestamp]); }, [cancelStartupSeek, mediaSeekToTimestamp]);
useEffect(() => { useEffect(() => {
if (startupRestoreInitializedRef.current) { const targetUniqueId = startupSeekTargetUniqueIdRef.current;
if (
!targetUniqueId ||
startupSeekAppliedRef.current ||
!currentSong ||
currentSong._uniqueId === targetUniqueId
) {
return;
}
cancelStartupSeek();
}, [cancelStartupSeek, currentSong]);
useEffect(() => {
if (startupRestoreInitializedRef.current || startupRestoreSessionHandled) {
return; return;
} }
@@ -81,9 +120,11 @@ export const useInitialTimestampRestore = () => {
} }
startupRestoreInitializedRef.current = true; startupRestoreInitializedRef.current = true;
startupRestoreSessionHandled = true;
if (timestamp > 0) { if (timestamp > 0) {
startupSeekArmedRef.current = timestamp; startupSeekArmedRef.current = timestamp;
startupSeekTargetUniqueIdRef.current = currentSong._uniqueId;
} }
if (playerStatus === PlayerStatus.PLAYING) { if (playerStatus === PlayerStatus.PLAYING) {
@@ -45,7 +45,7 @@ const getPositionValue = (seconds: number, useTicks: boolean) => {
return Math.round(seconds * 1e7); return Math.round(seconds * 1e7);
} }
return seconds; return seconds * 1000;
}; };
/* /*
@@ -459,12 +459,14 @@ export const useScrobble = () => {
lastProgressEventRef.current = properties.timestamp; lastProgressEventRef.current = properties.timestamp;
lastSeekEventRef.current = now; lastSeekEventRef.current = now;
const currentStatus = usePlayerStore.getState().player.status;
sendScrobble.mutate( sendScrobble.mutate(
{ {
apiClientProps: { serverId: currentSong._serverId || '' }, apiClientProps: { serverId: currentSong._serverId || '' },
query: { query: {
albumId: currentSong.albumId, albumId: currentSong.albumId,
event: 'timeupdate', event: currentStatus === PlayerStatus.PLAYING ? 'unpause' : 'pause',
id: currentSong.id, id: currentSong.id,
mediaType: mediaType, mediaType: mediaType,
playbackRate: playbackRate, playbackRate: playbackRate,
+1 -1
View File
@@ -1363,7 +1363,7 @@ export type ScrobbleArgs = BaseEndpointArgs & {
export type ScrobbleQuery = { export type ScrobbleQuery = {
albumId?: string; albumId?: string;
event?: 'pause' | 'start' | 'timeupdate' | 'unpause'; event?: 'pause' | 'start' | 'unpause';
id: string; id: string;
mediaType: 'podcast' | 'song'; mediaType: 'podcast' | 'song';
playbackRate: number; playbackRate: number;
+20 -1
View File
@@ -2,6 +2,20 @@ import type { HotkeyItem } from '@mantine/hooks';
const RESERVED_KEYS = new Set(['alt', 'ctrl', 'meta', 'mod', 'shift']); const RESERVED_KEYS = new Set(['alt', 'ctrl', 'meta', 'mod', 'shift']);
const PUNCTUATION_KEY_TO_PHYSICAL: Record<string, string> = {
"'": 'Quote',
',': 'Comma',
'-': 'Minus',
'.': 'Period',
'/': 'Slash',
';': 'Semicolon',
'=': 'Equal',
'[': 'BracketLeft',
'\\': 'Backslash',
']': 'BracketRight',
'`': 'Backquote',
};
/** /**
* Converts stored hotkey strings to Mantine's physical-key format. * Converts stored hotkey strings to Mantine's physical-key format.
* Mantine matches KeyboardEvent.code via normalizeKey, which turns Digit1 into * Mantine matches KeyboardEvent.code via normalizeKey, which turns Digit1 into
@@ -25,6 +39,11 @@ export const toPhysicalHotkey = (hotkey: string): string =>
return `Digit${part}`; return `Digit${part}`;
} }
const punctuationPhysical = PUNCTUATION_KEY_TO_PHYSICAL[part];
if (punctuationPhysical) {
return punctuationPhysical;
}
return part; return part;
}) })
.join('+'); .join('+');
@@ -33,5 +52,5 @@ export const withPhysicalKeys = (hotkeys: HotkeyItem[]): HotkeyItem[] =>
hotkeys.map(([hotkey, handler, options]) => [ hotkeys.map(([hotkey, handler, options]) => [
toPhysicalHotkey(hotkey), toPhysicalHotkey(hotkey),
handler, handler,
{ ...options, usePhysicalKeys: true }, { ...options, preventDefault: true, usePhysicalKeys: true },
]); ]);
@@ -3,7 +3,12 @@ const CODE_TO_HOTKEY_KEY: Record<string, string> = {
ArrowLeft: 'arrowleft', ArrowLeft: 'arrowleft',
ArrowRight: 'arrowright', ArrowRight: 'arrowright',
ArrowUp: 'arrowup', ArrowUp: 'arrowup',
Backquote: '`',
Backslash: '\\',
Backspace: 'backspace', Backspace: 'backspace',
BracketLeft: '[',
BracketRight: ']',
Comma: ',',
Delete: 'delete', Delete: 'delete',
End: 'end', End: 'end',
Enter: 'enter', Enter: 'enter',
@@ -14,6 +19,10 @@ const CODE_TO_HOTKEY_KEY: Record<string, string> = {
Minus: 'minus', Minus: 'minus',
PageDown: 'pagedown', PageDown: 'pagedown',
PageUp: 'pageup', PageUp: 'pageup',
Period: '.',
Quote: "'",
Semicolon: ';',
Slash: '/',
Space: 'space', Space: 'space',
Tab: 'tab', Tab: 'tab',
}; };