From 3c278d5e178803369938571493fdec5b29b9835d Mon Sep 17 00:00:00 2001 From: jeffvli Date: Wed, 9 Aug 2023 21:07:27 -0700 Subject: [PATCH 001/408] Add frame to macOS native window bar --- src/main/main.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/main.ts b/src/main/main.ts index 61adc9e0d..99409cbed 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -11,11 +11,6 @@ import { access, constants, readFile, writeFile } from 'fs'; import path, { join } from 'path'; import { deflate, inflate } from 'zlib'; -import electronLocalShortcut from 'electron-localshortcut'; -import log from 'electron-log'; -import { autoUpdater } from 'electron-updater'; -import uniq from 'lodash/uniq'; -import MpvAPI from 'node-mpv'; import { app, BrowserWindow, @@ -27,6 +22,11 @@ import { nativeImage, BrowserWindowConstructorOptions, } from 'electron'; +import electronLocalShortcut from 'electron-localshortcut'; +import log from 'electron-log'; +import { autoUpdater } from 'electron-updater'; +import uniq from 'lodash/uniq'; +import MpvAPI from 'node-mpv'; import { disableMediaKeys, enableMediaKeys } from './features/core/player/media-keys'; import { store } from './features/core/settings/index'; import MenuBuilder from './menu'; @@ -198,7 +198,7 @@ const createWindow = async () => { }, macOS: { autoHideMenuBar: true, - frame: false, + frame: true, titleBarStyle: 'hidden', trafficLightPosition: { x: 10, y: 10 }, }, From 6f969294b0c430b3a10ae7b2304630c4afff12bd Mon Sep 17 00:00:00 2001 From: jeffvli Date: Thu, 10 Aug 2023 01:54:15 -0700 Subject: [PATCH 002/408] Set macos titlebarstyle to default --- src/main/main.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/main.ts b/src/main/main.ts index 99409cbed..dca1182fd 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -199,7 +199,7 @@ const createWindow = async () => { macOS: { autoHideMenuBar: true, frame: true, - titleBarStyle: 'hidden', + titleBarStyle: 'default', trafficLightPosition: { x: 10, y: 10 }, }, windows: { From e5564c2ac2e6a5b5e385a2f080359371043623a9 Mon Sep 17 00:00:00 2001 From: jeffvli Date: Sat, 28 Oct 2023 16:51:07 -0700 Subject: [PATCH 003/408] Add additional dependencies to linux build (#320) --- package.json | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/package.json b/package.json index 2c0207cfd..c5ce73e3e 100644 --- a/package.json +++ b/package.json @@ -93,6 +93,39 @@ ], "icon": "assets/icons/icon.ico" }, + "deb": { + "depends": [ + "libgssapi_krb5.so.2", + "libavahi-common.so.3", + "libavahi-client.so.3", + "libkrb5.so.3", + "libkrb5support.so.0", + "libkeyutils.so.1", + "libcups.so.2" + ] + }, + "rpm": { + "depends": [ + "libgssapi_krb5.so.2", + "libavahi-common.so.3", + "libavahi-client.so.3", + "libkrb5.so.3", + "libkrb5support.so.0", + "libkeyutils.so.1", + "libcups.so.2" + ] + }, + "freebsd": { + "depends": [ + "libgssapi_krb5.so.2", + "libavahi-common.so.3", + "libavahi-client.so.3", + "libkrb5.so.3", + "libkrb5support.so.0", + "libkeyutils.so.1", + "libcups.so.2" + ] + }, "linux": { "target": [ "AppImage", From 4ec981df83cc5087713b647ab7d9d531b582a116 Mon Sep 17 00:00:00 2001 From: Kendall Garner <17521368+kgarner7@users.noreply.github.com> Date: Sun, 29 Oct 2023 03:00:01 +0000 Subject: [PATCH 004/408] [bugfix/feature]: Improve ratings (#332) * [bugfix/feature]: Improve ratings Fix: add preventDefault/stopPropagation to prevent scrolling to top in queue Feat: instead of double click for clear, click on same value --- src/remote/components/remote-container.tsx | 3 +- src/renderer/components/rating/index.tsx | 42 +++++++++++-------- .../virtual-table/cells/rating-cell.tsx | 23 ---------- .../albums/components/album-detail-header.tsx | 13 ------ .../components/album-artist-detail-header.tsx | 18 +------- .../context-menu/context-menu-provider.tsx | 6 --- .../player/components/right-controls.tsx | 1 - 7 files changed, 27 insertions(+), 79 deletions(-) diff --git a/src/remote/components/remote-container.tsx b/src/remote/components/remote-container.tsx index 24a0154a2..2213fcbb3 100644 --- a/src/remote/components/remote-container.tsx +++ b/src/remote/components/remote-container.tsx @@ -1,5 +1,5 @@ import { useCallback } from 'react'; -import { Group, Image, Rating, Text, Title } from '@mantine/core'; +import { Group, Image, Text, Title } from '@mantine/core'; import { useInfo, useSend, useShowImage } from '/@/remote/store'; import { RemoteButton } from '/@/remote/components/buttons/remote-button'; import formatDuration from 'format-duration'; @@ -18,6 +18,7 @@ import { import { PlayerRepeat, PlayerStatus } from '/@/renderer/types'; import { WrapperSlider } from '/@/remote/components/wrapped-slider'; import { Tooltip } from '/@/renderer/components/tooltip'; +import { Rating } from '/@/renderer/components'; export const RemoteContainer = () => { const { repeat, shuffle, song, status, volume } = useInfo(); diff --git a/src/renderer/components/rating/index.tsx b/src/renderer/components/rating/index.tsx index 9f308a14a..002b3cf7e 100644 --- a/src/renderer/components/rating/index.tsx +++ b/src/renderer/components/rating/index.tsx @@ -1,12 +1,8 @@ /* eslint-disable jsx-a11y/no-static-element-interactions */ -import { MouseEvent } from 'react'; -import { Rating as MantineRating, RatingProps as MantineRatingProps } from '@mantine/core'; +import { useCallback } from 'react'; +import { Rating as MantineRating, RatingProps } from '@mantine/core'; +import debounce from 'lodash/debounce'; import styled from 'styled-components'; -import { Tooltip } from '/@/renderer/components/tooltip'; - -interface RatingProps extends Omit { - onClick: (e: MouseEvent, value: number | undefined) => void; -} const StyledRating = styled(MantineRating)` & .mantine-Rating-symbolBody { @@ -16,18 +12,28 @@ const StyledRating = styled(MantineRating)` } `; -export const Rating = ({ onClick, ...props }: RatingProps) => { - // const debouncedOnClick = debounce(onClick, 100); +export const Rating = ({ onChange, ...props }: RatingProps) => { + const valueChange = useCallback( + (rating: number) => { + if (onChange) { + if (rating === props.value) { + onChange(0); + } else { + onChange(rating); + } + } + }, + [onChange, props.value], + ); + + const debouncedOnChange = debounce(valueChange, 100); return ( - - onClick(e, props.value)} - /> - + { + debouncedOnChange(e); + }} + /> ); }; diff --git a/src/renderer/components/virtual-table/cells/rating-cell.tsx b/src/renderer/components/virtual-table/cells/rating-cell.tsx index 596d1c876..5663cf502 100644 --- a/src/renderer/components/virtual-table/cells/rating-cell.tsx +++ b/src/renderer/components/virtual-table/cells/rating-cell.tsx @@ -1,5 +1,4 @@ /* eslint-disable import/no-cycle */ -import { MouseEvent } from 'react'; import type { ICellRendererParams } from '@ag-grid-community/core'; import { Rating } from '/@/renderer/components/rating'; import { CellContainer } from '/@/renderer/components/virtual-table/cells/generic-cell'; @@ -9,8 +8,6 @@ export const RatingCell = ({ value, node }: ICellRendererParams) => { const updateRatingMutation = useSetRating({}); const handleUpdateRating = (rating: number) => { - if (!value) return; - updateRatingMutation.mutate( { query: { @@ -27,32 +24,12 @@ export const RatingCell = ({ value, node }: ICellRendererParams) => { ); }; - const handleClearRating = (e: MouseEvent) => { - e.preventDefault(); - e.stopPropagation(); - updateRatingMutation.mutate( - { - query: { - item: [value], - rating: 0, - }, - serverId: value?.serverId, - }, - { - onSuccess: () => { - node.setData({ ...node.data, userRating: 0 }); - }, - }, - ); - }; - return ( ); diff --git a/src/renderer/features/albums/components/album-detail-header.tsx b/src/renderer/features/albums/components/album-detail-header.tsx index 7c648d144..157778f28 100644 --- a/src/renderer/features/albums/components/album-detail-header.tsx +++ b/src/renderer/features/albums/components/album-detail-header.tsx @@ -55,18 +55,6 @@ export const AlbumDetailHeader = forwardRef( }); }; - const handleClearRating = () => { - if (!detailQuery?.data || !detailQuery?.data.userRating) return; - - updateRatingMutation.mutate({ - query: { - item: [detailQuery.data], - rating: 0, - }, - serverId: detailQuery.data.serverId, - }); - }; - const showRating = detailQuery?.data?.serverType === ServerType.NAVIDROME; return ( @@ -96,7 +84,6 @@ export const AlbumDetailHeader = forwardRef( } value={detailQuery?.data?.userRating || 0} onChange={handleUpdateRating} - onClick={handleClearRating} /> )} diff --git a/src/renderer/features/artists/components/album-artist-detail-header.tsx b/src/renderer/features/artists/components/album-artist-detail-header.tsx index 1131f9e88..06bdaa02d 100644 --- a/src/renderer/features/artists/components/album-artist-detail-header.tsx +++ b/src/renderer/features/artists/components/album-artist-detail-header.tsx @@ -1,4 +1,4 @@ -import { forwardRef, Fragment, Ref, MouseEvent } from 'react'; +import { forwardRef, Fragment, Ref } from 'react'; import { Group, Rating, Stack } from '@mantine/core'; import { useParams } from 'react-router'; import { LibraryItem, ServerType } from '/@/renderer/api/types'; @@ -55,21 +55,6 @@ export const AlbumArtistDetailHeader = forwardRef( }); }; - const handleClearRating = (_e: MouseEvent, rating?: number) => { - if (!detailQuery?.data || !detailQuery?.data.userRating) return; - - const isSameRatingAsPrevious = rating === detailQuery.data.userRating; - if (!isSameRatingAsPrevious) return; - - updateRatingMutation.mutate({ - query: { - item: [detailQuery.data], - rating: 0, - }, - serverId: detailQuery.data.serverId, - }); - }; - const showRating = detailQuery?.data?.serverType === ServerType.NAVIDROME; return ( @@ -99,7 +84,6 @@ export const AlbumArtistDetailHeader = forwardRef( } value={detailQuery?.data?.userRating || 0} onChange={handleUpdateRating} - onClick={handleClearRating} /> )} diff --git a/src/renderer/features/context-menu/context-menu-provider.tsx b/src/renderer/features/context-menu/context-menu-provider.tsx index 50b087757..caf262da0 100644 --- a/src/renderer/features/context-menu/context-menu-provider.tsx +++ b/src/renderer/features/context-menu/context-menu-provider.tsx @@ -710,7 +710,6 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => { {}} /> ), onClick: () => handleUpdateRating(0), @@ -721,7 +720,6 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => { {}} /> ), onClick: () => handleUpdateRating(1), @@ -732,7 +730,6 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => { {}} /> ), onClick: () => handleUpdateRating(2), @@ -743,7 +740,6 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => { {}} /> ), onClick: () => handleUpdateRating(3), @@ -754,7 +750,6 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => { {}} /> ), onClick: () => handleUpdateRating(4), @@ -765,7 +760,6 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => { {}} /> ), onClick: () => handleUpdateRating(5), diff --git a/src/renderer/features/player/components/right-controls.tsx b/src/renderer/features/player/components/right-controls.tsx index dcf137e9e..84d1738d2 100644 --- a/src/renderer/features/player/components/right-controls.tsx +++ b/src/renderer/features/player/components/right-controls.tsx @@ -212,7 +212,6 @@ export const RightControls = () => { size="sm" value={currentSong?.userRating || 0} onChange={handleUpdateRating} - onClick={handleClearRating} /> )} From 9d780e0342a7ecac0f0e9e6437edf2e7b56f6d92 Mon Sep 17 00:00:00 2001 From: Kendall Garner <17521368+kgarner7@users.noreply.github.com> Date: Sun, 29 Oct 2023 04:10:52 +0000 Subject: [PATCH 005/408] [bugfix]: prevent default (#334) * [bugfix]: prevent default on rating --- src/renderer/components/rating/index.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/renderer/components/rating/index.tsx b/src/renderer/components/rating/index.tsx index 002b3cf7e..73b44f64c 100644 --- a/src/renderer/components/rating/index.tsx +++ b/src/renderer/components/rating/index.tsx @@ -34,6 +34,10 @@ export const Rating = ({ onChange, ...props }: RatingProps) => { onChange={(e) => { debouncedOnChange(e); }} + onClick={(e) => { + e.preventDefault(); + e.stopPropagation(); + }} /> ); }; From cf9c7e26406b532392c455f0916b74b06df4bd02 Mon Sep 17 00:00:00 2001 From: Nicholas Malcolm Date: Mon, 30 Oct 2023 21:50:21 -0400 Subject: [PATCH 006/408] Build docker container for arm (#336) * Build all supported container platforms * Remove less popular platforms --- .github/workflows/publish-docker-auto.yml | 8 ++++++++ .github/workflows/publish-docker.yml | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/.github/workflows/publish-docker-auto.yml b/.github/workflows/publish-docker-auto.yml index 6c7a0b04a..af48c86ab 100644 --- a/.github/workflows/publish-docker-auto.yml +++ b/.github/workflows/publish-docker-auto.yml @@ -37,6 +37,10 @@ jobs: type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} type=semver,pattern={{major}} + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + - name: Setup Docker buildx + uses: docker/setup-buildx-action@v3 - name: Build and push Docker image uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4 with: @@ -44,3 +48,7 @@ jobs: push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} + platforms: | + linux/amd64 + linux/arm/v7 + linux/arm64/v8 diff --git a/.github/workflows/publish-docker.yml b/.github/workflows/publish-docker.yml index c189d619a..ff4e0d53c 100644 --- a/.github/workflows/publish-docker.yml +++ b/.github/workflows/publish-docker.yml @@ -29,6 +29,10 @@ jobs: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} tags: | type=raw,value=latest,enable={{is_default_branch}} + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + - name: Setup Docker buildx + uses: docker/setup-buildx-action@v3 - name: Build and push Docker image uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4 with: @@ -36,3 +40,7 @@ jobs: push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} + platforms: | + linux/amd64 + linux/arm/v7 + linux/arm64/v8 From 11863fd4c19aa6aedb9dd51c5494df1bf2d6c3ed Mon Sep 17 00:00:00 2001 From: Martin Pauli Date: Tue, 31 Oct 2023 02:50:48 +0100 Subject: [PATCH 007/408] Fix clear rating hotkey (#338) --- .../player/components/right-controls.tsx | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/src/renderer/features/player/components/right-controls.tsx b/src/renderer/features/player/components/right-controls.tsx index 84d1738d2..d429f0cd9 100644 --- a/src/renderer/features/player/components/right-controls.tsx +++ b/src/renderer/features/player/components/right-controls.tsx @@ -1,4 +1,4 @@ -import { MouseEvent, useEffect } from 'react'; +import { useEffect } from 'react'; import { Flex, Group } from '@mantine/core'; import { useHotkeys, useMediaQuery } from '@mantine/hooks'; import isElectron from 'is-electron'; @@ -82,18 +82,6 @@ export const RightControls = () => { }); }; - const handleClearRating = (_e: MouseEvent | null, rating?: number) => { - if (!currentSong || !rating) return; - - updateRatingMutation.mutate({ - query: { - item: [currentSong], - rating: 0, - }, - serverId: currentSong?.serverId, - }); - }; - const handleRemoveFromFavorites = (song: QueueSong | undefined) => { if (!song?.id) return; @@ -152,7 +140,7 @@ export const RightControls = () => { bindings.favoritePreviousToggle.isGlobal ? '' : bindings.favoritePreviousToggle.hotkey, () => handleToggleFavorite(previousSong), ], - [bindings.rate0.isGlobal ? '' : bindings.rate0.hotkey, () => handleClearRating(null, 0)], + [bindings.rate0.isGlobal ? '' : bindings.rate0.hotkey, () => handleUpdateRating(0)], [bindings.rate1.isGlobal ? '' : bindings.rate1.hotkey, () => handleUpdateRating(1)], [bindings.rate2.isGlobal ? '' : bindings.rate2.hotkey, () => handleUpdateRating(2)], [bindings.rate3.isGlobal ? '' : bindings.rate3.hotkey, () => handleUpdateRating(3)], From 8430b1ec95c4cfc96e34d3c4efc7984abbca390c Mon Sep 17 00:00:00 2001 From: Jeff <42182408+jeffvli@users.noreply.github.com> Date: Mon, 30 Oct 2023 19:22:45 -0700 Subject: [PATCH 008/408] Add localization support (#333) * Add updated i18n config and en locale --- package-lock.json | 38 +- package.json | 8 +- src/i18n/i18n.js | 32 - src/i18n/i18n.ts | 72 +++ src/i18n/i18next-parser.config.js | 155 ++--- src/i18n/locales/en.json | 611 +++++++++++++++++- src/renderer/api/controller.ts | 18 +- src/renderer/api/navidrome/navidrome-api.ts | 16 +- src/renderer/api/subsonic/subsonic-api.ts | 3 +- src/renderer/app.tsx | 8 + .../components/feature-carousel/index.tsx | 4 +- .../components/query-builder/index.tsx | 11 +- .../components/virtual-table/index.tsx | 47 +- .../virtual-table/table-config-dropdown.tsx | 317 +++++++-- .../virtual-table/table-pagination.tsx | 8 +- .../routes/action-required-route.tsx | 8 +- .../components/album-detail-content.tsx | 11 +- .../components/album-list-header-filters.tsx | 133 +++- .../albums/components/album-list-header.tsx | 10 +- .../components/jellyfin-album-filters.tsx | 14 +- .../components/navidrome-album-filters.tsx | 17 +- ...um-artist-detail-top-songs-list-header.tsx | 8 +- .../album-artist-list-header-filters.tsx | 32 +- .../components/album-artist-list-header.tsx | 8 +- .../context-menu/context-menu-provider.tsx | 70 +- .../components/genre-list-header-filters.tsx | 38 +- .../genres/components/genre-list-header.tsx | 8 +- .../features/home/routes/home-route.tsx | 14 +- .../lyrics/components/lyrics-search-form.tsx | 15 +- .../features/lyrics/lyrics-actions.tsx | 10 +- .../components/play-queue-list-controls.tsx | 18 +- .../player/components/center-controls.tsx | 55 +- .../components/full-screen-player-queue.tsx | 12 +- .../player/components/full-screen-player.tsx | 93 ++- .../player/components/left-controls.tsx | 13 +- .../player/components/right-controls.tsx | 13 +- .../player/components/shuffle-all-modal.tsx | 3 +- .../player/hooks/use-center-controls.ts | 10 +- .../player/hooks/use-handle-playqueue-add.ts | 19 +- .../add-to-playlist-context-modal.tsx | 33 +- .../components/create-playlist-form.tsx | 32 +- .../components/playlist-detail-content.tsx | 11 +- ...aylist-detail-song-list-header-filters.tsx | 27 +- .../playlist-detail-song-list-header.tsx | 6 +- .../playlist-list-header-filters.tsx | 30 +- .../components/playlist-list-header.tsx | 13 +- .../components/playlist-query-builder.tsx | 20 +- .../components/save-as-playlist-form.tsx | 30 +- .../components/update-playlist-form.tsx | 35 +- .../playlist-detail-song-list-route.tsx | 4 +- .../search/components/go-to-commands.tsx | 36 +- .../search/components/home-commands.tsx | 30 +- .../components/library-command-item.tsx | 20 +- .../search/components/server-commands.tsx | 18 +- .../servers/components/add-server-form.tsx | 63 +- .../servers/components/edit-server-form.tsx | 58 +- .../servers/components/server-list.tsx | 14 +- .../general/application-settings.tsx | 84 ++- .../components/general/control-settings.tsx | 109 +++- .../components/general/remote-settings.tsx | 65 +- .../components/general/sidebar-settings.tsx | 25 +- .../components/general/theme-settings.tsx | 37 +- .../hotkeys/hotkey-manager-settings.tsx | 118 +++- .../hotkeys/window-hotkey-settings.tsx | 11 +- .../components/playback/audio-settings.tsx | 71 +- .../components/playback/lyric-settings.tsx | 32 +- .../components/playback/mpv-settings.tsx | 137 ++-- .../components/playback/playback-tab.tsx | 8 +- .../components/playback/scrobble-settings.tsx | 39 +- .../settings/components/settings-content.tsx | 20 +- .../settings/components/settings-header.tsx | 16 +- .../settings/components/settings-section.tsx | 2 +- .../components/window/discord-settings.tsx | 48 +- .../components/window/update-settings.tsx | 9 +- .../components/window/window-settings.tsx | 32 +- .../shared/components/library-header.tsx | 23 +- .../shared/components/order-toggle-button.tsx | 10 +- src/renderer/features/shared/utils.ts | 7 +- .../sidebar/components/action-bar.tsx | 4 +- .../sidebar/components/collapsed-sidebar.tsx | 6 +- .../components/sidebar-playlist-list.tsx | 17 +- .../features/sidebar/components/sidebar.tsx | 31 +- .../components/jellyfin-song-filters.tsx | 12 +- .../components/navidrome-song-filters.tsx | 10 +- .../components/song-list-header-filters.tsx | 46 +- .../songs/components/song-list-header.tsx | 6 +- .../features/titlebar/components/app-menu.tsx | 27 +- src/renderer/store/settings.store.ts | 59 +- src/renderer/utils/index.ts | 1 + src/renderer/utils/sentence-case.ts | 3 + 90 files changed, 2678 insertions(+), 907 deletions(-) delete mode 100644 src/i18n/i18n.js create mode 100644 src/i18n/i18n.ts create mode 100644 src/renderer/utils/sentence-case.ts diff --git a/package-lock.json b/package-lock.json index 7d13889f5..001eae968 100644 --- a/package-lock.json +++ b/package-lock.json @@ -42,7 +42,7 @@ "framer-motion": "^10.13.0", "fuse.js": "^6.6.2", "history": "^5.3.0", - "i18next": "^21.6.16", + "i18next": "^21.10.0", "idb-keyval": "^6.2.1", "immer": "^9.0.21", "is-electron": "^2.2.2", @@ -57,7 +57,7 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-error-boundary": "^3.1.4", - "react-i18next": "^11.16.7", + "react-i18next": "^11.18.6", "react-icons": "^4.10.1", "react-player": "^2.11.0", "react-router": "^6.16.0", @@ -125,7 +125,7 @@ "file-loader": "^6.2.0", "html-webpack-plugin": "^5.5.0", "husky": "^7.0.4", - "i18next-parser": "^6.3.0", + "i18next-parser": "^6.6.0", "identity-obj-proxy": "^3.0.0", "jest": "^27.5.1", "lint-staged": "^12.3.7", @@ -11871,7 +11871,8 @@ "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==" + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true }, "node_modules/html-minifier-terser": { "version": "6.1.0", @@ -12129,9 +12130,9 @@ } }, "node_modules/i18next": { - "version": "21.6.16", - "resolved": "https://registry.npmjs.org/i18next/-/i18next-21.6.16.tgz", - "integrity": "sha512-xJlzrVxG9CyAGsbMP1aKuiNr1Ed2m36KiTB7hjGMG2Zo4idfw3p9THUEu+GjBwIgEZ7F11ZbCzJcfv4uyfKNuw==", + "version": "21.10.0", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-21.10.0.tgz", + "integrity": "sha512-YeuIBmFsGjUfO3qBmMOc0rQaun4mIpGKET5WDwvu8lU7gvwpcariZLNtL0Fzj+zazcHUrlXHiptcFhBMFaxzfg==", "funding": [ { "type": "individual", @@ -16968,12 +16969,11 @@ "integrity": "sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==" }, "node_modules/react-i18next": { - "version": "11.16.7", - "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-11.16.7.tgz", - "integrity": "sha512-7yotILJLnKfvUfrl/nt9eK9vFpVFjZPLWAwBzWL6XppSZZEvlmlKk0GBGDCAPfLfs8oND7WAbry8wGzdoiW5Nw==", + "version": "11.18.6", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-11.18.6.tgz", + "integrity": "sha512-yHb2F9BiT0lqoQDt8loZ5gWP331GwctHz9tYQ8A2EIEUu+CcEdjBLQWli1USG3RdWQt3W+jqQLg/d4rrQR96LA==", "dependencies": { "@babel/runtime": "^7.14.5", - "html-escaper": "^2.0.2", "html-parse-stringify": "^3.0.1" }, "peerDependencies": { @@ -30233,7 +30233,8 @@ "html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==" + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true }, "html-minifier-terser": { "version": "6.1.0", @@ -30419,9 +30420,9 @@ "dev": true }, "i18next": { - "version": "21.6.16", - "resolved": "https://registry.npmjs.org/i18next/-/i18next-21.6.16.tgz", - "integrity": "sha512-xJlzrVxG9CyAGsbMP1aKuiNr1Ed2m36KiTB7hjGMG2Zo4idfw3p9THUEu+GjBwIgEZ7F11ZbCzJcfv4uyfKNuw==", + "version": "21.10.0", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-21.10.0.tgz", + "integrity": "sha512-YeuIBmFsGjUfO3qBmMOc0rQaun4mIpGKET5WDwvu8lU7gvwpcariZLNtL0Fzj+zazcHUrlXHiptcFhBMFaxzfg==", "requires": { "@babel/runtime": "^7.17.2" } @@ -33952,12 +33953,11 @@ "integrity": "sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==" }, "react-i18next": { - "version": "11.16.7", - "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-11.16.7.tgz", - "integrity": "sha512-7yotILJLnKfvUfrl/nt9eK9vFpVFjZPLWAwBzWL6XppSZZEvlmlKk0GBGDCAPfLfs8oND7WAbry8wGzdoiW5Nw==", + "version": "11.18.6", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-11.18.6.tgz", + "integrity": "sha512-yHb2F9BiT0lqoQDt8loZ5gWP331GwctHz9tYQ8A2EIEUu+CcEdjBLQWli1USG3RdWQt3W+jqQLg/d4rrQR96LA==", "requires": { "@babel/runtime": "^7.14.5", - "html-escaper": "^2.0.2", "html-parse-stringify": "^3.0.1" } }, diff --git a/package.json b/package.json index c5ce73e3e..54b2f648c 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "start:web": "cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack serve --config ./.erb/configs/webpack.config.renderer.web.ts", "test": "jest", "prepare": "husky install", - "i18next": "i18next -c src/renderer/i18n/i18next-parser.config.js", + "i18next": "i18next -c src/i18n/i18next-parser.config.js", "prod:buildserver": "pwsh -c \"./scripts/server-build.ps1\"", "prod:publishserver": "pwsh -c \"./scripts/server-publish.ps1\"" }, @@ -252,7 +252,7 @@ "file-loader": "^6.2.0", "html-webpack-plugin": "^5.5.0", "husky": "^7.0.4", - "i18next-parser": "^6.3.0", + "i18next-parser": "^6.6.0", "identity-obj-proxy": "^3.0.0", "jest": "^27.5.1", "lint-staged": "^12.3.7", @@ -321,7 +321,7 @@ "framer-motion": "^10.13.0", "fuse.js": "^6.6.2", "history": "^5.3.0", - "i18next": "^21.6.16", + "i18next": "^21.10.0", "idb-keyval": "^6.2.1", "immer": "^9.0.21", "is-electron": "^2.2.2", @@ -336,7 +336,7 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-error-boundary": "^3.1.4", - "react-i18next": "^11.16.7", + "react-i18next": "^11.18.6", "react-icons": "^4.10.1", "react-player": "^2.11.0", "react-router": "^6.16.0", diff --git a/src/i18n/i18n.js b/src/i18n/i18n.js deleted file mode 100644 index 1683c700b..000000000 --- a/src/i18n/i18n.js +++ /dev/null @@ -1,32 +0,0 @@ -import i18n from 'i18next'; -import { initReactI18next } from 'react-i18next'; -const en = require('./locales/en.json'); - -const resources = { - en: { translation: en }, -}; - -export const Languages = [ - { - label: 'English', - value: 'en', - }, -]; - -i18n - .use(initReactI18next) // passes i18n down to react-i18next - .init({ - fallbackLng: 'en', - // language to use, more information here: https://www.i18next.com/overview/configuration-options#languages-namespaces-resources - // you can use the i18n.changeLanguage function to change the language manually: https://www.i18next.com/overview/api#changelanguage - // if you're using a language detector, do not define the lng option - interpolation: { - escapeValue: false, // react already safes from xss - }, - - lng: 'en', - - resources, - }); - -export default i18n; diff --git a/src/i18n/i18n.ts b/src/i18n/i18n.ts new file mode 100644 index 000000000..4eeb146b1 --- /dev/null +++ b/src/i18n/i18n.ts @@ -0,0 +1,72 @@ +import { PostProcessorModule } from 'i18next'; +import i18n from 'i18next'; +import { initReactI18next } from 'react-i18next'; +import en from './locales/en.json'; + +const resources = { + en: { translation: en }, +}; + +export const languages = [ + { + label: 'English', + value: 'en', + }, +]; + +const lowerCasePostProcessor: PostProcessorModule = { + type: 'postProcessor', + name: 'lowerCase', + process: (value: string) => { + return value.toLocaleLowerCase(); + }, +}; + +const upperCasePostProcessor: PostProcessorModule = { + type: 'postProcessor', + name: 'upperCase', + process: (value: string) => { + return value.toLocaleUpperCase(); + }, +}; + +const titleCasePostProcessor: PostProcessorModule = { + type: 'postProcessor', + name: 'titleCase', + process: (value: string) => { + return value.replace(/\w\S*/g, (txt) => { + return txt.charAt(0).toUpperCase() + txt.slice(1).toLowerCase(); + }); + }, +}; + +const sentenceCasePostProcessor: PostProcessorModule = { + type: 'postProcessor', + name: 'sentenceCase', + process: (value: string) => { + const sentences = value.split('. '); + + return sentences + .map((sentence) => { + return sentence.charAt(0).toUpperCase() + sentence.slice(1).toLocaleLowerCase(); + }) + .join('. '); + }, +}; +i18n.use(lowerCasePostProcessor) + .use(upperCasePostProcessor) + .use(titleCasePostProcessor) + .use(sentenceCasePostProcessor) + .use(initReactI18next) // passes i18n down to react-i18next + .init({ + fallbackLng: 'en', + // language to use, more information here: https://www.i18next.com/overview/configuration-options#languages-namespaces-resources + // you can use the i18n.changeLanguage function to change the language manually: https://www.i18next.com/overview/api#changelanguage + // if you're using a language detector, do not define the lng option + interpolation: { + escapeValue: false, // react already safes from xss + }, + resources, + }); + +export default i18n; diff --git a/src/i18n/i18next-parser.config.js b/src/i18n/i18next-parser.config.js index d3d12e0ed..22071636c 100644 --- a/src/i18n/i18next-parser.config.js +++ b/src/i18n/i18next-parser.config.js @@ -1,117 +1,44 @@ -// i18next-parser.config.js +// Reference: https://github.com/i18next/i18next-parser#options module.exports = { - contextSeparator: '_', - // Key separator used in your translation keys - - createOldCatalogs: true, - - // Exit with an exit code of 1 when translations are updated (for CI purpose) - customValueTemplate: null, - - // Save the \_old files - defaultNamespace: 'translation', - - // Default namespace used in your i18next config - defaultValue: '', - - // Exit with an exit code of 1 on warnings - failOnUpdate: false, - - // Display info about the parsing including some stats - failOnWarnings: false, - - // The locale to compare with default values to determine whether a default value has been changed. - // If this is set and a default value differs from a translation in the specified locale, all entries - // for that key across locales are reset to the default value, and existing translations are moved to - // the `_old` file. - i18nextOptions: null, - - // Default value to give to empty keys - // You may also specify a function accepting the locale, namespace, and key as arguments - indentation: 2, - - // Plural separator used in your translation keys - // If you want to use plain english keys, separators such as `_` might conflict. You might want to set `pluralSeparator` to a different string that does not occur in your keys. - input: [ - '../components/**/*.{js,jsx,ts,tsx}', - '../features/**/*.{js,jsx,ts,tsx}', - '../layouts/**/*.{js,jsx,ts,tsx}', - '!../../src/node_modules/**', - '!../../src/**/*.prod.js', - ], - - // Indentation of the catalog files - keepRemoved: false, - - // Keep keys from the catalog that are no longer in code - keySeparator: '.', - - // Key separator used in your translation keys - // If you want to use plain english keys, separators such as `.` and `:` will conflict. You might want to set `keySeparator: false` and `namespaceSeparator: false`. That way, `t('Status: Loading...')` will not think that there are a namespace and three separator dots for instance. - // see below for more details - lexers: { - default: ['JavascriptLexer'], - handlebars: ['HandlebarsLexer'], - - hbs: ['HandlebarsLexer'], - htm: ['HTMLLexer'], - - html: ['HTMLLexer'], - js: ['JavascriptLexer'], - jsx: ['JsxLexer'], - - mjs: ['JavascriptLexer'], - // if you're writing jsx inside .js files, change this to JsxLexer - ts: ['JavascriptLexer'], - - tsx: ['JsxLexer'], - }, - - lineEnding: 'auto', - - // Control the line ending. See options at https://github.com/ryanve/eol - locales: ['en'], - - // An array of the locales in your applications - namespaceSeparator: false, - - // Namespace separator used in your translation keys - // If you want to use plain english keys, separators such as `.` and `:` will conflict. You might want to set `keySeparator: false` and `namespaceSeparator: false`. That way, `t('Status: Loading...')` will not think that there are a namespace and three separator dots for instance. - output: 'src/renderer/i18n/locales/$LOCALE.json', - - // Supports $LOCALE and $NAMESPACE injection - // Supports JSON (.json) and YAML (.yml) file formats - // Where to write the locale files relative to process.cwd() - pluralSeparator: '_', - - // If you wish to customize the value output the value as an object, you can set your own format. - // ${defaultValue} is the default value you set in your translation function. - // Any other custom property will be automatically extracted. - // - // Example: - // { - // message: "${defaultValue}", - // description: "${maxLength}", // - // } - resetDefaultValueLocale: 'en', - - // Whether or not to sort the catalog. Can also be a [compareFunction](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#parameters) - skipDefaultValues: false, - - // An array of globs that describe where to look for source files - // relative to the location of the configuration file - sort: true, - - // Whether to ignore default values - // You may also specify a function accepting the locale and namespace as arguments - useKeysAsDefaultValue: true, - - // Whether to use the keys as the default value; ex. "Hello": "Hello", "World": "World" - // This option takes precedence over the `defaultValue` and `skipDefaultValues` options - // You may also specify a function accepting the locale and namespace as arguments - verbose: false, - // If you wish to customize options in internally used i18next instance, you can define an object with any - // configuration property supported by i18next (https://www.i18next.com/overview/configuration-options). - // { compatibilityJSON: 'v3' } can be used to generate v3 compatible plurals. + contextSeparator: '_', + createOldCatalogs: true, + customValueTemplate: null, + defaultNamespace: 'translation', + defaultValue: '', + failOnUpdate: false, + failOnWarnings: false, + i18nextOptions: null, + indentation: 4, + input: [ + '../renderer/components/**/*.{js,jsx,ts,tsx}', + '../renderer/features/**/*.{js,jsx,ts,tsx}', + '../renderer/layouts/**/*.{js,jsx,ts,tsx}', + '!../src/node_modules/**', + '!../src/**/*.prod.js', + ], + keepRemoved: false, + keySeparator: '.', + lexers: { + default: ['JavascriptLexer'], + handlebars: ['HandlebarsLexer'], + hbs: ['HandlebarsLexer'], + htm: ['HTMLLexer'], + html: ['HTMLLexer'], + js: ['JavascriptLexer'], + jsx: ['JsxLexer'], + mjs: ['JavascriptLexer'], + ts: ['JavascriptLexer'], + tsx: ['JsxLexer'], + }, + lineEnding: 'auto', + locales: ['en'], + namespaceSeparator: false, + output: 'src/renderer/i18n/locales/$LOCALE.json', + pluralSeparator: '_', + resetDefaultValueLocale: 'en', + skipDefaultValues: false, + sort: true, + useKeysAsDefaultValue: true, + verbose: false, }; diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 147e95db7..5a2e94205 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -1,9 +1,606 @@ { - "player": { - "next": "player.next", - "play": "player.play", - "prev": "player.prev", - "seekBack": "player.seekBack", - "seekForward": "player.seekForward" - } + "action": { + "addToFavorites": "add to $t(entity.favorite_other)", + "addToPlaylist": "add to $t(entity.playlist_one)", + "clearQueue": "clear queue", + "createPlaylist": "create $t(entity.playlist_one)", + "deletePlaylist": "delete $t(entity.playlist_one)", + "deselectAll": "deselect all", + "editPlaylist": "edit $t(entity.playlist_one)", + "goToPage": "go to page", + "moveToBottom": "move to bottom", + "moveToTop": "move to top", + "refresh": "$t(common.refresh)", + "removeFromFavorites": "remove from $t(entity.favorite_other)", + "removeFromPlaylist": "remove from $t(entity.playlist_one)", + "removeFromQueue": "remove from queue", + "setRating": "set rating", + "toggleSmartPlaylistEditor": "toggle $t(entity.smartPlaylist) editor", + "viewPlaylists": "view $t(entity.playlist_other)" + }, + "common": { + "action_one": "action", + "action_other": "actions", + "add": "add", + "areYouSure": "are you sure?", + "ascending": "ascending", + "backward": "backward", + "biography": "biography", + "bitrate": "bitrate", + "bpm": "bpm", + "cancel": "cancel", + "center": "center", + "channel_one": "channel", + "channel_other": "channels", + "clear": "clear", + "collapse": "collapse", + "comingSoon": "coming soon...", + "configure": "configure", + "confirm": "confirm", + "create": "create", + "currentSong": "current $t(entity.track_one)", + "decrease": "decrease", + "delete": "delete", + "descending": "descending", + "description": "description", + "disable": "disable", + "disc": "disc", + "dismiss": "dismiss", + "duration": "duration", + "edit": "edit", + "enable": "enable", + "expand": "expand", + "favorite": "favorite", + "filter_one": "filter", + "filter_other": "filters", + "filters": "filters", + "forceRestartRequired": "restart to apply changes... close the notification to restart", + "forward": "forward", + "gap": "gap", + "home": "home", + "increase": "increase", + "left": "left", + "limit": "limit", + "manage": "manage", + "maximize": "maximize", + "menu": "menu", + "minimize": "minimize", + "modified": "modified", + "name": "name", + "no": "no", + "none": "none", + "noResultsFromQuery": "the query returned no results", + "note": "note", + "ok": "ok", + "owner": "owner", + "path": "path", + "playerMustBePaused": "player must be paused", + "previousSong": "previous $t(entity.track_one)", + "quit": "quit", + "random": "random", + "rating": "rating", + "refresh": "refresh", + "reset": "reset", + "resetToDefault": "reset to default", + "restartRequired": "restart required", + "right": "right", + "save": "save", + "saveAndReplace": "save and replace", + "saveAs": "save as", + "search": "search", + "setting": "setting", + "setting_other": "settings", + "size": "size", + "sortOrder": "order", + "title": "title", + "trackNumber": "track", + "unknown": "unknown", + "version": "version", + "year": "year", + "yes": "yes" + }, + "entity": { + "album_one": "album", + "album_other": "albums", + "albumArtist_one": "album artist", + "albumArtist_other": "album artists", + "albumArtistCount_one": "{{count}} album artist", + "albumArtistCount_other": "{{count}} album artists", + "albumWithCount_one": "{{count}} album", + "albumWithCount_other": "{{count}} albums", + "artist_one": "artist", + "artist_other": "artists", + "artistWithCount_one": "{{count}} artist", + "artistWithCount_other": "{{count}} artists", + "favorite_one": "favorite", + "favorite_other": "favorites", + "folder_one": "folder", + "folder_other": "folders", + "folderWithCount_one": "{{count}} folder", + "folderWithCount_other": "{{count}} folders", + "genre_one": "genre", + "genre_other": "genres", + "genreWithCount_one": "{{count}} genre", + "genreWithCount_other": "{{count}} genres", + "playlist_one": "playlist", + "playlist_other": "playlists", + "playlistWithCount_one": "{{count}} playlist", + "playlistWithCount_other": "{{count}} playlists", + "smartPlaylist": "smart $t(entity.playlist_one)", + "track_one": "track", + "track_other": "tracks", + "trackWithCount_one": "{{count}} track", + "trackWithCount_other": "{{count}} tracks" + }, + "error": { + "apiRouteError": "unable to route request", + "audioDeviceFetchError": "an error occurred when trying to get audio devices", + "authenticationFailed": "authentication failed", + "credentialsRequired": "credentials required", + "endpointNotImplementedError": "endpoint {{endpoint} is not implemented for {{serverType}}", + "genericError": "an error occurred", + "invalidServer": "invalid server", + "localFontAccessDenied": "access denied to local fonts", + "loginRateError": "too many login attempts, please try again in a few seconds", + "mpvRequired": "MPV required", + "playbackError": "an error occurred when trying to play the media", + "remoteDisableError": "an error occurred when trying to $t(common.disable) the remote server", + "remoteEnableError": "an error occurred when trying to $t(common.enable) the remote server", + "remotePortError": "an error occurred when trying to set the remote server port", + "remotePortWarning": "restart the server to apply the new port", + "serverNotSelectedError": "no server selected", + "serverRequired": "server required", + "sessionExpiredError": "your session has expired", + "systemFontError": "an error occurred when trying to get system fonts" + }, + "filter": { + "albumArtist": "$t(entity.albumArtist_one)", + "artist": "$t(entity.artist_one)", + "biography": "biography", + "bitrate": "bitrate", + "bpm": "bpm", + "communityRating": "community rating", + "criticRating": "critic rating", + "dateAdded": "date added", + "disc": "disc", + "duration": "duration", + "favorited": "favorited", + "fromYear": "from year", + "isCompilation": "is compilation", + "isFavorited": "is favorited", + "isRated": "is rated", + "isRecentlyPlayed": "is recently played", + "lastPlayed": "last played", + "mostPlayed": "most played", + "name": "name", + "note": "note", + "path": "path", + "playCount": "play count", + "random": "random", + "rating": "rating", + "recentlyAdded": "recently added", + "recentlyPlayed": "recently played", + "releaseDate": "release date", + "releaseYear": "release year", + "search": "search", + "songCount": "song count", + "title": "title", + "toYear": "to year", + "trackNumber": "track" + }, + "form": { + "addServer": { + "error_savePassword": "an error occurred when trying to save the password", + "ignoreCors": "ignore cors ($t(common.restartRequired))", + "ignoreSsl": "ignore ssl ($t(common.restartRequired))", + "input_legacyAuthentication": "enable legacy authentication", + "input_name": "server name", + "input_password": "password", + "input_savePassword": "save password", + "input_url": "url", + "input_username": "username", + "success": "server added successfully", + "title": "add server" + }, + "addToPlaylist": { + "input_playlists": "$t(entity.playlist_other)", + "input_skipDuplicates": "skip duplicates", + "success": "added {{message}} $t(entity.song_other) to {{numOfPlaylists}} $t(entity.playlist_other)", + "title": "add to $t(entity.playlist_one)" + }, + "createPlaylist": { + "input_description": "$t(common.description)", + "input_name": "$t(common.name)", + "input_owner": "$t(common.owner)", + "input_public": "public", + "success": "$t(entity.playlist_one) created successfully", + "title": "create $t(entity.playlist_one)" + }, + "deletePlaylist": { + "input_confirm": "type the name of the $t(entity.playlist_one) to confirm", + "success": "$t(entity.playlist_one) deleted successfully", + "title": "delete $t(entity.playlist_one)" + }, + "editPlaylist": { + "title": "edit $t(entity.playlist_one)" + }, + "lyricSearch": { + "input_artist": "$t(entity.artist_one)", + "input_name": "$t(common.name)", + "title": "lyric search" + }, + "queryEditor": { + "input_optionMatchAll": "match all", + "input_optionMatchAny": "match any" + }, + "updateServer": { + "success": "server updated successfully", + "title": "update server" + } + }, + "page": { + "albumArtistList": { + "title": "$t(entity.albumArtist_other)" + }, + "albumDetail": { + "moreFromArtist": "more from this $t(entity.genre_one)", + "moreFromGeneric": "more from {{item}}" + }, + "albumList": { + "title": "$t(entity.album_other)" + }, + "appMenu": { + "collapseSidebar": "collapse sidebar", + "expandSidebar": "expand sidebar", + "goBack": "go back", + "goForward": "go forward", + "manageServers": "manage servers", + "openBrowserDevtools": "open browser devtools", + "quit": "$t(common.quit)", + "selectServer": "select server", + "settings": "$t(common.setting_other)", + "version": "version {{version}}" + }, + "contextMenu": { + "addFavorite": "$t(action.addToFavorites)", + "addLast": "$t(player.addLast)", + "addNext": "$t(player.addNext)", + "addToFavorites": "$t(action.addToFavorites)", + "addToPlaylist": "$t(action.addToPlaylist)", + "createPlaylist": "$t(action.createPlaylist)", + "deletePlaylist": "$t(action.deletePlaylist)", + "deselectAll": "$t(action.deselectAll)", + "moveToBottom": "$t(action.moveToBottom)", + "moveToTop": "$t(action.moveToTop)", + "numberSelected": "{{count}} selected", + "play": "$t(player.play)", + "removeFromFavorites": "$t(action.removeFromFavorites)", + "removeFromPlaylist": "$t(action.removeFromPlaylist)", + "removeFromQueue": "$t(action.removeFromQueue)", + "setRating": "$t(action.setRating)" + }, + "fullscreenPlayer": { + "config": { + "dynamicBackground": "dynamic background", + "followCurrentLyric": "follow current lyric", + "lyricAlignment": "lyric alignment", + "lyricGap": "lyric gap", + "lyricSize": "lyric size", + "opacity": "opacity", + "showLyricMatch": "show lyric match", + "showLyricProvider": "show lyric provider", + "synchronized": "synchronized", + "unsynchronized": "unsynchronized", + "useImageAspectRatio": "use image aspect ratio" + }, + "lyrics": "lyrics", + "related": "related", + "upNext": "up next" + }, + "genreList": { + "title": "$t(entity.genre_other)" + }, + "globalSearch": { + "commands": { + "goToPage": "go to page", + "searchFor": "search for {{query}}", + "serverCommands": "server commands" + }, + "title": "commands" + }, + "home": { + "explore": "explore from your library", + "mostPlayed": "most played", + "newlyAdded": "newly added releases", + "recentlyPlayed": "recently played", + "title": "$t(common.home)" + }, + "playlistList": { + "title": "$t(entity.playlist_other)" + }, + "setting": { + "generalTab": "general", + "hotkeysTab": "hotkeys", + "playbackTab": "playback", + "windowTab": "window" + }, + "sidebar": { + "albumArtists": "$t(entity.albumArtist_other)", + "albums": "$t(entity.album_other)", + "artists": "$t(entity.artist_other)", + "folders": "$t(entity.folder_other)", + "genres": "$t(entity.genre_other)", + "home": "$t(common.home)", + "nowPlaying": "now playing", + "playlists": "$t(entity.playlist_other)", + "search": "$t(common.search)", + "settings": "$t(entity.setting_other)", + "tracks": "$t(entity.track_other)" + }, + "trackList": { + "title": "$t(entity.track_other)" + } + }, + "player": { + "addLast": "add last", + "addNext": "add next", + "favorite": "favorite", + "mute": "mute", + "muted": "muted", + "next": "next", + "play": "play", + "playbackFetchCancel": "this is taking a while... close the notification to cancel", + "playbackFetchInProgress": "loading songs...", + "playbackFetchNoResults": "no songs found", + "playbackSpeed": "playback speed", + "playRandom": "play random", + "previous": "previous", + "queue_clear": "clear queue", + "queue_moveToBottom": "move selected to top", + "queue_moveToTop": "move selected to bottom", + "queue_remove": "remove selected", + "repeat": "repeat", + "repeat_all": "repeat all", + "repeat_off": "repeat disabled", + "repeat_one": "repeat one", + "shuffle": "shuffle", + "shuffle_off": "shuffle disabled", + "skip": "skip", + "skip_back": "skip backwards", + "skip_forward": "skip forwards", + "stop": "stop", + "toggleFullscreenPlayer": "toggle fullscreen player", + "unfavorite": "unfavorite" + }, + "setting": { + "accentColor": "accent color", + "accentColor_description": "sets the accent color for the application", + "applicationHotkeys": "application hotkeys", + "applicationHotkeys_description": "configure application hotkeys. toggle the checkbox to set as a global hotkey (desktop only)", + "audioDevice": "audio device", + "audioDevice_description": "select the audio device to use for playback (web player only)", + "audioExclusiveMode": "audio exclusive mode", + "audioExclusiveMode_description": "enable exclusive output mode. In this mode, the system is usually locked out, and only mpv will be able to output audio", + "audioPlayer": "audio player", + "audioPlayer_description": "select the audio player to use for playback", + "crossfadeDuration": "crossfade duration", + "crossfadeDuration_description": "sets the duration of the crossfade effect", + "crossfadeStyle": "crossfade style", + "crossfadeStyle_description": "select the crossfade style to use for the audio player", + "customFontPath": "custom font path", + "customFontPath_description": "sets the path to the custom font to use for the application", + "disableAutomaticUpdates": "disable automatic updates", + "disableLibraryUpdateOnStartup": "disable checking for new versions on startup", + "discordApplicationId": "{{discord}} application id", + "discordApplicationId_description": "the application id for {{discord}} rich presence (defaults to {{defaultId}}", + "discordIdleStatus": "show rich presence idle status", + "discordIdleStatus_description": "when enabled, update status while player is idle", + "discordRichPresence": "{{discord}} rich presence", + "discordRichPresence_description": "enable playback status in {{discord}} rich presence. Image keys are: {{icon}}, {{playing}}, and {{paused}} ", + "discordUpdateInterval": "{{discord}} rich presence update interval", + "discordUpdateInterval_description": "the time in seconds between each update (minimum 15 seconds)", + "enableRemote": "enable remote control server", + "enableRemote_description": "enables the remote control server to allow other devices to control the application", + "exitToTray": "exit to tray", + "exitToTray_description": "exit the application to the system tray", + "floatingQueueArea": "show floating queue hover area", + "floatingQueueArea_description": "display a hover icon on the right side of the screen to view the play queue", + "followLyric": "follow current lyric", + "followLyric_description": "scroll the lyric to the current playing position", + "font": "font", + "font_description": "sets the font to use for the application", + "fontType": "font type", + "fontType_description": "built-in font selects one of the fonts provided by Feishin. system font allows you to select any font provided by your operating system. custom allows you to provide your own font", + "fontType_optionBuiltIn": "built-in font", + "fontType_optionCustom": "custom font", + "fontType_optionSystem": "system font", + "gaplessAudio": "gapless audio", + "gaplessAudio_description": "sets the gapless audio setting for mpv", + "gaplessAudio_optionWeak": "weak (recommended)", + "globalMediaHotkeys": "global media hotkeys", + "globalMediaHotkeys_description": "enable or disable the usage of your system media hotkeys to control playback", + "hotkey_browserBack": "browser back", + "hotkey_browserForward": "browser forward", + "hotkey_favoriteCurrentSong": "favorite $t(common.currentSong)", + "hotkey_favoritePreviousSong": "favorite $t(common.previousSong)", + "hotkey_globalSearch": "global search", + "hotkey_localSearch": "in-page search", + "hotkey_playbackNext": "next track", + "hotkey_playbackPause": "pause", + "hotkey_playbackPlay": "play", + "hotkey_playbackPlayPause": "play / pause", + "hotkey_playbackPrevious": "previous track", + "hotkey_playbackStop": "stop", + "hotkey_rate0": "rating clear", + "hotkey_rate1": "rating 1 star", + "hotkey_rate2": "rating 2 stars", + "hotkey_rate3": "rating 3 stars", + "hotkey_rate4": "rating 4 stars", + "hotkey_rate5": "rating 5 stars", + "hotkey_skipBackward": "skip backward", + "hotkey_skipForward": "skip forward", + "hotkey_toggleCurrentSongFavorite": "toggle $t(common.currentSong) favorite", + "hotkey_toggleFullScreenPlayer": "toggle full screen player", + "hotkey_togglePreviousSongFavorite": "toggle $t(common.previousSong) favorite", + "hotkey_toggleQueue": "toggle queue", + "hotkey_toggleRepeat": "toggle repeat", + "hotkey_toggleShuffle": "toggle shuffle", + "hotkey_unfavoriteCurrentSong": "unfavorite $t(common.currentSong)", + "hotkey_unfavoritePreviousSong": "unfavorite $t(common.previousSong)", + "hotkey_volumeDown": "volume down", + "hotkey_volumeMute": "volume mute", + "hotkey_volumeUp": "volume up", + "hotkey_zoomIn": "zoom in", + "hotkey_zoomOut": "zoom out", + "language": "language", + "language_description": "sets the language for the application ($t(common.restartRequired))", + "lyricFetch": "fetch lyrics from the internet", + "lyricFetch_description": "fetch lyrics from various internet sources", + "lyricFetchProvider": "providers to fetch lyrics from", + "lyricFetchProvider_description": "select the providers to fetch lyrics from. the order of the providers is the order in which they will be queried", + "lyricOffset": "lyric offset (ms)", + "lyricOffset_description": "offset the lyric by the specified amount of milliseconds", + "minimizeToTray": "minimize to tray", + "minimizeToTray_description": "minimize the application to the system tray", + "minimumScrobblePercentage": "minimum scrobble duration (percentage)", + "minimumScrobblePercentage_description": "the minimum percentage of the song that must be played before it is scrobbled", + "minimumScrobbleSeconds": "minimum scrobble (seconds)", + "minimumScrobbleSeconds_description": "the minimum duration in seconds of the song that must be played before it is scrobbled", + "mpvExecutablePath": "mpv executable path", + "mpvExecutablePath_description": "sets the path to the mpv executable", + "mpvExecutablePath_help": "one per line", + "mpvExtraParameters": "mpv parameters", + "playbackStyle": "playback style", + "playbackStyle_description": "select the playback style to use for the audio player", + "playbackStyle_optionCrossFade": "crossfade", + "playbackStyle_optionNormal": "normal", + "playButtonBehavior": "play button behavior", + "playButtonBehavior_description": "sets the default behavior of the play button when adding songs to the queue", + "playButtonBehavior_optionAddLast": "$t(player.addLast)", + "playButtonBehavior_optionAddNext": "$t(player.addNext)", + "playButtonBehavior_optionPlay": "$t(player.play)", + "remotePassword": "remote control server password", + "remotePassword_description": "sets the password for the remote control server. These credentials are by default transferred insecurely, so you should use a unique password that you do not care about", + "remotePort": "remote control server port", + "remotePort_description": "sets the port for the remote control server", + "remoteUsername": "remote control server username", + "remoteUsername_description": "sets the username for the remote control server. if both username and password are empty, authentication will be disabled", + "replayGainClipping": "{{ReplayGain}} clipping", + "replayGainClipping_description": "Prevent clipping caused by {{ReplayGain}} by automatically lowering the gain", + "replayGainFallback": "{{ReplayGain}} fallback", + "replayGainFallback_description": "gain in db to apply if the file has no {{ReplayGain}} tags", + "replayGainMode": "{{ReplayGain}} mode", + "replayGainMode_description": "adjust volume gain according to {{ReplayGain}} values stored in the file metadata", + "replayGainMode_optionAlbum": "$t(entity.album_one)", + "replayGainMode_optionNone": "$t(common.none)", + "replayGainMode_optionTrack": "$t(entity.track_one)", + "replayGainPreamp": "{{ReplayGain}} preamp (dB)", + "replayGainPreamp_description": "adjust the preamp gain applied to the {{ReplayGain}} values", + "sampleRate": "sample rate", + "sampleRate_description": "select the output sample rate to be used if the sample frequency selected is different from that of the current media", + "savePlayQueue": "save play queue", + "savePlayQueue_description": "save the play queue when the application is closed and restore it when the application is opened", + "scrobble": "scrobble", + "scrobble_description": "scrobble plays to your media server", + "showSkipButton": "show skip buttons", + "showSkipButton_description": "show or hide the skip buttons on the player bar", + "showSkipButtons": "show skip buttons", + "showSkipButtons_description": "show or hide the skip buttons on the player bar", + "sidebarCollapsedNavigation": "sidebar (collapsed) navigation", + "sidebarCollapsedNavigation_description": "show or hide the navigation in the collapsed sidebar", + "sidebarConfiguration": "sidebar configuration", + "sidebarConfiguration_description": "select the items and order in which they appear in the sidebar", + "sidebarPlaylistList": "sidebar playlist list", + "sidebarPlaylistList_description": "show or hide the playlist list in the sidebar", + "sidePlayQueueStyle": "side play queue style", + "sidePlayQueueStyle_description": "sets the style of the side play queue", + "sidePlayQueueStyle_optionAttached": "attached", + "sidePlayQueueStyle_optionDetached": "detached", + "skipDuration": "skip duration", + "skipDuration_description": "sets the duration to skip when using the skip buttons on the player bar", + "skipPlaylistPage": "skip playlist page", + "skipPlaylistPage_description": "when navigating to a playlist, go to the playlist song list page instead of the default page", + "theme": "theme", + "theme_description": "sets the theme to use for the application", + "themeDark": "theme (dark)", + "themeDark_description": "sets the dark theme to use for the application", + "themeLight": "theme (light)", + "themeLight_description": "sets the light theme to use for the application", + "useSystemTheme": "use system theme", + "useSystemTheme_description": "follow the system-defined light or dark preference", + "volumeWheelStep": "volume wheel step", + "volumeWheelStep_description": "the amount of volume to change when scrolling the mouse wheel on the volume slider", + "windowBarStyle": "window bar style", + "windowBarStyle_description": "select the style of the window bar", + "zoom": "zoom percentage", + "zoom_description": "sets the zoom percentage for the application" + }, + "table": { + "column": { + "album": "album", + "albumArtist": "album artist", + "albumCount": "$t(entity.album_other)", + "artist": "$t(entity.artist_one)", + "biography": "biography", + "bitrate": "bitrate", + "bpm": "bpm", + "channels": "$t(common.channel_other)", + "comment": "comment", + "dateAdded": "date added", + "discNumber": "disc", + "favorite": "favorite", + "genre": "$t(entity.genre_one)", + "lastPlayed": "last played", + "path": "path", + "playCount": "plays", + "rating": "rating", + "releaseDate": "release date", + "releaseYear": "year", + "songCount": "$t(entity.track_other)", + "title": "title", + "trackNumber": "track" + }, + "config": { + "general": { + "autoFitColumns": "auto fit columns", + "displayType": "display type", + "gap": "$t(common.gap)", + "size": "$t(common.size)", + "tableColumns": "table columns" + }, + "label": { + "actions": "$t(common.action_other)", + "album": "$t(entity.album_one)", + "albumArtist": "$t(entity.albumArtist_one)", + "artist": "$t(entity.artist_one)", + "biography": "$t(common.biography)", + "bitrate": "$t(common.bitrate)", + "bpm": "$t(common.bpm)", + "channels": "$t(common.channel_other)", + "dateAdded": "date added", + "discNumber": "disc number", + "duration": "$t(common.duration)", + "favorite": "$t(common.favorite)", + "genre": "$t(entity.genre_one)", + "lastPlayed": "last played", + "note": "$t(common.note)", + "owner": "$t(common.owner)", + "path": "$t(common.path)", + "playCount": "play count", + "rating": "$t(common.rating)", + "releaseDate": "release date", + "rowIndex": "row index", + "size": "$t(common.size)", + "title": "$t(common.title)", + "titleCombined": "$t(common.title) (combined)", + "trackNumber": "track number", + "year": "$t(common.year)" + }, + "view": { + "card": "card", + "poster": "poster", + "table": "table" + } + } + } } diff --git a/src/renderer/api/controller.ts b/src/renderer/api/controller.ts index 90b360a48..196a848c1 100644 --- a/src/renderer/api/controller.ts +++ b/src/renderer/api/controller.ts @@ -54,6 +54,7 @@ import { DeletePlaylistResponse, RandomSongListArgs } from './types'; import { ndController } from '/@/renderer/api/navidrome/navidrome-controller'; import { ssController } from '/@/renderer/api/subsonic/subsonic-controller'; import { jfController } from '/@/renderer/api/jellyfin/jellyfin-controller'; +import i18n from '/@/i18n/i18n'; export type ControllerEndpoint = Partial<{ addToPlaylist: (args: AddToPlaylistArgs) => Promise; @@ -212,7 +213,12 @@ const apiController = (endpoint: keyof ControllerEndpoint, type?: ServerType) => const serverType = type || useAuthStore.getState().currentServer?.type; if (!serverType) { - toast.error({ message: 'No server selected', title: 'Unable to route request' }); + toast.error({ + message: i18n.t('error.serverNotSelectedError', { + postProcess: 'sentenceCase', + }) as string, + title: i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' }) as string, + }); throw new Error(`No server selected`); } @@ -221,10 +227,16 @@ const apiController = (endpoint: keyof ControllerEndpoint, type?: ServerType) => if (typeof controllerFn !== 'function') { toast.error({ message: `Endpoint ${endpoint} is not implemented for ${serverType}`, - title: 'Unable to route request', + title: i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' }) as string, }); - throw new Error(`Endpoint ${endpoint} is not implemented for ${serverType}`); + throw new Error( + i18n.t('error.endpointNotImplementedError', { + endpoint, + postProcess: 'sentenceCase', + serverType, + }) as string, + ); } return endpoints[serverType][endpoint]; diff --git a/src/renderer/api/navidrome/navidrome-api.ts b/src/renderer/api/navidrome/navidrome-api.ts index 32852e93c..25cb1939b 100644 --- a/src/renderer/api/navidrome/navidrome-api.ts +++ b/src/renderer/api/navidrome/navidrome-api.ts @@ -9,6 +9,7 @@ import { authenticationFailure, resultWithHeaders } from '/@/renderer/api/utils' import { useAuthStore } from '/@/renderer/store'; import { ServerListItem } from '/@/renderer/types'; import { toast } from '/@/renderer/components'; +import i18n from '/@/i18n/i18n'; const localSettings = isElectron() ? window.electron.localSettings : null; @@ -276,9 +277,12 @@ axiosClient.interceptors.response.use( if (res.status === 429) { toast.error({ - message: - 'you have exceeded the number of allowed login requests. Please wait before logging, or consider tweaking AuthRequestLimit', - title: 'Your session has expired.', + message: i18n.t('error.loginRateError', { + postProcess: 'sentenceCase', + }) as string, + title: i18n.t('error.sessionExpiredError', { + postProcess: 'sentenceCase', + }) as string, }); const serverId = currentServer.id; @@ -292,7 +296,11 @@ axiosClient.interceptors.response.use( throw TIMEOUT_ERROR; } if (res.status !== 200) { - throw new Error('Failed to authenticate'); + throw new Error( + i18n.t('error.authenticatedFailed', { + postProcess: 'sentenceCase', + }) as string, + ); } const newCredential = res.data.token; diff --git a/src/renderer/api/subsonic/subsonic-api.ts b/src/renderer/api/subsonic/subsonic-api.ts index edd7ee3b2..858172f9a 100644 --- a/src/renderer/api/subsonic/subsonic-api.ts +++ b/src/renderer/api/subsonic/subsonic-api.ts @@ -6,6 +6,7 @@ import { z } from 'zod'; import { ssType } from '/@/renderer/api/subsonic/subsonic-types'; import { ServerListItem } from '/@/renderer/api/types'; import { toast } from '/@/renderer/components/toast/index'; +import i18n from '/@/i18n/i18n'; const c = initContract(); @@ -106,7 +107,7 @@ axiosClient.interceptors.response.use( if (data['subsonic-response'].error.code !== 0) { toast.error({ message: data['subsonic-response'].error.message, - title: 'Issue from Subsonic API', + title: i18n.t('error.genericError', { postProcess: 'sentenceCase' }) as string, }); } } diff --git a/src/renderer/app.tsx b/src/renderer/app.tsx index 39d1cd71c..1b6b82b0f 100644 --- a/src/renderer/app.tsx +++ b/src/renderer/app.tsx @@ -26,6 +26,7 @@ import { PlayerState, usePlayerStore, useQueueControls } from '/@/renderer/store import { FontType, PlaybackType, PlayerStatus } from '/@/renderer/types'; import '@ag-grid-community/styles/ag-grid.css'; import { useDiscordRpc } from '/@/renderer/features/discord-rpc/use-discord-rpc'; +import i18n from '/@/i18n/i18n'; ModuleRegistry.registerModules([ClientSideRowModelModule, InfiniteRowModelModule]); @@ -39,6 +40,7 @@ const remote = isElectron() ? window.electron.remote : null; export const App = () => { const theme = useTheme(); const accent = useSettingsStore((store) => store.general.accent); + const language = useSettingsStore((store) => store.general.language); const { builtIn, custom, system, type } = useSettingsStore((state) => state.font); const { type: playbackType } = usePlaybackSettings(); const { bindings } = useHotkeySettings(); @@ -178,6 +180,12 @@ export const App = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + useEffect(() => { + if (language) { + i18n.changeLanguage(language); + } + }, [language]); + return ( { + const { t } = useTranslation(); const handlePlayQueueAdd = usePlayQueueAdd(); const [itemIndex, setItemIndex] = useState(0); const [direction, setDirection] = useState(0); @@ -224,7 +226,7 @@ export const FeatureCarousel = ({ data }: FeatureCarouselProps) => { }); }} > - Play + {t('player.play', { postProcess: 'titleCase' })} - Display type + + {t('table.config.general.displayType', { postProcess: 'titleCase' })} + - Card + {t('table.config.view.card', { postProcess: 'titleCase' })} - Poster + {t('table.config.view.poster', { postProcess: 'titleCase' })} - Table + {t('table.config.view.table', { postProcess: 'titleCase' })} - Item size + + {t('table.config.general.size', { postProcess: 'titleCase' })} + - Table Columns + + {t('table.config.general.tableColumns', { + postProcess: 'titleCase', + })} + - Auto Fit Columns + + {t('table.config.general.autoFitColumns', { + postProcess: 'titleCase', + })} + ; @@ -25,6 +26,7 @@ interface GenreListHeaderProps { } export const GenreListHeader = ({ itemCount, gridRef, tableRef }: GenreListHeaderProps) => { + const { t } = useTranslation(); const cq = useContainerQuery(); const server = useCurrentServer(); const { pageKey } = useListContext(); @@ -66,7 +68,9 @@ export const GenreListHeader = ({ itemCount, gridRef, tableRef }: GenreListHeade w="100%" > - Genres + + {t('page.genreList.title', { postProcess: 'titleCase' })} + diff --git a/src/renderer/features/home/routes/home-route.tsx b/src/renderer/features/home/routes/home-route.tsx index e49f7554a..25b8bb10a 100644 --- a/src/renderer/features/home/routes/home-route.tsx +++ b/src/renderer/features/home/routes/home-route.tsx @@ -11,9 +11,11 @@ import { MemoizedSwiperGridCarousel } from '/@/renderer/components/grid-carousel import { Platform } from '/@/renderer/types'; import { useQueryClient } from '@tanstack/react-query'; import { queryKeys } from '/@/renderer/api/query-keys'; +import { useTranslation } from 'react-i18next'; import { RiRefreshLine } from 'react-icons/ri'; const HomeRoute = () => { + const { t } = useTranslation(); const queryClient = useQueryClient(); const scrollAreaRef = useRef(null); const server = useCurrentServer(); @@ -105,7 +107,7 @@ const HomeRoute = () => { data: random?.data?.items, sortBy: AlbumListSort.RANDOM, sortOrder: SortOrder.ASC, - title: 'Explore from your library', + title: t('page.home.explore', { postProcess: 'sentenceCase' }), uniqueId: 'random', }, { @@ -115,7 +117,7 @@ const HomeRoute = () => { }, sortBy: AlbumListSort.RECENTLY_PLAYED, sortOrder: SortOrder.DESC, - title: 'Recently played', + title: t('page.home.recentlyPlayed', { postProcess: 'sentenceCase' }), uniqueId: 'recentlyPlayed', }, { @@ -125,7 +127,7 @@ const HomeRoute = () => { }, sortBy: AlbumListSort.RECENTLY_ADDED, sortOrder: SortOrder.DESC, - title: 'Newly added releases', + title: t('page.home.newlyAdded', { postProcess: 'sentenceCase' }), uniqueId: 'recentlyAdded', }, { @@ -135,7 +137,7 @@ const HomeRoute = () => { }, sortBy: AlbumListSort.PLAY_COUNT, sortOrder: SortOrder.DESC, - title: 'Most played', + title: t('page.home.mostPlayed', { postProcess: 'sentenceCase' }), uniqueId: 'mostPlayed', }, ]; @@ -148,7 +150,9 @@ const HomeRoute = () => { backgroundColor: 'var(--titlebar-bg)', children: ( - Home + + {t('page.home.title', { postProcess: 'titleCase' })} + ), offset: 200, diff --git a/src/renderer/features/lyrics/components/lyrics-search-form.tsx b/src/renderer/features/lyrics/components/lyrics-search-form.tsx index b04fd5ab5..7555ded54 100644 --- a/src/renderer/features/lyrics/components/lyrics-search-form.tsx +++ b/src/renderer/features/lyrics/components/lyrics-search-form.tsx @@ -4,6 +4,7 @@ import { useForm } from '@mantine/form'; import { useDebouncedValue } from '@mantine/hooks'; import { openModal } from '@mantine/modals'; import orderBy from 'lodash/orderBy'; +import { useTranslation } from 'react-i18next'; import styled from 'styled-components'; import { InternetProviderLyricSearchResponse, @@ -12,6 +13,7 @@ import { } from '../../../api/types'; import { useLyricSearch } from '../queries/lyric-search-query'; import { ScrollArea, Spinner, Text, TextInput } from '/@/renderer/components'; +import i18n from '/@/i18n/i18n'; const SearchItem = styled.button` all: unset; @@ -84,6 +86,7 @@ interface LyricSearchFormProps { } export const LyricsSearchForm = ({ artist, name, onSearchOverride }: LyricSearchFormProps) => { + const { t } = useTranslation(); const form = useForm({ initialValues: { artist: artist || '', @@ -117,11 +120,17 @@ export const LyricsSearchForm = ({ artist, name, onSearchOverride }: LyricSearch @@ -170,6 +179,6 @@ export const openLyricSearchModal = ({ artist, name, onSearchOverride }: LyricSe /> ), size: 'lg', - title: 'Lyrics Search', + title: i18n.t('form.lyricSearch.title', { postProcess: 'titleCase' }) as string, }); }; diff --git a/src/renderer/features/lyrics/lyrics-actions.tsx b/src/renderer/features/lyrics/lyrics-actions.tsx index f6455ce15..3ce52b13e 100644 --- a/src/renderer/features/lyrics/lyrics-actions.tsx +++ b/src/renderer/features/lyrics/lyrics-actions.tsx @@ -1,5 +1,6 @@ import { Box, Group } from '@mantine/core'; import isElectron from 'is-electron'; +import { useTranslation } from 'react-i18next'; import { RiAddFill, RiSubtractFill } from 'react-icons/ri'; import { LyricsOverride } from '/@/renderer/api/types'; import { Button, NumberInput, Tooltip } from '/@/renderer/components'; @@ -22,6 +23,7 @@ export const LyricsActions = ({ onResetLyric, onSearchOverride, }: LyricsActionsProps) => { + const { t } = useTranslation(); const currentSong = useCurrentSong(); const { setSettings } = useSettingsStoreActions(); const { delayMs, sources } = useLyricsSettings(); @@ -54,7 +56,7 @@ export const LyricsActions = ({ }) } > - Search + {t('common.search', { postProcess: 'titleCase' })} ) : null} - Reset + {t('common.reset', { postProcess: 'sentenceCase' })} ) : null} @@ -104,7 +106,7 @@ export const LyricsActions = ({ variant="subtle" onClick={onRemoveLyric} > - Clear + {t('common.clear', { postProcess: 'sentenceCase' })} ) : null} diff --git a/src/renderer/features/now-playing/components/play-queue-list-controls.tsx b/src/renderer/features/now-playing/components/play-queue-list-controls.tsx index 29cc7700d..72879812a 100644 --- a/src/renderer/features/now-playing/components/play-queue-list-controls.tsx +++ b/src/renderer/features/now-playing/components/play-queue-list-controls.tsx @@ -3,6 +3,7 @@ import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/li import { Group } from '@mantine/core'; import { Button, Popover } from '/@/renderer/components'; import isElectron from 'is-electron'; +import { useTranslation } from 'react-i18next'; import { RiArrowDownLine, RiArrowUpLine, @@ -27,6 +28,7 @@ interface PlayQueueListOptionsProps { } export const PlayQueueListControls = ({ type, tableRef }: PlayQueueListOptionsProps) => { + const { t } = useTranslation(); const { clearQueue, moveToBottomOfQueue, moveToTopOfQueue, shuffleQueue, removeFromQueue } = useQueueControls(); @@ -115,7 +117,7 @@ export const PlayQueueListControls = ({ type, tableRef }: PlayQueueListOptionsPr @@ -462,7 +468,7 @@ export const PlaylistQueryBuilder = forwardRef( icon={} onClick={handleSave} > - Save and replace + {t('common.saveAndReplace', { postProcess: 'titleCase' })} diff --git a/src/renderer/features/playlists/components/save-as-playlist-form.tsx b/src/renderer/features/playlists/components/save-as-playlist-form.tsx index a0f87d081..5196b2456 100644 --- a/src/renderer/features/playlists/components/save-as-playlist-form.tsx +++ b/src/renderer/features/playlists/components/save-as-playlist-form.tsx @@ -4,6 +4,7 @@ import { CreatePlaylistBody, CreatePlaylistResponse, ServerType } from '/@/rende import { Button, Switch, TextInput, toast } from '/@/renderer/components'; import { useCreatePlaylist } from '/@/renderer/features/playlists/mutations/create-playlist-mutation'; import { useCurrentServer } from '/@/renderer/store'; +import { useTranslation } from 'react-i18next'; interface SaveAsPlaylistFormProps { body: Partial; @@ -18,6 +19,7 @@ export const SaveAsPlaylistForm = ({ onSuccess, onCancel, }: SaveAsPlaylistFormProps) => { + const { t } = useTranslation(); const mutation = useCreatePlaylist({}); const server = useCurrentServer(); @@ -40,10 +42,15 @@ export const SaveAsPlaylistForm = ({ { body: values, serverId }, { onError: (err) => { - toast.error({ message: err.message, title: 'Error creating playlist' }); + toast.error({ + message: err.message, + title: t('error.genericError', { postProcess: 'sentenceCase' }), + }); }, onSuccess: (data) => { - toast.success({ message: `Playlist has been created` }); + toast.success({ + message: t('form.createPlaylist.success', { postProcess: 'sentenceCase' }), + }); onSuccess(data); onCancel(); }, @@ -60,16 +67,25 @@ export const SaveAsPlaylistForm = ({ {isPublicDisplayed && ( )} @@ -78,7 +94,7 @@ export const SaveAsPlaylistForm = ({ variant="subtle" onClick={onCancel} > - Cancel + {t('common.cancel', { postProcess: 'titleCase' })} diff --git a/src/renderer/features/playlists/components/update-playlist-form.tsx b/src/renderer/features/playlists/components/update-playlist-form.tsx index 4dea0a700..1192fdad1 100644 --- a/src/renderer/features/playlists/components/update-playlist-form.tsx +++ b/src/renderer/features/playlists/components/update-playlist-form.tsx @@ -18,6 +18,8 @@ import { Button, Select, Switch, TextInput, toast } from '/@/renderer/components import { useUpdatePlaylist } from '/@/renderer/features/playlists/mutations/update-playlist-mutation'; import { queryClient } from '/@/renderer/lib/react-query'; import { useCurrentServer } from '/@/renderer/store'; +import { useTranslation } from 'react-i18next'; +import i18n from '/@/i18n/i18n'; interface UpdatePlaylistFormProps { body: Partial; @@ -27,6 +29,7 @@ interface UpdatePlaylistFormProps { } export const UpdatePlaylistForm = ({ users, query, body, onCancel }: UpdatePlaylistFormProps) => { + const { t } = useTranslation(); const mutation = useUpdatePlaylist({}); const server = useCurrentServer(); @@ -60,10 +63,12 @@ export const UpdatePlaylistForm = ({ users, query, body, onCancel }: UpdatePlayl }, { onError: (err) => { - toast.error({ message: err.message, title: 'Error updating playlist' }); + toast.error({ + message: err.message, + title: t('error.genericError', { postProcess: 'sentenceCase' }), + }); }, onSuccess: () => { - toast.success({ message: `Playlist has been saved` }); onCancel(); }, }, @@ -80,23 +85,35 @@ export const UpdatePlaylistForm = ({ users, query, body, onCancel }: UpdatePlayl {isOwnerDisplayed && ( ), - description: 'Sets the application language', + description: t('setting.language', { + context: 'description', + postProcess: 'sentenceCase', + }), isHidden: false, - title: 'Language', + title: t('setting.language', { postProcess: 'sentenceCase' }), }, { control: ( @@ -155,10 +187,12 @@ export const ApplicationSettings = () => { }} /> ), - description: - 'What font to use. Built-in font selects one of the fonts provided by Feishin. System font allows you to select any font provided by your OS. Custom allows you to provide your own font', + description: t('setting.fontType', { + context: 'description', + postProcess: 'sentenceCase', + }), isHidden: FONT_TYPES.length === 1, - title: 'Use system font', + title: t('setting.fontType', { postProcess: 'sentenceCase' }), }, { control: ( @@ -177,9 +211,9 @@ export const ApplicationSettings = () => { }} /> ), - description: 'Sets the application content font', + description: t('setting.font', { context: 'description', postProcess: 'sentenceCase' }), isHidden: localFonts && fontSettings.type !== FontType.BUILT_IN, - title: 'Font (Content)', + title: t('setting.font', { postProcess: 'sentenceCase' }), }, { control: ( @@ -199,9 +233,9 @@ export const ApplicationSettings = () => { }} /> ), - description: 'Sets the application content font', + description: t('setting.font', { context: 'description', postProcess: 'sentenceCase' }), isHidden: !localFonts || fontSettings.type !== FontType.SYSTEM, - title: 'Font (Content)', + title: t('setting.font', { postProcess: 'sentenceCase' }), }, { control: ( @@ -219,9 +253,12 @@ export const ApplicationSettings = () => { } /> ), - description: 'Path to custom font', + description: t('setting.customFontPath', { + context: 'description', + postProcess: 'sentenceCase', + }), isHidden: fontSettings.type !== FontType.CUSTOM, - title: 'Path to custom font', + title: t('setting.customFontPath', { postProcess: 'sentenceCase' }), }, { control: ( @@ -244,9 +281,14 @@ export const ApplicationSettings = () => { }} /> ), - description: 'Sets the application zoom factor in percent', + description: t('setting.zoom', { + context: 'description', + postProcess: 'sentenceCase', + }), isHidden: !isElectron(), - title: 'Zoom factor', + title: t('setting.zoom', { + postProcess: 'sentenceCase', + }), }, ]; diff --git a/src/renderer/features/settings/components/general/control-settings.tsx b/src/renderer/features/settings/components/general/control-settings.tsx index 0d8560942..8e9407d05 100644 --- a/src/renderer/features/settings/components/general/control-settings.tsx +++ b/src/renderer/features/settings/components/general/control-settings.tsx @@ -1,5 +1,6 @@ -import isElectron from 'is-electron'; import { Group } from '@mantine/core'; +import { t } from 'i18next'; +import isElectron from 'is-electron'; import { Select, Tooltip, NumberInput, Switch, Slider } from '/@/renderer/components'; import { SettingsSection } from '/@/renderer/features/settings/components/settings-section'; import { @@ -8,15 +9,29 @@ import { useSettingsStoreActions, } from '/@/renderer/store/settings.store'; import { Play } from '/@/renderer/types'; +import { useTranslation } from 'react-i18next'; const localSettings = isElectron() ? window.electron.localSettings : null; const SIDE_QUEUE_OPTIONS = [ - { label: 'Fixed', value: 'sideQueue' }, - { label: 'Floating', value: 'sideDrawerQueue' }, + { + label: t('setting.sidePlayQueueStyle', { + context: 'optionAttached', + postProcess: 'sentenceCase', + }), + value: 'sideQueue', + }, + { + label: t('setting.sidePlayQueueStyle', { + context: 'optionDetached', + postProcess: 'sentenceCase', + }), + value: 'sideDrawerQueue', + }, ]; export const ControlSettings = () => { + const { t } = useTranslation(); const settings = useGeneralSettings(); const { setSettings } = useSettingsStoreActions(); @@ -39,14 +54,17 @@ export const ControlSettings = () => { } /> ), - description: 'Show or hide the skip buttons on the playerbar', + description: t('setting.showSkipButtons', { + context: 'description', + postProcess: 'sentenceCase', + }), isHidden: false, - title: 'Show skip buttons', + title: t('setting.showSkipButtons', { postProcess: 'sentenceCase' }), }, { control: ( - + { } /> - + { ), - description: - 'The number (in seconds) to skip forward or backward when using the skip buttons', + description: t('setting.skipDuration', { + context: 'description', + postProcess: 'sentenceCase', + }), isHidden: false, - title: 'Skip duration', + title: t('setting.skipDuration', { postProcess: 'sentenceCase' }), }, { control: ( { } /> ), - description: 'Adjust the playback style (web player only)', + description: t('setting.playbackStyle', { + context: 'description', + postProcess: 'sentenceCase', + }), isHidden: settings.type !== PlaybackType.WEB, note: status === PlayerStatus.PLAYING ? 'Player must be paused' : undefined, - title: 'Playback style', + title: t('setting.playbackStyle', { + context: 'description', + postProcess: 'sentenceCase', + }), }, { control: ( @@ -116,10 +149,15 @@ export const AudioSettings = () => { } /> ), - description: 'Adjust the crossfade duration (web player only)', + description: t('setting.crossfadeDuration', { + context: 'description', + postProcess: 'sentenceCase', + }), isHidden: settings.type !== PlaybackType.WEB, note: status === PlayerStatus.PLAYING ? 'Player must be paused' : undefined, - title: 'Crossfade Duration', + title: t('setting.crossfadeDuration', { + postProcess: 'sentenceCase', + }), }, { control: ( @@ -153,10 +191,13 @@ export const AudioSettings = () => { }} /> ), - description: 'Change the crossfade algorithm (web player only)', + description: t('setting.crossfadeStyle', { + context: 'description', + postProcess: 'sentenceCase', + }), isHidden: settings.type !== PlaybackType.WEB, note: status === PlayerStatus.PLAYING ? 'Player must be paused' : undefined, - title: 'Crossfade Style', + title: t('setting.crossfadeStyle', { postProcess: 'sentenceCase' }), }, ]; diff --git a/src/renderer/features/settings/components/playback/lyric-settings.tsx b/src/renderer/features/settings/components/playback/lyric-settings.tsx index e8574b36b..19b373921 100644 --- a/src/renderer/features/settings/components/playback/lyric-settings.tsx +++ b/src/renderer/features/settings/components/playback/lyric-settings.tsx @@ -7,6 +7,7 @@ import { MultiSelect, MultiSelectProps, NumberInput, Switch } from '/@/renderer/ import isElectron from 'is-electron'; import styled from 'styled-components'; import { LyricSource } from '/@/renderer/api/types'; +import { useTranslation } from 'react-i18next'; const localSettings = isElectron() ? window.electron.localSettings : null; @@ -17,6 +18,7 @@ const WorkingButtonSelect = styled(MultiSelect)` `; export const LyricSettings = () => { + const { t } = useTranslation(); const settings = useLyricsSettings(); const { setSettings } = useSettingsStoreActions(); @@ -36,8 +38,11 @@ export const LyricSettings = () => { }} /> ), - description: 'Enable or disable following of current lyric', - title: 'Follow current lyric', + description: t('setting.followLyric', { + context: 'description', + postProcess: 'sentenceCase', + }), + title: t('setting.followLyric', { postProcess: 'sentenceCase' }), }, { control: ( @@ -54,9 +59,12 @@ export const LyricSettings = () => { }} /> ), - description: 'Enable or disable fetching lyrics for the current song', + description: t('setting.lyricFetch', { + context: 'description', + postProcess: 'sentenceCase', + }), isHidden: !isElectron(), - title: 'Fetch lyrics from the internet', + title: t('setting.lyricFetch', { postProcess: 'sentenceCase' }), }, { control: ( @@ -77,10 +85,12 @@ export const LyricSettings = () => { }} /> ), - description: - 'Lyric fetchers should be added in order of preference. This is the order in which they will be queried.', + description: t('setting.lyricFetchProvider', { + context: 'description', + postProcess: 'sentenceCase', + }), isHidden: !isElectron(), - title: 'Providers to fetch lyrics', + title: t('setting.lyricFetchProvider', { postProcess: 'sentenceCase' }), }, { control: ( @@ -99,10 +109,12 @@ export const LyricSettings = () => { }} /> ), - description: - 'Lyric offset (in milliseconds). Positive values mean that lyrics are shown later, and negative mean that lyrics are shown earlier', + description: t('setting.lyricOffset', { + context: 'description', + postProcess: 'sentenceCase', + }), isHidden: !isElectron(), - title: 'Lyric offset', + title: t('setting.lyricOffset', { postProcess: 'sentenceCase' }), }, ]; diff --git a/src/renderer/features/settings/components/playback/mpv-settings.tsx b/src/renderer/features/settings/components/playback/mpv-settings.tsx index 702ee7e8e..8ace79974 100644 --- a/src/renderer/features/settings/components/playback/mpv-settings.tsx +++ b/src/renderer/features/settings/components/playback/mpv-settings.tsx @@ -12,6 +12,7 @@ import { useSettingsStoreActions, } from '/@/renderer/store/settings.store'; import { PlaybackType } from '/@/renderer/types'; +import { useTranslation } from 'react-i18next'; const localSettings = isElectron() ? window.electron.localSettings : null; const mpvPlayer = isElectron() ? window.electron.mpvPlayer : null; @@ -60,6 +61,7 @@ export const getMpvProperties = (settings: SettingsState['playback']['mpvPropert }; export const MpvSettings = () => { + const { t } = useTranslation(); const settings = usePlaybackSettings(); const { setSettings } = useSettingsStoreActions(); @@ -116,10 +118,13 @@ export const MpvSettings = () => { onChange={handleSetMpvPath} /> ), - description: 'The location of your mpv executable', + description: t('setting.mpvExecutablePath', { + context: 'description', + postProcess: 'sentenceCase', + }), isHidden: settings.type !== PlaybackType.LOCAL, note: 'Restart required', - title: 'MPV executable path', + title: t('setting.mpvExecutablePath', { postProcess: 'sentenceCase' }), }, { control: ( @@ -128,9 +133,10 @@ export const MpvSettings = () => { autosize defaultValue={settings.mpvExtraParameters.join('\n')} minRows={4} - placeholder={ - '(Add one per line):\n--gapless-audio=weak\n--prefetch-playlist=yes' - } + placeholder={`(${t('setting.mpvExtraParameters', { + context: 'help', + postProcess: 'sentenceCase', + })}):\n--gapless-audio=weak\n--prefetch-playlist=yes`} width={225} onBlur={(e) => { handleSetExtraParameters(e.currentTarget.value.split('\n')); @@ -145,7 +151,10 @@ export const MpvSettings = () => { $secondary size="sm" > - Options to pass to the player + {t('setting.mpvExtraParameters', { + context: 'description', + postProcess: 'sentenceCase', + })} { ), isHidden: settings.type !== PlaybackType.LOCAL, - note: 'Restart required', - title: 'MPV parameters', + note: t('common.restartRequired', { + postProcess: 'sentenceCase', + }), + title: t('setting.mpvExtraParameters', { + postProcess: 'sentenceCase', + }), }, ]; @@ -169,18 +182,26 @@ export const MpvSettings = () => { control: ( handleSetMpvProperty('replayGainMode', e)} /> ), - description: - 'Adjust volume gain according to replaygain values stored in the file metadata (--replaygain)', - note: 'Restart required', - title: 'ReplayGain mode', + description: t('setting.replayGainMode', { + ReplayGain: 'ReplayGain', + context: 'description', + postProcess: 'sentenceCase', + }), + note: t('common.restartRequired', { postProcess: 'sentenceCase' }), + title: t('setting.replayGainMode', { + ReplayGain: 'ReplayGain', + postProcess: 'sentenceCase', + }), }, { control: ( @@ -244,9 +293,15 @@ export const MpvSettings = () => { onChange={(e) => handleSetMpvProperty('replayGainPreampDB', e)} /> ), - description: - 'Pre-amplification gain in dB to apply to the selected replaygain gain (--replaygain-preamp)', - title: 'ReplayGain preamp (dB)', + description: t('setting.replayGainMode', { + ReplayGain: 'ReplayGain', + context: 'description', + postProcess: 'sentenceCase', + }), + title: t('setting.replayGainPreamp', { + ReplayGain: 'ReplayGain', + postProcess: 'sentenceCase', + }), }, { control: ( @@ -257,9 +312,14 @@ export const MpvSettings = () => { } /> ), - description: - 'Prevent clipping caused by replaygain by automatically lowering the gain (--replaygain-clip)', - title: 'ReplayGain clipping', + description: t('setting.replayGainClipping', { + ReplayGain: 'ReplayGain', + postProcess: 'sentenceCase', + }), + title: t('setting.replayGainClipping', { + ReplayGain: 'ReplayGain', + postProcess: 'sentenceCase', + }), }, { control: ( @@ -269,9 +329,14 @@ export const MpvSettings = () => { onBlur={(e) => handleSetMpvProperty('replayGainFallbackDB', e)} /> ), - description: - 'Gain in dB to apply if the file has no replay gain tags. This option is always applied if the replaygain logic is somehow inactive. If this is applied, no other replaygain options are applied', - title: 'ReplayGain fallback (dB)', + description: t('setting.replayGainFallback', { + ReplayGain: 'ReplayGain', + postProcess: 'sentenceCase', + }), + title: t('setting.replayGainFallback', { + ReplayGain: 'ReplayGain', + postProcess: 'sentenceCase', + }), }, ]; diff --git a/src/renderer/features/settings/components/playback/playback-tab.tsx b/src/renderer/features/settings/components/playback/playback-tab.tsx index 6cb77b8b9..a9a240c16 100644 --- a/src/renderer/features/settings/components/playback/playback-tab.tsx +++ b/src/renderer/features/settings/components/playback/playback-tab.tsx @@ -20,12 +20,8 @@ export const PlaybackTab = () => { }>{hasFancyAudio && } - {isElectron() && ( - <> - - - - )} + + ); diff --git a/src/renderer/features/settings/components/playback/scrobble-settings.tsx b/src/renderer/features/settings/components/playback/scrobble-settings.tsx index cfef1ffdf..dd57e5b52 100644 --- a/src/renderer/features/settings/components/playback/scrobble-settings.tsx +++ b/src/renderer/features/settings/components/playback/scrobble-settings.tsx @@ -1,8 +1,10 @@ -import { NumberInput, Slider, Switch, Text } from '/@/renderer/components'; +import { NumberInput, Slider, Switch } from '/@/renderer/components'; import { usePlaybackSettings, useSettingsStoreActions } from '/@/renderer/store/settings.store'; import { SettingOption, SettingsSection } from '../settings-section'; +import { useTranslation } from 'react-i18next'; export const ScrobbleSettings = () => { + const { t } = useTranslation(); const settings = usePlaybackSettings(); const { setSettings } = useSettingsStoreActions(); @@ -25,8 +27,11 @@ export const ScrobbleSettings = () => { }} /> ), - description: 'Enable or disable scrobbling to your media server', - title: 'Scrobble', + description: t('setting.scrobble', { + context: 'description', + postProcess: 'sentenceCase', + }), + title: t('setting.scrobble', { postProcess: 'sentenceCase' }), }, { control: ( @@ -50,9 +55,11 @@ export const ScrobbleSettings = () => { }} /> ), - description: - 'The percentage of the song that must be played before submitting a scrobble', - title: 'Minimum scrobble percentage*', + description: t('setting.minimumScrobblePercentage', { + context: 'description', + postProcess: 'sentenceCase', + }), + title: t('setting.minimumScrobblePercentage', { postProcess: 'sentenceCase' }), }, { control: ( @@ -76,21 +83,13 @@ export const ScrobbleSettings = () => { }} /> ), - description: - 'The duration in seconds of a song that must be played before submitting a scrobble', - title: 'Minimum scrobble duration (seconds)*', + description: t('setting.minimumScrobblePercentage', { + context: 'description', + postProcess: 'sentenceCase', + }), + title: t('setting.minimumScrobbleSeconds', { postProcess: 'sentenceCase' }), }, ]; - return ( - <> - - - *The scrobble will be submitted if one or more of the above conditions is met - - - ); + return ; }; diff --git a/src/renderer/features/settings/components/settings-content.tsx b/src/renderer/features/settings/components/settings-content.tsx index 7d4864e08..fdeea8732 100644 --- a/src/renderer/features/settings/components/settings-content.tsx +++ b/src/renderer/features/settings/components/settings-content.tsx @@ -2,6 +2,7 @@ import { lazy } from 'react'; import { Tabs } from '/@/renderer/components'; import { useSettingsStore, useSettingsStoreActions } from '/@/renderer/store/settings.store'; import isElectron from 'is-electron'; +import { useTranslation } from 'react-i18next'; import styled from 'styled-components'; const GeneralTab = lazy(() => @@ -36,6 +37,7 @@ const TabContainer = styled.div` `; export const SettingsContent = () => { + const { t } = useTranslation(); const currentTab = useSettingsStore((state) => state.tab); const { setSettings } = useSettingsStoreActions(); @@ -49,10 +51,20 @@ export const SettingsContent = () => { onTabChange={(e) => e && setSettings({ tab: e })} > - General - Playback - Hotkeys - {isElectron() && Window} + + {t('page.setting.generalTab', { postProcess: 'sentenceCase' })} + + + {t('page.setting.playbackTab', { postProcess: 'sentenceCase' })} + + + {t('page.setting.hotkeysTab', { postProcess: 'sentenceCase' })} + + {isElectron() && ( + + {t('page.setting.windowTab', { postProcess: 'sentenceCase' })} + + )} diff --git a/src/renderer/features/settings/components/settings-header.tsx b/src/renderer/features/settings/components/settings-header.tsx index 4dba5a399..01fef382d 100644 --- a/src/renderer/features/settings/components/settings-header.tsx +++ b/src/renderer/features/settings/components/settings-header.tsx @@ -1,11 +1,13 @@ import { Flex, Group } from '@mantine/core'; import { closeAllModals, openModal } from '@mantine/modals'; +import { useTranslation } from 'react-i18next'; import { RiSettings2Fill } from 'react-icons/ri'; import { Button, ConfirmModal, PageHeader } from '/@/renderer/components'; import { LibraryHeaderBar } from '/@/renderer/features/shared'; import { useSettingsStoreActions } from '../../../store/settings.store'; export const SettingsHeader = () => { + const { t } = useTranslation(); const { reset } = useSettingsStoreActions(); const handleResetToDefault = () => { @@ -15,8 +17,12 @@ export const SettingsHeader = () => { const openResetConfirmModal = () => { openModal({ - children: Are you sure?, - title: 'Reset settings to default', + children: ( + + {t('common.areYouSure', { postProcess: 'sentenceCase' })} + + ), + title: t('common.resetToDefault', { postProcess: 'sentenceCase' }), }); }; @@ -31,14 +37,16 @@ export const SettingsHeader = () => { > - Settings + + {t('common.setting', { count: 2, postProcess: 'titleCase' })} + diff --git a/src/renderer/features/settings/components/settings-section.tsx b/src/renderer/features/settings/components/settings-section.tsx index 47a1442bd..cee5b526d 100644 --- a/src/renderer/features/settings/components/settings-section.tsx +++ b/src/renderer/features/settings/components/settings-section.tsx @@ -20,7 +20,7 @@ export const SettingsSection = ({ options }: SettingsSectionProps) => { .filter((o) => !o.isHidden) .map((option) => ( ))} diff --git a/src/renderer/features/settings/components/window/discord-settings.tsx b/src/renderer/features/settings/components/window/discord-settings.tsx index 2481613cd..f30fd4593 100644 --- a/src/renderer/features/settings/components/window/discord-settings.tsx +++ b/src/renderer/features/settings/components/window/discord-settings.tsx @@ -5,8 +5,10 @@ import { SettingsSection, } from '/@/renderer/features/settings/components/settings-section'; import { useDiscordSetttings, useSettingsStoreActions } from '/@/renderer/store'; +import { useTranslation } from 'react-i18next'; export const DiscordSettings = () => { + const { t } = useTranslation(); const settings = useDiscordSetttings(); const { setSettings } = useSettingsStoreActions(); @@ -25,10 +27,19 @@ export const DiscordSettings = () => { }} /> ), - description: - 'Enable playback status in Discord rich presence. Image keys include: "icon", "playing", and "paused"', + description: t('setting.discordRichPresence', { + context: 'description', + discord: 'Discord', + icon: 'icon', + paused: 'paused', + playing: 'playing', + postProcess: 'sentenceCase', + }), isHidden: !isElectron(), - title: 'Discord rich presence', + title: t('setting.discordRichPresence', { + discord: 'Discord', + postProcess: 'sentenceCase', + }), }, { control: ( @@ -44,9 +55,17 @@ export const DiscordSettings = () => { }} /> ), - description: 'The Discord application ID (defaults to 1165957668758900787)', + description: t('setting.discordApplicationId', { + context: 'description', + defaultId: '1165957668758900787', + discord: 'Discord', + postProcess: 'sentenceCase', + }), isHidden: !isElectron(), - title: 'Discord application ID', + title: t('setting.discordApplicationId', { + discord: 'Discord', + postProcess: 'sentenceCase', + }), }, { control: ( @@ -67,9 +86,15 @@ export const DiscordSettings = () => { }} /> ), - description: 'The time in seconds between each update (minimum 15 seconds)', + description: t('setting.discordUpdateInterval', { + context: 'description', + postProcess: 'sentenceCase', + }), isHidden: !isElectron(), - title: 'Rich presence update interval (seconds)', + title: t('setting.discordUpdateInterval', { + discord: 'Discord', + postProcess: 'sentenceCase', + }), }, { control: ( @@ -85,9 +110,14 @@ export const DiscordSettings = () => { }} /> ), - description: 'When enabled, the rich presence will update while player is idle', + description: t('setting.discordIdleStatus', { + context: 'description', + postProcess: 'sentenceCase', + }), isHidden: !isElectron(), - title: 'Show rich presence when idle', + title: t('setting.discordIdleStatus', { + postProcess: 'sentenceCase', + }), }, ]; diff --git a/src/renderer/features/settings/components/window/update-settings.tsx b/src/renderer/features/settings/components/window/update-settings.tsx index a5b343333..ddb94d31e 100644 --- a/src/renderer/features/settings/components/window/update-settings.tsx +++ b/src/renderer/features/settings/components/window/update-settings.tsx @@ -1,4 +1,5 @@ import isElectron from 'is-electron'; +import { useTranslation } from 'react-i18next'; import { useWindowSettings, useSettingsStoreActions } from '../../../../store/settings.store'; import { SettingsSection, @@ -9,6 +10,7 @@ import { Switch } from '/@/renderer/components'; const localSettings = isElectron() ? window.electron.localSettings : null; export const UpdateSettings = () => { + const { t } = useTranslation(); const settings = useWindowSettings(); const { setSettings } = useSettingsStoreActions(); @@ -31,9 +33,12 @@ export const UpdateSettings = () => { }} /> ), - description: 'Enabling this option will disable checking for new versions on startup', + description: t('setting.disableAutomaticUpdates', { + context: 'description', + postProcess: 'sentenceCase', + }), isHidden: !isElectron(), - title: 'Disable automatic updates', + title: t('setting.disableAutomaticUpdates', { postProcess: 'sentenceCase' }), }, ]; diff --git a/src/renderer/features/settings/components/window/window-settings.tsx b/src/renderer/features/settings/components/window/window-settings.tsx index 0dcfa88f8..899b328bc 100644 --- a/src/renderer/features/settings/components/window/window-settings.tsx +++ b/src/renderer/features/settings/components/window/window-settings.tsx @@ -1,5 +1,6 @@ import isElectron from 'is-electron'; import { Platform } from '/@/renderer/types'; +import { useTranslation } from 'react-i18next'; import { useWindowSettings, useSettingsStoreActions } from '../../../../store/settings.store'; import { SettingsSection, @@ -17,6 +18,7 @@ const WINDOW_BAR_OPTIONS = [ const localSettings = isElectron() ? window.electron.localSettings : null; export const WindowSettings = () => { + const { t } = useTranslation(); const settings = useWindowSettings(); const { setSettings } = useSettingsStoreActions(); @@ -43,12 +45,15 @@ export const WindowSettings = () => { toast.info({ autoClose: false, id: 'restart-toast', - message: - 'Restart to apply changes... close the notification to restart Feishin', + message: t('common.forceRestartRequired', { + postProcess: 'sentenceCase', + }), onClose: () => { window.electron.ipc!.send('app-restart'); }, - title: 'Restart required', + title: t('common.restartRequired', { + postProcess: 'sentenceCase', + }), }); } else { toast.update({ @@ -69,9 +74,12 @@ export const WindowSettings = () => { }} /> ), - description: 'Adjust the style of the application window bar', + description: t('setting.windowBarStyle', { + context: 'description', + postProcess: 'sentenceCase', + }), isHidden: !isElectron(), - title: 'Window bar style', + title: t('setting.windowBarStyle', { postProcess: 'sentenceCase' }), }, { control: ( @@ -91,9 +99,12 @@ export const WindowSettings = () => { }} /> ), - description: 'Minimize the application to the system tray', + description: t('setting.minimizeToTray', { + context: 'description', + postProcess: 'sentenceCase', + }), isHidden: !isElectron(), - title: 'Minimize to tray', + title: t('setting.minimizeToTray', { postProcess: 'sentenceCase' }), }, { control: ( @@ -113,9 +124,12 @@ export const WindowSettings = () => { }} /> ), - description: 'Exit the application to the system tray', + description: t('setting.exitToTray', { + context: 'description', + postProcess: 'sentenceCase', + }), isHidden: !isElectron(), - title: 'Exit to tray', + title: t('setting.exitToTray', { postProcess: 'sentenceCase' }), }, ]; diff --git a/src/renderer/features/shared/components/library-header.tsx b/src/renderer/features/shared/components/library-header.tsx index f1d960913..1a4614553 100644 --- a/src/renderer/features/shared/components/library-header.tsx +++ b/src/renderer/features/shared/components/library-header.tsx @@ -1,5 +1,6 @@ -import { Group } from '@mantine/core'; import { forwardRef, ReactNode, Ref, useState } from 'react'; +import { Group } from '@mantine/core'; +import { useTranslation } from 'react-i18next'; import { Link } from 'react-router-dom'; import styles from './library-header.module.scss'; import { LibraryItem } from '/@/renderer/api/types'; @@ -20,12 +21,30 @@ export const LibraryHeader = forwardRef( { imageUrl, imagePlaceholderUrl, background, title, item, children }: LibraryHeaderProps, ref: Ref, ) => { + const { t } = useTranslation(); const [isImageError, setIsImageError] = useState(false); const onImageError = () => { setIsImageError(true); }; + const itemTypeString = () => { + switch (item.type) { + case LibraryItem.ALBUM: + return t('entity.album', { count: 1 }); + case LibraryItem.ARTIST: + return t('entity.artist', { count: 1 }); + case LibraryItem.ALBUM_ARTIST: + return t('entity.albumArtist', { count: 1 }); + case LibraryItem.PLAYLIST: + return t('entity.playlist', { count: 1 }); + case LibraryItem.SONG: + return t('entity.track', { count: 1 }); + default: + return t('common.unknown'); + } + }; + return (
- {item.type} + {itemTypeString()}

{title}

diff --git a/src/renderer/features/shared/components/order-toggle-button.tsx b/src/renderer/features/shared/components/order-toggle-button.tsx index 78cc562bb..44fafd6cc 100644 --- a/src/renderer/features/shared/components/order-toggle-button.tsx +++ b/src/renderer/features/shared/components/order-toggle-button.tsx @@ -1,4 +1,5 @@ import { ButtonProps } from '@mantine/core'; +import { useTranslation } from 'react-i18next'; import { RiSortAsc, RiSortDesc } from 'react-icons/ri'; import { SortOrder } from '/@/renderer/api/types'; import { Button, Tooltip } from '/@/renderer/components'; @@ -10,8 +11,15 @@ interface OrderToggleButtonProps { } export const OrderToggleButton = ({ sortOrder, onToggle, buttonProps }: OrderToggleButtonProps) => { + const { t } = useTranslation(); return ( - +
+ {mbzId ? ( + + ) : null} + + + ) : null} {comment && ( { + const { t } = useTranslation(); + const { externalLinks } = useGeneralSettings(); const { albumArtistId } = useParams() as { albumArtistId: string }; const cq = useContainerQuery(); const handlePlayQueueAdd = usePlayQueueAdd(); @@ -324,6 +329,7 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten detailQuery?.data?.biography !== undefined && detailQuery?.data?.biography !== null; const showTopSongs = topSongsQuery?.data?.items?.length; const showGenres = detailQuery?.data?.genres ? detailQuery?.data?.genres.length !== 0 : false; + const mbzId = detailQuery?.data?.mbz; const isLoading = detailQuery?.isLoading || @@ -411,6 +417,50 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten ) : null} + {externalLinks ? ( + + + + {mbzId ? ( + + ) : null} + + + ) : null} {showBiography ? ( { isHidden: false, title: t('setting.skipPlaylistPage', { postProcess: 'sentenceCase' }), }, + { + control: ( + { + setSettings({ + general: { + ...settings, + externalLinks: e.currentTarget.checked, + }, + }); + }} + /> + ), + description: t('setting.externalLinks', { + context: 'description', + postProcess: 'sentenceCase', + }), + title: t('setting.externalLinks', { postProcess: 'sentenceCase' }), + }, ]; return ; diff --git a/src/renderer/store/settings.store.ts b/src/renderer/store/settings.store.ts index 751444757..a795178fe 100644 --- a/src/renderer/store/settings.store.ts +++ b/src/renderer/store/settings.store.ts @@ -170,6 +170,7 @@ export interface SettingsState { general: { accent: string; defaultFullPlaylist: boolean; + externalLinks: boolean; followSystemTheme: boolean; language: string; playButtonBehavior: Play; @@ -281,6 +282,7 @@ const initialState: SettingsState = { general: { accent: 'rgb(53, 116, 252)', defaultFullPlaylist: true, + externalLinks: false, followSystemTheme: false, language: 'en', playButtonBehavior: Play.NOW, From a0b761c9ac2e56c5599218ae30969355aa4c168c Mon Sep 17 00:00:00 2001 From: Tarulia <556162+mihawk90@users.noreply.github.com> Date: Mon, 22 Jan 2024 04:41:23 +0100 Subject: [PATCH 094/408] AppImage: Set proper categories on desktop entry (#430) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6d350d1fc..118059e80 100644 --- a/package.json +++ b/package.json @@ -132,7 +132,7 @@ "tar.xz" ], "icon": "assets/icons/icon.png", - "category": "Development" + "category": "AudioVideo;Audio;Player" }, "directories": { "app": "release/app", From 2095ff6ab993e75a2452a82850ca97af690399e5 Mon Sep 17 00:00:00 2001 From: Leonardo Salgueiro Date: Sun, 21 Jan 2024 19:43:09 -0800 Subject: [PATCH 095/408] Update scrobble-settings.tsx (#440) --- .../features/settings/components/playback/scrobble-settings.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/renderer/features/settings/components/playback/scrobble-settings.tsx b/src/renderer/features/settings/components/playback/scrobble-settings.tsx index dd57e5b52..f8fbcb522 100644 --- a/src/renderer/features/settings/components/playback/scrobble-settings.tsx +++ b/src/renderer/features/settings/components/playback/scrobble-settings.tsx @@ -83,7 +83,7 @@ export const ScrobbleSettings = () => { }} /> ), - description: t('setting.minimumScrobblePercentage', { + description: t('setting.minimumScrobbleSeconds', { context: 'description', postProcess: 'sentenceCase', }), From dcccccea2fac90a93fe599749bbff4767a22d7b1 Mon Sep 17 00:00:00 2001 From: SlyFabi <10423192+SlyFabi@users.noreply.github.com> Date: Mon, 22 Jan 2024 04:46:34 +0100 Subject: [PATCH 096/408] Use startIndex in Jellyfin getPlaylistSongList (#449) --- src/renderer/api/jellyfin/jellyfin-controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/renderer/api/jellyfin/jellyfin-controller.ts b/src/renderer/api/jellyfin/jellyfin-controller.ts index dd53aab6d..d1c9faff4 100644 --- a/src/renderer/api/jellyfin/jellyfin-controller.ts +++ b/src/renderer/api/jellyfin/jellyfin-controller.ts @@ -538,7 +538,7 @@ const getPlaylistSongList = async (args: PlaylistSongListArgs): Promise Date: Mon, 22 Jan 2024 05:06:46 +0100 Subject: [PATCH 097/408] Improve the playback speed control (#437) * Change playback speed selector to slider * Improve playback speed bar Display BPM Improve style Add markers * Improve playback speed slider style --- .../player/components/right-controls.tsx | 51 +++++++++++++++---- 1 file changed, 40 insertions(+), 11 deletions(-) diff --git a/src/renderer/features/player/components/right-controls.tsx b/src/renderer/features/player/components/right-controls.tsx index 93b3327b8..6ae0de9e0 100644 --- a/src/renderer/features/player/components/right-controls.tsx +++ b/src/renderer/features/player/components/right-controls.tsx @@ -28,12 +28,11 @@ import { LibraryItem, QueueSong, ServerType, Song } from '/@/renderer/api/types' import { useCreateFavorite, useDeleteFavorite, useSetRating } from '/@/renderer/features/shared'; import { DropdownMenu, Rating } from '/@/renderer/components'; import { PlayerbarSlider } from '/@/renderer/features/player/components/playerbar-slider'; +import { Slider } from '/@/renderer/components/slider'; const ipc = isElectron() ? window.electron.ipc : null; const remote = isElectron() ? window.electron.remote : null; -const PLAYBACK_SPEEDS = [0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0]; - export const RightControls = () => { const { t } = useTranslation(); const isMinWidth = useMediaQuery('(max-width: 480px)'); @@ -110,6 +109,14 @@ export const RightControls = () => { setSideBar({ rightExpanded: !isQueueExpanded }); }; + const formatPlaybackSpeedSliderLabel = (value: number) => { + const bpm = Number(currentSong?.bpm); + if (bpm > 0) { + return `${value} x / ${(bpm * value).toFixed(1)} BPM`; + } + return `${value} x`; + }; + const isSongDefined = Boolean(currentSong?.id); const showRating = isSongDefined && server?.type === ServerType.NAVIDROME; @@ -210,7 +217,13 @@ export const RightControls = () => { align="center" spacing="xs" > - + {speed} x} @@ -222,14 +235,30 @@ export const RightControls = () => { /> - {PLAYBACK_SPEEDS.map((speed) => ( - handleSpeed(Number(speed))} - > - {speed} - - ))} + handleSpeed(1)} + /> Date: Mon, 22 Jan 2024 04:33:03 +0000 Subject: [PATCH 098/408] Bump axios from 1.5.1 to 1.6.0 Bumps [axios](https://github.com/axios/axios) from 1.5.1 to 1.6.0. - [Release notes](https://github.com/axios/axios/releases) - [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md) - [Commits](https://github.com/axios/axios/compare/v1.5.1...v1.6.0) --- updated-dependencies: - dependency-name: axios dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- package-lock.json | 14 +++++++------- package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index e16f8f94c..debe111fd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,7 +28,7 @@ "@tanstack/react-query-persist-client": "^4.32.1", "@ts-rest/core": "^3.23.0", "@xhayper/discord-rpc": "^1.0.24", - "axios": "^1.4.0", + "axios": "^1.6.0", "clsx": "^2.0.0", "cmdk": "^0.2.0", "dayjs": "^1.11.6", @@ -6325,9 +6325,9 @@ } }, "node_modules/axios": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.5.1.tgz", - "integrity": "sha512-Q28iYCWzNHjAm+yEAot5QaAMxhMghWLFVf7rRdwhUI+c2jix2DUXjAHXVi+s1ibs3mjPO/cCgbA++3BjD0vP/A==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.0.tgz", + "integrity": "sha512-EZ1DYihju9pwVB+jg67ogm+Tmqc6JmhamRN6I4Zt8DfZu5lbcQGw3ozH9lFejSJgs/ibaef3A9PMXPLeefFGJg==", "dependencies": { "follow-redirects": "^1.15.0", "form-data": "^4.0.0", @@ -26001,9 +26001,9 @@ "dev": true }, "axios": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.5.1.tgz", - "integrity": "sha512-Q28iYCWzNHjAm+yEAot5QaAMxhMghWLFVf7rRdwhUI+c2jix2DUXjAHXVi+s1ibs3mjPO/cCgbA++3BjD0vP/A==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.0.tgz", + "integrity": "sha512-EZ1DYihju9pwVB+jg67ogm+Tmqc6JmhamRN6I4Zt8DfZu5lbcQGw3ozH9lFejSJgs/ibaef3A9PMXPLeefFGJg==", "requires": { "follow-redirects": "^1.15.0", "form-data": "^4.0.0", diff --git a/package.json b/package.json index 118059e80..babd556bf 100644 --- a/package.json +++ b/package.json @@ -307,7 +307,7 @@ "@tanstack/react-query-persist-client": "^4.32.1", "@ts-rest/core": "^3.23.0", "@xhayper/discord-rpc": "^1.0.24", - "axios": "^1.4.0", + "axios": "^1.6.0", "clsx": "^2.0.0", "cmdk": "^0.2.0", "dayjs": "^1.11.6", From 85e889a41464703dc0694e0fbd7bdc7f59a1b9f6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 Jan 2024 04:33:07 +0000 Subject: [PATCH 099/408] Bump zod from 3.21.4 to 3.22.3 Bumps [zod](https://github.com/colinhacks/zod) from 3.21.4 to 3.22.3. - [Release notes](https://github.com/colinhacks/zod/releases) - [Changelog](https://github.com/colinhacks/zod/blob/master/CHANGELOG.md) - [Commits](https://github.com/colinhacks/zod/compare/v3.21.4...v3.22.3) --- updated-dependencies: - dependency-name: zod dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- package-lock.json | 14 +++++++------- package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index e16f8f94c..75338e652 100644 --- a/package-lock.json +++ b/package-lock.json @@ -68,7 +68,7 @@ "react-window-infinite-loader": "^1.0.9", "styled-components": "^6.0.8", "swiper": "^9.3.1", - "zod": "^3.21.4", + "zod": "^3.22.3", "zustand": "^4.3.9" }, "devDependencies": { @@ -21387,9 +21387,9 @@ } }, "node_modules/zod": { - "version": "3.21.4", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.21.4.tgz", - "integrity": "sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==", + "version": "3.22.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.3.tgz", + "integrity": "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug==", "funding": { "url": "https://github.com/sponsors/colinhacks" } @@ -37255,9 +37255,9 @@ "dev": true }, "zod": { - "version": "3.21.4", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.21.4.tgz", - "integrity": "sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==" + "version": "3.22.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.3.tgz", + "integrity": "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug==" }, "zustand": { "version": "4.3.9", diff --git a/package.json b/package.json index 118059e80..c46a90c0d 100644 --- a/package.json +++ b/package.json @@ -347,7 +347,7 @@ "react-window-infinite-loader": "^1.0.9", "styled-components": "^6.0.8", "swiper": "^9.3.1", - "zod": "^3.21.4", + "zod": "^3.22.3", "zustand": "^4.3.9" }, "resolutions": { From 432a128b85b41a9e138324aa90843462c7b0a0aa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 Jan 2024 04:33:13 +0000 Subject: [PATCH 100/408] Bump @babel/traverse from 7.22.19 to 7.23.7 Bumps [@babel/traverse](https://github.com/babel/babel/tree/HEAD/packages/babel-traverse) from 7.22.19 to 7.23.7. - [Release notes](https://github.com/babel/babel/releases) - [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md) - [Commits](https://github.com/babel/babel/commits/v7.23.7/packages/babel-traverse) --- updated-dependencies: - dependency-name: "@babel/traverse" dependency-type: indirect ... Signed-off-by: dependabot[bot] --- package-lock.json | 180 +++++++++++++++++++++++----------------------- 1 file changed, 90 insertions(+), 90 deletions(-) diff --git a/package-lock.json b/package-lock.json index e16f8f94c..4f78883a0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -286,11 +286,11 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.22.13", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", - "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", + "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", "dependencies": { - "@babel/highlight": "^7.22.13", + "@babel/highlight": "^7.23.4", "chalk": "^2.4.2" }, "engines": { @@ -407,11 +407,11 @@ } }, "node_modules/@babel/generator": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.15.tgz", - "integrity": "sha512-Zu9oWARBqeVOW0dZOjXc3JObrzuqothQ3y/n1kUtrjCoCPLkXUwMvOo/F/TCfoHMbWIFlWwpZtkZVb9ga4U2pA==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz", + "integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==", "dependencies": { - "@babel/types": "^7.22.15", + "@babel/types": "^7.23.6", "@jridgewell/gen-mapping": "^0.3.2", "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" @@ -548,20 +548,20 @@ } }, "node_modules/@babel/helper-environment-visitor": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz", - "integrity": "sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-function-name": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz", - "integrity": "sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", "dependencies": { - "@babel/template": "^7.22.5", - "@babel/types": "^7.22.5" + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" }, "engines": { "node": ">=6.9.0" @@ -703,17 +703,17 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", - "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", + "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.19", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.19.tgz", - "integrity": "sha512-Tinq7ybnEPFFXhlYOYFiSjespWQk0dq2dRNAiMdRTOYQzEGqnnNyrTxPYHP5r6wGjlF1rFgABdDV0g8EwD6Qbg==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", "engines": { "node": ">=6.9.0" } @@ -753,11 +753,11 @@ } }, "node_modules/@babel/highlight": { - "version": "7.22.13", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.13.tgz", - "integrity": "sha512-C/BaXcnnvBCmHTpz/VGZ8jgtE2aYlW4hxDhseJAWZb7gqGM/qtCK6iZUb0TyKFf7BOUsBH7Q7fkRsDRhg1XklQ==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", + "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", "dependencies": { - "@babel/helper-validator-identifier": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", "chalk": "^2.4.2", "js-tokens": "^4.0.0" }, @@ -830,9 +830,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.22.16", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.16.tgz", - "integrity": "sha512-+gPfKv8UWeKKeJTUxe59+OobVcrYHETCsORl61EmSkmgymguYk/X5bp7GuUIXaFsc6y++v8ZxPsLSSuujqDphA==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.6.tgz", + "integrity": "sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ==", "bin": { "parser": "bin/babel-parser.js" }, @@ -2178,19 +2178,19 @@ } }, "node_modules/@babel/traverse": { - "version": "7.22.19", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.19.tgz", - "integrity": "sha512-ZCcpVPK64krfdScRbpxF6xA5fz7IOsfMwx1tcACvCzt6JY+0aHkBk7eIU8FRDSZRU5Zei6Z4JfgAxN1bqXGECg==", - "dependencies": { - "@babel/code-frame": "^7.22.13", - "@babel/generator": "^7.22.15", - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-function-name": "^7.22.5", + "version": "7.23.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.7.tgz", + "integrity": "sha512-tY3mM8rH9jM0YHFGyfC0/xf+SB5eKUu7HPj7/k3fpi9dAlsMc5YbQvDi0Sh2QTPXqMhyaAtzAr807TIyfQrmyg==", + "dependencies": { + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.6", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", "@babel/helper-hoist-variables": "^7.22.5", "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.22.16", - "@babel/types": "^7.22.19", - "debug": "^4.1.0", + "@babel/parser": "^7.23.6", + "@babel/types": "^7.23.6", + "debug": "^4.3.1", "globals": "^11.1.0" }, "engines": { @@ -2206,12 +2206,12 @@ } }, "node_modules/@babel/types": { - "version": "7.22.19", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.19.tgz", - "integrity": "sha512-P7LAw/LbojPzkgp5oznjE6tQEIWbp4PkkfrZDINTro9zgBRtI324/EYsiSI7lhPbpIQ+DCeR2NNmMWANGGfZsg==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz", + "integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==", "dependencies": { - "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.19", + "@babel/helper-string-parser": "^7.23.4", + "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" }, "engines": { @@ -21510,11 +21510,11 @@ } }, "@babel/code-frame": { - "version": "7.22.13", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", - "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", + "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", "requires": { - "@babel/highlight": "^7.22.13", + "@babel/highlight": "^7.23.4", "chalk": "^2.4.2" }, "dependencies": { @@ -21604,11 +21604,11 @@ } }, "@babel/generator": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.15.tgz", - "integrity": "sha512-Zu9oWARBqeVOW0dZOjXc3JObrzuqothQ3y/n1kUtrjCoCPLkXUwMvOo/F/TCfoHMbWIFlWwpZtkZVb9ga4U2pA==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz", + "integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==", "requires": { - "@babel/types": "^7.22.15", + "@babel/types": "^7.23.6", "@jridgewell/gen-mapping": "^0.3.2", "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" @@ -21715,17 +21715,17 @@ } }, "@babel/helper-environment-visitor": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz", - "integrity": "sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q==" + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==" }, "@babel/helper-function-name": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz", - "integrity": "sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", "requires": { - "@babel/template": "^7.22.5", - "@babel/types": "^7.22.5" + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" } }, "@babel/helper-hoist-variables": { @@ -21822,14 +21822,14 @@ } }, "@babel/helper-string-parser": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", - "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==" + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", + "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==" }, "@babel/helper-validator-identifier": { - "version": "7.22.19", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.19.tgz", - "integrity": "sha512-Tinq7ybnEPFFXhlYOYFiSjespWQk0dq2dRNAiMdRTOYQzEGqnnNyrTxPYHP5r6wGjlF1rFgABdDV0g8EwD6Qbg==" + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==" }, "@babel/helper-validator-option": { "version": "7.22.15", @@ -21857,11 +21857,11 @@ } }, "@babel/highlight": { - "version": "7.22.13", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.13.tgz", - "integrity": "sha512-C/BaXcnnvBCmHTpz/VGZ8jgtE2aYlW4hxDhseJAWZb7gqGM/qtCK6iZUb0TyKFf7BOUsBH7Q7fkRsDRhg1XklQ==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", + "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", "requires": { - "@babel/helper-validator-identifier": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", "chalk": "^2.4.2", "js-tokens": "^4.0.0" }, @@ -21918,9 +21918,9 @@ } }, "@babel/parser": { - "version": "7.22.16", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.16.tgz", - "integrity": "sha512-+gPfKv8UWeKKeJTUxe59+OobVcrYHETCsORl61EmSkmgymguYk/X5bp7GuUIXaFsc6y++v8ZxPsLSSuujqDphA==" + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.6.tgz", + "integrity": "sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ==" }, "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { "version": "7.22.15", @@ -22789,19 +22789,19 @@ } }, "@babel/traverse": { - "version": "7.22.19", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.19.tgz", - "integrity": "sha512-ZCcpVPK64krfdScRbpxF6xA5fz7IOsfMwx1tcACvCzt6JY+0aHkBk7eIU8FRDSZRU5Zei6Z4JfgAxN1bqXGECg==", - "requires": { - "@babel/code-frame": "^7.22.13", - "@babel/generator": "^7.22.15", - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-function-name": "^7.22.5", + "version": "7.23.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.7.tgz", + "integrity": "sha512-tY3mM8rH9jM0YHFGyfC0/xf+SB5eKUu7HPj7/k3fpi9dAlsMc5YbQvDi0Sh2QTPXqMhyaAtzAr807TIyfQrmyg==", + "requires": { + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.6", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", "@babel/helper-hoist-variables": "^7.22.5", "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.22.16", - "@babel/types": "^7.22.19", - "debug": "^4.1.0", + "@babel/parser": "^7.23.6", + "@babel/types": "^7.23.6", + "debug": "^4.3.1", "globals": "^11.1.0" }, "dependencies": { @@ -22813,12 +22813,12 @@ } }, "@babel/types": { - "version": "7.22.19", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.19.tgz", - "integrity": "sha512-P7LAw/LbojPzkgp5oznjE6tQEIWbp4PkkfrZDINTro9zgBRtI324/EYsiSI7lhPbpIQ+DCeR2NNmMWANGGfZsg==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz", + "integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==", "requires": { - "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.19", + "@babel/helper-string-parser": "^7.23.4", + "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" } }, From 8fcfbce0d5c22f44f886e7880c4eadafdbd78803 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 Jan 2024 04:33:15 +0000 Subject: [PATCH 101/408] Bump postcss from 8.4.27 to 8.4.33 Bumps [postcss](https://github.com/postcss/postcss) from 8.4.27 to 8.4.33. - [Release notes](https://github.com/postcss/postcss/releases) - [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md) - [Commits](https://github.com/postcss/postcss/compare/8.4.27...8.4.33) --- updated-dependencies: - dependency-name: postcss dependency-type: indirect ... Signed-off-by: dependabot[bot] --- package-lock.json | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/package-lock.json b/package-lock.json index e16f8f94c..8760aca37 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15083,9 +15083,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", - "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", "funding": [ { "type": "github", @@ -15970,9 +15970,9 @@ } }, "node_modules/postcss": { - "version": "8.4.27", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.27.tgz", - "integrity": "sha512-gY/ACJtJPSmUFPDCHtX78+01fHa64FaU4zaaWfuh1MhGJISufJAH4cun6k/8fwsHYeK4UQmENQK+tRLCFJE8JQ==", + "version": "8.4.33", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.33.tgz", + "integrity": "sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg==", "funding": [ { "type": "opencollective", @@ -15988,7 +15988,7 @@ } ], "dependencies": { - "nanoid": "^3.3.6", + "nanoid": "^3.3.7", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" }, @@ -32603,9 +32603,9 @@ } }, "nanoid": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", - "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==" + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==" }, "natural-compare": { "version": "1.4.0", @@ -33273,11 +33273,11 @@ } }, "postcss": { - "version": "8.4.27", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.27.tgz", - "integrity": "sha512-gY/ACJtJPSmUFPDCHtX78+01fHa64FaU4zaaWfuh1MhGJISufJAH4cun6k/8fwsHYeK4UQmENQK+tRLCFJE8JQ==", + "version": "8.4.33", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.33.tgz", + "integrity": "sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg==", "requires": { - "nanoid": "^3.3.6", + "nanoid": "^3.3.7", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } From 7c0320d69a18a616996c30503d8333e1e0138af5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 Jan 2024 04:33:28 +0000 Subject: [PATCH 102/408] Bump electron from 25.3.0 to 25.8.4 in /release/app Bumps [electron](https://github.com/electron/electron) from 25.3.0 to 25.8.4. - [Release notes](https://github.com/electron/electron/releases) - [Changelog](https://github.com/electron/electron/blob/main/docs/breaking-changes.md) - [Commits](https://github.com/electron/electron/compare/v25.3.0...v25.8.4) --- updated-dependencies: - dependency-name: electron dependency-type: direct:development ... Signed-off-by: dependabot[bot] --- release/app/package-lock.json | 14 +++++++------- release/app/package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/release/app/package-lock.json b/release/app/package-lock.json index c62fb8848..25809ed40 100644 --- a/release/app/package-lock.json +++ b/release/app/package-lock.json @@ -15,7 +15,7 @@ "ws": "^8.13.0" }, "devDependencies": { - "electron": "25.3.0" + "electron": "25.8.4" } }, "node_modules/@electron/get": { @@ -453,9 +453,9 @@ "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==" }, "node_modules/electron": { - "version": "25.3.0", - "resolved": "https://registry.npmjs.org/electron/-/electron-25.3.0.tgz", - "integrity": "sha512-cyqotxN+AroP5h2IxUsJsmehYwP5LrFAOO7O7k9tILME3Sa1/POAg3shrhx4XEnaAMyMqMLxzGvkzCVxzEErnA==", + "version": "25.8.4", + "resolved": "https://registry.npmjs.org/electron/-/electron-25.8.4.tgz", + "integrity": "sha512-hUYS3RGdaa6E1UWnzeGnsdsBYOggwMMg4WGxNGvAoWtmRrr6J1BsjFW/yRq4WsJHJce2HdzQXtz4OGXV6yUCLg==", "dev": true, "hasInstallScript": true, "dependencies": { @@ -1672,9 +1672,9 @@ "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==" }, "electron": { - "version": "25.3.0", - "resolved": "https://registry.npmjs.org/electron/-/electron-25.3.0.tgz", - "integrity": "sha512-cyqotxN+AroP5h2IxUsJsmehYwP5LrFAOO7O7k9tILME3Sa1/POAg3shrhx4XEnaAMyMqMLxzGvkzCVxzEErnA==", + "version": "25.8.4", + "resolved": "https://registry.npmjs.org/electron/-/electron-25.8.4.tgz", + "integrity": "sha512-hUYS3RGdaa6E1UWnzeGnsdsBYOggwMMg4WGxNGvAoWtmRrr6J1BsjFW/yRq4WsJHJce2HdzQXtz4OGXV6yUCLg==", "dev": true, "requires": { "@electron/get": "^2.0.0", diff --git a/release/app/package.json b/release/app/package.json index 107699b3d..14c28c461 100644 --- a/release/app/package.json +++ b/release/app/package.json @@ -18,7 +18,7 @@ "ws": "^8.13.0" }, "devDependencies": { - "electron": "25.3.0" + "electron": "25.8.4" }, "license": "GPL-3.0" } From f169fc7f3b7d22ab8a509a8dce4e0c9acbe5e926 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 Jan 2024 04:41:16 +0000 Subject: [PATCH 103/408] Bump follow-redirects from 1.15.2 to 1.15.5 Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.2 to 1.15.5. - [Release notes](https://github.com/follow-redirects/follow-redirects/releases) - [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.2...v1.15.5) --- updated-dependencies: - dependency-name: follow-redirects dependency-type: indirect ... Signed-off-by: dependabot[bot] --- package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index debe111fd..0e4da747b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11056,9 +11056,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", - "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", + "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", "funding": [ { "type": "individual", @@ -29589,9 +29589,9 @@ } }, "follow-redirects": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", - "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==" + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", + "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==" }, "form-data": { "version": "4.0.0", From 33972c2a831336e0bd354b711240fedb5c95ca75 Mon Sep 17 00:00:00 2001 From: Kendall Garner <17521368+kgarner7@users.noreply.github.com> Date: Sun, 21 Jan 2024 22:47:59 -0800 Subject: [PATCH 104/408] titlebar switching --- src/main/features/core/settings/index.ts | 7 ++++++- src/main/main.ts | 6 ++++++ src/main/preload/local-settings.ts | 5 +++++ .../components/general/theme-settings.tsx | 21 ++++++++++++++++++- 4 files changed, 37 insertions(+), 2 deletions(-) diff --git a/src/main/features/core/settings/index.ts b/src/main/features/core/settings/index.ts index f45addea5..4d2a40643 100644 --- a/src/main/features/core/settings/index.ts +++ b/src/main/features/core/settings/index.ts @@ -1,4 +1,4 @@ -import { ipcMain, safeStorage } from 'electron'; +import { ipcMain, nativeTheme, safeStorage } from 'electron'; import Store from 'electron-store'; export const store = new Store(); @@ -48,3 +48,8 @@ ipcMain.handle('password-set', (_event, password: string, server: string) => { } return false; }); + +ipcMain.on('theme-set', (_event, theme: 'dark' | 'light' | 'system') => { + store.set('theme', theme); + nativeTheme.themeSource = theme; +}); diff --git a/src/main/main.ts b/src/main/main.ts index 8f92a810e..388351c0d 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -20,6 +20,7 @@ import { Tray, Menu, nativeImage, + nativeTheme, BrowserWindowConstructorOptions, protocol, net, @@ -414,6 +415,11 @@ const createWindow = async () => { // eslint-disable-next-line new AppUpdater(); } + + const theme = store.get('theme') as 'dark' | 'light' | 'system' | undefined; + if (theme) { + nativeTheme.themeSource = theme; + } }; app.commandLine.appendSwitch('disable-features', 'HardwareMediaKeyHandling,MediaSessionService'); diff --git a/src/main/preload/local-settings.ts b/src/main/preload/local-settings.ts index a57b08c4f..ca8937bb8 100644 --- a/src/main/preload/local-settings.ts +++ b/src/main/preload/local-settings.ts @@ -43,6 +43,10 @@ const fontError = (cb: (event: IpcRendererEvent, file: string) => void) => { ipcRenderer.on('custom-font-error', cb); }; +const themeSet = (theme: 'dark' | 'light' | 'system'): void => { + ipcRenderer.send('theme-set', theme); +}; + export const localSettings = { disableMediaKeys, enableMediaKeys, @@ -54,6 +58,7 @@ export const localSettings = { restart, set, setZoomFactor, + themeSet, }; export type LocalSettings = typeof localSettings; diff --git a/src/renderer/features/settings/components/general/theme-settings.tsx b/src/renderer/features/settings/components/general/theme-settings.tsx index 07b3b8b7d..db82832fe 100644 --- a/src/renderer/features/settings/components/general/theme-settings.tsx +++ b/src/renderer/features/settings/components/general/theme-settings.tsx @@ -7,8 +7,11 @@ import { import { THEME_DATA } from '/@/renderer/hooks'; import { useGeneralSettings, useSettingsStoreActions } from '/@/renderer/store/settings.store'; import { AppTheme } from '/@/renderer/themes/types'; +import isElectron from 'is-electron'; import { useTranslation } from 'react-i18next'; +const localSettings = isElectron() ? window.electron.localSettings : null; + export const ThemeSettings = () => { const { t } = useTranslation(); const settings = useGeneralSettings(); @@ -26,6 +29,15 @@ export const ThemeSettings = () => { followSystemTheme: e.currentTarget.checked, }, }); + if (localSettings) { + localSettings.themeSet( + e.currentTarget.checked + ? 'system' + : settings.theme === AppTheme.DEFAULT_DARK + ? 'dark' + : 'light', + ); + } }} /> ), @@ -42,12 +54,19 @@ export const ThemeSettings = () => { data={THEME_DATA} defaultValue={settings.theme} onChange={(e) => { + const theme = e as AppTheme; setSettings({ general: { ...settings, - theme: e as AppTheme, + theme, }, }); + if (localSettings) { + console.log(theme); + localSettings.themeSet( + theme === AppTheme.DEFAULT_DARK ? 'dark' : 'light', + ); + } }} /> ), From b3a9e7ccba5c48db65a42ea1d62ba0f60742b0e2 Mon Sep 17 00:00:00 2001 From: Kendall Garner <17521368+kgarner7@users.noreply.github.com> Date: Sun, 21 Jan 2024 22:58:10 -0800 Subject: [PATCH 105/408] default theme dark --- src/main/main.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/main.ts b/src/main/main.ts index 388351c0d..501168054 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -417,9 +417,7 @@ const createWindow = async () => { } const theme = store.get('theme') as 'dark' | 'light' | 'system' | undefined; - if (theme) { - nativeTheme.themeSource = theme; - } + nativeTheme.themeSource = theme || 'dark'; }; app.commandLine.appendSwitch('disable-features', 'HardwareMediaKeyHandling,MediaSessionService'); From c8701d1da4267e167b732b36399f15fd0e4d8c8b Mon Sep 17 00:00:00 2001 From: David Rappo Date: Mon, 22 Jan 2024 10:09:15 -0800 Subject: [PATCH 106/408] Fixed incorrect label for album sort by release date (#458) --- .../features/albums/components/album-list-header-filters.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/renderer/features/albums/components/album-list-header-filters.tsx b/src/renderer/features/albums/components/album-list-header-filters.tsx index 268458b74..ffe54b422 100644 --- a/src/renderer/features/albums/components/album-list-header-filters.tsx +++ b/src/renderer/features/albums/components/album-list-header-filters.tsx @@ -73,7 +73,7 @@ const FILTERS = { }, { defaultOrder: SortOrder.DESC, - name: i18n.t('filter.recentlyAdded', { postProcess: 'titleCase' }), + name: i18n.t('filter.releaseDate', { postProcess: 'titleCase' }), value: AlbumListSort.RELEASE_DATE, }, ], From 5e9ef9f23f53c3efb92d495be05cb5f7aa969cdd Mon Sep 17 00:00:00 2001 From: Kendall Garner <17521368+kgarner7@users.noreply.github.com> Date: Mon, 22 Jan 2024 18:52:14 -0800 Subject: [PATCH 107/408] use type, remove console --- src/main/features/core/settings/index.ts | 3 ++- src/main/main.ts | 3 ++- src/main/preload/local-settings.ts | 3 ++- .../features/settings/components/general/theme-settings.tsx | 1 - src/renderer/types.ts | 2 ++ 5 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/main/features/core/settings/index.ts b/src/main/features/core/settings/index.ts index 4d2a40643..f829ea588 100644 --- a/src/main/features/core/settings/index.ts +++ b/src/main/features/core/settings/index.ts @@ -1,5 +1,6 @@ import { ipcMain, nativeTheme, safeStorage } from 'electron'; import Store from 'electron-store'; +import type { TitleTheme } from '/@/renderer/types'; export const store = new Store(); @@ -49,7 +50,7 @@ ipcMain.handle('password-set', (_event, password: string, server: string) => { return false; }); -ipcMain.on('theme-set', (_event, theme: 'dark' | 'light' | 'system') => { +ipcMain.on('theme-set', (_event, theme: TitleTheme) => { store.set('theme', theme); nativeTheme.themeSource = theme; }); diff --git a/src/main/main.ts b/src/main/main.ts index 501168054..6649fa767 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -35,6 +35,7 @@ import { store } from './features/core/settings/index'; import MenuBuilder from './menu'; import { hotkeyToElectronAccelerator, isLinux, isMacOS, isWindows, resolveHtmlPath } from './utils'; import './features'; +import type { TitleTheme } from '/@/renderer/types'; declare module 'node-mpv'; @@ -416,7 +417,7 @@ const createWindow = async () => { new AppUpdater(); } - const theme = store.get('theme') as 'dark' | 'light' | 'system' | undefined; + const theme = store.get('theme') as TitleTheme | undefined; nativeTheme.themeSource = theme || 'dark'; }; diff --git a/src/main/preload/local-settings.ts b/src/main/preload/local-settings.ts index ca8937bb8..7b1b37320 100644 --- a/src/main/preload/local-settings.ts +++ b/src/main/preload/local-settings.ts @@ -1,5 +1,6 @@ import { IpcRendererEvent, ipcRenderer, webFrame } from 'electron'; import Store from 'electron-store'; +import type { TitleTheme } from '/@/renderer/types'; const store = new Store(); @@ -43,7 +44,7 @@ const fontError = (cb: (event: IpcRendererEvent, file: string) => void) => { ipcRenderer.on('custom-font-error', cb); }; -const themeSet = (theme: 'dark' | 'light' | 'system'): void => { +const themeSet = (theme: TitleTheme): void => { ipcRenderer.send('theme-set', theme); }; diff --git a/src/renderer/features/settings/components/general/theme-settings.tsx b/src/renderer/features/settings/components/general/theme-settings.tsx index db82832fe..af9230ca2 100644 --- a/src/renderer/features/settings/components/general/theme-settings.tsx +++ b/src/renderer/features/settings/components/general/theme-settings.tsx @@ -62,7 +62,6 @@ export const ThemeSettings = () => { }, }); if (localSettings) { - console.log(theme); localSettings.themeSet( theme === AppTheme.DEFAULT_DARK ? 'dark' : 'light', ); diff --git a/src/renderer/types.ts b/src/renderer/types.ts index a40018b70..1ede3dea6 100644 --- a/src/renderer/types.ts +++ b/src/renderer/types.ts @@ -209,3 +209,5 @@ export enum FontType { CUSTOM = 'custom', SYSTEM = 'system', } + +export type TitleTheme = 'dark' | 'light' | 'system'; From 5f1d0a3b5ee5427b9b083751f9c990381198ebb7 Mon Sep 17 00:00:00 2001 From: Kendall Garner <17521368+kgarner7@users.noreply.github.com> Date: Thu, 25 Jan 2024 04:36:20 +0000 Subject: [PATCH 108/408] [bugfix]: Validate audio sample range, catch AudioContext error (#470) --- src/i18n/locales/en.json | 2 +- .../components/audio-player/index.tsx | 23 +++++++++++++++---- .../components/playback/mpv-settings.tsx | 9 ++++++-- src/renderer/store/settings.store.ts | 6 +++++ 4 files changed, 32 insertions(+), 8 deletions(-) diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 3997bd640..d0d97cce2 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -510,7 +510,7 @@ "replayGainPreamp": "{{ReplayGain}} preamp (dB)", "replayGainPreamp_description": "adjust the preamp gain applied to the {{ReplayGain}} values", "sampleRate": "sample rate", - "sampleRate_description": "select the output sample rate to be used if the sample frequency selected is different from that of the current media", + "sampleRate_description": "select the output sample rate to be used if the sample frequency selected is different from that of the current media. a value less than 8000 will use the default frequency", "savePlayQueue": "save play queue", "savePlayQueue_description": "save the play queue when the application is closed and restore it when the application is opened", "scrobble": "scrobble", diff --git a/src/renderer/components/audio-player/index.tsx b/src/renderer/components/audio-player/index.tsx index 7f8b19044..6490144c2 100644 --- a/src/renderer/components/audio-player/index.tsx +++ b/src/renderer/components/audio-player/index.tsx @@ -7,10 +7,11 @@ import { crossfadeHandler, gaplessHandler, } from '/@/renderer/components/audio-player/utils/list-handlers'; -import { useSettingsStore } from '/@/renderer/store/settings.store'; +import { useSettingsStore, useSettingsStoreActions } from '/@/renderer/store/settings.store'; import type { CrossfadeStyle } from '/@/renderer/types'; import { PlaybackStyle, PlayerStatus } from '/@/renderer/types'; import { useSpeed } from '/@/renderer/store'; +import { toast } from '/@/renderer/components/toast'; interface AudioPlayerProps extends ReactPlayerProps { crossfadeDuration: number; @@ -60,6 +61,7 @@ export const AudioPlayer = forwardRef( const [isTransitioning, setIsTransitioning] = useState(false); const audioDeviceId = useSettingsStore((state) => state.playback.audioDeviceId); const playback = useSettingsStore((state) => state.playback.mpvProperties); + const { resetSampleRate } = useSettingsStoreActions(); const playbackSpeed = useSpeed(); const [webAudio, setWebAudio] = useState(null); @@ -119,10 +121,21 @@ export const AudioPlayer = forwardRef( useEffect(() => { if ('AudioContext' in window) { - const context = new AudioContext({ - latencyHint: 'playback', - sampleRate: playback.audioSampleRateHz || undefined, - }); + let context: AudioContext; + + try { + context = new AudioContext({ + latencyHint: 'playback', + sampleRate: playback.audioSampleRateHz || undefined, + }); + } catch (error) { + // In practice, this should never be hit because the UI should validate + // the range. However, the actual supported range is not guaranteed + toast.error({ message: (error as Error).message }); + context = new AudioContext({ latencyHint: 'playback' }); + resetSampleRate(); + } + const gain = context.createGain(); gain.connect(context.destination); diff --git a/src/renderer/features/settings/components/playback/mpv-settings.tsx b/src/renderer/features/settings/components/playback/mpv-settings.tsx index a7ab461f5..154436732 100644 --- a/src/renderer/features/settings/components/playback/mpv-settings.tsx +++ b/src/renderer/features/settings/components/playback/mpv-settings.tsx @@ -206,11 +206,16 @@ export const MpvSettings = () => { { control: ( { const value = Number(e.currentTarget.value); - handleSetMpvProperty('audioSampleRateHz', value > 0 ? value : undefined); + // Setting a value of `undefined` causes an error for MPV. Use 0 instead + handleSetMpvProperty('audioSampleRateHz', value >= 8000 ? value : value); }} /> ), diff --git a/src/renderer/store/settings.store.ts b/src/renderer/store/settings.store.ts index 751444757..f933bf2e0 100644 --- a/src/renderer/store/settings.store.ts +++ b/src/renderer/store/settings.store.ts @@ -251,6 +251,7 @@ export interface SettingsState { export interface SettingsSlice extends SettingsState { actions: { reset: () => void; + resetSampleRate: () => void; setSettings: (data: Partial) => void; setSidebarItems: (items: SidebarItemType[]) => void; setTable: (type: TableType, data: DataTableProps) => void; @@ -567,6 +568,11 @@ export const useSettingsStore = create()( set(initialState); } }, + resetSampleRate: () => { + set((state) => { + state.playback.mpvProperties.audioSampleRateHz = 0; + }); + }, setSettings: (data) => { set({ ...get(), ...data }); }, From 26102bd70a7e10e75860eb20c44bfa7f1af43b15 Mon Sep 17 00:00:00 2001 From: Kendall Garner <17521368+kgarner7@users.noreply.github.com> Date: Wed, 24 Jan 2024 21:05:26 -0800 Subject: [PATCH 109/408] [bugfix]: do not change scroll position when focus changes --- .../now-playing/components/play-queue.tsx | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/renderer/features/now-playing/components/play-queue.tsx b/src/renderer/features/now-playing/components/play-queue.tsx index 31189ea4d..98a8516b6 100644 --- a/src/renderer/features/now-playing/components/play-queue.tsx +++ b/src/renderer/features/now-playing/components/play-queue.tsx @@ -60,6 +60,7 @@ export const PlayQueue = forwardRef(({ type }: QueueProps, ref: Ref) => { const { play } = usePlayerControls(); const volume = useVolume(); const isFocused = useAppFocus(); + const isFocusedRef = useRef(isFocused); useEffect(() => { if (tableRef.current) { @@ -211,7 +212,29 @@ export const PlayQueue = forwardRef(({ type }: QueueProps, ref: Ref) => { } } } - }, [currentSong, previousSong, tableConfig.followCurrentSong, status, isFocused]); + }, [currentSong, previousSong, tableConfig.followCurrentSong, status]); + + // As a separate rule, update the current row when focus changes. This is + // to prevent queue scrolling when the application loses and then gains focus. + // The body should only fire when focus changes, even though it depends on current song + useEffect(() => { + if (isFocused !== isFocusedRef.current && tableRef?.current) { + const { api, columnApi } = tableRef.current; + if (api == null || columnApi == null) { + return; + } + + const currentNode = currentSong?.uniqueId + ? api.getRowNode(currentSong.uniqueId) + : undefined; + + if (currentNode) { + api.redrawRows({ rowNodes: [currentNode] }); + } + + isFocusedRef.current = isFocused; + } + }, [currentSong, isFocused]); const onCellContextMenu = useHandleTableContextMenu(LibraryItem.SONG, QUEUE_CONTEXT_MENU_ITEMS); From 362a88b6bcd09a8294fc05a9070259f841916938 Mon Sep 17 00:00:00 2001 From: Kendall Garner <17521368+kgarner7@users.noreply.github.com> Date: Wed, 24 Jan 2024 21:15:11 -0800 Subject: [PATCH 110/408] [bugfix]: fix race condition for opening play queue/playlist details --- src/renderer/features/now-playing/components/play-queue.tsx | 2 +- .../playlists/components/playlist-detail-song-list-content.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/renderer/features/now-playing/components/play-queue.tsx b/src/renderer/features/now-playing/components/play-queue.tsx index 98a8516b6..0b8bc5815 100644 --- a/src/renderer/features/now-playing/components/play-queue.tsx +++ b/src/renderer/features/now-playing/components/play-queue.tsx @@ -173,7 +173,7 @@ export const PlayQueue = forwardRef(({ type }: QueueProps, ref: Ref) => { const handleGridSizeChange = () => { if (tableConfig.autoFit) { - tableRef?.current?.api.sizeColumnsToFit(); + tableRef?.current?.api?.sizeColumnsToFit(); } }; diff --git a/src/renderer/features/playlists/components/playlist-detail-song-list-content.tsx b/src/renderer/features/playlists/components/playlist-detail-song-list-content.tsx index dc4063525..298e47429 100644 --- a/src/renderer/features/playlists/components/playlist-detail-song-list-content.tsx +++ b/src/renderer/features/playlists/components/playlist-detail-song-list-content.tsx @@ -140,7 +140,7 @@ export const PlaylistDetailSongListContent = ({ tableRef }: PlaylistDetailConten const handleGridSizeChange = () => { if (page.table.autoFit) { - tableRef?.current?.api.sizeColumnsToFit(); + tableRef?.current?.api?.sizeColumnsToFit(); } }; From 527e6a76b541bb9d23ab496b64c094cd6cb6aab9 Mon Sep 17 00:00:00 2001 From: Kendall Garner <17521368+kgarner7@users.noreply.github.com> Date: Wed, 24 Jan 2024 21:35:58 -0800 Subject: [PATCH 111/408] [bugfix]: disable play button when queue is empty --- src/renderer/features/player/components/center-controls.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/renderer/features/player/components/center-controls.tsx b/src/renderer/features/player/components/center-controls.tsx index e1e1cb732..6c2b50fda 100644 --- a/src/renderer/features/player/components/center-controls.tsx +++ b/src/renderer/features/player/components/center-controls.tsx @@ -219,6 +219,7 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => { /> )} From 45e589fbb1ab3b25f29f20bf2bb775b52f3f30b4 Mon Sep 17 00:00:00 2001 From: Kendall Garner <17521368+kgarner7@users.noreply.github.com> Date: Wed, 24 Jan 2024 23:39:50 -0800 Subject: [PATCH 112/408] [enhancement]: use ColorInput for text rgb selection --- .../settings/components/general/theme-settings.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/renderer/features/settings/components/general/theme-settings.tsx b/src/renderer/features/settings/components/general/theme-settings.tsx index af9230ca2..d908ee546 100644 --- a/src/renderer/features/settings/components/general/theme-settings.tsx +++ b/src/renderer/features/settings/components/general/theme-settings.tsx @@ -1,5 +1,5 @@ -import { ColorPicker, Stack } from '@mantine/core'; -import { Switch, Select, Text } from '/@/renderer/components'; +import { ColorInput, Stack } from '@mantine/core'; +import { Switch, Select } from '/@/renderer/components'; import { SettingsSection, SettingOption, @@ -123,7 +123,7 @@ export const ThemeSettings = () => { { control: ( - { 'rgb(170, 110, 216)', ]} swatchesPerRow={5} + withEyeDropper={false} onChangeEnd={(e) => { setSettings({ general: { @@ -143,7 +144,6 @@ export const ThemeSettings = () => { }); }} /> - {settings.accent} ), description: t('setting.accentColor', { From bbf59a4942883a25b60b6036c3795494b4bf3bed Mon Sep 17 00:00:00 2001 From: Kendall Garner <17521368+kgarner7@users.noreply.github.com> Date: Thu, 25 Jan 2024 23:58:21 -0800 Subject: [PATCH 113/408] [bugfix]: Fix add to playlist success message The prior code used `form.addToPlaylist`, not `.success`. Also fixes English pluralization and uses the correct `entity.track` as opposed to `entity.song` for other languages (I am not sure if the en syntax could be applied to other languages, so I will just leave pluralization as-is for now). --- src/i18n/locales/cs.json | 2 +- src/i18n/locales/de.json | 2 +- src/i18n/locales/en.json | 2 +- src/i18n/locales/es.json | 2 +- src/i18n/locales/fr.json | 2 +- src/i18n/locales/it.json | 2 +- src/i18n/locales/ja.json | 2 +- src/i18n/locales/pl.json | 2 +- src/i18n/locales/ru.json | 2 +- src/i18n/locales/sr.json | 2 +- src/i18n/locales/sv.json | 2 +- src/i18n/locales/zh-Hans.json | 2 +- .../playlists/components/add-to-playlist-context-modal.tsx | 4 ++-- 13 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/i18n/locales/cs.json b/src/i18n/locales/cs.json index 66ddd11c9..0ee357e6a 100644 --- a/src/i18n/locales/cs.json +++ b/src/i18n/locales/cs.json @@ -559,7 +559,7 @@ "error_savePassword": "při ukládání hesla se vyskytla chyba" }, "addToPlaylist": { - "success": "přidáno {{message}} $t(entity.song_other) do {{numOfPlaylists}} $t(entity.playlist_other)", + "success": "přidáno {{message}} $t(entity.track_other) do {{numOfPlaylists}} $t(entity.playlist_other)", "title": "přidat do $t(entity.playlist_one)", "input_skipDuplicates": "přeskočit duplicity", "input_playlists": "$t(entity.playlist_other)" diff --git a/src/i18n/locales/de.json b/src/i18n/locales/de.json index a6b757599..dcbb8667d 100644 --- a/src/i18n/locales/de.json +++ b/src/i18n/locales/de.json @@ -191,7 +191,7 @@ "error_savePassword": "Beim Versuch, das Passwort zu speichern, ist ein Fehler aufgetreten" }, "addToPlaylist": { - "success": "{{message}} $t(entity.song_other) zu {{numOfPlaylists}} $t(entity.playlist_other) hinzugefügt", + "success": "{{message}} $t(entity.track_other) zu {{numOfPlaylists}} $t(entity.playlist_other) hinzugefügt", "title": "Zu $t(entity.playlist_one) hinzufügen", "input_skipDuplicates": "Duplikate überspringen", "input_playlists": "$t(entity.playlist_other)" diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index d0d97cce2..d47480bb4 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -216,7 +216,7 @@ "addToPlaylist": { "input_playlists": "$t(entity.playlist_other)", "input_skipDuplicates": "skip duplicates", - "success": "added {{message}} $t(entity.song_other) to {{numOfPlaylists}} $t(entity.playlist_other)", + "success": "added $t(entity.trackWithCount, {\"count\": {{message}} }) to $t(entity.playlistWithCount, {\"count\": {{numOfPlaylists}} })", "title": "add to $t(entity.playlist_one)" }, "createPlaylist": { diff --git a/src/i18n/locales/es.json b/src/i18n/locales/es.json index d5c4c26fb..e7a4b6685 100644 --- a/src/i18n/locales/es.json +++ b/src/i18n/locales/es.json @@ -491,7 +491,7 @@ "error_savePassword": "un error ocurrió cuando se intentó guardar la contraseña" }, "addToPlaylist": { - "success": "añadido {{message}} $t(entity.song_other) a {{numOfPlaylists}} $t(entity.playlist_other)", + "success": "añadido {{message}} $t(entity.track_other) a {{numOfPlaylists}} $t(entity.playlist_other)", "title": "añadir a $t(entity.playlist_one)", "input_skipDuplicates": "saltar duplicados", "input_playlists": "$t(entity.playlist_other)" diff --git a/src/i18n/locales/fr.json b/src/i18n/locales/fr.json index cad5f5327..eb31ba844 100644 --- a/src/i18n/locales/fr.json +++ b/src/i18n/locales/fr.json @@ -483,7 +483,7 @@ "error_savePassword": "une erreur s’est produite lors de la tentative de sauvegarde du mot de passe" }, "addToPlaylist": { - "success": "{{message}} $t(entity.song_other) ajouté à {{numOfPlaylists}} $t(entity.playlist_other)", + "success": "{{message}} $t(entity.track_other) ajouté à {{numOfPlaylists}} $t(entity.playlist_other)", "title": "ajouter à $t(entity.playlist_one)", "input_skipDuplicates": "sauter les doublons", "input_playlists": "$t(entity.playlist_other)" diff --git a/src/i18n/locales/it.json b/src/i18n/locales/it.json index 74f3076b0..d830ab1ec 100644 --- a/src/i18n/locales/it.json +++ b/src/i18n/locales/it.json @@ -487,7 +487,7 @@ "error_savePassword": "si è verificato un errore quando si è provato a salvare la password" }, "addToPlaylist": { - "success": "aggiunto {{message}} $t(entity.song_other) a {{numOfPlaylists}} $t(entity.playlist_other)", + "success": "aggiunto {{message}} $t(entity.track_other) a {{numOfPlaylists}} $t(entity.playlist_other)", "title": "aggiungi a $t(entity.playlist_one)", "input_skipDuplicates": "salta duplicati", "input_playlists": "$t(entity.playlist_other)" diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index e2b382e31..08e36d917 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -553,7 +553,7 @@ "error_savePassword": "パスワードを保存する際にエラーが発生しました" }, "addToPlaylist": { - "success": "{{message}} $t(entity.song_other) を {{numOfPlaylists}} $t(entity.playlist_other) に追加しました", + "success": "{{message}} $t(entity.track_other) を {{numOfPlaylists}} $t(entity.playlist_other) に追加しました", "title": "$t(entity.playlist_one) に追加", "input_skipDuplicates": "重複をスキップ", "input_playlists": "$t(entity.playlist_other)" diff --git a/src/i18n/locales/pl.json b/src/i18n/locales/pl.json index 4e1f468a1..747ff0163 100644 --- a/src/i18n/locales/pl.json +++ b/src/i18n/locales/pl.json @@ -242,7 +242,7 @@ "error_savePassword": "wystąpił błąd podczas próby zapisania hasła" }, "addToPlaylist": { - "success": "dodano {{message}} $t(entity.song_other) do {{numOfPlaylists}} $t(entity.playlist_other)", + "success": "dodano {{message}} $t(entity.track_other) do {{numOfPlaylists}} $t(entity.playlist_other)", "title": "dodano do $t(entity.playlist_one)", "input_skipDuplicates": "pomiń duplikaty", "input_playlists": "$t(entity.playlist_other)" diff --git a/src/i18n/locales/ru.json b/src/i18n/locales/ru.json index 30e6be664..5c008ee72 100644 --- a/src/i18n/locales/ru.json +++ b/src/i18n/locales/ru.json @@ -434,7 +434,7 @@ "error_savePassword": "произошла ошибка во время попытки сохранения пароля" }, "addToPlaylist": { - "success": "добавлено(а) {{message}} $t(entity.song_other) в {{numOfPlaylists}} $t(entity.playlist_other)", + "success": "добавлено(а) {{message}} $t(entity.track_other) в {{numOfPlaylists}} $t(entity.playlist_other)", "title": "добавить в $t(entity.playlist_one)", "input_skipDuplicates": "пропустить дубликаты", "input_playlists": "$t(entity.playlist_other)" diff --git a/src/i18n/locales/sr.json b/src/i18n/locales/sr.json index c512564d6..7706514da 100644 --- a/src/i18n/locales/sr.json +++ b/src/i18n/locales/sr.json @@ -559,7 +559,7 @@ "error_savePassword": "došlo je do greške prilikom pokušaja čuvanja lozinke" }, "addToPlaylist": { - "success": "dodato {{message}} $t(entity.song_other) u {{numOfPlaylists}} $t(entity.playlist_other)", + "success": "dodato {{message}} $t(entity.track_other) u {{numOfPlaylists}} $t(entity.playlist_other)", "title": "dodaj u $t(entity.playlist_one)", "input_skipDuplicates": "preskoči duplikate", "input_playlists": "$t(entity.playlist_other)" diff --git a/src/i18n/locales/sv.json b/src/i18n/locales/sv.json index 4776f361b..587dc3a10 100644 --- a/src/i18n/locales/sv.json +++ b/src/i18n/locales/sv.json @@ -190,7 +190,7 @@ "error_savePassword": "ett fel uppstod när lösenordet skulle sparas" }, "addToPlaylist": { - "success": "tillade {{message}} $t(entity.song_other) til {{numOfPlaylists}} $t(entity.playlist_other)", + "success": "tillade {{message}} $t(entity.track_other) til {{numOfPlaylists}} $t(entity.playlist_other)", "title": "lägg till i $t(entity.playlist_one)", "input_skipDuplicates": "hoppa över dubbletter", "input_playlists": "$t(entity.playlist_other)" diff --git a/src/i18n/locales/zh-Hans.json b/src/i18n/locales/zh-Hans.json index 368bf66e2..896fea67e 100644 --- a/src/i18n/locales/zh-Hans.json +++ b/src/i18n/locales/zh-Hans.json @@ -495,7 +495,7 @@ "input_url": "url" }, "addToPlaylist": { - "success": "添加 {{message}} $t(entity.song_other) 到 {{numOfPlaylists}} $t(entity.playlist_other)", + "success": "添加 {{message}} $t(entity.track_other) 到 {{numOfPlaylists}} $t(entity.playlist_other)", "title": "添加到$t(entity.playlist_one)", "input_skipDuplicates": "跳过重复", "input_playlists": "$t(entity.playlist_other)" diff --git a/src/renderer/features/playlists/components/add-to-playlist-context-modal.tsx b/src/renderer/features/playlists/components/add-to-playlist-context-modal.tsx index 08a623a22..f0844479b 100644 --- a/src/renderer/features/playlists/components/add-to-playlist-context-modal.tsx +++ b/src/renderer/features/playlists/components/add-to-playlist-context-modal.tsx @@ -191,12 +191,12 @@ export const AddToPlaylistContextModal = ({ const addMessage = values.skipDuplicates && allSongIds.length * values.playlistId.length !== totalUniquesAdded - ? `${Math.floor(totalUniquesAdded / values.playlistId.length)}` + ? Math.floor(totalUniquesAdded / values.playlistId.length) : allSongIds.length; setIsLoading(false); toast.success({ - message: t('form.addToPlaylist', { + message: t('form.addToPlaylist.success', { message: addMessage, numOfPlaylists: values.playlistId.length, postProcess: 'sentenceCase', From bc7f4a57225b543f0052c36fd5dfd86762b323b2 Mon Sep 17 00:00:00 2001 From: Kendall Garner <17521368+kgarner7@users.noreply.github.com> Date: Sat, 27 Jan 2024 01:10:34 +0000 Subject: [PATCH 114/408] [bugfix]: Fix safari audio context (#471) * test 3 * comments * [bugfix]: SAFARI WHY. Use GainNode volume instead for volume control * force vercel refresh? * Revert "force vercel refresh?" This reverts commit af31f38e03a6682ded13a9c944b6f37d08eb9d17. * move volume scaling calculation to setGain --- .../components/audio-player/index.tsx | 64 +++++++++++-------- 1 file changed, 38 insertions(+), 26 deletions(-) diff --git a/src/renderer/components/audio-player/index.tsx b/src/renderer/components/audio-player/index.tsx index 6490144c2..4c924c107 100644 --- a/src/renderer/components/audio-player/index.tsx +++ b/src/renderer/components/audio-player/index.tsx @@ -40,6 +40,14 @@ type WebAudio = { gain: GainNode; }; +// Credits: http://stackoverflow.com/questions/12150729/ddg +// This is used so that the player will always have an