Showing preview only (6,681K chars total). Download the full file or copy to clipboard to get everything.
Repository: jeffvli/feishin
Branch: development
Commit: 62ba721f268b
Files: 1083
Total size: 6.2 MB
Directory structure:
gitextract_6koal0cm/
├── .dockerignore
├── .editorconfig
├── .gitattributes
├── .github/
│ ├── FUNDING.yml
│ ├── ISSUE_TEMPLATE/
│ │ ├── 01-feature_request.yml
│ │ ├── 02-bug_report.yml
│ │ └── config.yml
│ ├── config.yml
│ ├── stale.yml
│ └── workflows/
│ ├── publish-alpha.yml
│ ├── publish-beta.yml
│ ├── publish-docker-auto.yml
│ ├── publish-docker.yml
│ ├── publish-linux.yml
│ ├── publish-macos.yml
│ ├── publish-pr-comment.yml
│ ├── publish-pr.yml
│ ├── publish-windows.yml
│ ├── publish-winget.yml
│ ├── publish.yml
│ ├── stale.yml
│ └── test.yml
├── .gitignore
├── .npmrc
├── .prettierignore
├── .prettierrc.yaml
├── .stylelintrc.json
├── .vscode/
│ ├── extensions.json
│ ├── launch.json
│ └── settings.json
├── CHANGELOG.md
├── Dockerfile
├── LICENSE
├── README.md
├── assets/
│ ├── assets.d.ts
│ ├── entitlements.mac.plist
│ └── icons/
│ └── icon.icns
├── dev-app-update.yml
├── docker-compose.yaml
├── docs/
│ └── ENV_SETTINGS.md
├── electron-builder-alpha.yml
├── electron-builder-beta.yml
├── electron-builder.yml
├── electron.vite.config.ts
├── eslint.config.mjs
├── feishin.desktop.tmpl
├── install-feishin-appimage
├── ng.conf.template
├── org.jeffvli.feishin.metainfo.xml
├── package.json
├── postcss.config.cjs
├── remote.vite.config.ts
├── scripts/
│ ├── after-all-artifact-build.mjs
│ └── update-app-stream.mjs
├── settings.js.template
├── src/
│ ├── i18n/
│ │ ├── i18n.ts
│ │ ├── i18next-parser.config.js
│ │ └── locales/
│ │ ├── ar.json
│ │ ├── ca.json
│ │ ├── cs.json
│ │ ├── da.json
│ │ ├── de.json
│ │ ├── en.json
│ │ ├── es.json
│ │ ├── eu.json
│ │ ├── fa.json
│ │ ├── fi.json
│ │ ├── fr.json
│ │ ├── hu.json
│ │ ├── id.json
│ │ ├── it.json
│ │ ├── ja.json
│ │ ├── ko.json
│ │ ├── nb-NO.json
│ │ ├── nl.json
│ │ ├── pl.json
│ │ ├── pt-BR.json
│ │ ├── pt.json
│ │ ├── ro.json
│ │ ├── ru.json
│ │ ├── sk.json
│ │ ├── sl.json
│ │ ├── sr.json
│ │ ├── sv.json
│ │ ├── ta.json
│ │ ├── tr.json
│ │ ├── uk.json
│ │ ├── zh-Hans.json
│ │ └── zh-Hant.json
│ ├── main/
│ │ ├── features/
│ │ │ ├── core/
│ │ │ │ ├── autodiscover/
│ │ │ │ │ └── index.ts
│ │ │ │ ├── discord-rpc/
│ │ │ │ │ └── index.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── lyrics/
│ │ │ │ │ ├── genius.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── lrclib.ts
│ │ │ │ │ ├── netease.ts
│ │ │ │ │ ├── shared.ts
│ │ │ │ │ └── simpmusic.ts
│ │ │ │ ├── player/
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── media-keys.ts
│ │ │ │ ├── remote/
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── manifest.json
│ │ │ │ └── settings/
│ │ │ │ └── index.ts
│ │ │ ├── darwin/
│ │ │ │ ├── dock-menu.ts
│ │ │ │ └── index.ts
│ │ │ ├── index.ts
│ │ │ ├── linux/
│ │ │ │ ├── index.ts
│ │ │ │ └── mpris.ts
│ │ │ └── win32/
│ │ │ └── index.ts
│ │ ├── index.ts
│ │ ├── menu.ts
│ │ └── utils.ts
│ ├── preload/
│ │ ├── autodiscover.ts
│ │ ├── browser.ts
│ │ ├── discord-rpc.ts
│ │ ├── index.d.ts
│ │ ├── index.ts
│ │ ├── ipc.ts
│ │ ├── local-settings.ts
│ │ ├── lyrics.ts
│ │ ├── mpris.ts
│ │ ├── mpv-player.ts
│ │ ├── remote.ts
│ │ └── utils.ts
│ ├── remote/
│ │ ├── app.tsx
│ │ ├── components/
│ │ │ ├── buttons/
│ │ │ │ ├── image-button.tsx
│ │ │ │ ├── reconnect-button.tsx
│ │ │ │ └── theme-button.tsx
│ │ │ ├── player-image.module.css
│ │ │ ├── player-image.tsx
│ │ │ ├── remote-container.module.css
│ │ │ ├── remote-container.tsx
│ │ │ ├── shell.tsx
│ │ │ └── wrapped-slider.tsx
│ │ ├── index.html
│ │ ├── index.tsx
│ │ ├── manifest.json
│ │ ├── service-worker.ts
│ │ ├── store/
│ │ │ └── index.ts
│ │ └── worker.js
│ ├── renderer/
│ │ ├── api/
│ │ │ ├── controller.ts
│ │ │ ├── index.ts
│ │ │ ├── jellyfin/
│ │ │ │ ├── jellyfin-api.ts
│ │ │ │ └── jellyfin-controller.ts
│ │ │ ├── navidrome/
│ │ │ │ ├── navidrome-api.ts
│ │ │ │ └── navidrome-controller.ts
│ │ │ ├── query-keys.ts
│ │ │ ├── subsonic/
│ │ │ │ ├── subsonic-api.ts
│ │ │ │ └── subsonic-controller.ts
│ │ │ ├── utils-list-count.ts
│ │ │ ├── utils-music-folder.ts
│ │ │ └── utils.ts
│ │ ├── app.tsx
│ │ ├── assets/
│ │ │ ├── assets.d.ts
│ │ │ ├── entitlements.mac.plist
│ │ │ └── icons/
│ │ │ └── icon.icns
│ │ ├── components/
│ │ │ ├── drag-preview/
│ │ │ │ ├── drag-preview.module.css
│ │ │ │ └── drag-preview.tsx
│ │ │ ├── export-import-settings-modal/
│ │ │ │ └── export-import-settings-modal.tsx
│ │ │ ├── feature-carousel/
│ │ │ │ ├── feature-carousel.module.css
│ │ │ │ ├── feature-carousel.tsx
│ │ │ │ └── single-feature-carousel.tsx
│ │ │ ├── grid-carousel/
│ │ │ │ ├── grid-carousel-v2.tsx
│ │ │ │ └── grid-carousel.module.css
│ │ │ ├── item-card/
│ │ │ │ ├── item-card-controls.module.css
│ │ │ │ ├── item-card-controls.tsx
│ │ │ │ ├── item-card.module.css
│ │ │ │ └── item-card.tsx
│ │ │ ├── item-image/
│ │ │ │ └── item-image.tsx
│ │ │ ├── item-list/
│ │ │ │ ├── expanded-list-container.module.css
│ │ │ │ ├── expanded-list-container.tsx
│ │ │ │ ├── expanded-list-item.module.css
│ │ │ │ ├── expanded-list-item.tsx
│ │ │ │ ├── helpers/
│ │ │ │ │ ├── extract-row-id.ts
│ │ │ │ │ ├── get-dragged-items.ts
│ │ │ │ │ ├── get-title-path.ts
│ │ │ │ │ ├── item-list-controls.ts
│ │ │ │ │ ├── item-list-infinite-loader.ts
│ │ │ │ │ ├── item-list-paginated-loader.ts
│ │ │ │ │ ├── item-list-reducer-utils.ts
│ │ │ │ │ ├── item-list-state.ts
│ │ │ │ │ ├── parse-table-columns.ts
│ │ │ │ │ ├── use-grid-rows.ts
│ │ │ │ │ ├── use-is-fetching-item-list.ts
│ │ │ │ │ ├── use-item-list-column-reorder.ts
│ │ │ │ │ ├── use-item-list-column-resize.ts
│ │ │ │ │ ├── use-item-list-scroll-persist.ts
│ │ │ │ │ └── use-list-hotkeys.ts
│ │ │ │ ├── item-detail-list/
│ │ │ │ │ ├── columns/
│ │ │ │ │ │ ├── actions-column.tsx
│ │ │ │ │ │ ├── album-artist-column.tsx
│ │ │ │ │ │ ├── album-column.tsx
│ │ │ │ │ │ ├── artist-column.tsx
│ │ │ │ │ │ ├── bit-depth-column.tsx
│ │ │ │ │ │ ├── bit-rate-column.tsx
│ │ │ │ │ │ ├── bpm-column.tsx
│ │ │ │ │ │ ├── channels-column.tsx
│ │ │ │ │ │ ├── codec-column.tsx
│ │ │ │ │ │ ├── comment-column.tsx
│ │ │ │ │ │ ├── composer-column.tsx
│ │ │ │ │ │ ├── date-added-column.tsx
│ │ │ │ │ │ ├── default-column.tsx
│ │ │ │ │ │ ├── disc-number-column.tsx
│ │ │ │ │ │ ├── duration-column.tsx
│ │ │ │ │ │ ├── favorite-column.tsx
│ │ │ │ │ │ ├── genre-badge-column.module.css
│ │ │ │ │ │ ├── genre-badge-column.tsx
│ │ │ │ │ │ ├── genre-column.tsx
│ │ │ │ │ │ ├── image-column.module.css
│ │ │ │ │ │ ├── image-column.tsx
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ ├── last-played-column.tsx
│ │ │ │ │ │ ├── path-column.tsx
│ │ │ │ │ │ ├── play-count-column.tsx
│ │ │ │ │ │ ├── rating-column.tsx
│ │ │ │ │ │ ├── release-date-column.tsx
│ │ │ │ │ │ ├── row-index-column.module.css
│ │ │ │ │ │ ├── row-index-column.tsx
│ │ │ │ │ │ ├── sample-rate-column.tsx
│ │ │ │ │ │ ├── size-column.tsx
│ │ │ │ │ │ ├── title-artist-column.tsx
│ │ │ │ │ │ ├── title-column.module.css
│ │ │ │ │ │ ├── title-column.tsx
│ │ │ │ │ │ ├── title-combined-column.tsx
│ │ │ │ │ │ ├── track-number-column.tsx
│ │ │ │ │ │ ├── types.ts
│ │ │ │ │ │ └── year-column.tsx
│ │ │ │ │ ├── item-detail-list.module.css
│ │ │ │ │ ├── item-detail-list.tsx
│ │ │ │ │ └── utils.ts
│ │ │ │ ├── item-grid-list/
│ │ │ │ │ ├── item-grid-list.module.css
│ │ │ │ │ └── item-grid-list.tsx
│ │ │ │ ├── item-list-pagination/
│ │ │ │ │ ├── item-list-pagination.module.css
│ │ │ │ │ ├── item-list-pagination.tsx
│ │ │ │ │ └── use-item-list-pagination.ts
│ │ │ │ ├── item-table-list/
│ │ │ │ │ ├── album-group-header.module.css
│ │ │ │ │ ├── album-group-header.tsx
│ │ │ │ │ ├── cell-component-factory.tsx
│ │ │ │ │ ├── columns/
│ │ │ │ │ │ ├── actions-column.tsx
│ │ │ │ │ │ ├── album-artists-column.module.css
│ │ │ │ │ │ ├── album-artists-column.tsx
│ │ │ │ │ │ ├── album-column.module.css
│ │ │ │ │ │ ├── album-column.tsx
│ │ │ │ │ │ ├── album-group-column.tsx
│ │ │ │ │ │ ├── artists-column.module.css
│ │ │ │ │ │ ├── artists-column.tsx
│ │ │ │ │ │ ├── composer-column.module.css
│ │ │ │ │ │ ├── composer-column.tsx
│ │ │ │ │ │ ├── count-column.tsx
│ │ │ │ │ │ ├── date-column.tsx
│ │ │ │ │ │ ├── default-column.tsx
│ │ │ │ │ │ ├── duration-column.tsx
│ │ │ │ │ │ ├── favorite-column.tsx
│ │ │ │ │ │ ├── genre-badge-column.module.css
│ │ │ │ │ │ ├── genre-badge-column.tsx
│ │ │ │ │ │ ├── genre-column.module.css
│ │ │ │ │ │ ├── genre-column.tsx
│ │ │ │ │ │ ├── image-column.module.css
│ │ │ │ │ │ ├── image-column.tsx
│ │ │ │ │ │ ├── numeric-column.tsx
│ │ │ │ │ │ ├── path-column.tsx
│ │ │ │ │ │ ├── playlist-reorder-column.module.css
│ │ │ │ │ │ ├── playlist-reorder-column.tsx
│ │ │ │ │ │ ├── rating-column.tsx
│ │ │ │ │ │ ├── row-index-column.module.css
│ │ │ │ │ │ ├── row-index-column.tsx
│ │ │ │ │ │ ├── size-column.tsx
│ │ │ │ │ │ ├── text-column.module.css
│ │ │ │ │ │ ├── text-column.tsx
│ │ │ │ │ │ ├── title-artist-column.module.css
│ │ │ │ │ │ ├── title-artist-column.tsx
│ │ │ │ │ │ ├── title-column.module.css
│ │ │ │ │ │ ├── title-column.tsx
│ │ │ │ │ │ ├── title-combined-column.module.css
│ │ │ │ │ │ ├── title-combined-column.tsx
│ │ │ │ │ │ └── year-column.tsx
│ │ │ │ │ ├── default-columns.ts
│ │ │ │ │ ├── hooks/
│ │ │ │ │ │ ├── use-container-width-tracking.ts
│ │ │ │ │ │ ├── use-item-drag-drop-state.tsx
│ │ │ │ │ │ ├── use-row-interaction-delegate.ts
│ │ │ │ │ │ ├── use-sticky-group-row-positioning.ts
│ │ │ │ │ │ ├── use-sticky-header-positioning.ts
│ │ │ │ │ │ ├── use-sticky-table-group-rows.tsx
│ │ │ │ │ │ ├── use-sticky-table-header.tsx
│ │ │ │ │ │ ├── use-table-column-model.ts
│ │ │ │ │ │ ├── use-table-imperative-handle.ts
│ │ │ │ │ │ ├── use-table-initial-scroll.ts
│ │ │ │ │ │ ├── use-table-keyboard-navigation.ts
│ │ │ │ │ │ ├── use-table-pane-sync.ts
│ │ │ │ │ │ ├── use-table-row-model.ts
│ │ │ │ │ │ └── use-table-scroll-to-index.ts
│ │ │ │ │ ├── item-table-list-column.module.css
│ │ │ │ │ ├── item-table-list-column.tsx
│ │ │ │ │ ├── item-table-list-context.tsx
│ │ │ │ │ ├── item-table-list.module.css
│ │ │ │ │ ├── item-table-list.tsx
│ │ │ │ │ └── memoized-cell-router.tsx
│ │ │ │ ├── selection-dialog.module.css
│ │ │ │ ├── selection-dialog.tsx
│ │ │ │ └── types.ts
│ │ │ ├── motion/
│ │ │ │ └── index.tsx
│ │ │ ├── native-scroll-area/
│ │ │ │ ├── native-scroll-area.module.css
│ │ │ │ └── native-scroll-area.tsx
│ │ │ ├── page-header/
│ │ │ │ ├── page-header.module.css
│ │ │ │ └── page-header.tsx
│ │ │ ├── query-builder/
│ │ │ │ ├── index.tsx
│ │ │ │ └── query-builder-option.tsx
│ │ │ ├── select-with-invalid-data/
│ │ │ │ └── index.tsx
│ │ │ ├── settings-diff-visualiser/
│ │ │ │ └── settings-diff-visualiser.tsx
│ │ │ └── simple-item-table/
│ │ │ ├── simple-item-table.module.css
│ │ │ └── simple-item-table.tsx
│ │ ├── context/
│ │ │ └── list-context.tsx
│ │ ├── env.d.ts
│ │ ├── events/
│ │ │ ├── event-emitter.ts
│ │ │ ├── events.ts
│ │ │ └── types.ts
│ │ ├── features/
│ │ │ ├── action-required/
│ │ │ │ ├── components/
│ │ │ │ │ ├── action-required-container.tsx
│ │ │ │ │ ├── error-fallback.module.css
│ │ │ │ │ ├── error-fallback.tsx
│ │ │ │ │ ├── server-credential-required.tsx
│ │ │ │ │ └── server-required.tsx
│ │ │ │ ├── routes/
│ │ │ │ │ ├── action-required-route.tsx
│ │ │ │ │ ├── invalid-route.tsx
│ │ │ │ │ └── no-network-route.tsx
│ │ │ │ └── utils/
│ │ │ │ └── window-properties.tsx
│ │ │ ├── albums/
│ │ │ │ ├── api/
│ │ │ │ │ └── album-api.ts
│ │ │ │ ├── components/
│ │ │ │ │ ├── album-detail-content.module.css
│ │ │ │ │ ├── album-detail-content.tsx
│ │ │ │ │ ├── album-detail-header.module.css
│ │ │ │ │ ├── album-detail-header.tsx
│ │ │ │ │ ├── album-grid-carousel.tsx
│ │ │ │ │ ├── album-infinite-carousel.tsx
│ │ │ │ │ ├── album-list-content.tsx
│ │ │ │ │ ├── album-list-header-filters.tsx
│ │ │ │ │ ├── album-list-header.tsx
│ │ │ │ │ ├── album-list-infinite-detail.tsx
│ │ │ │ │ ├── album-list-infinite-grid.tsx
│ │ │ │ │ ├── album-list-infinite-table.tsx
│ │ │ │ │ ├── album-list-paginated-detail.tsx
│ │ │ │ │ ├── album-list-paginated-grid.tsx
│ │ │ │ │ ├── album-list-paginated-table.tsx
│ │ │ │ │ ├── expanded-album-list-item.module.css
│ │ │ │ │ ├── expanded-album-list-item.tsx
│ │ │ │ │ ├── jellyfin-album-filters.tsx
│ │ │ │ │ ├── joined-artists.tsx
│ │ │ │ │ ├── navidrome-album-filters.tsx
│ │ │ │ │ └── subsonic-album-filters.tsx
│ │ │ │ ├── hooks/
│ │ │ │ │ └── use-album-list-filters.ts
│ │ │ │ └── routes/
│ │ │ │ ├── album-detail-route.tsx
│ │ │ │ ├── album-list-route.tsx
│ │ │ │ ├── dummy-album-detail-route.module.css
│ │ │ │ └── dummy-album-detail-route.tsx
│ │ │ ├── analytics/
│ │ │ │ ├── hooks/
│ │ │ │ │ ├── use-analytics-disabled.ts
│ │ │ │ │ ├── use-app-tracker.ts
│ │ │ │ │ └── use-page-tracker.ts
│ │ │ │ └── utils/
│ │ │ │ └── get-route-pattern.ts
│ │ │ ├── artists/
│ │ │ │ ├── api/
│ │ │ │ │ └── artists-api.ts
│ │ │ │ ├── components/
│ │ │ │ │ ├── album-artist-detail-content.module.css
│ │ │ │ │ ├── album-artist-detail-content.tsx
│ │ │ │ │ ├── album-artist-detail-discography-list.tsx
│ │ │ │ │ ├── album-artist-detail-favorite-songs-list-header-filters.tsx
│ │ │ │ │ ├── album-artist-detail-favorite-songs-list-header.tsx
│ │ │ │ │ ├── album-artist-detail-header.module.css
│ │ │ │ │ ├── album-artist-detail-header.tsx
│ │ │ │ │ ├── album-artist-detail-top-songs-list-header.tsx
│ │ │ │ │ ├── album-artist-grid-carousel.tsx
│ │ │ │ │ ├── album-artist-infinite-carousel.tsx
│ │ │ │ │ ├── album-artist-list-content.tsx
│ │ │ │ │ ├── album-artist-list-header-filters.tsx
│ │ │ │ │ ├── album-artist-list-header.tsx
│ │ │ │ │ ├── album-artist-list-infinite-grid.tsx
│ │ │ │ │ ├── album-artist-list-infinite-table.tsx
│ │ │ │ │ ├── album-artist-list-paginated-grid.tsx
│ │ │ │ │ ├── album-artist-list-paginated-table.tsx
│ │ │ │ │ ├── artist-list-content.tsx
│ │ │ │ │ ├── artist-list-header-filters.tsx
│ │ │ │ │ ├── artist-list-header.tsx
│ │ │ │ │ ├── artist-list-infinite-grid.tsx
│ │ │ │ │ ├── artist-list-infinite-table.tsx
│ │ │ │ │ ├── artist-list-paginated-grid.tsx
│ │ │ │ │ └── artist-list-paginated-table.tsx
│ │ │ │ ├── hooks/
│ │ │ │ │ ├── use-album-artist-list-filters.ts
│ │ │ │ │ ├── use-artist-albums-grouped.ts
│ │ │ │ │ └── use-artist-list-filters.ts
│ │ │ │ └── routes/
│ │ │ │ ├── album-artist-detail-favorite-songs-list-route.tsx
│ │ │ │ ├── album-artist-detail-route.tsx
│ │ │ │ ├── album-artist-detail-top-songs-list-route.tsx
│ │ │ │ ├── album-artist-list-route.tsx
│ │ │ │ └── artist-list-route.tsx
│ │ │ ├── context-menu/
│ │ │ │ ├── actions/
│ │ │ │ │ ├── add-to-playlist-action.tsx
│ │ │ │ │ ├── delete-playlist-action.tsx
│ │ │ │ │ ├── download-action.tsx
│ │ │ │ │ ├── edit-playlist-action.tsx
│ │ │ │ │ ├── get-info-action.tsx
│ │ │ │ │ ├── go-to-action.tsx
│ │ │ │ │ ├── move-queue-items-action.tsx
│ │ │ │ │ ├── play-action.tsx
│ │ │ │ │ ├── play-album-radio-action.tsx
│ │ │ │ │ ├── play-artist-radio-action.tsx
│ │ │ │ │ ├── play-track-radio-action.tsx
│ │ │ │ │ ├── remove-from-playlist-action.tsx
│ │ │ │ │ ├── remove-from-queue-action.tsx
│ │ │ │ │ ├── set-favorite-action.tsx
│ │ │ │ │ ├── set-rating-action.tsx
│ │ │ │ │ ├── share-action.tsx
│ │ │ │ │ ├── show-in-file-explorer-action.tsx
│ │ │ │ │ └── shuffle-items-action.tsx
│ │ │ │ ├── components/
│ │ │ │ │ ├── context-menu-preview.module.css
│ │ │ │ │ └── context-menu-preview.tsx
│ │ │ │ ├── context-menu-controller.tsx
│ │ │ │ └── menus/
│ │ │ │ ├── album-artist-context-menu.tsx
│ │ │ │ ├── album-context-menu.tsx
│ │ │ │ ├── artist-context-menu.tsx
│ │ │ │ ├── folder-context-menu.tsx
│ │ │ │ ├── genre-context-menu.tsx
│ │ │ │ ├── playlist-context-menu.tsx
│ │ │ │ ├── playlist-song-context-menu.tsx
│ │ │ │ ├── queue-context-menu.tsx
│ │ │ │ └── song-context-menu.tsx
│ │ │ ├── discord-rpc/
│ │ │ │ └── use-discord-rpc.ts
│ │ │ ├── favorites/
│ │ │ │ ├── components/
│ │ │ │ │ ├── favorites-content.tsx
│ │ │ │ │ └── favorites-header.tsx
│ │ │ │ └── routes/
│ │ │ │ └── favorites-route.tsx
│ │ │ ├── folders/
│ │ │ │ ├── api/
│ │ │ │ │ └── folder-api.ts
│ │ │ │ ├── components/
│ │ │ │ │ ├── folder-list-content.tsx
│ │ │ │ │ ├── folder-list-header-filters.tsx
│ │ │ │ │ ├── folder-list-header.tsx
│ │ │ │ │ ├── folder-tree-browser.module.css
│ │ │ │ │ └── folder-tree-browser.tsx
│ │ │ │ ├── hooks/
│ │ │ │ │ └── use-folder-list-filters.ts
│ │ │ │ └── routes/
│ │ │ │ └── folder-list-route.tsx
│ │ │ ├── genres/
│ │ │ │ ├── api/
│ │ │ │ │ └── genres-api.ts
│ │ │ │ ├── components/
│ │ │ │ │ ├── genre-detail-content.tsx
│ │ │ │ │ ├── genre-detail-header.tsx
│ │ │ │ │ ├── genre-list-content.tsx
│ │ │ │ │ ├── genre-list-header-filters.tsx
│ │ │ │ │ ├── genre-list-header.tsx
│ │ │ │ │ ├── genre-list-infinite-grid.tsx
│ │ │ │ │ ├── genre-list-infinite-table.tsx
│ │ │ │ │ ├── genre-list-paginated-grid.tsx
│ │ │ │ │ └── genre-list-paginated-table.tsx
│ │ │ │ ├── hooks/
│ │ │ │ │ └── use-genre-list-filters.ts
│ │ │ │ └── routes/
│ │ │ │ ├── genre-detail-route.tsx
│ │ │ │ └── genre-list-route.tsx
│ │ │ ├── home/
│ │ │ │ ├── api/
│ │ │ │ │ └── home-api.ts
│ │ │ │ ├── components/
│ │ │ │ │ ├── album-infinite-feature-carousel.tsx
│ │ │ │ │ ├── album-infinite-single-feature-carousel.tsx
│ │ │ │ │ ├── featured-genres.module.css
│ │ │ │ │ └── featured-genres.tsx
│ │ │ │ └── routes/
│ │ │ │ └── home-route.tsx
│ │ │ ├── item-details/
│ │ │ │ └── components/
│ │ │ │ ├── item-details-modal.tsx
│ │ │ │ └── song-path.tsx
│ │ │ ├── login/
│ │ │ │ └── routes/
│ │ │ │ └── login-route.tsx
│ │ │ ├── lyrics/
│ │ │ │ ├── api/
│ │ │ │ │ ├── lyric-translate.ts
│ │ │ │ │ └── lyrics-api.ts
│ │ │ │ ├── components/
│ │ │ │ │ ├── lyrics-export-form.tsx
│ │ │ │ │ ├── lyrics-search-form.module.css
│ │ │ │ │ ├── lyrics-search-form.tsx
│ │ │ │ │ ├── lyrics-settings-form.tsx
│ │ │ │ │ └── lyrics-settings-modal.tsx
│ │ │ │ ├── lyric-line.module.css
│ │ │ │ ├── lyric-line.tsx
│ │ │ │ ├── lyrics-actions.tsx
│ │ │ │ ├── lyrics.module.css
│ │ │ │ ├── lyrics.tsx
│ │ │ │ ├── synchronized-lyrics.module.css
│ │ │ │ ├── synchronized-lyrics.tsx
│ │ │ │ ├── unsynchronized-lyrics.module.css
│ │ │ │ ├── unsynchronized-lyrics.tsx
│ │ │ │ └── utils/
│ │ │ │ └── open-lyrics-settings-modal.ts
│ │ │ ├── now-playing/
│ │ │ │ ├── components/
│ │ │ │ │ ├── drawer-play-queue.tsx
│ │ │ │ │ ├── now-playing-header.tsx
│ │ │ │ │ ├── play-queue-list-controls.tsx
│ │ │ │ │ ├── play-queue.module.css
│ │ │ │ │ ├── play-queue.tsx
│ │ │ │ │ ├── popover-play-queue.tsx
│ │ │ │ │ ├── sidebar-play-queue.module.css
│ │ │ │ │ └── sidebar-play-queue.tsx
│ │ │ │ └── routes/
│ │ │ │ └── now-playing-route.tsx
│ │ │ ├── player/
│ │ │ │ ├── audio-player/
│ │ │ │ │ ├── engine/
│ │ │ │ │ │ ├── mpv-player-engine.tsx
│ │ │ │ │ │ ├── wavesurfer-player-engine.tsx
│ │ │ │ │ │ └── web-player-engine.tsx
│ │ │ │ │ ├── hooks/
│ │ │ │ │ │ ├── use-main-player-listener.tsx
│ │ │ │ │ │ ├── use-player-events.ts
│ │ │ │ │ │ └── use-stream-url.tsx
│ │ │ │ │ ├── mpv-player.tsx
│ │ │ │ │ ├── types.ts
│ │ │ │ │ ├── utils/
│ │ │ │ │ │ ├── list-handlers.ts
│ │ │ │ │ │ └── player-utils.ts
│ │ │ │ │ ├── wavesurfer-player.tsx
│ │ │ │ │ └── web-player.tsx
│ │ │ │ ├── components/
│ │ │ │ │ ├── audio-players.tsx
│ │ │ │ │ ├── center-controls.module.css
│ │ │ │ │ ├── center-controls.tsx
│ │ │ │ │ ├── full-screen-player-image.module.css
│ │ │ │ │ ├── full-screen-player-image.tsx
│ │ │ │ │ ├── full-screen-player-queue.module.css
│ │ │ │ │ ├── full-screen-player-queue.tsx
│ │ │ │ │ ├── full-screen-player.module.css
│ │ │ │ │ ├── full-screen-player.tsx
│ │ │ │ │ ├── full-screen-similar-songs.tsx
│ │ │ │ │ ├── full-screen-visualizer-song-info.tsx
│ │ │ │ │ ├── full-screen-visualizer.module.css
│ │ │ │ │ ├── full-screen-visualizer.tsx
│ │ │ │ │ ├── left-controls.module.css
│ │ │ │ │ ├── left-controls.tsx
│ │ │ │ │ ├── mobile-fullscreen-player-album-art.tsx
│ │ │ │ │ ├── mobile-fullscreen-player-bottom-controls.tsx
│ │ │ │ │ ├── mobile-fullscreen-player-controls.tsx
│ │ │ │ │ ├── mobile-fullscreen-player-header.tsx
│ │ │ │ │ ├── mobile-fullscreen-player-metadata.tsx
│ │ │ │ │ ├── mobile-fullscreen-player-progress.tsx
│ │ │ │ │ ├── mobile-fullscreen-player.module.css
│ │ │ │ │ ├── mobile-fullscreen-player.tsx
│ │ │ │ │ ├── mobile-playerbar.module.css
│ │ │ │ │ ├── mobile-playerbar.tsx
│ │ │ │ │ ├── player-button.module.css
│ │ │ │ │ ├── player-button.tsx
│ │ │ │ │ ├── player-config.tsx
│ │ │ │ │ ├── playerbar-seek-slider.tsx
│ │ │ │ │ ├── playerbar-slider.module.css
│ │ │ │ │ ├── playerbar-slider.tsx
│ │ │ │ │ ├── playerbar-waveform.module.css
│ │ │ │ │ ├── playerbar-waveform.tsx
│ │ │ │ │ ├── playerbar.module.css
│ │ │ │ │ ├── playerbar.tsx
│ │ │ │ │ ├── radio-metadata-display.tsx
│ │ │ │ │ ├── right-controls.tsx
│ │ │ │ │ ├── shuffle-all-modal.tsx
│ │ │ │ │ └── sleep-timer-button.tsx
│ │ │ │ ├── context/
│ │ │ │ │ ├── player-context.tsx
│ │ │ │ │ └── webaudio-context.ts
│ │ │ │ ├── hooks/
│ │ │ │ │ ├── use-auto-dj.ts
│ │ │ │ │ ├── use-autosave.ts
│ │ │ │ │ ├── use-is-current-song.ts
│ │ │ │ │ ├── use-media-session.ts
│ │ │ │ │ ├── use-mpris.ts
│ │ │ │ │ ├── use-playback-hotkeys.ts
│ │ │ │ │ ├── use-power-save-blocker.ts
│ │ │ │ │ ├── use-queue-restore.ts
│ │ │ │ │ ├── use-scrobble.ts
│ │ │ │ │ ├── use-update-current-song.ts
│ │ │ │ │ └── use-webaudio.ts
│ │ │ │ ├── mutations/
│ │ │ │ │ └── scrobble-mutation.ts
│ │ │ │ ├── ref/
│ │ │ │ │ └── players-ref.tsx
│ │ │ │ ├── update-remote-song.tsx
│ │ │ │ ├── utils/
│ │ │ │ │ └── open-visualizer-settings-modal.ts
│ │ │ │ └── utils.ts
│ │ │ ├── playlists/
│ │ │ │ ├── api/
│ │ │ │ │ └── playlists-api.ts
│ │ │ │ ├── components/
│ │ │ │ │ ├── add-to-playlist-context-modal.module.css
│ │ │ │ │ ├── add-to-playlist-context-modal.tsx
│ │ │ │ │ ├── client-side-song-filters.tsx
│ │ │ │ │ ├── create-playlist-form.tsx
│ │ │ │ │ ├── playlist-detail-album-view.tsx
│ │ │ │ │ ├── playlist-detail-song-list-content.tsx
│ │ │ │ │ ├── playlist-detail-song-list-grid.tsx
│ │ │ │ │ ├── playlist-detail-song-list-header-filters.tsx
│ │ │ │ │ ├── playlist-detail-song-list-header.tsx
│ │ │ │ │ ├── playlist-detail-song-list-table.tsx
│ │ │ │ │ ├── playlist-list-content.tsx
│ │ │ │ │ ├── playlist-list-header-filters.tsx
│ │ │ │ │ ├── playlist-list-header.tsx
│ │ │ │ │ ├── playlist-list-infinite-grid.tsx
│ │ │ │ │ ├── playlist-list-infinite-table.tsx
│ │ │ │ │ ├── playlist-list-paginated-grid.tsx
│ │ │ │ │ ├── playlist-list-paginated-table.tsx
│ │ │ │ │ ├── playlist-query-builder.tsx
│ │ │ │ │ ├── playlist-query-editor.tsx
│ │ │ │ │ ├── save-and-replace-context-modal.tsx
│ │ │ │ │ ├── save-as-playlist-form.tsx
│ │ │ │ │ ├── update-playlist-form.tsx
│ │ │ │ │ └── update-playlist-modal.ts
│ │ │ │ ├── hooks/
│ │ │ │ │ ├── use-playlist-list-filters.ts
│ │ │ │ │ ├── use-playlist-song-list-filters.ts
│ │ │ │ │ ├── use-playlist-track-list.ts
│ │ │ │ │ └── use-recent-playlists.ts
│ │ │ │ ├── mutations/
│ │ │ │ │ ├── add-to-playlist-mutation.ts
│ │ │ │ │ ├── create-playlist-mutation.ts
│ │ │ │ │ ├── delete-playlist-mutation.ts
│ │ │ │ │ ├── playlist-optimistic-updates.ts
│ │ │ │ │ ├── remove-from-playlist-mutation.ts
│ │ │ │ │ └── update-playlist-mutation.ts
│ │ │ │ ├── routes/
│ │ │ │ │ ├── playlist-detail-song-list-route.tsx
│ │ │ │ │ └── playlist-list-route.tsx
│ │ │ │ └── utils.ts
│ │ │ ├── radio/
│ │ │ │ ├── api/
│ │ │ │ │ └── radio-api.ts
│ │ │ │ ├── components/
│ │ │ │ │ ├── create-radio-station-form.tsx
│ │ │ │ │ ├── edit-radio-station-form.tsx
│ │ │ │ │ ├── radio-list-content.tsx
│ │ │ │ │ ├── radio-list-header-filters.tsx
│ │ │ │ │ ├── radio-list-header.tsx
│ │ │ │ │ ├── radio-list-items.module.css
│ │ │ │ │ ├── radio-list-items.tsx
│ │ │ │ │ └── radio-web-player.tsx
│ │ │ │ ├── hooks/
│ │ │ │ │ └── use-radio-player.ts
│ │ │ │ ├── mutations/
│ │ │ │ │ ├── create-radio-station-mutation.ts
│ │ │ │ │ ├── delete-radio-station-mutation.ts
│ │ │ │ │ └── update-radio-station-mutation.ts
│ │ │ │ ├── routes/
│ │ │ │ │ └── radio-list-route.tsx
│ │ │ │ └── store/
│ │ │ │ └── radio-store.ts
│ │ │ ├── remote/
│ │ │ │ └── hooks/
│ │ │ │ └── use-remote.tsx
│ │ │ ├── search/
│ │ │ │ ├── api/
│ │ │ │ │ └── search-api.ts
│ │ │ │ ├── components/
│ │ │ │ │ ├── collapsible-command-group.module.css
│ │ │ │ │ ├── collapsible-command-group.tsx
│ │ │ │ │ ├── command-item-selectable.tsx
│ │ │ │ │ ├── command-palette.tsx
│ │ │ │ │ ├── command.css
│ │ │ │ │ ├── command.tsx
│ │ │ │ │ ├── go-to-commands.tsx
│ │ │ │ │ ├── home-commands.tsx
│ │ │ │ │ ├── library-command-item.module.css
│ │ │ │ │ ├── library-command-item.tsx
│ │ │ │ │ ├── search-album-artists-section.tsx
│ │ │ │ │ ├── search-albums-section.tsx
│ │ │ │ │ ├── search-content.tsx
│ │ │ │ │ ├── search-header.tsx
│ │ │ │ │ ├── search-songs-section.tsx
│ │ │ │ │ └── server-commands.tsx
│ │ │ │ └── routes/
│ │ │ │ └── search-route.tsx
│ │ │ ├── servers/
│ │ │ │ └── components/
│ │ │ │ ├── add-server-form.tsx
│ │ │ │ ├── edit-server-form.tsx
│ │ │ │ ├── ignore-cors-ssl-switches.tsx
│ │ │ │ ├── server-list-item.tsx
│ │ │ │ ├── server-list.tsx
│ │ │ │ └── server-section.tsx
│ │ │ ├── settings/
│ │ │ │ ├── components/
│ │ │ │ │ ├── advanced/
│ │ │ │ │ │ ├── advanced-tab.tsx
│ │ │ │ │ │ ├── analytics-settings.tsx
│ │ │ │ │ │ ├── export-import-settings.tsx
│ │ │ │ │ │ ├── logger-settings.tsx
│ │ │ │ │ │ └── styles-settings.tsx
│ │ │ │ │ ├── general/
│ │ │ │ │ │ ├── application-settings.tsx
│ │ │ │ │ │ ├── art-resolution-settings.tsx
│ │ │ │ │ │ ├── artist-settings.tsx
│ │ │ │ │ │ ├── control-settings.tsx
│ │ │ │ │ │ ├── draggable-item.tsx
│ │ │ │ │ │ ├── draggable-items.tsx
│ │ │ │ │ │ ├── external-links-settings.tsx
│ │ │ │ │ │ ├── fullscreen-player-settings.tsx
│ │ │ │ │ │ ├── general-tab.tsx
│ │ │ │ │ │ ├── home-settings.tsx
│ │ │ │ │ │ ├── lyric-settings.tsx
│ │ │ │ │ │ ├── path-settings.tsx
│ │ │ │ │ │ ├── query-builder-settings.tsx
│ │ │ │ │ │ ├── scrobble-settings.tsx
│ │ │ │ │ │ ├── sidebar-reorder.tsx
│ │ │ │ │ │ ├── sidebar-settings.tsx
│ │ │ │ │ │ └── theme-settings.tsx
│ │ │ │ │ ├── hotkeys/
│ │ │ │ │ │ ├── hotkey-manager-settings.tsx
│ │ │ │ │ │ ├── hotkeys-manager-settings.module.css
│ │ │ │ │ │ ├── hotkeys-tab.tsx
│ │ │ │ │ │ ├── media-session-settings.tsx
│ │ │ │ │ │ └── window-hotkey-settings.tsx
│ │ │ │ │ ├── playback/
│ │ │ │ │ │ ├── audio-settings.tsx
│ │ │ │ │ │ ├── auto-dj-settings.tsx
│ │ │ │ │ │ ├── mpv-properties.ts
│ │ │ │ │ │ ├── mpv-settings.tsx
│ │ │ │ │ │ ├── playback-tab.tsx
│ │ │ │ │ │ ├── player-filter-settings.tsx
│ │ │ │ │ │ └── transcode-settings.tsx
│ │ │ │ │ ├── settings-content.tsx
│ │ │ │ │ ├── settings-header.tsx
│ │ │ │ │ ├── settings-modal.tsx
│ │ │ │ │ ├── settings-option.tsx
│ │ │ │ │ ├── settings-section.tsx
│ │ │ │ │ └── window/
│ │ │ │ │ ├── cache-settngs.tsx
│ │ │ │ │ ├── discord-settings.tsx
│ │ │ │ │ ├── password-settings.tsx
│ │ │ │ │ ├── remote-settings.tsx
│ │ │ │ │ ├── update-settings.tsx
│ │ │ │ │ ├── window-settings.tsx
│ │ │ │ │ └── window-tab.tsx
│ │ │ │ ├── context/
│ │ │ │ │ └── search-context.tsx
│ │ │ │ ├── restart-toast.ts
│ │ │ │ ├── routes/
│ │ │ │ │ └── settings-route.tsx
│ │ │ │ └── utils/
│ │ │ │ └── open-settings-modal.ts
│ │ │ ├── shared/
│ │ │ │ ├── api/
│ │ │ │ │ └── shared-api.ts
│ │ │ │ ├── components/
│ │ │ │ │ ├── animated-page.module.css
│ │ │ │ │ ├── animated-page.tsx
│ │ │ │ │ ├── component-error-boundary.tsx
│ │ │ │ │ ├── display-type-toggle-button.tsx
│ │ │ │ │ ├── filter-bar.module.css
│ │ │ │ │ ├── filter-bar.tsx
│ │ │ │ │ ├── filter-button.tsx
│ │ │ │ │ ├── folder-button.tsx
│ │ │ │ │ ├── grid-config.tsx
│ │ │ │ │ ├── json-preview.module.css
│ │ │ │ │ ├── json-preview.tsx
│ │ │ │ │ ├── library-background-overlay.module.css
│ │ │ │ │ ├── library-background-overlay.tsx
│ │ │ │ │ ├── library-container.module.css
│ │ │ │ │ ├── library-container.tsx
│ │ │ │ │ ├── library-header-bar.module.css
│ │ │ │ │ ├── library-header-bar.tsx
│ │ │ │ │ ├── library-header.module.css
│ │ │ │ │ ├── library-header.tsx
│ │ │ │ │ ├── list-config-menu.tsx
│ │ │ │ │ ├── list-display-type-toggle-button.tsx
│ │ │ │ │ ├── list-filters.tsx
│ │ │ │ │ ├── list-music-folder-dropdown.tsx
│ │ │ │ │ ├── list-refresh-button.tsx
│ │ │ │ │ ├── list-search-input.tsx
│ │ │ │ │ ├── list-select-filter.tsx
│ │ │ │ │ ├── list-sort-by-dropdown.tsx
│ │ │ │ │ ├── list-sort-order-toggle-button.tsx
│ │ │ │ │ ├── list-with-sidebar-container.module.css
│ │ │ │ │ ├── list-with-sidebar-container.tsx
│ │ │ │ │ ├── more-button.tsx
│ │ │ │ │ ├── multi-select-rows.module.css
│ │ │ │ │ ├── multi-select-rows.tsx
│ │ │ │ │ ├── order-toggle-button.tsx
│ │ │ │ │ ├── page-error-boundary.tsx
│ │ │ │ │ ├── play-button-group.module.css
│ │ │ │ │ ├── play-button-group.tsx
│ │ │ │ │ ├── play-button.module.css
│ │ │ │ │ ├── play-button.tsx
│ │ │ │ │ ├── refresh-button.tsx
│ │ │ │ │ ├── resize-handle.module.css
│ │ │ │ │ ├── resize-handle.tsx
│ │ │ │ │ ├── router-error-boundary.tsx
│ │ │ │ │ ├── save-as-collection-button.module.css
│ │ │ │ │ ├── save-as-collection-button.tsx
│ │ │ │ │ ├── search-input.tsx
│ │ │ │ │ ├── settings-button.tsx
│ │ │ │ │ ├── table-config.module.css
│ │ │ │ │ ├── table-config.tsx
│ │ │ │ │ └── tag-filter.tsx
│ │ │ │ ├── hooks/
│ │ │ │ │ ├── use-list-filter-persistence.ts
│ │ │ │ │ ├── use-music-folder-id-filter.ts
│ │ │ │ │ ├── use-play-button-click.ts
│ │ │ │ │ ├── use-search-term-filter.ts
│ │ │ │ │ ├── use-select-filter.ts
│ │ │ │ │ ├── use-set-favorite.ts
│ │ │ │ │ ├── use-set-rating.ts
│ │ │ │ │ ├── use-sort-by-filter.ts
│ │ │ │ │ └── use-sort-order-filter.ts
│ │ │ │ ├── mutations/
│ │ │ │ │ ├── create-favorite-mutation.ts
│ │ │ │ │ ├── delete-favorite-mutation.ts
│ │ │ │ │ ├── favorite-optimistic-updates.ts
│ │ │ │ │ ├── rating-optimistic-updates.ts
│ │ │ │ │ └── set-rating-mutation.ts
│ │ │ │ └── utils.ts
│ │ │ ├── sharing/
│ │ │ │ ├── components/
│ │ │ │ │ └── share-item-context-modal.tsx
│ │ │ │ └── mutations/
│ │ │ │ └── share-item-mutation.ts
│ │ │ ├── sidebar/
│ │ │ │ └── components/
│ │ │ │ ├── action-bar.module.css
│ │ │ │ ├── action-bar.tsx
│ │ │ │ ├── collapsed-sidebar-button.module.css
│ │ │ │ ├── collapsed-sidebar-button.tsx
│ │ │ │ ├── collapsed-sidebar-item.module.css
│ │ │ │ ├── collapsed-sidebar-item.tsx
│ │ │ │ ├── collapsed-sidebar.module.css
│ │ │ │ ├── collapsed-sidebar.tsx
│ │ │ │ ├── mobile-sidebar.module.css
│ │ │ │ ├── mobile-sidebar.tsx
│ │ │ │ ├── server-selector-items.tsx
│ │ │ │ ├── server-selector.module.css
│ │ │ │ ├── server-selector.tsx
│ │ │ │ ├── sidebar-collection-list.module.css
│ │ │ │ ├── sidebar-collection-list.tsx
│ │ │ │ ├── sidebar-icon.module.css
│ │ │ │ ├── sidebar-icon.tsx
│ │ │ │ ├── sidebar-item.module.css
│ │ │ │ ├── sidebar-item.tsx
│ │ │ │ ├── sidebar-playlist-list.module.css
│ │ │ │ ├── sidebar-playlist-list.tsx
│ │ │ │ ├── sidebar.module.css
│ │ │ │ └── sidebar.tsx
│ │ │ ├── similar-songs/
│ │ │ │ └── components/
│ │ │ │ └── similar-songs-list.tsx
│ │ │ ├── songs/
│ │ │ │ ├── api/
│ │ │ │ │ └── songs-api.ts
│ │ │ │ ├── components/
│ │ │ │ │ ├── jellyfin-song-filters.tsx
│ │ │ │ │ ├── navidrome-song-filters.tsx
│ │ │ │ │ ├── song-infinite-carousel.tsx
│ │ │ │ │ ├── song-list-content.tsx
│ │ │ │ │ ├── song-list-header-filters.tsx
│ │ │ │ │ ├── song-list-header.tsx
│ │ │ │ │ ├── song-list-infinite-grid.tsx
│ │ │ │ │ ├── song-list-infinite-table.tsx
│ │ │ │ │ ├── song-list-paginated-grid.tsx
│ │ │ │ │ ├── song-list-paginated-table.tsx
│ │ │ │ │ └── subsonic-song-filters.tsx
│ │ │ │ ├── hooks/
│ │ │ │ │ └── use-song-list-filters.ts
│ │ │ │ └── routes/
│ │ │ │ └── song-list-route.tsx
│ │ │ ├── titlebar/
│ │ │ │ └── components/
│ │ │ │ ├── app-menu.tsx
│ │ │ │ ├── titlebar.module.css
│ │ │ │ └── titlebar.tsx
│ │ │ ├── visualizer/
│ │ │ │ └── components/
│ │ │ │ ├── audiomotionanalyzer/
│ │ │ │ │ ├── presets.ts
│ │ │ │ │ ├── visualizer-settings-form.module.css
│ │ │ │ │ ├── visualizer-settings-form.tsx
│ │ │ │ │ ├── visualizer-settings-modal.tsx
│ │ │ │ │ ├── visualizer.module.css
│ │ │ │ │ └── visualizer.tsx
│ │ │ │ └── butternchurn/
│ │ │ │ ├── butterchurn.d.ts
│ │ │ │ ├── visualizer.module.css
│ │ │ │ └── visualizer.tsx
│ │ │ └── window-controls/
│ │ │ └── components/
│ │ │ ├── window-controls.module.css
│ │ │ └── window-controls.tsx
│ │ ├── global.d.ts
│ │ ├── hooks/
│ │ │ ├── index.ts
│ │ │ ├── use-app-focus.ts
│ │ │ ├── use-check-for-updates.ts
│ │ │ ├── use-container-query.ts
│ │ │ ├── use-drag-drop.tsx
│ │ │ ├── use-fast-average-color.tsx
│ │ │ ├── use-garbage-collection.ts
│ │ │ ├── use-genre-route.ts
│ │ │ ├── use-hide-scrollbar.ts
│ │ │ ├── use-is-mobile.ts
│ │ │ ├── use-is-mounted.ts
│ │ │ ├── use-server-authenticated.ts
│ │ │ ├── use-should-pad-titlebar.tsx
│ │ │ └── use-sync-settings-to-main.ts
│ │ ├── index.html
│ │ ├── layouts/
│ │ │ ├── auth-layout.module.css
│ │ │ ├── auth-layout.tsx
│ │ │ ├── authentication-outlet.tsx
│ │ │ ├── default-layout/
│ │ │ │ ├── full-screen-overlay.tsx
│ │ │ │ ├── full-screen-visualizer-overlay.tsx
│ │ │ │ ├── left-sidebar.module.css
│ │ │ │ ├── left-sidebar.tsx
│ │ │ │ ├── main-content.module.css
│ │ │ │ ├── main-content.tsx
│ │ │ │ ├── player-bar.module.css
│ │ │ │ ├── player-bar.tsx
│ │ │ │ ├── right-sidebar.module.css
│ │ │ │ ├── right-sidebar.tsx
│ │ │ │ ├── side-drawer-queue.module.css
│ │ │ │ └── side-drawer-queue.tsx
│ │ │ ├── default-layout.module.css
│ │ │ ├── default-layout.tsx
│ │ │ ├── mobile-layout/
│ │ │ │ ├── mobile-layout.module.css
│ │ │ │ └── mobile-layout.tsx
│ │ │ ├── responsive-layout.tsx
│ │ │ ├── window-bar.module.css
│ │ │ └── window-bar.tsx
│ │ ├── lib/
│ │ │ ├── react-query.ts
│ │ │ └── zustand.ts
│ │ ├── main.tsx
│ │ ├── release-notes-modal.tsx
│ │ ├── router/
│ │ │ ├── app-outlet.tsx
│ │ │ ├── app-router.tsx
│ │ │ ├── routes.ts
│ │ │ ├── titlebar-outlet.module.css
│ │ │ └── titlebar-outlet.tsx
│ │ ├── store/
│ │ │ ├── app.store.ts
│ │ │ ├── auth.store.ts
│ │ │ ├── env-settings-overrides.ts
│ │ │ ├── full-screen-player.store.ts
│ │ │ ├── index.ts
│ │ │ ├── player.store.ts
│ │ │ ├── scroll.store.ts
│ │ │ ├── settings.store.ts
│ │ │ ├── sleep-timer.store.ts
│ │ │ ├── timestamp.store.ts
│ │ │ └── utils.ts
│ │ ├── styles/
│ │ │ ├── helpers.ts
│ │ │ └── overlayscrollbars.css
│ │ ├── themes/
│ │ │ ├── mantine-theme.tsx
│ │ │ └── use-app-theme.ts
│ │ ├── types/
│ │ │ ├── emotion.d.ts
│ │ │ └── fonts.ts
│ │ ├── update-available-dialog.tsx
│ │ └── utils/
│ │ ├── constrain-sidebar-width.ts
│ │ ├── format.tsx
│ │ ├── get-header-color.ts
│ │ ├── index.ts
│ │ ├── linkify.tsx
│ │ ├── logger-message.ts
│ │ ├── logger.ts
│ │ ├── normalize-release-types.tsx
│ │ ├── normalize-server-url.ts
│ │ ├── parse-search-params.ts
│ │ ├── query-params.ts
│ │ ├── random-string.ts
│ │ ├── rgb-to-rgba.ts
│ │ ├── sanitize.ts
│ │ ├── sentence-case.ts
│ │ ├── set-local-storage-setttings.ts
│ │ ├── shuffle.ts
│ │ ├── title-case.ts
│ │ └── truncate-middle.ts
│ ├── shared/
│ │ ├── api/
│ │ │ ├── jellyfin/
│ │ │ │ ├── jellyfin-normalize.ts
│ │ │ │ └── jellyfin-types.ts
│ │ │ ├── navidrome/
│ │ │ │ ├── navidrome-normalize.ts
│ │ │ │ └── navidrome-types.ts
│ │ │ ├── subsonic/
│ │ │ │ ├── subsonic-normalize.ts
│ │ │ │ └── subsonic-types.ts
│ │ │ └── utils.ts
│ │ ├── assets.d.ts
│ │ ├── components/
│ │ │ ├── accordion/
│ │ │ │ ├── accordion.module.css
│ │ │ │ └── accordion.tsx
│ │ │ ├── action-icon/
│ │ │ │ ├── action-icon.module.css
│ │ │ │ └── action-icon.tsx
│ │ │ ├── angle-slider/
│ │ │ │ └── angle-slider.tsx
│ │ │ ├── animations/
│ │ │ │ ├── animation-props.ts
│ │ │ │ └── animation-variants.ts
│ │ │ ├── badge/
│ │ │ │ ├── badge.module.css
│ │ │ │ └── badge.tsx
│ │ │ ├── box/
│ │ │ │ └── box.tsx
│ │ │ ├── breadcrumb/
│ │ │ │ └── breadcrumb.tsx
│ │ │ ├── button/
│ │ │ │ ├── button.module.css
│ │ │ │ └── button.tsx
│ │ │ ├── center/
│ │ │ │ └── center.tsx
│ │ │ ├── checkbox/
│ │ │ │ ├── checkbox.module.css
│ │ │ │ └── checkbox.tsx
│ │ │ ├── checkbox-select/
│ │ │ │ ├── checkbox-select.module.css
│ │ │ │ └── checkbox-select.tsx
│ │ │ ├── code/
│ │ │ │ ├── code.module.css
│ │ │ │ └── code.tsx
│ │ │ ├── color-input/
│ │ │ │ ├── color-input.module.css
│ │ │ │ └── color-input.tsx
│ │ │ ├── context-menu/
│ │ │ │ ├── context-menu.module.css
│ │ │ │ └── context-menu.tsx
│ │ │ ├── copy-button/
│ │ │ │ └── copy-button.tsx
│ │ │ ├── date-picker/
│ │ │ │ ├── date-picker.module.css
│ │ │ │ └── date-picker.tsx
│ │ │ ├── date-time-picker/
│ │ │ │ ├── date-time-picker.module.css
│ │ │ │ └── date-time-picker.tsx
│ │ │ ├── dialog/
│ │ │ │ ├── dialog.module.css
│ │ │ │ └── dialog.tsx
│ │ │ ├── divider/
│ │ │ │ ├── divider.module.css
│ │ │ │ └── divider.tsx
│ │ │ ├── drag-drop-zone/
│ │ │ │ └── drag-drop-zone.tsx
│ │ │ ├── drawer/
│ │ │ │ └── drawer.tsx
│ │ │ ├── dropdown-menu/
│ │ │ │ ├── dropdown-menu.module.css
│ │ │ │ └── dropdown-menu.tsx
│ │ │ ├── explicit-indicator/
│ │ │ │ ├── explicit-indicator.module.css
│ │ │ │ └── explicit-indicator.tsx
│ │ │ ├── fieldset/
│ │ │ │ ├── fieldset.module.css
│ │ │ │ └── fieldset.tsx
│ │ │ ├── file-input/
│ │ │ │ ├── file-input.module.css
│ │ │ │ └── file-input.tsx
│ │ │ ├── flex/
│ │ │ │ └── flex.tsx
│ │ │ ├── grid/
│ │ │ │ └── grid.tsx
│ │ │ ├── group/
│ │ │ │ └── group.tsx
│ │ │ ├── hover-card/
│ │ │ │ ├── hover-card.module.css
│ │ │ │ └── hover-card.tsx
│ │ │ ├── icon/
│ │ │ │ ├── icon.module.css
│ │ │ │ └── icon.tsx
│ │ │ ├── image/
│ │ │ │ ├── image.module.css
│ │ │ │ ├── image.tsx
│ │ │ │ └── use-native-image.ts
│ │ │ ├── json-input/
│ │ │ │ ├── json-input.module.css
│ │ │ │ └── json-input.tsx
│ │ │ ├── kbd/
│ │ │ │ └── kbd.tsx
│ │ │ ├── loading-overlay/
│ │ │ │ └── loading-overlay.tsx
│ │ │ ├── modal/
│ │ │ │ ├── modal.module.css
│ │ │ │ ├── modal.tsx
│ │ │ │ └── model-shared.tsx
│ │ │ ├── multi-select/
│ │ │ │ ├── multi-select.module.css
│ │ │ │ ├── multi-select.tsx
│ │ │ │ ├── virtual-multi-select.module.css
│ │ │ │ └── virtual-multi-select.tsx
│ │ │ ├── number-input/
│ │ │ │ ├── number-input.module.css
│ │ │ │ └── number-input.tsx
│ │ │ ├── option/
│ │ │ │ ├── option.module.css
│ │ │ │ └── option.tsx
│ │ │ ├── pagination/
│ │ │ │ ├── pagination.module.css
│ │ │ │ └── pagination.tsx
│ │ │ ├── paper/
│ │ │ │ ├── paper.module.css
│ │ │ │ └── paper.tsx
│ │ │ ├── password-input/
│ │ │ │ ├── password-input.module.css
│ │ │ │ └── password-input.tsx
│ │ │ ├── pill/
│ │ │ │ ├── pill.module.css
│ │ │ │ └── pill.tsx
│ │ │ ├── popover/
│ │ │ │ ├── popover.module.css
│ │ │ │ └── popover.tsx
│ │ │ ├── portal/
│ │ │ │ └── portal.tsx
│ │ │ ├── rating/
│ │ │ │ ├── rating.module.css
│ │ │ │ └── rating.tsx
│ │ │ ├── read-only-rating/
│ │ │ │ ├── read-only-rating.module.css
│ │ │ │ └── read-only-rating.tsx
│ │ │ ├── scroll-area/
│ │ │ │ ├── scroll-area.css
│ │ │ │ ├── scroll-area.module.css
│ │ │ │ └── scroll-area.tsx
│ │ │ ├── segmented-control/
│ │ │ │ ├── segmented-control.module.css
│ │ │ │ └── segmented-control.tsx
│ │ │ ├── select/
│ │ │ │ ├── select.module.css
│ │ │ │ └── select.tsx
│ │ │ ├── separator/
│ │ │ │ ├── separator.module.css
│ │ │ │ └── separator.tsx
│ │ │ ├── skeleton/
│ │ │ │ ├── skeleton.module.css
│ │ │ │ └── skeleton.tsx
│ │ │ ├── slider/
│ │ │ │ ├── slider.module.css
│ │ │ │ └── slider.tsx
│ │ │ ├── spinner/
│ │ │ │ ├── spinner.module.css
│ │ │ │ └── spinner.tsx
│ │ │ ├── spoiler/
│ │ │ │ ├── spoiler.module.css
│ │ │ │ └── spoiler.tsx
│ │ │ ├── stack/
│ │ │ │ └── stack.tsx
│ │ │ ├── switch/
│ │ │ │ ├── switch.module.css
│ │ │ │ └── switch.tsx
│ │ │ ├── table/
│ │ │ │ ├── table.module.css
│ │ │ │ └── table.tsx
│ │ │ ├── tabs/
│ │ │ │ ├── tabs.module.css
│ │ │ │ └── tabs.tsx
│ │ │ ├── text/
│ │ │ │ ├── text.module.css
│ │ │ │ └── text.tsx
│ │ │ ├── text-input/
│ │ │ │ ├── text-input.module.css
│ │ │ │ └── text-input.tsx
│ │ │ ├── text-title/
│ │ │ │ ├── text-title.module.css
│ │ │ │ └── text-title.tsx
│ │ │ ├── textarea/
│ │ │ │ ├── textarea.module.css
│ │ │ │ └── textarea.tsx
│ │ │ ├── toast/
│ │ │ │ ├── toast.module.css
│ │ │ │ └── toast.tsx
│ │ │ ├── tooltip/
│ │ │ │ ├── tooltip.module.css
│ │ │ │ └── tooltip.tsx
│ │ │ └── yes-no-select/
│ │ │ └── yes-no-select.tsx
│ │ ├── constants/
│ │ │ └── playback-selectors.ts
│ │ ├── hooks/
│ │ │ ├── use-click-outside.ts
│ │ │ ├── use-container-query.ts
│ │ │ ├── use-debounced-callback.ts
│ │ │ ├── use-debounced-state.ts
│ │ │ ├── use-debounced-value.ts
│ │ │ ├── use-disclosure.ts
│ │ │ ├── use-double-click.ts
│ │ │ ├── use-element-size.ts
│ │ │ ├── use-focus-trap.ts
│ │ │ ├── use-focus-within.ts
│ │ │ ├── use-form.ts
│ │ │ ├── use-hotkeys.ts
│ │ │ ├── use-in-viewport.ts
│ │ │ ├── use-intersection.ts
│ │ │ ├── use-is-overflow.ts
│ │ │ ├── use-local-storage.ts
│ │ │ ├── use-long-press.ts
│ │ │ ├── use-media-query.ts
│ │ │ ├── use-merged-ref.ts
│ │ │ ├── use-session-storage.ts
│ │ │ ├── use-set-state.ts
│ │ │ ├── use-throttled-callback.ts
│ │ │ ├── use-throttled-value.ts
│ │ │ └── use-timeout.ts
│ │ ├── styles/
│ │ │ ├── ag-grid.css
│ │ │ └── global.css
│ │ ├── themes/
│ │ │ ├── app-theme-types.ts
│ │ │ ├── app-theme.ts
│ │ │ ├── ayu-dark/
│ │ │ │ └── ayu-dark.ts
│ │ │ ├── ayu-light/
│ │ │ │ └── ayu-light.ts
│ │ │ ├── catppuccin-latte/
│ │ │ │ └── catppuccin-latte.ts
│ │ │ ├── catppuccin-mocha/
│ │ │ │ └── catppuccin-mocha.ts
│ │ │ ├── default-dark/
│ │ │ │ └── default-dark.ts
│ │ │ ├── default-light/
│ │ │ │ └── default-light.ts
│ │ │ ├── default.ts
│ │ │ ├── dracula/
│ │ │ │ └── dracula.ts
│ │ │ ├── github-dark/
│ │ │ │ └── github-dark.ts
│ │ │ ├── github-light/
│ │ │ │ └── github-light.ts
│ │ │ ├── glassy-dark/
│ │ │ │ ├── glassy-dark.ts
│ │ │ │ └── glassy_overrides.css
│ │ │ ├── gruvbox-dark/
│ │ │ │ └── gruvbox-dark.ts
│ │ │ ├── gruvbox-light/
│ │ │ │ └── gruvbox-light.ts
│ │ │ ├── high-contrast-dark/
│ │ │ │ └── high-contrast-dark.ts
│ │ │ ├── high-contrast-light/
│ │ │ │ └── high-contrast-light.ts
│ │ │ ├── material-dark/
│ │ │ │ └── material-dark.ts
│ │ │ ├── material-light/
│ │ │ │ └── material-light.ts
│ │ │ ├── monokai/
│ │ │ │ └── monokai.ts
│ │ │ ├── night-owl/
│ │ │ │ └── night-owl.ts
│ │ │ ├── nord/
│ │ │ │ └── nord.ts
│ │ │ ├── one-dark/
│ │ │ │ └── one-dark.ts
│ │ │ ├── rose-pine/
│ │ │ │ └── rose-pine.ts
│ │ │ ├── rose-pine-dawn/
│ │ │ │ └── rose-pine-dawn.ts
│ │ │ ├── rose-pine-moon/
│ │ │ │ └── rose-pine-moon.ts
│ │ │ ├── shades-of-purple/
│ │ │ │ └── shades-of-purple.ts
│ │ │ ├── solarized-dark/
│ │ │ │ └── solarized-dark.ts
│ │ │ ├── solarized-light/
│ │ │ │ └── solarized-light.ts
│ │ │ ├── tokyo-night/
│ │ │ │ └── tokyo-night.ts
│ │ │ ├── vscode-dark-plus/
│ │ │ │ └── vscode-dark-plus.ts
│ │ │ └── vscode-light-plus/
│ │ │ └── vscode-light-plus.ts
│ │ ├── types/
│ │ │ ├── css-modules.d.ts
│ │ │ ├── domain-types.ts
│ │ │ ├── drag-and-drop.ts
│ │ │ ├── features-types.ts
│ │ │ ├── remote-types.ts
│ │ │ └── types.ts
│ │ └── utils/
│ │ ├── create-polymorphic-component.ts
│ │ ├── create-use-external-events.ts
│ │ ├── double-click-handler.ts
│ │ ├── is-light-color.ts
│ │ └── string-to-color.ts
│ └── types/
│ ├── mantine.d.ts
│ └── mpris-service.d.ts
├── tsconfig.json
├── tsconfig.node.json
├── tsconfig.web.json
└── web.vite.config.ts
================================================
FILE CONTENTS
================================================
================================================
FILE: .dockerignore
================================================
node_modules
Dockerfile
docker-compose.*
================================================
FILE: .editorconfig
================================================
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 4
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
================================================
FILE: .gitattributes
================================================
* text eol=lf
*.exe binary
*.png binary
*.jpg binary
*.jpeg binary
*.ico binary
*.icns binary
*.webp binary
*.eot binary
*.otf binary
*.ttf binary
*.woff binary
*.woff2 binary
================================================
FILE: .github/FUNDING.yml
================================================
# These are supported funding model platforms
ko_fi: jeffvli
================================================
FILE: .github/ISSUE_TEMPLATE/01-feature_request.yml
================================================
name: Feature request
description: Request a feature to be added to Feishin
title: '[Feature]: '
labels: ['enhancement']
body:
- type: checkboxes
id: check-duplicate
attributes:
label: I have already checked through the existing feature requests and found no duplicates
options:
- label: 'Yes'
required: true
- type: dropdown
id: server-specific
attributes:
label: Is this a server-specific feature?
options:
- Not server-specific
- OpenSubsonic
- Jellyfin
- Navidrome
default: 0
validations:
required: true
- type: textarea
id: solution
attributes:
label: What do you want to be added?
placeholder: I would like to see [...]
validations:
required: true
================================================
FILE: .github/ISSUE_TEMPLATE/02-bug_report.yml
================================================
name: Bug report
description: You're having technical issues.
title: '[Bug]: '
labels: ['bug']
body:
- type: checkboxes
id: check-duplicate
attributes:
label: I have already checked through the existing bug reports and found no duplicates
options:
- label: 'Yes'
required: true
- type: input
id: version
attributes:
label: App Version
description: What version of the app are you running?
placeholder: ex. 1.0.0
validations:
required: true
- type: input
id: server-version
attributes:
label: Music Server and Version
description: What music server are you using?
placeholder: ex. Navidrome v0.55.0, LMS v3.67.0, Jellyfin v10.10.7, etc.
validations:
required: true
- type: dropdown
id: environments
attributes:
label: What local environments are you seeing the problem on?
multiple: true
options:
- Desktop Windows
- Desktop macOS
- Desktop Linux
- Web Firefox
- Web Chrome
- Web Safari
- Web Microsoft Edge
- Other (please specify in the next field)
- type: textarea
id: what-happened
attributes:
label: What happened?
description: Also tell us, what did you expect to happen?
placeholder: Include screenshots and error logs if possible. The browser devtools can be opened using CTRL + SHIFT + I (Windows/Linux) or CMD + SHIFT + I (macOS).
validations:
required: true
- type: textarea
id: reproduction
attributes:
label: Steps to reproduce
description: How can we reproduce this issue? Are there any specific settings that are enabled that could be the cause?
placeholder: |
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
validations:
required: true
- type: textarea
id: logs
attributes:
label: Relevant log output
description: Please copy and paste any relevant log output. This will be automatically formatted into code.
render: shell
================================================
FILE: .github/ISSUE_TEMPLATE/config.yml
================================================
blank_issues_enabled: false
contact_links:
- name: Questions or help
url: https://github.com/jeffvli/feishin/discussions
about: Ask questions or get help in the discussions section
- name: Discord Community
url: https://discord.gg/FVKpcMDy5f
about: The discord/matrix servers are bridged so you can join whichever you prefer
- name: Matrix Community
url: https://matrix.to/#/#sonixd:matrix.org
about: The discord/matrix servers are bridged so you can join whichever you prefer
================================================
FILE: .github/config.yml
================================================
requiredHeaders:
- Prerequisites
- Expected Behavior
- Current Behavior
- Possible Solution
- Your Environment
================================================
FILE: .github/stale.yml
================================================
# Number of days of inactivity before an issue becomes stale
daysUntilStale: 60
# Number of days of inactivity before a stale issue is closed
daysUntilClose: 7
# Issues with these labels will never be considered stale
exemptLabels:
- discussion
- security
# Label to use when marking an issue as stale
staleLabel: wontfix
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: >
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.
# Comment to post when closing a stale issue. Set to `false` to disable
closeComment: false
================================================
FILE: .github/workflows/publish-alpha.yml
================================================
# Alpha builds published to Cloudflare R2 with date versioning (e.g. 1.0.0-alpha-20260205).
# Required repo secrets: R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY (from R2 API token in Cloudflare dashboard).
name: Publish Alpha
on:
workflow_dispatch:
inputs:
version:
description: 'Semantic version number (e.g., 1.0.0) - alpha suffix will be added automatically'
required: false
type: string
schedule:
# Run at 3:00 AM PST daily (11:00 UTC; PST = UTC-8)
- cron: '0 11 * * *'
jobs:
check-new-commits:
runs-on: ubuntu-latest
outputs:
has_new_commits: ${{ steps.manual.outputs.has_new_commits || steps.check.outputs['has-new-commits'] }}
steps:
- name: Set has new commits (manual trigger)
id: manual
if: github.event_name == 'workflow_dispatch'
run: echo "has_new_commits=true" >> "$GITHUB_OUTPUT"
- name: Check for new commits (24 hr interval)
id: check
if: github.event_name != 'workflow_dispatch'
uses: adriangl/check-new-commits-action@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
seconds: 86400
prepare:
needs: check-new-commits
if: needs.check-new-commits.outputs.has_new_commits == 'true'
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
steps:
- name: Checkout git repo
uses: actions/checkout@v6
- name: Install Node and PNPM
uses: pnpm/action-setup@v4
with:
version: 10
- name: Install dependencies
run: pnpm install
- name: Set date-based alpha version
id: version
shell: pwsh
run: |
$inputVersion = "${{ github.event.inputs.version }}"
Write-Host "Input version: $inputVersion"
if ($inputVersion -eq "" -or $inputVersion -eq "null") {
# No input version provided (scheduled run or manual without input), auto-increment patch version
Write-Host "No version provided, auto-incrementing patch version..."
$currentVersion = (Get-Content package.json | ConvertFrom-Json).version
Write-Host "Current version: $currentVersion"
$cleanVersion = $currentVersion -replace '-.*$', ''
$versionParts = $cleanVersion.Split('.')
if ($versionParts.Length -ne 3) {
Write-Error "Current version format is invalid: $cleanVersion"
exit 1
}
$major = [int]$versionParts[0]
$minor = [int]$versionParts[1]
$patch = [int]$versionParts[2]
$newPatch = $patch + 1
$inputVersion = "$major.$minor.$newPatch"
Write-Host "Auto-generated version: $inputVersion"
} else {
# Validate semantic version format (major.minor.patch)
$versionPattern = '^\d+\.\d+\.\d+$'
if ($inputVersion -notmatch $versionPattern) {
Write-Error "Invalid version format. Expected semantic version (e.g., 1.0.0), got: $inputVersion"
exit 1
}
}
# Date in YYYYMMDD (PST / America/Los_Angeles)
$pst = [TimeZoneInfo]::FindSystemTimeZoneById('America/Los_Angeles')
$dateInPst = [TimeZoneInfo]::ConvertTimeFromUtc([DateTime]::UtcNow, $pst)
$dateStr = $dateInPst.ToString("yyyyMMdd")
$alphaVersion = "$inputVersion-alpha-$dateStr"
Write-Host "Alpha version: $alphaVersion"
# Update package.json
$packageJson = Get-Content package.json | ConvertFrom-Json
$packageJson.version = $alphaVersion
$packageJson | ConvertTo-Json -Depth 10 | Set-Content package.json
echo "version=$alphaVersion" >> $env:GITHUB_OUTPUT
cleanup:
needs: prepare
runs-on: ubuntu-latest
env:
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
R2_ENDPOINT_URL: ${{ secrets.R2_ENDPOINT_URL }}
steps:
- name: Delete all objects in R2 bucket
run: |
aws s3 rm s3://feishin-nightly --recursive --endpoint-url $R2_ENDPOINT_URL
publish:
needs: [prepare, cleanup]
runs-on: ${{ matrix.os }}
env:
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
strategy:
matrix:
os: [windows-latest, macos-latest, ubuntu-latest]
steps:
- name: Checkout git repo
uses: actions/checkout@v6
- name: Install Node and PNPM
uses: pnpm/action-setup@v4
with:
version: 10
- name: Install dependencies
run: pnpm install
- name: Set version from prepare job
shell: pwsh
run: |
$version = "${{ needs.prepare.outputs.version }}"
Write-Host "Setting version: $version"
$packageJson = Get-Content package.json | ConvertFrom-Json
$packageJson.version = $version
$packageJson | ConvertTo-Json -Depth 10 | Set-Content package.json
- name: Build and Publish to R2 (Windows)
if: matrix.os == 'windows-latest'
uses: nick-invision/retry@v3.0.2
with:
timeout_minutes: 30
max_attempts: 3
retry_on: error
command: |
pnpm run publish:win:alpha
on_retry_command: pnpm cache delete
- name: Build and Publish to R2 (macOS)
if: matrix.os == 'macos-latest'
uses: nick-invision/retry@v3.0.2
with:
timeout_minutes: 30
max_attempts: 3
retry_on: error
command: |
pnpm run publish:mac:alpha
on_retry_command: pnpm cache delete
- name: Build and Publish to R2 (Linux)
if: matrix.os == 'ubuntu-latest'
uses: nick-invision/retry@v3.0.2
with:
timeout_minutes: 30
max_attempts: 3
retry_on: error
command: |
pnpm run publish:linux:alpha
on_retry_command: pnpm cache delete
- name: Build and Publish to R2 (Linux ARM64)
if: matrix.os == 'ubuntu-latest'
uses: nick-invision/retry@v3.0.2
with:
timeout_minutes: 30
max_attempts: 3
retry_on: error
command: |
pnpm run publish:linux-arm64:alpha
on_retry_command: pnpm cache delete
================================================
FILE: .github/workflows/publish-beta.yml
================================================
name: Publish Beta (Manual)
on:
workflow_dispatch:
inputs:
version:
description: 'Semantic version number (e.g., 1.0.0) - beta suffix will be added automatically'
required: false
type: string
jobs:
prepare:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
steps:
- name: Checkout git repo
uses: actions/checkout@v6
- name: Install Node and PNPM
uses: pnpm/action-setup@v4
with:
version: 10
- name: Install dependencies
run: pnpm install
- name: Validate and set version with incrementing beta suffix
id: version
shell: pwsh
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
$inputVersion = "${{ github.event.inputs.version }}"
Write-Host "Input version: $inputVersion"
if ($inputVersion -eq "" -or $inputVersion -eq "null") {
# No input version provided, auto-increment patch version
Write-Host "No version provided, auto-incrementing patch version..."
# Get current version from package.json
$currentVersion = (Get-Content package.json | ConvertFrom-Json).version
Write-Host "Current version: $currentVersion"
# Remove any existing suffix (like -beta) to get clean semantic version
$cleanVersion = $currentVersion -replace '-.*$', ''
# Extract major, minor, patch components
$versionParts = $cleanVersion.Split('.')
if ($versionParts.Length -ne 3) {
Write-Error "Current version format is invalid: $cleanVersion"
exit 1
}
$major = [int]$versionParts[0]
$minor = [int]$versionParts[1]
$patch = [int]$versionParts[2]
# Increment patch version
$newPatch = $patch + 1
$inputVersion = "$major.$minor.$newPatch"
Write-Host "Auto-generated version: $inputVersion"
} else {
# Validate semantic version format (major.minor.patch)
$versionPattern = '^\d+\.\d+\.\d+$'
if ($inputVersion -notmatch $versionPattern) {
Write-Error "Invalid version format. Expected semantic version (e.g., 1.0.0), got: $inputVersion"
exit 1
}
}
# Check for existing beta releases with the same base version
Write-Host "Checking for existing beta releases with base version: $inputVersion"
$existingReleases = gh release list --limit 100 --json tagName,isPrerelease | ConvertFrom-Json | Where-Object { $_.isPrerelease -eq $true }
$maxBetaNumber = 0
foreach ($release in $existingReleases) {
$tagName = $release.tagName
Write-Host "Checking tag: $tagName"
# Extract beta number from tag name (format: v1.0.0-beta.1)
if ($tagName -match "v$([regex]::Escape($inputVersion))-beta\.(\d+)$") {
$betaNumber = [int]$matches[1]
Write-Host "Found beta release with number: $betaNumber"
if ($betaNumber -gt $maxBetaNumber) {
$maxBetaNumber = $betaNumber
}
}
}
# Calculate next beta number
$nextBetaNumber = $maxBetaNumber + 1
Write-Host "Next beta number: $nextBetaNumber"
# Create beta suffix with incrementing number
$betaSuffix = "beta.$nextBetaNumber"
$versionWithBeta = "$inputVersion-$betaSuffix"
Write-Host "Setting version to: $versionWithBeta"
# Update package.json
$packageJson = Get-Content package.json | ConvertFrom-Json
$packageJson.version = $versionWithBeta
$packageJson | ConvertTo-Json -Depth 10 | Set-Content package.json
Write-Host "Updated package.json version to: $versionWithBeta"
# Set output for other jobs
echo "version=$versionWithBeta" >> $env:GITHUB_OUTPUT
publish:
needs: prepare
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [windows-latest, macos-latest, ubuntu-latest]
steps:
- name: Checkout git repo
uses: actions/checkout@v6
- name: Install Node and PNPM
uses: pnpm/action-setup@v4
with:
version: 10
- name: Install dependencies
run: pnpm install
- name: Set version from prepare job
shell: pwsh
run: |
$versionWithBeta = "${{ needs.prepare.outputs.version }}"
Write-Host "Setting version from prepare job: $versionWithBeta"
# Update package.json with the version from prepare job
$packageJson = Get-Content package.json | ConvertFrom-Json
$packageJson.version = $versionWithBeta
$packageJson | ConvertTo-Json -Depth 10 | Set-Content package.json
Write-Host "Updated package.json version to: $versionWithBeta"
- name: Build and Publish releases (Windows)
if: matrix.os == 'windows-latest'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
uses: nick-invision/retry@v3.0.2
with:
timeout_minutes: 30
max_attempts: 3
retry_on: error
command: |
pnpm run publish:win:beta
on_retry_command: pnpm cache delete
- name: Build and Publish releases (macOS)
if: matrix.os == 'macos-latest'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
uses: nick-invision/retry@v3.0.2
with:
timeout_minutes: 30
max_attempts: 3
retry_on: error
command: |
pnpm run publish:mac:beta
on_retry_command: pnpm cache delete
- name: Build and Publish releases (Linux)
if: matrix.os == 'ubuntu-latest'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
uses: nick-invision/retry@v3.0.2
with:
timeout_minutes: 30
max_attempts: 3
retry_on: error
command: |
pnpm run publish:linux:beta
on_retry_command: pnpm cache delete
- name: Build and Publish releases (Linux ARM64)
if: matrix.os == 'ubuntu-latest'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
uses: nick-invision/retry@v3.0.2
with:
timeout_minutes: 30
max_attempts: 3
retry_on: error
command: |
pnpm run publish:linux-arm64:beta
on_retry_command: pnpm cache delete
edit-release:
needs: [prepare, publish]
runs-on: ubuntu-latest
steps:
- name: Checkout git repo
uses: actions/checkout@v6
- name: Edit release with commits and title
shell: pwsh
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# Get the version from the prepare job
$versionWithBeta = "${{ needs.prepare.outputs.version }}"
$tagVersion = "v" + $versionWithBeta
Write-Host "Editing release for tag: $tagVersion"
# Check if release exists
$releaseExists = gh release view $tagVersion 2>$null
if ($LASTEXITCODE -eq 0) {
Write-Host "Found release with tag $tagVersion"
# Get current release notes
# Find the latest non-prerelease tag
Write-Host "Finding latest non-prerelease tag..."
$latestNonPrerelease = gh release list --limit 100 --json tagName,isPrerelease | ConvertFrom-Json | Where-Object { $_.isPrerelease -eq $false -and $_.tagName -ne $tagVersion } | Select-Object -First 1
if ($latestNonPrerelease) {
$latestTag = $latestNonPrerelease.tagName
Write-Host "Latest non-prerelease tag: $latestTag"
# Get commits between latest non-prerelease and current HEAD
Write-Host "Getting commits between $latestTag and HEAD..."
# Use proper git range syntax and handle PowerShell string interpolation
$gitRange = "$latestTag..HEAD"
Write-Host "Git range: $gitRange"
# Get commits using proper git command with datetime
$commits = git log --oneline --pretty=format:"%ad|%s|%h" --date=short $gitRange
# Check if commits exist
if ($commits -and $commits.Trim() -ne "") {
Write-Host "Found commits:"
Write-Host $commits
# Group commits by date
$groupedCommits = @{}
foreach ($line in $commits) {
if ($line.Trim() -ne "") {
$parts = $line.Split('|')
$date = $parts[0]
$message = $parts[1]
$hash = $parts[2]
if (-not $groupedCommits.ContainsKey($date)) {
$groupedCommits[$date] = @()
}
$groupedCommits[$date] += "- $message ($hash)"
}
}
# Build formatted release notes grouped by date
$commitNotes = "## Changes since $latestTag`n`n"
$sortedDates = $groupedCommits.Keys | Sort-Object -Descending
foreach ($date in $sortedDates) {
$commitNotes += "### $date`n"
foreach ($commit in $groupedCommits[$date]) {
$commitNotes += "$commit`n"
}
$commitNotes += "`n"
}
$releaseNotes = $commitNotes
} else {
Write-Host "No commits found between $latestTag and HEAD"
Write-Host "Trying alternative approach..."
# Alternative: get commits since the tag (not range) with datetime
$commits = git log --oneline --pretty=format:"%ad|%s|%h" --date=short $latestTag.. --not $latestTag
if ($commits -and $commits.Trim() -ne "") {
Write-Host "Found commits with alternative method:"
Write-Host $commits
# Group commits by date
$groupedCommits = @{}
foreach ($line in $commits) {
if ($line.Trim() -ne "") {
$parts = $line.Split('|')
$date = $parts[0]
$message = $parts[1]
$hash = $parts[2]
if (-not $groupedCommits.ContainsKey($date)) {
$groupedCommits[$date] = @()
}
$groupedCommits[$date] += "- $message ($hash)"
}
}
# Build formatted release notes grouped by date
$commitNotes = "## Changes since $latestTag`n`n"
$sortedDates = $groupedCommits.Keys | Sort-Object -Descending
foreach ($date in $sortedDates) {
$commitNotes += "### $date`n"
foreach ($commit in $groupedCommits[$date]) {
$commitNotes += "$commit`n"
}
$commitNotes += "`n"
}
$releaseNotes = $commitNotes
} else {
Write-Host "Still no commits found, using basic release notes"
$releaseNotes = "## Beta Release`n`nThis is a beta release."
}
}
} else {
Write-Host "No non-prerelease tags found, using basic release notes"
$releaseNotes = "## Beta Release`n`nThis is a beta release."
}
# Prepend beta update instructions to release notes
$betaInstructions = "To receive automatic beta updates, set the release channel to ``Beta`` under ``Advanced`` settings.`n`n"
$releaseNotes = $betaInstructions + $releaseNotes
# Update the release with new title and notes
Write-Host "Updating release with title 'Beta' and new notes..."
gh release edit $tagVersion --title "Beta" --notes "$releaseNotes"
Write-Host "Successfully updated release title to 'Beta' and added commit notes"
} else {
Write-Host "No release found with tag $tagVersion"
}
- name: Set release as prerelease
shell: pwsh
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# Get the version from the prepare job
$versionWithBeta = "${{ needs.prepare.outputs.version }}"
$tagVersion = "v" + $versionWithBeta
Write-Host "Setting release as prerelease for tag: $tagVersion"
gh release edit $tagVersion --prerelease --draft=false
Write-Host "Successfully set release as prerelease"
cleanup:
needs: [prepare, publish, edit-release]
runs-on: ubuntu-latest
steps:
- name: Checkout git repo
uses: actions/checkout@v6
- name: Delete existing prereleases
shell: pwsh
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# Get the current version that was just created
$versionWithBeta = "${{ needs.prepare.outputs.version }}"
Write-Host "Current release version: $versionWithBeta"
# Find and delete any old prereleases (excluding the current one)
Write-Host "Deleting old prereleases..."
Write-Host "Searching for releases with isPrerelease 'true'..."
$betaReleases = gh release list --limit 100 --json tagName,isPrerelease,name | ConvertFrom-Json | Where-Object { $_.isPrerelease -eq $true }
if ($betaReleases) {
Write-Host "Found $($betaReleases.Count) release(s) with isPrerelease 'true':"
foreach ($release in $betaReleases) {
$tagName = $release.tagName
# Skip the current release
if ($tagName -ne "v$versionWithBeta") {
Write-Host " - Tag: $tagName, Title: $($release.name)"
gh release delete $tagName --yes --cleanup-tag
Write-Host " Deleted release with tag: $tagName"
} else {
Write-Host " - Skipping current release: $tagName"
}
}
} else {
Write-Host "No releases found with isPrerelease 'true'"
}
================================================
FILE: .github/workflows/publish-docker-auto.yml
================================================
# Referenced from: https://docs.github.com/en/actions/publishing-packages/publishing-docker-images#introduction
name: Publish Docker to GHCR
permissions: write-all
on:
workflow_dispatch:
push:
tags:
- 'v*.*.*'
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build-and-push-image:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Log in to the Container registry
uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=pr
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:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
platforms: |
linux/amd64
linux/arm/v7
linux/arm64/v8
================================================
FILE: .github/workflows/publish-docker.yml
================================================
# Referenced from: https://docs.github.com/en/actions/publishing-packages/publishing-docker-images#introduction
name: Publish Docker to GHCR (Manual)
on: workflow_dispatch
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build-and-push-image:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Log in to the Container registry
uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
- 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:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
platforms: |
linux/amd64
linux/arm/v7
linux/arm64/v8
================================================
FILE: .github/workflows/publish-linux.yml
================================================
name: Publish Linux (Manual)
on: workflow_dispatch
jobs:
publish:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest]
steps:
- name: Checkout git repo
uses: actions/checkout@v6
- name: Install Node and PNPM
uses: pnpm/action-setup@v4
with:
version: 10
- name: Install dependencies
run: pnpm install
- name: Build and Publish releases
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
uses: nick-invision/retry@v3.0.2
with:
timeout_minutes: 30
max_attempts: 3
retry_on: error
command: |
pnpm run publish:linux
on_retry_command: pnpm cache delete
- name: Build and Publish releases (arm64)
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
uses: nick-invision/retry@v3.0.2
with:
timeout_minutes: 30
max_attempts: 3
retry_on: error
command: |
pnpm run publish:linux-arm64
on_retry_command: pnpm cache delete
================================================
FILE: .github/workflows/publish-macos.yml
================================================
name: Publish macOS (Manual)
on: workflow_dispatch
jobs:
publish:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [macos-latest]
steps:
- name: Checkout git repo
uses: actions/checkout@v6
- name: Install Node and PNPM
uses: pnpm/action-setup@v4
with:
version: 10
- name: Install dependencies
run: pnpm install
- name: Build and Publish releases
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
uses: nick-invision/retry@v3.0.2
with:
timeout_minutes: 30
max_attempts: 3
retry_on: error
command: |
pnpm run publish:mac
on_retry_command: pnpm cache delete
================================================
FILE: .github/workflows/publish-pr-comment.yml
================================================
name: Comment on pull request
on:
workflow_run:
workflows: ['Publish (PR)']
types: [completed]
jobs:
pr_comment:
if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v6
with:
# This snippet is public-domain, taken from
# https://github.com/oprypin/nightly.link/blob/master/.github/workflows/pr-comment.yml
script: |
async function upsertComment(owner, repo, issue_number, purpose, body) {
const {data: comments} = await github.rest.issues.listComments(
{owner, repo, issue_number});
const marker = `<!-- bot: ${purpose} -->`;
body = marker + "\n" + body;
const existing = comments.filter((c) => c.body.includes(marker));
if (existing.length > 0) {
const last = existing[existing.length - 1];
core.info(`Updating comment ${last.id}`);
await github.rest.issues.updateComment({
owner, repo,
body,
comment_id: last.id,
});
} else {
core.info(`Creating a comment in issue / PR #${issue_number}`);
await github.rest.issues.createComment({issue_number, body, owner, repo});
}
}
const {owner, repo} = context.repo;
const run_id = ${{github.event.workflow_run.id}};
const pull_requests = ${{ toJSON(github.event.workflow_run.pull_requests) }};
if (!pull_requests.length) {
return core.error("This workflow doesn't match any pull requests!");
}
const artifacts = await github.paginate(
github.rest.actions.listWorkflowRunArtifacts, {owner, repo, run_id});
if (!artifacts.length) {
return core.error(`No artifacts found`);
}
let body = `Download the artifacts for this pull request:\n`;
for (const art of artifacts) {
body += `\n* [${art.name}.zip](https://nightly.link/${owner}/${repo}/actions/artifacts/${art.id}.zip)`;
}
core.info("Review thread message body:", body);
for (const pr of pull_requests) {
await upsertComment(owner, repo, pr.number,
"nightly-link", body);
}
================================================
FILE: .github/workflows/publish-pr.yml
================================================
name: Publish (PR)
on:
workflow_dispatch:
pull_request:
branches:
- development
paths:
- 'src/**'
- 'electron-builder*.yml'
jobs:
wait-for-lint:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- name: Wait for Test workflow to complete
uses: lewagon/wait-on-check-action@v1.4.1
with:
ref: ${{ github.event.pull_request.head.sha }}
check-name: 'lint'
repo-token: ${{ secrets.GITHUB_TOKEN }}
wait-interval: 10
allowed-conclusions: success
publish:
needs: wait-for-lint
if: always() && (needs.wait-for-lint.result == 'success' || needs.wait-for-lint.result == 'skipped')
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [macos-latest, ubuntu-latest, windows-latest]
steps:
- name: Checkout git repo
uses: actions/checkout@v6
- name: Install Node and PNPM
uses: pnpm/action-setup@v4
with:
version: 10
- name: Install dependencies
run: pnpm install
- name: Build for Windows
if: ${{ matrix.os == 'windows-latest' }}
uses: nick-invision/retry@v3.0.2
with:
timeout_minutes: 30
max_attempts: 3
retry_on: error
command: |
pnpm run package:win:pr
- name: Build for Linux
if: ${{ matrix.os == 'ubuntu-latest' }}
uses: nick-invision/retry@v3.0.2
with:
timeout_minutes: 30
max_attempts: 3
retry_on: error
command: |
pnpm run package:linux:pr
- name: Build for MacOS
if: ${{ matrix.os == 'macos-latest' }}
uses: nick-invision/retry@v3.0.2
with:
timeout_minutes: 30
max_attempts: 3
retry_on: error
command: |
pnpm run package:mac:pr
- name: Zip Windows Binaries
if: ${{ matrix.os == 'windows-latest' }}
shell: pwsh
run: |
Compress-Archive -Path "dist/*.exe" -DestinationPath "dist/windows-binaries.zip" -Force
- name: Zip Linux Binaries
if: ${{ matrix.os == 'ubuntu-latest' }}
run: |
zip -r dist/linux-binaries.zip dist/*.{AppImage,deb,rpm}
- name: Zip MacOS Binaries
if: ${{ matrix.os == 'macos-latest' }}
run: |
zip -r dist/macos-binaries.zip dist/*.dmg
- name: Upload Windows Binaries
if: ${{ matrix.os == 'windows-latest' }}
uses: actions/upload-artifact@v7
with:
name: windows-binaries
path: dist/windows-binaries.zip
- name: Upload Linux Binaries
if: ${{ matrix.os == 'ubuntu-latest' }}
uses: actions/upload-artifact@v7
with:
name: linux-binaries
path: dist/linux-binaries.zip
- name: Upload MacOS Binaries
if: ${{ matrix.os == 'macos-latest' }}
uses: actions/upload-artifact@v7
with:
name: macos-binaries
path: dist/macos-binaries.zip
================================================
FILE: .github/workflows/publish-windows.yml
================================================
name: Publish Windows (Manual)
on: workflow_dispatch
jobs:
publish:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [windows-latest]
steps:
- name: Checkout git repo
uses: actions/checkout@v6
- name: Install Node and PNPM
uses: pnpm/action-setup@v4
with:
version: 10
- name: Install dependencies
run: pnpm install
- name: Build and Publish releases
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
uses: nick-invision/retry@v3.0.2
with:
timeout_minutes: 30
max_attempts: 3
retry_on: error
command: |
pnpm run publish:win
on_retry_command: pnpm cache delete
================================================
FILE: .github/workflows/publish-winget.yml
================================================
name: Publish release to WinGet
on:
release:
types: [released]
workflow_dispatch:
inputs:
tag_name:
description: "Specific tag name"
required: false
type: string
jobs:
publish:
runs-on: windows-latest
steps:
- uses: vedantmgoyal9/winget-releaser@main
with:
identifier: jeffvli.Feishin
installers-regex: 'Feishin-*-win-(x64|arm64)\.exe'
token: ${{ secrets.WINGET_ACC_TOKEN }}
================================================
FILE: .github/workflows/publish.yml
================================================
name: Publish (Manual)
on: workflow_dispatch
jobs:
publish:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [windows-latest, macos-latest, ubuntu-latest]
steps:
- name: Checkout git repo
uses: actions/checkout@v6
- name: Install Node and PNPM
uses: pnpm/action-setup@v4
with:
version: 10
- name: Install dependencies
run: pnpm install
- name: Build and Publish releases (Windows)
if: matrix.os == 'windows-latest'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
uses: nick-invision/retry@v3.0.2
with:
timeout_minutes: 30
max_attempts: 3
retry_on: error
command: |
pnpm run publish:win
on_retry_command: pnpm cache delete
- name: Build and Publish releases (macOS)
if: matrix.os == 'macos-latest'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
uses: nick-invision/retry@v3.0.2
with:
timeout_minutes: 30
max_attempts: 3
retry_on: error
command: |
pnpm run publish:mac
on_retry_command: pnpm cache delete
- name: Build and Publish releases (Linux)
if: matrix.os == 'ubuntu-latest'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
uses: nick-invision/retry@v3.0.2
with:
timeout_minutes: 30
max_attempts: 3
retry_on: error
command: |
pnpm run publish:linux
on_retry_command: pnpm cache delete
- name: Build and Publish releases (Linux ARM64)
if: matrix.os == 'ubuntu-latest'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
uses: nick-invision/retry@v3.0.2
with:
timeout_minutes: 30
max_attempts: 3
retry_on: error
command: |
pnpm run publish:linux-arm64
on_retry_command: pnpm cache delete
================================================
FILE: .github/workflows/stale.yml
================================================
name: 'Close stale issues and PRs'
on:
workflow_dispatch:
schedule:
- cron: '30 1 * * *'
permissions:
contents: read
jobs:
stale:
permissions:
issues: write
pull-requests: write
runs-on: ubuntu-latest
steps:
- uses: dessant/lock-threads@v5
with:
process-only: 'issues, prs'
issue-inactive-days: 120
pr-inactive-days: 120
log-output: true
add-issue-labels: 'frozen-due-to-age'
add-pr-labels: 'frozen-due-to-age'
- uses: actions/stale@v9
with:
operations-per-run: 999
days-before-issue-stale: 180
days-before-pr-stale: 180
days-before-issue-close: 30
days-before-pr-close: 30
stale-issue-message: >
This issue has been automatically marked as stale because it has not had recent activity. The resources of the Feishin team are limited, and so we are asking for your help.
If this is a **bug** and you can still reproduce this error on the <code>development</code> branch, please reply with all of the information you have about it in order to keep the issue open.
This issue will automatically be closed in the near future if no further activity occurs. Thank you for all your contributions.
stale-pr-message: >
This PR has been automatically marked as stale because it has not had recent activity. The resources of the Feishin team are limited, and so we are asking for your help.
This PR will automatically be closed in the near future if no further activity occurs. Thank you for all your contributions.
stale-issue-label: 'stale'
exempt-issue-labels: 'keep,security'
stale-pr-label: 'stale'
exempt-pr-labels: 'keep,security'
================================================
FILE: .github/workflows/test.yml
================================================
name: Test
on: [push, pull_request]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- name: Check out Git repository
uses: actions/checkout@v6
- name: Install Node.js and PNPM
uses: pnpm/action-setup@v4
with:
version: 10
- name: Install dependencies
run: pnpm install
- name: Lint Files
run: pnpm run lint
================================================
FILE: .gitignore
================================================
node_modules
dist
out
.DS_Store
.eslintcache
*.log*
release
================================================
FILE: .npmrc
================================================
legacy-peer-deps=true
================================================
FILE: .prettierignore
================================================
out
dist
pnpm-lock.yaml
LICENSE.md
tsconfig.json
tsconfig.*.json
================================================
FILE: .prettierrc.yaml
================================================
singleQuote: true
semi: true
printWidth: 100
tabWidth: 4
trailingComma: all
useTabs: false
arrowParens: always
proseWrap: never
htmlWhitespaceSensitivity: strict
endOfLine: lf
singleAttributePerLine: false
bracketSpacing: true
plugins:
- prettier-plugin-packagejson
================================================
FILE: .stylelintrc.json
================================================
{
"extends": [
"stylelint-config-standard",
"stylelint-config-css-modules",
"stylelint-config-recess-order"
],
"rules": {
"block-no-empty": null,
"selector-type-case": ["lower", { "ignoreTypes": ["/^\\$\\w+/"] }],
"selector-type-no-unknown": [true, { "ignoreTypes": ["/-styled-mixin/", "/^\\$\\w+/"] }],
"declaration-block-no-shorthand-property-overrides": null,
"declaration-block-no-redundant-longhand-properties": null,
"at-rule-no-unknown": [true, { "ignoreAtRules": ["mixin", "value"] }],
"function-no-unknown": [true, { "ignoreFunctions": ["darken", "alpha", "lighten"] }],
"declaration-property-value-no-unknown": null,
"no-descending-specificity": null,
"no-empty-source": null
}
}
================================================
FILE: .vscode/extensions.json
================================================
{
"recommendations": ["dbaeumer.vscode-eslint"]
}
================================================
FILE: .vscode/launch.json
================================================
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug Main Process",
"type": "node",
"request": "launch",
"cwd": "${workspaceRoot}",
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite",
"windows": {
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite.cmd"
},
"runtimeArgs": ["--sourcemap"],
"env": {
"REMOTE_DEBUGGING_PORT": "9222"
}
},
{
"name": "Debug Renderer Process",
"port": 9222,
"request": "attach",
"type": "chrome",
"webRoot": "${workspaceFolder}/src/renderer",
"timeout": 60000,
"presentation": {
"hidden": true
}
}
],
"compounds": [
{
"name": "Debug All",
"configurations": ["Debug Main Process", "Debug Renderer Process"],
"presentation": {
"order": 1
}
}
]
}
================================================
FILE: .vscode/settings.json
================================================
{
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"files.associations": {
".eslintrc": "jsonc",
".prettierrc": "jsonc",
".eslintignore": "ignore"
},
"eslint.validate": ["typescript", "typescriptreact"],
"eslint.workingDirectories": [{ "directory": "./", "changeProcessCWD": true }],
"typescript.tsserver.experimental.enableProjectDiagnostics": false,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit",
"source.fixAll.stylelint": "explicit",
"source.organizeImports": "never",
"source.formatDocument": "explicit"
},
"css.validate": true,
"javascript.validate.enable": false,
"javascript.format.enable": false,
"typescript.format.enable": false,
"search.exclude": {
".git": true,
".eslintcache": true,
".erb/dll": true,
"release/{build,app/dist}": true,
"node_modules": true,
"npm-debug.log.*": true,
"test/**/__snapshots__": true,
"package-lock.json": true,
"*.{css,sass,scss}.d.ts": true,
"out/**/*": true,
"dist/**/*": true
},
"i18n-ally.localesPaths": ["src/i18n", "src/i18n/locales"],
"typescript.tsdk": "node_modules/typescript/lib",
"typescript.preferences.importModuleSpecifier": "non-relative",
"stylelint.config": null,
"stylelint.validate": ["css", "postcss"],
"typescript.updateImportsOnFileMove.enabled": "always",
"typescript.preferences.autoImportFileExcludePatterns": [
"@mantine/core",
"@mantine/modals",
"@mantine/dates",
"@mantine/hooks",
"@mantine/form",
"@radix-ui/react-context-menu"
],
"[typescriptreact]": { "editor.defaultFormatter": "esbenp.prettier-vscode" },
"typescript.format.insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces": true,
"folderTemplates.structures": [
{
"name": "TypeScript Feature Component With CSS Modules",
"omitParentDirectory": true,
"structure": [
{
"fileName": "<FTName | kebabcase>.tsx",
"template": "Functional Component with CSS Modules"
},
{
"fileName": "<FTName | kebabcase>.module.css"
}
]
}
],
"folderTemplates.fileTemplates": {
"Functional Component with CSS Modules": [
"import styles from './<FTName | kebabcase>.module.css';",
"",
"interface <FTName | pascalcase>Props {}",
"",
"export const <FTName | pascalcase> = ({}: <FTName | pascalcase>Props) => {",
" return <div></div>;",
"};"
]
}
}
================================================
FILE: CHANGELOG.md
================================================
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
================================================
FILE: Dockerfile
================================================
# --- Builder stage
FROM node:23-alpine AS builder
WORKDIR /app
# Copy package.json first to cache node_modules
COPY package.json pnpm-lock.yaml .
RUN npm install -g pnpm
RUN pnpm install
# Copy code and build with cached modules
COPY . .
RUN pnpm run build:web
# --- Production stage
FROM nginxinc/nginx-unprivileged:alpine-slim
COPY --chown=nginx:nginx --from=builder /app/out/web /usr/share/nginx/html
COPY --chown=nginx:nginx ./settings.js.template /etc/nginx/templates/settings.js.template
COPY --chown=nginx:nginx ng.conf.template /etc/nginx/templates/default.conf.template
ENV SERVER_LOCK=false SERVER_NAME="" SERVER_TYPE="" SERVER_URL="" REMOTE_URL=""
ENV LEGACY_AUTHENTICATION="" ANALYTICS_DISABLED="" PUBLIC_PATH="/"
EXPOSE 9180
CMD ["nginx", "-g", "daemon off;"]
================================================
FILE: LICENSE
================================================
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<http://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<http://www.gnu.org/philosophy/why-not-lgpl.html>.
================================================
FILE: README.md
================================================
<img src="assets/icons/icon.png" alt="logo" title="feishin" align="right" height="60px" width="60px" />
# Feishin
<p align="center">
<a href="https://github.com/jeffvli/feishin/blob/main/LICENSE">
<img src="https://img.shields.io/github/license/jeffvli/feishin?style=flat-square&color=brightgreen"
alt="License">
</a>
<a href="https://github.com/jeffvli/feishin/releases">
<img src="https://img.shields.io/github/v/release/jeffvli/feishin?style=flat-square&color=blue"
alt="Release">
</a>
<a href="https://github.com/jeffvli/feishin/releases">
<img src="https://img.shields.io/github/downloads/jeffvli/feishin/total?style=flat-square&color=orange"
alt="Downloads">
</a>
</p>
<p align="center">
<a href="https://discord.gg/FVKpcMDy5f">
<img src="https://img.shields.io/discord/922656312888811530?color=black&label=discord&logo=discord&logoColor=white"
alt="Discord">
</a>
<a href="https://matrix.to/#/#sonixd:matrix.org">
<img src="https://img.shields.io/matrix/sonixd:matrix.org?color=black&label=matrix&logo=matrix&logoColor=white"
alt="Matrix">
</a>
</p>
---
Rewrite of [Sonixd](https://github.com/jeffvli/sonixd).
## Features
- [x] MPV player backend
- [x] Web player backend
- [x] Modern UI
- [x] Scrobble playback to your server
- [x] Smart playlist editor (Navidrome)
- [x] Synchronized and unsynchronized lyrics support
- [ ] [Request a feature](https://github.com/jeffvli/feishin/issues) or [view taskboard](https://github.com/users/jeffvli/projects/5/views/1)
## Screenshots
<a href="./media/preview_full_screen_player.png"><img src="./media/preview_full_screen_player.png" width="49.5%"/></a> <a href="./media/preview_album_artist_detail.png"><img src="./media/preview_album_artist_detail.png" width="49.5%"/></a> <a href="./media/preview_album_detail.png"><img src="./media/preview_album_detail.png" width="49.5%"/></a> <a href="./media/preview_smart_playlist.png"><img src="./media/preview_smart_playlist.png" width="49.5%"/></a>
## Getting Started
### Desktop (recommended)
Download the [latest desktop client](https://github.com/jeffvli/feishin/releases). The desktop client is the recommended way to use Feishin. It supports both the MPV and web player backends, as well as includes built-in fetching for lyrics.
#### macOS Notes
If you're using a device running macOS 12 (Monterey) or higher, [check here](https://github.com/jeffvli/feishin/issues/104#issuecomment-1553914730) for instructions on how to remove the app from quarantine.
For media keys to work, you will be prompted to allow Feishin to be a Trusted Accessibility Client. After allowing, you will need to restart Feishin for the privacy settings to take effect.
#### Linux Notes
We provide a small install script to download the latest `.AppImage`, make it executable, and also download the icons required by Desktop Environments. Finally, it generates a `.desktop` file to add Feishin to your Application Launcher.
Simply run the installer like this:
```sh
dir=/your/application/directory
curl 'https://raw.githubusercontent.com/jeffvli/feishin/refs/heads/development/install-feishin-appimage' | sh -s -- "$dir"
```
The script also has an option to add launch arguments to run Feishin in native Wayland mode. Note that this is experimental in Electron and therefore not officially supported. If you want to use it, run this instead:
```sh
dir=/your/application/directory
curl 'https://raw.githubusercontent.com/jeffvli/feishin/refs/heads/development/install-feishin-appimage' | sh -s -- "$dir" wayland-native
```
It also provides a simple uninstall routine, removing the downloaded files:
```sh
dir=/your/application/directory
curl 'https://raw.githubusercontent.com/jeffvli/feishin/refs/heads/development/install-feishin-appimage' | sh -s -- "$dir" remove
```
The entry should show up in your Application Launcher immediately. If it does not, simply log out, wait 10 seconds, and log back in. Your Desktop Environment may alternatively provide a way to reload entries.
### Web and Docker
Visit [https://feishin.vercel.app](https://feishin.vercel.app) to use the hosted web version of Feishin. The web client only supports the web player backend.
Feishin is also available as a Docker image. The images are hosted via `ghcr.io` and are available to view [here](https://github.com/jeffvli/feishin/pkgs/container/feishin). You can run the container using the following commands:
```bash
# Run the latest version
docker run --name feishin -p 9180:9180 ghcr.io/jeffvli/feishin:latest
# Build the image locally
docker build -t feishin .
docker run --name feishin -p 9180:9180 feishin
```
#### Docker Compose
To install via Docker Compose, use the following snippet. This also works on Portainer.
```yaml
services:
feishin:
container_name: feishin
image: 'ghcr.io/jeffvli/feishin:latest'
restart: unless-stopped
environment:
- SERVER_NAME=jellyfin # pre-defined server name
- SERVER_LOCK=true # When true AND name/type/url are set, only username/password can be toggled
- SERVER_TYPE=jellyfin # the allowed types are: jellyfin, navidrome, subsonic. These values are case insensitive
- SERVER_URL= # http://address:port or https://address:port
- REMOTE_URL= # http://address or https://address
- LEGACY_AUTHENTICATION=false # When SERVER_LOCK is true, sets the legacy (plaintext) authentication flag for Subsonic/OpenSubsonic servers
- ANALYTICS_DISABLED=true # Set to true to disable Umami analytics tracking
ports:
- 9180:9180
# Alternatively, to restrict to only localhost, - 127.0.0.1:9180:8190
```
### Configuration
1. Upon startup you will be greeted with a prompt to select the path to your MPV binary. If you do not have MPV installed, you can download it [here](https://mpv.io/installation/) or install it using any package manager supported by your OS. After inputting the path, restart the app.
2. After restarting the app, you will be prompted to select a server. Click the `Open menu` button and select `Manage servers`. Click the `Add server` button in the popup and fill out all applicable details. You will need to enter the full URL to your server, including the protocol and port if applicable (e.g. `https://navidrome.my-server.com` or `http://192.168.0.1:4533`).
- **Navidrome** - For the best experience, select "Save password" when creating the server and configure the `SessionTimeout` setting in your Navidrome config to a larger value (e.g. 72h).
- **Linux users** - The default password store uses `libsecret`. `kwallet4/5/6` are also supported, but must be explicitly set in Settings > Window > Passwords/secret store.
3. _Optional_ - If you want to host Feishin on a subpath (not `/`), then pass in the following environment variable: `PUBLIC_PATH=PATH`. For example, to host on `/feishin`, pass in `PUBLIC_PATH=/feishin`.
4. _Optional_ - To hard code the server url, pass the following environment variables: `SERVER_NAME`, `SERVER_TYPE` (one of `jellyfin` or `navidrome` or `subsonic`), `SERVER_URL`. To prevent users from changing these settings, pass `SERVER_LOCK=true`. This can only be set if all three of the previous values are set. When `SERVER_LOCK=true`, you can also set `LEGACY_AUTHENTICATION=true` or `LEGACY_AUTHENTICATION=false` to configure the legacy authentication flag for the server (only applicable for Subsonic/OpenSubsonic servers).
5. _Optional_ - If your server uses a separate public-facing URL than what integrating applications use internally to communicate with your server, such as a separate Navidrome `ShareURL`, set `REMOTE_URL` to said public-facing URL.
6. _Optional_ - To disable Umami analytics tracking in the Docker/web version, set the environment variable `ANALYTICS_DISABLED=true`. When enabled, the analytics script will not be loaded and all tracking will be disabled.
7. _Optional_ - App settings (theme, language, sidebar options, etc.) can be overridden with environment variables on first run. The variables use the `FS_` prefix (e.g. `FS_GENERAL_THEME=defaultDark`, `FS_GENERAL_LANGUAGE=de`). See [the settings environment variable documentation](docs/ENV_SETTINGS.md) for the full list.
## FAQ
### MPV is either not working or is rapidly switching between pause/play states
First thing to do is check that your MPV binary path is correct. Navigate to the settings page and re-set the path and restart the app. If your issue still isn't resolved, try reinstalling MPV. Known working versions include `v0.35.x` and `v0.36.x`. `v0.34.x` is a known broken version.
### What music servers does Feishin support?
Feishin supports any music server that implements a [Navidrome](https://www.navidrome.org/), [Jellyfin](https://jellyfin.org/), or [OpenSubsonic compatible](https://opensubsonic.netlify.app/) API.
- [Navidrome](https://github.com/navidrome/navidrome)
- [Jellyfin](https://github.com/jellyfin/jellyfin)
- [OpenSubsonic](https://opensubsonic.netlify.app/) compatible servers, such as...
- [Airsonic-Advanced](https://github.com/airsonic-advanced/airsonic-advanced)
- [Ampache](https://ampache.org)
- [Astiga](https://asti.ga/)
- [Funkwhale](https://www.funkwhale.audio/)
- [Gonic](https://github.com/sentriz/gonic)
- [LMS](https://github.com/epoupon/lms)
- [Nextcloud Music](https://apps.nextcloud.com/apps/music)
- [Supysonic](https://github.com/spl0k/supysonic)
- [Qm-Music](https://github.com/chenqimiao/qm-music)
- More (?)
### I have the issue "The SUID sandbox helper binary was found, but is not configured correctly" on Linux
This happens when you have user (unprivileged) namespaces disabled (`sysctl kernel.unprivileged_userns_clone` returns 0). You can fix this by either enabling unprivileged namespaces, or by making the `chrome-sandbox` Setuid.
```bash
chmod 4755 chrome-sandbox
sudo chown root:root chrome-sandbox
```
Ubuntu 24.04 specifically introduced breaking changes that affect how namespaces work. Please see https://discourse.ubuntu.com/t/ubuntu-24-04-lts-noble-numbat-release-notes/39890#:~:text=security%20improvements%20 for possible fixes.
## Development
Built and tested using Node `v23.11.0`.
This project is built off of [electron-vite](https://github.com/alex8088/electron-vite)
- `pnpm run dev` - Start the development server
- `pnpm run dev:watch` - Start the development server in watch mode (for main / preload HMR)
- `pnpm run start` - Starts the app in production preview mode
- `pnpm run build` - Builds the app for desktop
- `pnpm run build:electron` - Build the electron app (main, preload, and renderer)
- `pnpm run build:remote` - Build the remote app (remote)
- `pnpm run build:web` - Build the standalone web app (renderer)
- `pnpm run package` - Package the project
- `pnpm run package:dev` - Package the project for development locally
- `pnpm run package:linux` - Package the project for Linux locally
- `pnpm run package:mac` - Package the project for Mac locally
- `pnpm run package:win` - Package the project for Windows locally
- `pnpm run publish:linux` - Publish the project for Linux
- `pnpm run publish:linux:beta` - Publish the project for Linux (beta channel)
- `pnpm run publish:linux-arm64` - Publish the project for Linux ARM64
- `pnpm run publish:linux-arm64:beta` - Publish the project for Linux ARM64 (beta channel)
- `pnpm run publish:mac` - Publish the project for Mac
- `pnpm run publish:mac:beta` - Publish the project for Mac (beta channel)
- `pnpm run publish:win` - Publish the project for Windows
- `pnpm run publish:win:beta` - Publish the project for Windows (beta channel)
- `pnpm run typecheck` - Type check the project
- `pnpm run typecheck:node` - Type check the project with tsconfig.node.json
- `pnpm run typecheck:web` - Type check the project with tsconfig.web.json
- `pnpm run lint` - Lint the project
- `pnpm run lint:fix` - Lint the project and fix linting errors
- `pnpm run i18next` - Generate i18n files
## Translation
This project uses [Weblate](https://hosted.weblate.org/projects/feishin/) for translations. If you would like to contribute, please visit the link and submit a translation.
## License
[GNU General Public License v3.0 ©](https://github.com/jeffvli/feishin/blob/dev/LICENSE)
================================================
FILE: assets/assets.d.ts
================================================
type Styles = Record<string, string>;
declare module '*.svg' {
const content: string;
export default content;
}
declare module '*.png' {
const content: string;
export default content;
}
declare module '*.jpg' {
const content: string;
export default content;
}
declare module '*.scss' {
const content: Styles;
export default content;
}
declare module '*.sass' {
const content: Styles;
export default content;
}
declare module '*.css' {
const content: Styles;
export default content;
}
================================================
FILE: assets/entitlements.mac.plist
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
<true/>
</dict>
</plist>
================================================
FILE: dev-app-update.yml
================================================
provider: generic
url: https://example.com/auto-updates
updaterCacheDirName: feishin-updater
================================================
FILE: docker-compose.yaml
================================================
services:
feishin:
container_name: feishin
image: "ghcr.io/jeffvli/feishin:latest"
restart: unless-stopped
environment:
- SERVER_NAME=jellyfin # pre-defined server name
- SERVER_LOCK=false # When true AND name/type/url are set, only username/password can be toggled
- SERVER_TYPE=jellyfin # the allowed types are: jellyfin, navidrome, subsonic. These values are case insensitive
- SERVER_URL=http://localhost:8096 # http://address:port or https://address:port
# - REMOTE_URL=http://share.localhost # Used for compatibility with external functionality, such as custom sharing URLs on Navidrome
- LEGACY_AUTHENTICATION=false # When SERVER_LOCK is true, sets the legacyauth flag for server authentication (true or false)
- ANALYTICS_DISABLED=false # Set to true to disable Umami analytics tracking
ports:
- 9180:9180
# Alternatively, to restrict to only localhost, - 127.0.0.1:9180:8190
================================================
FILE: docs/ENV_SETTINGS.md
================================================
# Environment variables for settings (web / Docker)
These variables override app settings **on first run** when no persisted settings exist. They are injected via `settings.js` (from `settings.js.template`) and only apply to the **web** build.
**Format:** All values are strings; booleans use `true`/`false`, numbers are numeric strings. Leave unset or empty to use the default.
---
## General
| Setting | Default | Env variable | Available values / Description |
|-------------|---------|--------------|--------------------------------|
| `general.accent` | `rgb(53, 116, 252)` | `FS_GENERAL_ACCENT` | CSS `rgb(r, g, b)` string (e.g. `rgb(53, 116, 252)`). Invalid values are ignored. |
| `general.albumBackground` | `false` | `FS_GENERAL_ALBUM_BACKGROUND` | `true` / `false` — Show album background image. |
| `general.albumBackgroundBlur` | `3` | `FS_GENERAL_ALBUM_BACKGROUND_BLUR` | Blur amount for album background (number). |
| `general.artistBackground` | `true` | `FS_GENERAL_ARTIST_BACKGROUND` | `true` / `false` — Show artist background image. |
| `general.artistBackgroundBlur` | `3` | `FS_GENERAL_ARTIST_BACKGROUND_BLUR` | Blur amount for artist background (number). |
| `general.blurExplicitImages` | `false` | `FS_GENERAL_BLUR_EXPLICIT_IMAGES` | `true` / `false` — Blur explicit images. |
| `general.combinedLyricsAndVisualizer` | `false` | `FS_GENERAL_COMBINED_LYRICS_AND_VISUALIZER` | `true` / `false` — Combine lyrics and visualizer panel. |
| `general.enableGridMultiSelect` | `false` | `FS_GENERAL_ENABLE_GRID_MULTI_SELECT` | `true` / `false` — Enable multi-select in grid views. |
| `general.externalLinks` | `true` | `FS_GENERAL_EXTERNAL_LINKS` | `true` / `false` — Show external links in UI. |
| `general.followCurrentSong` | `true` | `FS_GENERAL_FOLLOW_CURRENT_SONG` | `true` / `false` — Follow current song in list. |
| `general.followSystemTheme` | `false` | `FS_GENERAL_FOLLOW_SYSTEM_THEME` | `true` / `false` — Use OS light/dark preference. |
| `general.homeFeature` | `true` | `FS_GENERAL_HOME_FEATURE` | `true` / `false` — Show home featured carousel. |
| `general.homeFeatureStyle` | `single` | `FS_GENERAL_HOME_FEATURE_STYLE` | `multiple` / `single` — Home featured carousel style. |
| `general.language` | `en` | `FS_GENERAL_LANGUAGE` | UI language code (e.g. `en`, `de`, `fr`). |
| `general.theme` | `defaultDark` | `FS_GENERAL_THEME` | One of: `ayuDark`, `ayuLight`, `catppuccinLatte`, `catppuccinMocha`, `defaultDark`, `defaultLight`, `dracula`, `githubDark`, `githubLight`, `glassyDark`, `gruvboxDark`, `gruvboxLight`, `highContrastDark`, `highContrastLight`, `materialDark`, `materialLight`, `monokai`, `nightOwl`, `nord`, `oneDark`, `rosePine`, `rosePineDawn`, `rosePineMoon`, `shadesOfPurple`, `solarizedDark`, `solarizedLight`, `tokyoNight`, `vscodeDarkPlus`, `vscodeLightPlus`. |
| `general.themeDark` | `defaultDark` | `FS_GENERAL_THEME_DARK` | Same as theme (used when system is dark). |
| `general.themeLight` | `defaultLight` | `FS_GENERAL_THEME_LIGHT` | Same as theme (used when system is light). |
| `general.lastfmApiKey` | *(empty)* | `FS_GENERAL_LASTFM_API_KEY` | Last.fm API key. |
| `general.lastFM` | `true` | `FS_GENERAL_LAST_FM` | `true` / `false` — Enable Last.fm. |
| `general.listenBrainz` | `true` | `FS_GENERAL_LISTEN_BRAINZ` | `true` / `false` — ListenBrainz links. |
| `general.musicBrainz` | `true` | `FS_GENERAL_MUSIC_BRAINZ` | `true` / `false` — MusicBrainz links. |
| `general.nativeAspectRatio` | `false` | `FS_GENERAL_NATIVE_ASPECT_RATIO` | `true` / `false` — Use native cover art aspect ratio. |
| `general.pathReplace` | *(empty)* | `FS_GENERAL_PATH_REPLACE` | Path pattern to replace (e.g. server path in Docker). |
| `general.pathReplaceWith` | *(empty)* | `FS_GENERAL_PATH_REPLACE_WITH` | Replacement path. |
| `general.playerbarOpenDrawer` | `false` | `FS_GENERAL_PLAYERBAR_OPEN_DRAWER` | `true` / `false` — Open queue/lyrics as drawer from player bar. |
| `general.primaryShade` | `6` | `FS_GENERAL_PRIMARY_SHADE` | Mantine primary shade 0–9 (number). |
| `general.qobuz` | `true` | `FS_GENERAL_QOBUZ` | `true` / `false` — Qobuz links. |
| `general.resume` | `true` | `FS_GENERAL_RESUME` | `true` / `false` — Resume playback on load. |
| `general.showLyricsInSidebar` | `true` | `FS_GENERAL_SHOW_LYRICS_IN_SIDEBAR` | `true` / `false` — Show lyrics in sidebar. |
| `general.showRatings` | `true` | `FS_GENERAL_SHOW_RATINGS` | `true` / `false` — Show star ratings. |
| `general.showVisualizerInSidebar` | `true` | `FS_GENERAL_SHOW_VISUALIZER_IN_SIDEBAR` | `true` / `false` — Show visualizer in sidebar. |
| `general.sidebarCollapsedNavigation` | `true` | `FS_GENERAL_SIDEBAR_COLLAPSED_NAVIGATION` | `true` / `false` — Start with collapsed sidebar nav. |
| `general.sidebarCollapseShared` | `false` | `FS_GENERAL_SIDEBAR_COLLAPSE_SHARED` | `true` / `false` — Share sidebar collapse state. |
| `general.sidebarPlaylistList` | `true` | `FS_GENERAL_SIDEBAR_PLAYLIST_LIST` | `true` / `false` — Show playlist list in sidebar. |
| `general.sidebarPlaylistSorting` | `false` | `FS_GENERAL_SIDEBAR_PLAYLIST_SORTING` | `true` / `false` — Enable playlist sorting in sidebar. |
| `general.sideQueueType` | `sideQueue` | `FS_GENERAL_SIDE_QUEUE_TYPE` | `sideDrawerQueue` / `sideQueue` — Side play queue style. |
| `general.sideQueueLayout` | `horizontal` | `FS_GENERAL_SIDE_QUEUE_LAYOUT` | `horizontal` / `vertical` — Attached side queue layout orientation. |
| `general.useThemeAccentColor` | `false` | `FS_GENERAL_USE_THEME_ACCENT_COLOR` | `true` / `false` — Use theme’s accent color instead of custom. |
| `general.useThemePrimaryShade` | `true` | `FS_GENERAL_USE_THEME_PRIMARY_SHADE` | `true` / `false` — Use theme’s primary shade. |
| `general.zoomFactor` | `100` | `FS_GENERAL_ZOOM_FACTOR` | UI zoom percentage (number). |
---
## Playback
| Setting path | Default | Env variable | Available values / Description |
|-------------|---------|--------------|--------------------------------|
| `playback.mediaSession` | `false` | `FS_PLAYBACK_MEDIA_SESSION` | `true` / `false` — Media Session API (e.g. browser/media keys). |
| `playback.webAudio` | `true` | `FS_PLAYBACK_WEB_AUDIO` | `true` / `false` — Use Web Audio for playback. |
| `playback.audioFadeOnStatusChange` | `true` | `FS_PLAYBACK_AUDIO_FADE_ON_STATUS_CHANGE` | `true` / `false` — Fade on play/pause. |
| `playback.preservePitch` | `true` | `FS_PLAYBACK_PRESERVE_PITCH` | `true` / `false` — Preserve pitch when changing speed. |
| `playback.scrobble.enabled` | `true` | `FS_PLAYBACK_SCROBBLE_ENABLED` | `true` / `false` — Enable scrobbling. |
| `playback.scrobble.notify` | `false` | `FS_PLAYBACK_SCROBBLE_NOTIFY` | `true` / `false` — Scrobble notifications. |
| `playback.scrobble.scrobbleAtDuration` | `240` | `FS_PLAYBACK_SCROBBLE_AT_DURATION` | Seconds of playback before scrobble. |
| `playback.scrobble.scrobbleAtPercentage` | `75` | `FS_PLAYBACK_SCROBBLE_AT_PERCENTAGE` | Percentage of track before scrobble. |
| `playback.transcode.enabled` | `false` | `FS_PLAYBACK_TRANSCODE_ENABLED` | `true` / `false` — Enable transcoding. |
---
## Discord
| Setting path | Default | Env variable | Available values / Description |
|-------------|---------|--------------|--------------------------------|
| `discord.enabled` | `false` | `FS_DISCORD_ENABLED` | `true` / `false` — Discord rich presence. |
| `discord.clientId` | *(built-in)* | `FS_DISCORD_CLIENT_ID` | Custom Discord application ID. |
| `discord.displayType` | `feishin` | `FS_DISCORD_DISPLAY_TYPE` | `artist` / `feishin` / `song`. |
| `discord.linkType` | `none` | `FS_DISCORD_LINK_TYPE` | `last_fm` / `musicbrainz` / `musicbrainz_last_fm` / `none`. |
| `discord.showAsListening` | `false` | `FS_DISCORD_SHOW_AS_LISTENING` | `true` / `false`. |
| `discord.showPaused` | `true` | `FS_DISCORD_SHOW_PAUSED` | `true` / `false` — Show paused state. |
| `discord.showServerImage` | `false` | `FS_DISCORD_SHOW_SERVER_IMAGE` | `true` / `false`. |
| `discord.showStateIcon` | `true` | `FS_DISCORD_SHOW_STATE_ICON` | `true` / `false`. |
---
## Lyrics
| Setting path | Default | Env variable | Available values / Description |
|-------------|---------|--------------|--------------------------------|
| `lyrics.fetch` | `true` | `FS_LYRICS_FETCH` | `true` / `false` — Fetch lyrics. |
| `lyrics.follow` | `true` | `FS_LYRICS_FOLLOW` | `true` / `false` — Follow current line. |
| `lyrics.delayMs` | `0` | `FS_LYRICS_DELAY_MS` | Sync delay in milliseconds. |
| `lyrics.preferLocalLyrics` | `true` | `FS_LYRICS_PREFER_LOCAL` | `true` / `false` — Prefer local lyric files. |
| `lyrics.showMatch` | `true` | `FS_LYRICS_SHOW_MATCH` | `true` / `false`. |
| `lyrics.showProvider` | `true` | `FS_LYRICS_SHOW_PROVIDER` | `true` / `false`. |
| `lyrics.enableAutoTranslation` | `false` | `FS_LYRICS_ENABLE_AUTO_TRANSLATION` | `true` / `false`. |
| `lyrics.translationApiKey` | *(empty)* | `FS_LYRICS_TRANSLATION_API_KEY` | API key for lyric translation. |
| `lyrics.translationTargetLanguage` | `en` | `FS_LYRICS_TRANSLATION_TARGET_LANGUAGE` | Target language code. |
| `lyrics.alignment` | `center` | `FS_LYRICS_ALIGNMENT` | `center` / `left` / `right`. |
---
## Auto DJ
| Setting path | Default | Env variable | Available values / Description |
|-------------|---------|--------------|--------------------------------|
| `autoDJ.enabled` | `false` | `FS_AUTO_DJ_ENABLED` | `true` / `false`. |
| `autoDJ.itemCount` | `5` | `FS_AUTO_DJ_ITEM_COUNT` | Number of items to add. |
| `autoDJ.timing` | `1` | `FS_AUTO_DJ_TIMING` | Timing value (number). |
---
## CSS
| Setting path | Default | Env variable | Available values / Description |
|-------------|---------|--------------|--------------------------------|
| `css.content` | *(empty)* | `FS_CSS_CONTENT` | Custom CSS string (sanitized like in-app custom CSS). Set `FS_CSS_ENABLED=true` to apply. |
| `css.enabled` | `false` | `FS_CSS_ENABLED` | `true` / `false` — Enable custom CSS. |
---
## Font
| Setting path | Default | Env variable | Available values / Description |
|-------------|---------|--------------|--------------------------------|
| `font.type` | `builtIn` | `FS_FONT_TYPE` | `builtIn` / `system` / `custom`. |
| `font.builtIn` | `Inter` | `FS_FONT_BUILT_IN` | Built-in font name. |
| `font.system` | *(empty)* | `FS_FONT_SYSTEM` | System font name (when type is `system`). |
================================================
FILE: electron-builder-alpha.yml
================================================
appId: org.jeffvli.feishin
productName: Feishin
artifactName: ${productName}-${version}-${os}-${arch}.${ext}
electronVersion: 39.4.0
directories:
buildResources: assets
files:
- 'out/**/*'
- 'package.json'
extraResources:
- assets/**
asarUnpack:
- resources/**
win:
target:
- target: zip
arch:
- x64
- arm64
- target: nsis
arch:
- x64
- arm64
icon: assets/icons/icon.ico
nsis:
allowToChangeInstallationDirectory: true
oneClick: false
shortcutName: ${productName}
uninstallDisplayName: ${productName}
createDesktopShortcut: always
mac:
target:
- target: dmg
arch:
- arm64
- x64
- target: zip
arch:
- arm64
- x64
icon: assets/icons/icon.icns
type: distribution
hardenedRuntime: false
identity: "-"
gatekeeperAssess: false
notarize: false
dmg:
contents: [{ x: 130, y: 220 }, { x: 410, y: 220, type: link, path: /Applications }]
linux:
target:
- AppImage
- deb
- tar.xz
category: AudioVideo;Audio;Player
icon: assets/icons/icon.png
artifactName: ${productName}-${os}-${arch}.${ext}
toolsets:
appimage: "1.0.2"
npmRebuild: false
publish:
provider: s3
bucket: feishin-nightly
channel: alpha
endpoint: https://065f090c64de2dc707dd70ac72db9669.r2.cloudflarestorage.com
================================================
FILE: electron-builder-beta.yml
================================================
appId: org.jeffvli.feishin
productName: Feishin
artifactName: ${productName}-${version}-${os}-${arch}.${ext}
electronVersion: 39.4.0
directories:
buildResources: assets
files:
- 'out/**/*'
- 'package.json'
extraResources:
- assets/**
asarUnpack:
- resources/**
win:
target:
- target: zip
arch:
- x64
- arm64
- target: nsis
arch:
- x64
- arm64
icon: assets/icons/icon.ico
nsis:
allowToChangeInstallationDirectory: true
oneClick: false
shortcutName: ${productName}
uninstallDisplayName: ${productName}
createDesktopShortcut: always
mac:
target:
- target: dmg
arch:
- arm64
- x64
- target: zip
arch:
- arm64
- x64
icon: assets/icons/icon.icns
type: distribution
hardenedRuntime: false
identity: "-"
gatekeeperAssess: false
notarize: false
dmg:
contents: [{ x: 130, y: 220 }, { x: 410, y: 220, type: link, path: /Applications }]
linux:
target:
- AppImage
- deb
- tar.xz
category: AudioVideo;Audio;Player
icon: assets/icons/icon.png
artifactName: ${productName}-${os}-${arch}.${ext}
toolsets:
appimage: "1.0.2"
npmRebuild: false
publish:
provider: github
owner: jeffvli
repo: feishin
channel: beta
releaseType: draft
================================================
FILE: electron-builder.yml
================================================
appId: org.jeffvli.feishin
productName: Feishin
artifactName: ${productName}-${version}-${os}-${arch}.${ext}
electronVersion: 39.4.0
directories:
buildResources: assets
files:
- 'out/**/*'
- 'package.json'
extraResources:
- assets/**
asarUnpack:
- resources/**
win:
target:
- target: zip
arch:
- x64
- arm64
- target: nsis
arch:
- x64
- arm64
icon: assets/icons/icon.ico
nsis:
allowToChangeInstallationDirectory: true
oneClick: false
shortcutName: ${productName}
uninstallDisplayName: ${productName}
createDesktopShortcut: always
mac:
target:
- target: dmg
arch:
- arm64
- x64
- target: zip
arch:
- arm64
- x64
icon: assets/icons/icon.icns
type: distribution
hardenedRuntime: false
identity: "-"
gatekeeperAssess: false
notarize: false
dmg:
contents: [{ x: 130, y: 220 }, { x: 410, y: 220, type: link, path: /Applications }]
linux:
target:
- AppImage
- deb
- tar.xz
category: AudioVideo;Audio;Player
icon: assets/icons/icon.png
artifactName: ${productName}-${os}-${arch}.${ext}
toolsets:
appimage: "1.0.2"
npmRebuild: false
afterAllArtifactBuild: scripts/after-all-artifact-build.mjs
publish:
provider: github
owner: jeffvli
repo: feishin
channel: latest
releaseType: draft
================================================
FILE: electron.vite.config.ts
================================================
import react from '@vitejs/plugin-react';
import { externalizeDepsPlugin, UserConfig } from 'electron-vite';
import { resolve } from 'path';
import conditionalImportPlugin from 'vite-plugin-conditional-import';
import dynamicImportPlugin from 'vite-plugin-dynamic-import';
import { ViteEjsPlugin } from 'vite-plugin-ejs';
const currentOSEnv = process.platform;
const electronRendererTarget = 'chrome87';
const config: UserConfig = {
main: {
build: {
rollupOptions: {
external: ['source-map-support'],
},
sourcemap: true,
},
define: {
'import.meta.env.IS_LINUX': JSON.stringify(currentOSEnv === 'linux'),
'import.meta.env.IS_MACOS': JSON.stringify(currentOSEnv === 'darwin'),
'import.meta.env.IS_WIN': JSON.stringify(currentOSEnv === 'win32'),
},
plugins: [
externalizeDepsPlugin(),
dynamicImportPlugin(),
conditionalImportPlugin({
currentEnv: currentOSEnv,
envs: ['win32', 'linux', 'darwin'],
}),
],
resolve: {
alias: {
'/@/main': resolve('src/main'),
'/@/shared': resolve('src/shared'),
},
},
},
preload: {
build: {
sourcemap: true,
},
plugins: [externalizeDepsPlugin()],
resolve: {
alias: {
'/@/preload': resolve('src/preload'),
'/@/shared': resolve('src/shared'),
},
},
},
renderer: {
build: {
cssMinify: 'esbuild',
minify: 'esbuild',
modulePreload: {
polyfill: false,
},
sourcemap: true,
target: electronRendererTarget,
},
css: {
modules: {
generateScopedName: 'fs-[name]-[local]',
localsConvention: 'camelCase',
},
},
plugins: [react(), ViteEjsPlugin({ web: false })],
resolve: {
alias: {
'/@/i18n': resolve('src/i18n'),
'/@/remote': resolve('src/remote'),
'/@/renderer': resolve('src/renderer'),
'/@/shared': resolve('src/shared'),
},
},
},
};
export default config;
================================================
FILE: eslint.config.mjs
================================================
import eslintConfigPrettier from '@electron-toolkit/eslint-config-prettier';
import tseslint from '@electron-toolkit/eslint-config-ts';
import perfectionist from 'eslint-plugin-perfectionist';
import eslintPluginReact from 'eslint-plugin-react';
import eslintPluginReactHooks from 'eslint-plugin-react-hooks';
import eslintPluginReactRefresh from 'eslint-plugin-react-refresh';
export default tseslint.config(
{ ignores: ['**/node_modules', '**/dist', '**/out'] },
tseslint.configs.recommended,
perfectionist.configs['recommended-natural'],
eslintPluginReact.configs.flat.recommended,
eslintPluginReact.configs.flat['jsx-runtime'],
{
settings: {
react: {
version: 'detect',
},
},
},
{
files: ['**/*.{ts,tsx}'],
plugins: {
'react-hooks': eslintPluginReactHooks,
'react-refresh': eslintPluginReactRefresh,
},
rules: {
...eslintPluginReactHooks.configs.recommended.rules,
...eslintPluginReactRefresh.configs.vite.rules,
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/no-duplicate-enum-values': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-unused-vars': 'warn',
curly: ['error', 'all'],
indent: [
'error',
'tab',
{
offsetTernaryExpressions: true,
SwitchCase: 1,
},
],
'no-unused-vars': 'off',
'no-use-before-define': 'off',
quotes: ['error', 'single'],
'react-hooks/refs': 'off',
'react-hooks/set-state-in-effect': 'off',
'react-refresh/only-export-components': 'off',
'react/display-name': 'off',
semi: ['error', 'always'],
'single-attribute-per-line': 'off',
},
},
eslintConfigPrettier,
);
================================================
FILE: feishin.desktop.tmpl
================================================
[Desktop Entry]
Name=Feishin
GenericName=Music player
Exec=${FEISHIN_DESKTOP_EXECUTABLE} ${FEISHIN_DESKTOP_ARGS}
TryExec=${FEISHIN_DESKTOP_EXECUTABLE}
Terminal=false
Type=Application
Icon=org.jeffvli.feishin
StartupWMClass=feishin
SingleMainWindow=true
Categories=AudioVideo;Audio;Player;Music;
Keywords=Navidrome;Jellyfin;Subsonic;OpenSubsonic
Comment=A player for your self-hosted music server
================================================
FILE: install-feishin-appimage
================================================
#!/bin/sh
set -eu
if [ "$#" -lt 1 ]; then
echo "Usage: $0 <installation-directory> <option>"
echo "Options:"
echo " wayland-native Enable native Wayland support"
echo " remove Remove Feishin AppImage and desktop entries"
exit 1
fi
dir="$(readlink -f "${1}")"
arg="${2:-""}"
arch="$(uname -m)"
if [ "$arg" != "wayland-native" ] && [ "$arg" != "remove" ] && [ "$arg" != "" ]; then
echo "Invalid option: $arg"
echo "Valid options are: wayland-native, remove"
exit 1
fi
if [ "${arch}" != "x86_64" ] && [ "${arch}" != "aarch64" ]; then
echo "CPU architecture not recognised (not x86_64 or aarch64). Aborting."
exit 1
fi
# workaround if we're not renaming the artifact
if [ "${arch}" = "aarch64" ]; then
arch="arm64"
fi
if [ ! -d "${dir}" ]; then
echo "${dir} is not a directory or does not exist. Please provide an existing directory."
exit 1
fi
localShare="${XDG_DATA_HOME:-$HOME/.local/share}"
localShareIcons="${localShare}/icons/hicolor"
if [ "${arg}" = "remove" ]; then
rm -v \
"${localShareIcons}/512x512/apps/org.jeffvli.feishin.png" \
"${localShareIcons}/256x256/apps/org.jeffvli.feishin.png" \
"${localShareIcons}/128x128/apps/org.jeffvli.feishin.png" \
"${localShareIcons}/64x64/apps/org.jeffvli.feishin.png" \
"${localShareIcons}/32x32/apps/org.jeffvli.feishin.png" \
"${localShare}/applications/org.jeffvli.feishin.desktop" \
"${dir}/Feishin-linux-${arch}.AppImage"
exit 0
fi
curl --fail -L --create-dirs --write-out '%{filename_effective}\n' \
-o "${dir}/Feishin-linux-${arch}.AppImage" "https://github.com/jeffvli/feishin/releases/latest/download/Feishin-linux-${arch}.AppImage" \
-o "${localShareIcons}/512x512/apps/org.jeffvli.feishin.png" 'https://github.com/jeffvli/feishin/blob/development/assets/icons/512x512.png?raw=true' \
-o "${localShareIcons}/256x256/apps/org.jeffvli.feishin.png" 'https://github.com/jeffvli/feishin/blob/development/assets/icons/256x256.png?raw=true' \
-o "${localShareIcons}/128x128/apps/org.jeffvli.feishin.png" 'https://github.com/jeffvli/feishin/blob/development/assets/icons/128x128.png?raw=true' \
-o "${localShareIcons}/64x64/apps/org.jeffvli.feishin.png" 'https://github.com/jeffvli/feishin/blob/development/assets/icons/64x64.png?raw=true' \
-o "${localShareIcons}/32x32/apps/org.jeffvli.feishin.png" 'https://github.com/jeffvli/feishin/blob/development/assets/icons/32x32.png?raw=true'
chmod -v u+x "${dir}/Feishin-linux-${arch}.AppImage"
waylandFlags=""
if [ "${arg}" = "wayland-native" ]; then
waylandFlags="--enable-features=UseOzonePlatform,WaylandWindowDecorations --ozone-platform-hint=auto"
fi
# this is for Debian-based kernels and ALT respectively
# https://unix.stackexchange.com/a/303214/145722
sandboxFlag=""
if [ "$(sysctl kernel.unprivileged_userns_clone 2>/dev/null)" = "0" ] \
|| [ "$(sysctl kernel.userns_restrict 2>/dev/null)" = "1" ]; then
sandboxFlag="--no-sandbox"
fi
mkdir -pv "${localShare}/applications"
export FEISHIN_DESKTOP_EXECUTABLE="${dir}/Feishin-linux-${arch}.AppImage"
export FEISHIN_DESKTOP_ARGS="${sandboxFlag} ${waylandFlags}"
curl --fail https://raw.githubusercontent.com/jeffvli/feishin/refs/heads/development/feishin.desktop.tmpl | envsubst > "${localShare}/applications/org.jeffvli.feishin.desktop"
================================================
FILE: ng.conf.template
================================================
server {
listen 9180;
listen [::]:9180;
sendfile on;
default_type application/octet-stream;
gzip on;
gzip_http_version 1.1;
gzip_disable "MSIE [1-6]\.";
gzip_min_length 256;
gzip_vary on;
gzip_proxied expired no-cache no-store private auth;
gzip_types text/plain text/css application/json application/javascript application/x-javascript text/xml application/xml application/xml+rss text/javascript;
gzip_comp_level 9;
location ${PUBLIC_PATH} {
alias /usr/share/nginx/html/;
try_files $uri $uri/ /index.html =404;
}
location ${PUBLIC_PATH}settings.js {
alias /etc/nginx/conf.d/settings.js;
add_header Cache-Control "no-store";
}
location ${PUBLIC_PATH}/settings.js {
alias /etc/nginx/conf.d/settings.js;
add_header Cache-Control "no-store";
}
}
================================================
FILE: org.jeffvli.feishin.metainfo.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<component type="desktop-application">
<id>org.jeffvli.feishin</id>
<name>Feishin</name>
<summary>Jellyfin, Navidrome, and OpenSubsonic Compatible Music Player</summary>
<metadata_license>CC0-1.0</metadata_license>
<project_license>GPL-3.0-only</project_license>
<content_rating type="oars-1.1"/>
<description>
<p>A modern, cross-platform music player for Jellyfin, Navidrome, and OpenSubsonic servers.</p>
<p>Features</p>
<ul>
<li>MPV player backend</li>
<li>Web player backend</li>
<li>Jellyfin server support</li>
<li>Navidrome server support</li>
<li>OpenSubsonic server support</li>
<li>Modern UI</li>
<li>Scrobble playback to your server</li>
<li>Smart playlist editor (Navidrome)</li>
<li>Synchronized and unsynchronized lyrics support</li>
</ul>
</description>
<developer id="org.jeffvli">
<name>jeffvli</name>
</developer>
<launchable type="desktop-id">org.jeffvli.feishin.desktop</launchable>
<url type="homepage">https://github.com/jeffvli/feishin</url>
<screenshots>
<screenshot type="default">
<caption>The main menu</caption>
<image type="source">https://raw.githubusercontent.com/jeffvli/feishin/development/media/preview_home.png</image>
</screenshot>
<screenshot>
<caption>Browsing an album</caption>
<image type="source">https://raw.githubusercontent.com/jeffvli/feishin/development/media/preview_album_detail.png</image>
</screenshot>
<screenshot>
<caption>Smart playlist creation</caption>
<image type="source">https://raw.githubusercontent.com/jeffvli/feishin/development/media/preview_smart_playlist.png</image>
</screenshot>
</screenshots>
<categories>
<category>AudioVideo</category>
<category>Audio</category>
<category>Player</category>
<category>Music</category>
</categories>
<releases>
<release date="2025-10-13" type="stable" version="0.21.2"></release>
<release date="2025-10-13" type="stable" version="0.21.1"></release>
<release date="2025-10-13" type="stable" version="0.21.0"></release>
<release date="2025-09-11" type="stable" version="0.20.1"></release>
<release date="2025-09-07" type="stable" version="0.20.0"></release>
<release date="2025-07-31" type="stable" version="0.19.0"></release>
<release date="2025-07-08" type="stable" version="0.18.0"></release>
<release date="2025-06-30" type="stable" version="0.17.0"></release>
<release date="2025-06-26" type="stable" version="0.16.0"></release>
<release date="2025-06-25" type="stable" version="0.15.1"></release>
<release date="2025-06-25" type="stable" version="0.15.0"></release>
<release date="2025-06-03" type="stable" version="0.14.0"></release>
<release date="2025-05-26" type="stable" version="0.13.0"></release>
<release date="2025-05-13" type="stable" version="0.12.7"></release>
<release date="2025-05-08" type="stable" version="0.12.6"></release>
<release date="2025-05-07" type="stable" version="0.12.5"></release>
<release date="2025-03-10" type="stable" version="0.12.3"></release>
<release date="2025-01-25" type="stable" version="0.12.2"></release>
<release date="2024-11-20" type="stable" version="0.12.1"></release>
<release date="2024-11-19" type="stable" version="0.12.0"></release>
<release date="2024-10-15" type="stable" version="0.11.1"></release>
<release date="2024-10-10" type="stable" version="0.11.0"></release>
<release date="2024-09-29" type="stable" version="0.10.1"></release>
<release date="2024-09-27" type="stable" version="0.10.0"></release>
<release date="2024-09-11" type="stable" version="0.9.0"></release>
<release date="2024-09-04" type="stable" version="0.8.1"></release>
<release date="2024-09-03" type="stable" version="0.8.0"></release>
<release date="2024-07-30" type="stable" version="0.7.3"></release>
<release date="2024-07-30" type="stable" version="0.7.2"></release>
<release date="2024-05-07" type="stable" version="0.7.1"></release>
<release date="2024-05-07" type="stable" version="0.7.0"></release>
<release date="2024-03-13" type="stable" version="0.6.1"></release>
<release date="2024-03-06" type="stable" version="0.6.0"></release>
<release date="2023-12-14" type="stable" version="0.5.3"></release>
<release date="2023-11-18" type="stable" version="0.5.2"></release>
<release date="2023-11-02" type="stable" version="0.5.1"></release>
<release date="2023-10-31" type="stable" version="0.5.0"></release>
<release date="2023-10-08" type="stable" version="0.4.1"></release>
<release date="2023-09-25" type="stable" version="0.4.0"></release>
<release date="2023-08-08" type="stable" version="0.3.0"></release>
<release date="2023-06-14" type="stable" version="0.2.0"></release>
<release date="2023-05-22" type="stable" version="0.1.1"></release>
<release date="2023-05-22" type="stable" version="0.1.0"></release>
<release date="2023-04-03" type="development" version="0.0.1-alpha6"></release>
<release date="2023-02-09" type="development" version="0.0.1-alpha5"></release>
<release date="2023-01-16" type="development" version="0.0.1-alpha4"></release>
<release date="2023-01-03" type="development" version="0.0.1-alpha3"></release>
<release date="2022-12-30" type="development" version="0.0.1-alpha2"></release>
<release date="2022-11-21" type="development" version="0.0.1-alpha1"></release>
</releases>
</component>
================================================
FILE: package.json
================================================
{
"name": "feishin",
"version": "1.9.0",
"description": "A modern self-hosted music player.",
"keywords": [
"subsonic",
"navidrome",
"jellyfin",
"react",
"electron"
],
"homepage": "https://github.com/jeffvli/feishin",
"bugs": {
"url": "https://github.com/jeffvli/feishin/issues"
},
"license": "GPL-3.0",
"author": {
"name": "jeffvli",
"email": "feishin@users.noreply.github.com",
"url": "https://github.com/jeffvli/"
},
"main": "./out/main/index.js",
"scripts": {
"build": "pnpm run build:electron && pnpm run build:remote",
"build:electron": "electron-vite build",
"build:remote": "vite build --config remote.vite.config.ts",
"build:web": "vite build --config web.vite.config.ts",
"dev": "electron-vite dev",
"dev:remote": "vite dev --config remote.vite.config.ts",
"dev:watch": "electron-vite dev --watch",
"i18next": "i18next -c src/i18n/i18next-parser.config.js",
"postinstall": "electron-builder install-app-deps",
"lint": "pnpm run typecheck && pnpm run lint-code && pnpm run lint-styles",
"lint-code": "eslint --max-warnings=0 --cache .",
"lint-code:fix": "eslint --cache --fix .",
"lint-styles": "stylelint --max-warnings=0 'src/**/*.{css,scss}'",
"lint-styles:fix": "stylelint 'src/**/*.{css,scss}' --fix",
"lint:fix": "pnpm run lint-code:fix && pnpm run lint-styles:fix",
"package": "pnpm run build && electron-builder",
"package:dev": "pnpm run build && electron-builder --dir",
"package:linux": "pnpm run build && electron-builder --linux",
"package:linux-arm64:pr": "pnpm run build && electron-builder --linux --arm64 --publish never",
"package:linux:pr": "pnpm run build && electron-builder --linux --publish never",
"package:mac": "pnpm run build && electron-builder --mac",
"package:mac:pr": "pnpm run build && electron-builder --mac --publish never",
"package:win": "pnpm run build && electron-builder --win",
"package:win-arm64:pr": "pnpm run build && electron-builder --win --arm64 --publish never",
"package:win:pr": "pnpm run build && electron-builder --win --publish never",
"publish:linux": "pnpm run build && electron-builder --publish always --linux",
"publish:linux-arm64": "pnpm run build && electron-builder --publish always --linux --arm64",
"publish:linux-arm64:alpha": "pnpm run build && electron-builder --config electron-builder-alpha.yml --publish always --linux --arm64",
"publish:linux-arm64:beta": "pnpm run build && electron-builder --config electron-builder-beta.yml --publish always --linux --arm64",
"publish:linux:alpha": "pnpm run build && electron-builder --config electron-builder-alpha.yml --publish always --linux",
"publish:linux:beta": "pnpm run build && electron-builder --config electron-builder-beta.yml --publish always --linux",
"publish:mac": "pnpm run build && electron-builder --publish always --mac",
"publish:mac:alpha": "pnpm run build && electron-builder --config electron-builder-alpha.yml --publish always --mac",
"publish:mac:beta": "pnpm run build && electron-builder --config electron-builder-beta.yml --publish always --mac",
"publish:win": "pnpm run build && electron-builder --publish always --win",
"publish:win-arm64": "pnpm run build && electron-builder --publish always --win --arm64",
"publish:win-arm64:alpha": "pnpm run build && electron-builder --config electron-builder-alpha.yml --publish always --win --arm64",
"publish:win-arm64:beta": "pnpm run build && electron-builder --config electron-builder-beta.yml --publish always --win --arm64",
"publish:win:alpha": "pnpm run build && electron-builder --config electron-builder-alpha.yml --publish always --win",
"publish:win:beta": "pnpm run build && electron-builder --config electron-builder-beta.yml --publish always --win",
"start": "electron-vite preview",
"typecheck": "pnpm run typecheck:node && pnpm run typecheck:web",
"typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false",
"typecheck:web": "tsc --noEmit -p tsconfig.web.json --composite false",
"version": "pnpm version --no-git-tag-version",
"postversion": "node ./scripts/update-app-stream.mjs"
},
"dependencies": {
"@atlaskit/pragmatic-drag-and-drop": "1.7.7",
"@atlaskit/pragmatic-drag-and-drop-auto-scroll": "^2.1.2",
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.1.0",
"@electron-toolkit/preload": "^3.0.1",
"@electron-toolkit/utils": "^4.0.0",
"@mantine/colors-generator": "^8.3.8",
"@mantine/core": "^8.3.8",
"@mantine/dates": "^8.3.8",
"@mantine/form": "^8.3.8",
"@mantine/hooks": "^8.3.8",
"@mantine/modals": "^8.3.8",
"@mantine/notifications": "^8.3.8",
"@radix-ui/react-context-menu": "^2.2.16",
"@tanstack/react-query": "^5.90.9",
"@tanstack/react-query-devtools": "^5.90.2",
"@tanstack/react-query-persist-client": "^5.90.11",
"@ts-rest/core": "^3.52.1",
"@wavesurfer/react": "^1.0.11",
"@xhayper/discord-rpc": "^1.3.0",
"audiomotion-analyzer": "^4.5.1",
"axios": "^1.13.5",
"butterchurn": "^3.0.0-beta.5",
"butterchurn-presets": "^3.0.0-beta.4",
"cheerio": "^1.1.2",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"dayjs": "^1.11.19",
"dompurify": "^3.3.0",
"electron-debug": "^3.2.0",
"electron-localshortcut": "^3.2.1",
"electron-log": "^5.4.3",
"electron-store": "^8.2.0",
"electron-updater": "^6.6.2",
"fast-average-color": "^9.5.0",
"fast-xml-parser": "^5.3.6",
"format-duration": "^3.0.2",
"fuse.js": "^7.1.0",
"i18next": "^25.6.2",
"icecast-metadata-stats": "^0.1.12",
"idb-keyval": "^6.2.2",
"immer": "^10.2.0",
"is-electron": "^2.2.2",
"lodash": "^4.17.23",
"md5": "^2.3.0",
"motion": "^12.23.24",
"mpris-service": "^2.1.2",
"nanoid": "^3.3.11",
"node-mpv": "github:jeffvli/Node-MPV#32b4d64395289ad710c41d481d2707a7acfc228f",
"nuqs": "^2.7.1",
"overlayscrollbars": "^2.11.1",
"overlayscrollbars-react": "^0.5.6",
"qs": "^6.14.2",
"react": "^19.1.0",
"react-call": "^1.8.1",
"react-dom": "^19.1.0",
"react-error-boundary": "^5.0.0",
"react-i18next": "^16.3.3",
"react-icons": "^5.5.0",
"react-player": "^2.16.0",
"react-router": "^7.13.1",
"react-split-pane": "^3.0.4",
"react-virtualized-auto-sizer": "^1.0.26",
"react-window": "1.8.11",
"react-window-v2": "npm:react-window@^2.2.3",
"semver": "^7.5.4",
"string-to-color": "^2.2.2",
"wavesurfer.js": "^7.11.1",
"ws": "^8.18.2",
"zod": "^3.22.3",
"zustand": "^5.0.5"
},
"devDependencies": {
"@electron-toolkit/eslint-config-prettier": "^3.0.0",
"@electron-toolkit/eslint-config-ts": "^3.0.0",
"@electron-toolkit/tsconfig": "^2.0.0",
"@types/electron-localshortcut": "^3.1.0",
"@types/lodash": "^4.17.18",
"@types/md5": "^2.3.5",
"@types/node": "^24.10.1",
"@types/react": "^19.2.5",
"@types/react-dom": "^19.2.3",
"@types/react-window": "^1.8.8",
"@types/source-map-support": "^0.5.10",
"@types/ws": "^8.18.1",
"@vitejs/plugin-react": "^5.1.1",
"concurrently": "^9.2.1",
"cross-env": "^10.1.0",
"electron": "^39.4.0",
"electron-builder": "^26.8.2",
"electron-devtools-installer": "^4.0.0",
"electron-vite": "^4.0.1",
"eslint": "^9.24.0",
"eslint-plugin-perfectionist": "^4.13.0",
"eslint-plugin-prettier": "^5.4.0",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.24",
"i18next-parser": "^9.3.0",
"postcss-preset-mantine": "^1.18.0",
"postcss-simple-vars": "^7.0.1",
"prettier": "^3.6.2",
"prettier-plugin-packagejson": "^2.5.19",
"stylelint": "^16.25.0",
"stylelint-config-css-modules": "^4.5.1",
"stylelint-config-recess-order": "^7.4.0",
"stylelint-config-standard": "^39.0.1",
"typescript": "^5.8.3",
"vite": "^7.2.2",
"vite-plugin-conditional-import": "^0.1.7",
"vite-plugin-dynamic-import": "^1.6.0",
"vite-plugin-ejs": "^1.7.0",
"vite-plugin-pwa": "^1.1.0"
},
"pnpm": {
"onlyBuiltDependencies": [
"electron",
"esbuild"
]
},
"productName": "feishin"
}
================================================
FILE: postcss.config.cjs
================================================
module.exports = {
plugins: {
'postcss-preset-mantine': {},
'postcss-simple-vars': {
variables: {
'mantine-breakpoint-2xl': '120em',
'mantine-breakpoint-3xl': '160em',
'mantine-breakpoint-lg': '75em',
'mantine-breakpoint-md': '62em',
'mantine-breakpoint-sm': '48em',
'mantine-breakpoint-xl': '88em',
'mantine-breakpoint-xs': '36em',
},
},
},
};
================================================
FILE: remote.vite.config.ts
================================================
import react from '@vitejs/plugin-react';
import path from 'path';
import { defineConfig, normalizePath } from 'vite';
import { ViteEjsPlugin } from 'vite-plugin-ejs';
import { version } from './package.json';
export default defineConfig({
build: {
cssMinify: 'esbuild',
emptyOutDir: true,
minify: 'esbuild',
outDir: path.resolve(__dirname, './out/remote'),
rollupOptions: {
input: {
favicon: normalizePath(path.resolve(__dirname, './assets/icons/favicon.ico')),
index: normalizePath(path.resolve(__dirname, './src/remote/index.html')),
manifest: normalizePath(path.resolve(__dirname, './src/remote/manifest.json')),
remote: normalizePath(path.resolve(__dirname, './src/remote/index.tsx')),
worker: normalizePath(path.resolve(__dirname, './src/remote/service-worker.ts')),
},
output: {
assetFileNames: '[name].[ext]',
chunkFileNames: '[name].js',
entryFileNames: '[name].js',
sourcemapExcludeSources: false,
},
},
sourcemap: true,
},
css: {
modules: {
generateScopedName: 'fs-[name]-[local]',
localsConvention: 'camelCase',
},
},
plugins: [
react(),
ViteEjsPlugin({
prod: process.env.NODE_ENV === 'production',
root: normalizePath(path.resolve(__dirname, './src/remote')),
version,
}),
],
resolve: {
alias: {
'/@/i18n': path.resolve(__dirname, './src/i18n'),
'/@/remote': path.resolve(__dirname, './src/remote'),
'/@/renderer': path.resolve(__dirname, './src/renderer'),
'/@/shared': path.resolve(__dirname, './src/shared'),
},
},
root: path.resolve(__dirname, './src/remote'),
});
================================================
FILE: scripts/after-all-artifact-build.mjs
================================================
import { execSync } from 'child_process';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
/**
* Electron-builder afterAllArtifactBuild hook
* Runs the app stream update script only for Linux builds
* Returns the metainfo file path to be included in published artifacts
*/
// This is not a typescript file, and is called by electron-builder, so we cannot use typescript features here.
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export default async function afterAllArtifactBuild(buildResult) {
// Check if this build includes Linux as a target
const isLinux = Array.from(buildResult.platformToTargets.keys()).some(
(platform) => platform.name === 'linux',
);
if (isLinux) {
const updateScriptPath = path.join(__dirname, 'update-app-stream.mjs');
const projectRoot = path.resolve(__dirname, '..');
const metainfoFile = path.resolve(projectRoot, 'org.jeffvli.feishin.metainfo.xml');
console.log('Running app stream update for Linux build...');
try {
execSync(`node ${updateScriptPath} --replace-if-version-missing`, {
cwd: projectRoot,
stdio: 'inherit',
});
// Return the metainfo file to be included in published artifacts
return [metainfoFile];
} catch (error) {
console.error('Failed to update app stream:', error.message);
throw error;
}
}
// Return empty array if not a Linux build
return [];
}
================================================
FILE: scripts/update-app-stream.mjs
================================================
import { XMLBuilder, XMLParser } from 'fast-xml-parser';
import fs from 'fs';
import path from 'path';
const args = process.argv.slice(2);
// Parse flags and positional arguments
const flags = args.filter((arg) => arg.startsWith('--'));
const positionalArgs = args.filter((arg) => !arg.startsWith('--'));
const replaceIfVersionMissing = flags.includes('--replace-if-version-missing');
if (positionalArgs.length > 3) {
console.error(
'Usage: node update-app-stream.js [package-file] [date] [metainfo-file] [--replace-if-version-missing]',
);
process.exit(1);
}
const packageFile = positionalArgs[0] || path.resolve(process.cwd(), 'package.json');
const packageContent = fs.readFileSync(packageFile, 'utf8');
const packageJson = JSON.parse(packageContent);
const version = packageJson.version;
const time = Math.floor((Date.parse(positionalArgs[1]) || Date.now()) / 1000);
const metainfoFile =
positionalArgs[2] || path.resolve(process.cwd(), 'org.jeffvli.feishin.metainfo.xml');
const parser = new XMLParser({ ignoreAttributes: false });
const metainfoContent = fs.readFileSync(metainfoFile, 'utf8');
const metainfo = parser.parse(metainfoContent);
const newRelease = {
'@_date': new Date(time * 1000).toISOString().split('T')[0],
'@_type': version.includes('-') ? 'development' : 'stable',
'@_version': version,
};
if (replaceIfVersionMissing) {
// Replace all releases with only the current version
metainfo.component.releases.release = [newRelease];
} else {
// Default behavior: add new release if it doesn't exist
const releaseExists =
metainfo.component.releases.release.findIndex(
(release) => release['@_version'] === version,
) !== -1;
if (!releaseExists) {
metainfo.component.releases.release.unshift(newRelease);
}
}
const builder = new XMLBuilder({ format: true, ignoreAttributes: false, indentBy: ' ' });
fs.writeFileSync(metainfoFile, builder.build(metainfo), 'utf8');
console.log(`Updated ${metainfoFile} with version ${version}`);
================================================
FILE: settings.js.template
================================================
"use strict";
window.SERVER_URL = "${SERVER_URL}";
window.REMOTE_URL = "${REMOTE_URL}";
window.SERVER_NAME = "${SERVER_NAME}";
window.SERVER_TYPE = "${SERVER_TYPE}";
window.SERVER_LOCK = "${SERVER_LOCK}";
window.LEGACY_AUTHENTICATION = "${LEGACY_AUTHENTICATION}";
window.ANALYTICS_DISABLED = "${ANALYTICS_DISABLED}";
window.FS_GENERAL_ACCENT = "${FS_GENERAL_ACCENT}";
window.FS_GENERAL_ALBUM_BACKGROUND = "${FS_GENERAL_ALBUM_BACKGROUND}";
window.FS_GENERAL_ALBUM_BACKGROUND_BLUR = "${FS_GENERAL_ALBUM_BACKGROUND_BLUR}";
window.FS_GENERAL_ARTIST_BACKGROUND = "${FS_GENERAL_ARTIST_BACKGROUND}";
window.FS_GENERAL_ARTIST_BACKGROUND_BLUR = "${FS_GENERAL_ARTIST_BACKGROUND_BLUR}";
window.FS_GENERAL_BLUR_EXPLICIT_IMAGES = "${FS_GENERAL_BLUR_EXPLICIT_IMAGES}";
window.FS_GENERAL_COMBINED_LYRICS_AND_VISUALIZER = "${FS_GENERAL_COMBINED_LYRICS_AND_VISUALIZER}";
window.FS_GENERAL_ENABLE_GRID_MULTI_SELECT = "${FS_GENERAL_ENABLE_GRID_MULTI_SELECT}";
window.FS_GENERAL_EXTERNAL_LINKS = "${FS_GENERAL_EXTERNAL_LINKS}";
window.FS_GENERAL_FOLLOW_CURRENT_SONG = "${FS_GENERAL_FOLLOW_CURRENT_SONG}";
window.FS_GENERAL_FOLLOW_SYSTEM_THEME = "${FS_GENERAL_FOLLOW_SYSTEM_THEME}";
window.FS_GENERAL_HOME_FEATURE = "${FS_GENERAL_HOME_FEATURE}";
window.FS_GENERAL_HOME_FEATURE_STYLE = "${FS_GENERAL_HOME_FEATURE_STYLE}";
window.FS_GENERAL_LANGUAGE = "${FS_GENERAL_LANGUAGE}";
window.FS_GENERAL_LAST_FM = "${FS_GENERAL_LAST_FM}";
window.FS_GENERAL_LASTFM_API_KEY = "${FS_GENERAL_LASTFM_API_KEY}";
window.FS_GENERAL_LISTEN_BRAINZ = "${FS_GENERAL_LISTEN_BRAINZ}";
window.FS_GENERAL_MUSIC_BRAINZ = "${FS_GENERAL_MUSIC_BRAINZ}";
window.FS_GENERAL_NATIVE_ASPECT_RATIO = "${FS_GENERAL_NATIVE_ASPECT_RATIO}";
window.FS_GENERAL_PATH_REPLACE = "${FS_GENERAL_PATH_REPLACE}";
window.FS_GENERAL_PATH_REPLACE_WITH = "${FS_GENERAL_PATH_REPLACE_WITH}";
window.FS_GENERAL_PLAYERBAR_OPEN_DRAWER = "${FS_GENERAL_PLAYERBAR_OPEN_DRAWER}";
window.FS_GENERAL_PRIMARY_SHADE = "${FS_GENERAL_PRIMARY_SHADE}";
window.FS_GENERAL_QOBUZ = "${FS_GENERAL_QOBUZ}";
window.FS_GENERAL_RESUME = "${FS_GENERAL_RESUME}";
window.FS_GENERAL_SHOW_LYRICS_IN_SIDEBAR = "${FS_GENERAL_SHOW_LYRICS_IN_SIDEBAR}";
window.FS_GENERAL_SHOW_RATINGS = "${FS_GENERAL_SHOW_RATINGS}";
window.FS_GENERAL_SHOW_VISUALIZER_IN_SIDEBAR = "${FS_GENERAL_SHOW_VISUALIZER_IN_SIDEBAR}";
window.FS_GENERAL_SIDEBAR_COLLAPSED_NAVIGATION = "${FS_GENERAL_SIDEBAR_COLLAPSED_NAVIGATION}";
window.FS_GENERAL_SIDEBAR_COLLAPSE_SHARED = "${FS_GENERAL_SIDEBAR_COLLAPSE_SHARED}";
window.FS_GENERAL_SIDEBAR_PLAYLIST_LIST = "${FS_GENERAL_SIDEBAR_PLAYLIST_LIST}";
window.FS_GENERAL_SIDEBAR_PLAYLIST_SORTING = "${FS_GENERAL_SIDEBAR_PLAYLIST_SORTING}";
window.FS_GENERAL_SIDE_QUEUE_TYPE = "${FS_GENERAL_SIDE_QUEUE_TYPE}";
window.FS_GENERAL_SIDE_QUEUE_LAYOUT = "${FS_GENERAL_SIDE_QUEUE_LAYOUT}";
window.FS_GENERAL_THEME = "${FS_GENERAL_THEME}";
window.FS_GENERAL_THEME_DARK = "${FS_GENERAL_THEME_DARK}";
window.FS_GENERAL_THEME_LIGHT = "${FS_GENERAL_THEME_LIGHT}";
window.FS_GENERAL_USE_THEME_ACCENT_COLOR = "${FS_GENERAL_USE_THEME_ACCENT_COLOR}";
window.FS_GENERAL_USE_THEME_PRIMARY_SHADE = "${FS_GENERAL_USE_THEME_PRIMARY_SHADE}";
window.FS_GENERAL_ZOOM_FACTOR = "${FS_GENERAL_ZOOM_FACTOR}";
window.FS_PLAYBACK_MEDIA_SESSION = "${FS_PLAYBACK_MEDIA_SESSION}";
window.FS_PLAYBACK_WEB_AUDIO = "${FS_PLAYBACK_WEB_AUDIO}";
window.FS_PLAYBACK_AUDIO_FADE_ON_STATUS_CHANGE = "${FS_PLAYBACK_AUDIO_FADE_ON_STATUS_CHANGE}";
window.FS_PLAYBACK_PRESERVE_PITCH = "${FS_PLAYBACK_PRESERVE_PITCH}";
window.FS_PLAYBACK_SCROBBLE_ENABLED = "${FS_PLAYBACK_SCROBBLE_ENABLED}";
window.FS_PLAYBACK_SCROBBLE_NOTIFY = "${FS_PLAYBACK_SCROBBLE_NOTIFY}";
window.FS_PLAYBACK_SCROBBLE_AT_DURATION = "${FS_PLAYBACK_SCROBBLE_AT_DURATION}";
window.FS_PLAYBACK_SCROBBLE_AT_PERCENTAGE = "${FS_PLAYBACK_SCROBBLE_AT_PERCENTAGE}";
window.FS_PLAYBACK_TRANSCODE_ENABLED = "${FS_PLAYBACK_TRANSCODE_ENABLED}";
window.FS_DISCORD_ENABLED = "${FS_DISCORD_ENABLED}";
window.FS_DISCORD_CLIENT_ID = "${FS_DISCORD_CLIENT_ID}";
window.FS_DISCORD_DISPLAY_TYPE = "${FS_DISCORD_DISPLAY_TYPE}";
window.FS_DISCORD_LINK_TYPE = "${FS_DISCORD_LINK_TYPE}";
window.FS_DISCORD_SHOW_AS_LISTENING = "${FS_DISCORD_SHOW_AS_LISTENING}";
window.FS_DISCORD_SHOW_PAUSED = "${FS_DISCORD_SHOW_PAUSED}";
window.FS_DISCORD_SHOW_SERVER_IMAGE = "${FS_DISCORD_SHOW_SERVER_IMAGE}";
window.FS_DISCORD_SHOW_STATE_ICON = "${FS_DISCORD_SHOW_STATE_ICON}";
window.FS_LYRICS_FETCH = "${FS_LYRICS_FETCH}";
window.FS_LYRICS_FOLLOW = "${FS_LYRICS_FOLLOW}";
window.FS_LYRICS_DELAY_MS = "${FS_LYRICS_DELAY_MS}";
window.FS_LYRICS_PREFER_LOCAL = "${FS_LYRICS_PREFER_LOCAL}";
window.FS_LYRICS_SHOW_MATCH = "${FS_LYRICS_SHOW_MATCH}";
window.FS_LYRICS_SHOW_PROVIDER = "${FS_LYRICS_SHOW_PROVIDER}";
window.FS_LYRICS_ENABLE_AUTO_TRANSLATION = "${FS_LYRICS_ENABLE_AUTO_TRANSLATION}";
window.FS_LYRICS_TRANSLATION_API_KEY = "${FS_LYRICS_TRANSLATION_API_KEY}";
window.FS_LYRICS_TRANSLATION_TARGET_LANGUAGE = "${FS_LYRICS_TRANSLATION_TARGET_LANGUAGE}";
window.FS_LYRICS_ALIGNMENT = "${FS_LYRICS_ALIGNMENT}";
window.FS_AUTO_DJ_ENABLED = "${FS_AUTO_DJ_ENABLED}";
window.FS_AUTO_DJ_ITEM_COUNT = "${FS_AUTO_DJ_ITEM_COUNT}";
window.FS_AUTO_DJ_TIMING = "${FS_AUTO_DJ_TIMING}";
window.FS_CSS_CONTENT = "${FS_CSS_CONTENT}";
window.FS_CSS_ENABLED = "${FS_CSS_ENABLED}";
window.FS_FONT_TYPE = "${FS_FONT_TYPE}";
window.FS_FONT_BUILT_IN = "${FS_FONT_BUILT_IN}";
window.FS_FONT_SYSTEM = "${FS_FONT_SYSTEM}";
================================================
FILE: src/i18n/i18n.ts
================================================
import { PostProcessorModule, TOptions } from 'i18next';
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import ar from './locales/ar.json';
import ca from './locales/ca.json';
import cs from './locales/cs.json';
import de from './locales/de.json';
import en from './locales/en.json';
import es from './locales/es.json';
import eu from './locales/eu.json';
import fa from './locales/fa.json';
import fi from './locales/fi.json';
import fr from './locales/fr.json';
import hu from './locales/hu.json';
import id from './locales/id.json';
import it from './locales/it.json';
import ja from './locales/ja.json';
import ko from './locales/ko.json';
import nbNO from './locales/nb-NO.json';
import nl from './locales/nl.json';
import pl from './locales/pl.json';
import ptBr from './locales/pt-BR.json';
import pt from './locales/pt.json';
import ru from './locales/ru.json';
import sl from './locales/sl.json';
import sr from './locales/sr.json';
import sv from './locales/sv.json';
import ta from './locales/ta.json';
import tr from './locales/tr.json';
import zhHans from './locales/zh-Hans.json';
import zhHant from './locales/zh-Hant.json';
const resources = {
ar: { translation: ar },
ca: { translation: ca },
cs: { translation: cs },
de: { translation: de },
en: { translation: en },
es: { translation: es },
eu: { translation: eu },
fa: { translation: fa },
fi: { translation: fi },
fr: { translation: fr },
hu: { translation: hu },
id: { translation: id },
it: { translation: it },
ja: { translation: ja },
ko: { translation: ko },
'nb-NO': { translation: nbNO },
nl: { translation: nl },
pl: { translation: pl },
pt: { translation: pt },
'pt-BR': { translation: ptBr },
ru: { translation: ru },
sl: { translation: sl },
sr: { translation: sr },
sv: { translation: sv },
ta: { translation: ta },
tr: { translation: tr },
'zh-Hans': { translation: zhHans },
'zh-Hant': { translation: zhHant },
};
export const languages = [
{
label: 'English',
value: 'en',
},
{
label: 'العربية',
value: 'ar',
},
{
label: 'Català',
value: 'ca',
},
{
label: 'Čeština',
value: 'cs',
},
{
label: 'Deutsch',
value: 'de',
},
{
label: 'Español',
value: 'es',
},
{
label: 'Basque',
value: 'eu',
},
{
label: 'Français',
value: 'fr',
},
{
label: 'Bahasa Indonesia',
value: 'id',
},
{
label: 'Suomeksi',
value: 'fi',
},
{
label: 'Magyar',
value: 'hu',
},
{
label: 'Italiano',
value: 'it',
},
{
label: '日本語',
value: 'ja',
},
{
label: '한국어',
value: 'ko',
},
{
label: 'Nederlands',
value: 'nl',
},
{
label: 'Norsk (Bokmål)',
value: 'nb-NO',
},
{
label: 'فارسی',
value: 'fa',
},
{
label: 'Português',
value: 'pt',
},
{
label: 'Português (Brasil)',
value: 'pt-BR',
},
{
label: 'Polski',
value: 'pl',
},
{
label: 'Русский',
value: 'ru',
},
{
label: 'Slovenščina',
value: 'sl',
},
{
label: 'Srpski',
value: 'sr',
},
{
label: 'Svenska',
value: 'sv',
},
{
label: 'Tamil',
value: 'ta',
},
{
label: 'Türkçe',
value: 'tr',
},
{
label: '简体中文',
value: 'zh-Hans',
},
{
label: '繁體中文',
value: 'zh-Hant',
},
];
const lowerCasePostProcessor: PostProcessorModule = {
name: 'lowerCase',
process: (value: string) => {
return value.toLocaleLowerCase();
},
type: 'postProcessor',
};
const upperCasePostProcessor: PostProcessorModule = {
name: 'upperCase',
process: (value: string) => {
return value.toLocaleUpperCase();
},
type: 'postProcessor',
};
const titleCasePostProcessor: PostProcessorModule = {
name: 'titleCase',
process: (value: string) => {
return value.replace(/\S\S*/g, (txt) => {
return txt.charAt(0).toLocaleUpperCase() + txt.slice(1).toLowerCase();
});
},
type: 'postProcessor',
};
const ignoreSentenceCaseLanguages = ['de'];
const sentenceCasePostProcessor: PostProcessorModule = {
name: 'sentenceCase',
process: (
value: string,
_key: string,
_options: TOptions<Record<string, string>>,
translator: any,
) => {
const sentences = value.split('. ');
return sentences
.map((sentence) => {
return (
sentence.charAt(0).toLocaleUpperCase() +
(!ignoreSentenceCaseLanguages.includes(translator.language)
? sentence.slice(1).toLocaleLowerCase()
: sentence.slice(1))
);
})
.join('. ');
},
type: 'postProcessor',
};
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;
================================================
FILE: src/i18n/i18next-parser.config.js
================================================
// Reference: https://github.com/i18next/i18next-parser#options
module.exports = {
contextSeparator: '_',
createOldCatalogs: true,
customValueTemplate: null,
defaultNamespace: 'translation',
defaultValue: function (locale, namespace, key) {
return key;
},
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',
sort: true,
verbose: false,
};
================================================
FILE: src/i18n/locales/ar.json
================================================
{
"action": {
"addToFavorites": "إضافة الى $t(entity.favorite, {\"count\": 2})",
"addToPlaylist": "إضافة الى $t(entity.playlist, {\"count\": 1})",
"clearQueue": "مسح قائمة الإنتظار",
"createPlaylist": "إنشاء $t(entity.playlist, {\"count\": 1})",
"deletePlaylist": "حذف $t(entity.playlist, {\"count\": 1})",
"deselectAll": "إلغاء تحديد الكل",
"editPlaylist": "تعديل $t(entity.playlist, {\"count\": 1})",
"goToPage": "اذهب الى صفحة",
"moveToNext": "الذهاب الى التالي",
"moveToBottom": "الذهاب الى الأسفل",
"moveToTop": "الذهاب الى الأعلى",
"refresh": "$t(common.refresh)",
"removeFromFavorites": "حذف من $t(entity.favorite, {\"count\": 2})",
"removeFromPlaylist": "حذف من $t(entity.playlist, {\"count\": 1})",
"removeFromQueue": "حذف من قائمة الإنتظار",
"setRating": "تحديد التقييم",
"toggleSmartPlaylistEditor": "تشغيل / إطفاء وضع التعديل لـ $t(entity.smartPlaylist)",
"viewPlaylists": "إظهار $t(entity.playlist, {\"count\": 2})",
"openIn": {
"lastfm": "فتح في Last.fm",
"musicbrainz": "فتح في MusicBrainz"
},
"addOrRemoveFromSelection": "إضافة أو إزالة من الإختيارات",
"selectRangeOfItems": "اختر مجموعة من العناصر",
"goToCurrent": "الانتقال إلى العنصر الحالي",
"createRadioStation": "يخلق $t(entity.radioStation, {\"count\": 1})",
"deleteRadioStation": "يمسح $t(entity.radioStation, {\"count\": 1})",
"selectAll": "تحديد الكل"
},
"common": {
"action_zero": "عملية",
"action_one": "عملية",
"action_two": "عمليتين",
"action_few": "عمليات",
"action_many": "عمليات",
"action_other": "عمليات",
"add": "إضافة",
"additionalParticipants": "مشاركين إضافيين",
"newVersion": "تم تثبيت تحديث جديد {{version}}",
"viewReleaseNotes": "عرض معلومات الإصدار",
"albumGain": "مستوى صوت الألبوم",
"albumPeak": "اعلى مستوى للألبوم",
"areYouSure": "هل أنت متأكد؟",
"ascending": "تصاعدي",
"backward": "خلف",
"biography": "سيرة",
"bitDepth": "عمق البت",
"bitrate": "معدل البت (البت ريت)",
"bpm": "نبضة في الدقيقة",
"cancel": "إلغاء",
"center": "منتصف",
"channel_zero": "قناة",
"channel_one": "قناة",
"channel_two": "قناتين",
"channel_few": "قنوات",
"channel_many": "قنوات",
"channel_other": "قنوات",
"clear": "مسح",
"close": "إغلاق",
"codec": "كوديك",
"collapse": "طي",
"comingSoon": "قريبًا…",
"configure": "تعديل",
"confirm": "تأكيد",
"create": "إنشاء",
"currentSong": "$t(entity.track, {\"count\": 1}) الحالي",
"decrease": "تنقيص",
"delete": "حذف",
"descending": "تنازلي",
"description": "وصف",
"disable": "تعطيل",
"disc": "قرص",
"dismiss": "إخفاء",
"duration": "مدة",
"edit": "تعديل",
"enable": "تفعيل",
"expand": "توسيع",
"favorite": "مفضلة",
"filter_zero": "فلتر",
"filter_one": "فلتر",
"filter_two": "فلاتر",
"filter_few": "فلاتر",
"filter_many": "فلاتر",
"filter_other": "فلاتر",
"filters": "فلاتر",
"forceRestartRequired": "اعد التشغيل لتطبيق التعديلات... اغلق التنبية لإعادة التشغيل",
"forward": "امام",
"gap": "فجوة",
"home": "الرئيسية",
"increase": "زيادة",
"left": "يسار",
"limit": "حد",
"manage": "إدارة",
"maximize": "تكبير",
"menu": "القائمة",
"minimize": "تصغير",
"modified": "تم تعديله",
"mbid": "معرف MusicBrainz",
"name": "إسم",
"no": "لا",
"none": "لا شي",
"noResultsFromQuery": "لا توجد نتائج",
"note": "ملاحظة",
"ok": "نعم",
"owner": "المالك",
"path": "المسار",
"playerMustBePaused": "يجب إيقاف المشغل",
"preview": "معاينة",
"previousSong": "$t(entity.track, {\"count\": 1}) السابق",
"quit": "خروج",
"random": "عشوائي",
"rating": "التقييم",
"refresh": "تحديث",
"reload": "تحديث",
"reset": "إعادة تعيين",
"resetToDefault": "إعادة تعيين الى الافتراضي",
"restartRequired": "يجب إعادة التشغيل",
"right": "يمين",
"sampleRate": "معدل العينة (sample rate)",
"save": "حفظ",
"saveAndReplace": "حفظ واستبدال",
"saveAs": "حفظ بإسم",
"search": "بحث",
"setting_zero": "إعداد",
"setting_one": "",
"setting_two": "",
"setting_few": "",
"setting_many": "",
"setting_other": "",
"share": "نشر",
"size": "حجم",
"sortOrder": "الترتيب",
"tags": "العلامات",
"title": "العنوان",
"trackNumber": "رقم المسار",
"trackGain": "مستوى صوت المسار",
"trackPeak": "اعلى مستوى للمسار",
"translation": "الترجمة",
"unknown": "غير معروف",
"version": "الإصدار",
"year": "السنة",
"yes": "نعم"
},
"entity": {
"album_zero": "الالبوم",
"album_one": "الالبوم",
"album_two": "الالبومين",
"album_few": "الالبومات",
"album_many": "الالبومات",
"album_other": "الالبومات",
"albumArtist_zero": "فنان الالبوم",
"albumArtist_one": "فنان الالبوم",
"albumArtist_two": "فنان الالبومين",
"albumArtist_few": "فنان الالبومات",
"albumArtist_many": "فنان الالبومات",
"albumArtist_other": "فنان الالبومات"
}
}
================================================
FILE: src/i18n/locales/ca.json
================================================
{
"page": {
"sidebar": {
"myLibrary": "La meva llibreria",
"albumArtists": "$t(entity.albumArtist, {\"count\": 2})",
"albums": "$t(entity.album, {\"count\": 2})",
"artists": "$t(entity.artist, {\"count\": 2})",
"folders": "$t(entity.folder, {\"count\": 2})",
"genres": "$t(entity.genre, {\"count\": 2})",
"home": "$t(common.home)",
"playlists": "$t(entity.playlist, {\"count\": 2})",
"search": "$t(common.search)",
"settings": "$t(common.setting, {\"count\": 2})",
"tracks": "$t(entity.track, {\"count\": 2})",
"nowPlaying": "s'està reproduint",
"shared": "$t(entity.playlist, {\"count\": 2}) compartides",
"favorites": "$t(entity.favorite, {\"count\": 2})",
"radio": "$t(entity.radioStation, {\"count\": 2})",
"collections": "col·leccions"
},
"albumArtistDetail": {
"relatedArtists": "$t(entity.artist, {\"count\": 2}) similars",
"viewAllTracks": "mostra totes les $t(entity.track, {\"count\": 2})",
"about": "Sobre {{artist}}",
"appearsOn": "apareix a",
"recentReleases": "Llançaments recents",
"viewDiscography": "Mosta la discografia",
"topSongs": "millors cançons",
"topSongsFrom": "les millors cançons de {{title}}",
"viewAll": "mostra-ho tot",
"groupingTypeAll": "tots els tipus de llançaments",
"groupingTypePrimary": "tipus principals de llançament",
"favoriteSongs": "Cançons preferides",
"topSongsCommunity": "comunitat",
"topSongsPersonal": "personal",
"favoriteSongsFrom": "cançons preferides de {{title}}"
},
"albumArtistList": {
"title": "$t(entity.albumArtist, {\"count\": 2})"
},
"albumDetail": {
"moreFromArtist": "més d'aquest $t(entity.artist, {\"count\": 1})",
"moreFromGeneric": "més de {{item}}",
"released": "publicat"
},
"albumList": {
"title": "$t(entity.album, {\"count\": 2})",
"artistAlbums": "àlbums de {{artist}}",
"genreAlbums": "\"{{genre}}\" $t(entity.album, {\"count\": 2})"
},
"appMenu": {
"quit": "$t(common.quit)",
"settings": "$t(common.setting, {\"count\": 2})",
"goBack": "torna enrere",
"goForward": "avança",
"collapseSidebar": "replega la barra lateral",
"expandSidebar": "expandeix la barra lateral",
"manageServers": "gestionar servidors",
"selectServer": "seleccionar servidor",
"version": "versió {{version}}",
"openBrowserDevtools": "obre les eines de desenvolupament del navegador",
"privateModeOff": "desactiva el mode privat",
"privateModeOn": "activa el mode privat",
"commandPalette": "obre la paleta d'ordres",
"selectMusicFolder": "selecciona una carpeta de música",
"noMusicFolder": "no s'ha seleccionat cap carpeta de música",
"multipleMusicFolders": "{{count}} carpetes de música seleccionades"
},
"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)",
"moveToNext": "$t(action.moveToNext)",
"moveToBottom": "$t(action.moveToBottom)",
"moveToTop": "$t(action.moveToTop)",
"play": "$t(player.play)",
"playSimilarSongs": "$t(player.playSimilarSongs)",
"removeFromFavorites": "$t(action.removeFromFavorites)",
"removeFromPlaylist": "$t(action.removeFromPlaylist)",
"removeFromQueue": "$t(action.removeFromQueue)",
"setRating": "$t(action.setRating)",
"playShuffled": "$t(player.shuffle)",
"download": "descarregar",
"showDetails": "informació",
"numberSelected": "{{count}} seleccionat",
"shareItem": "comparteix l'element",
"goToAlbumArtist": "Ves a $t(entity.albumArtist, {\"count\": 1})",
"goToAlbum": "ves a $t(entity.album, {\"count\": 1})",
"moveItems": "$t(action.moveItems)",
"goTo": "ves a"
},
"genreList": {
"title": "$t(entity.genre, {\"count\": 2})",
"showAlbums": "mostra $t(entity.album, {\"count\": 2}) de $t(entity.genre, {\"count\": 1})",
"showTracks": "mostra $t(entity.track, {\"count\": 2}) de $t(entity.genre, {\"count\": 1})"
},
"home": {
"title": "$t(common.home)",
"explore": "explora la teva biblioteca",
"newlyAdded": "afegits recentment",
"mostPlayed": "els més reproduïts",
"recentlyPlayed": "reproduït recentment",
"recentlyReleased": "estrenat fa poc",
"genres": "$t(entity.genre, {\"count\": 2})"
},
"playlistList": {
"title": "$t(entity.playlist, {\"count\": 2})"
},
"trackList": {
"title": "$t(entity.track, {\"count\": 2})",
"artistTracks": "pistes de {{artist}}",
"genreTracks": "$t(entity.track, {\"count\": 2}) \"{{genre}}\""
},
"manageServers": {
"username": "nom d'usuari",
"title": "gestionar servidors",
"serverDetails": "detalls del servidor",
"editServerDetailsTooltip": "editar els detalls del servidor",
"removeServer": "eliminar el servidor",
"url": "URL"
},
"fullscreenPlayer": {
"config": {
"opacity": "opacitat",
"synchronized": "sincronitzat",
"unsynchronized": "no sincronitzat",
"useImageAspectRatio": "utilitza la relació d'aspecte de la imatge",
"dynamicBackground": "fons dinàmic",
"dynamicIsImage": "activar la imatge de fons",
"followCurrentLyric": "seguir la lletra actual",
"lyricAlignment": "alineació de la lletra",
"lyricSize": "tamany de la lletra",
"dynamicImageBlur": "mida del desenfocament de la imatge",
"lyricOffset": "demora de la lletra (ms)",
"showLyricMatch": "mosta coincidències de lletres",
"showLyricProvider": "mostra el proveïdor de la lletra",
"lyricGap": "espera entre lletres"
},
"lyrics": "lletres",
"visualizer": "visualitzador",
"noLyrics": "no s'ha trobat cap lletra",
"related": "relacionat",
"upNext": "a continuació"
},
"setting": {
"advanced": "avançat",
"generalTab": "general",
"hotkeysTab": "tecles d'accés ràpid",
"playbackTab": "reproducció",
"windowTab": "finestra",
"analytics": "analítiques",
"updates": "actualitza",
"cache": "memòria cau",
"application": "aplicació",
"queryBuilder": "constructor de consultes",
"theme": "tema",
"controls": "controls",
"sidebar": "barra lateral",
"remote": "remot",
"exportImport": "importa/exporta",
"scrobble": "scrobble",
"audio": "àudio",
"lyrics": "lletra",
"transcoding": "transcodificació",
"discord": "discord",
"logger": "registres",
"playerFilters": "filtres de reproducció",
"lyricsDisplay": "mostra la lletra"
},
"globalSearch": {
"commands": {
"goToPage": "anar a la pàgina",
"searchFor": "cerca {{query}}",
"serverCommands": "ordres del servidor"
},
"title": "ordres"
},
"itemDetail": {
"copyPath": "copia ruta al porta-retalls",
"copiedPath": "ruta copiada correctament",
"openFile": "mostra la pista al gestor d'arxius"
},
"playlist": {
"reorder": "el reordenament només s'activa quan s'ordena per id"
},
"favorites": {
"title": "$t(entity.favorite, {\"count\": 2})"
},
"folderList": {
"title": "$t(entity.folder, {\"count\": 2})"
},
"radioList": {
"title": "emissores de ràdio"
},
"windowBar": {
"paused": "(en pausa) ",
"privateMode": "(mode privat)"
},
"collections": {
"overrideExisting": "sobreescriu existents",
"saveAsCollection": "desa com a col·lecció"
},
"releasenotes": {
"commitsSinceStable": "commits des de {{stable}}",
"noNewCommits": "no hi ha hagut commits en aquest període",
"noStableReleaseToCompare": "no hi ha actualitzacions disponibles amb les quals comparar"
}
},
"common": {
"home": "inici",
"year": "any",
"add": "afegir",
"ascending": "ascendent",
"biography": "biografia",
"bitrate": "taxa de bits",
"bpm": "bpm",
"cancel": "cancel·lar",
"center": "centrar",
"close": "tancar",
"codec": "còdec",
"configure": "configurar",
"confirm": "confirmar",
"create": "crear",
"decrease": "disminuir",
"delete": "eliminar",
"descending": "descendent",
"description": "descripció",
"disable": "desactivar",
"disc": "disc",
"dismiss": "descartar",
"duration": "duració",
"edit": "editar",
"enable": "activar",
"expand": "expandir",
"filters": "filtres",
"increase": "incrementar",
"left": "esquerra",
"maximize": "maximitzar",
"menu": "menú",
"minimize": "minimitzar",
"modified": "modificació",
"name": "nom",
"no": "no",
"none": "cap",
"note": "nota",
"ok": "bé",
"preview": "vista prèvia",
"quit": "sortir",
"random": "aleatori",
"rating": "valoració",
"reload": "torna a carregar",
"reset": "restablir",
"right": "dreta",
"save": "desar",
"search": "cercar",
"share": "compartir",
"size": "mida",
"sortOrder": "ordenar",
"tags": "etiquetes",
"title": "títol",
"translation": "traducció",
"unknown": "desconegut",
"version": "versió",
"yes": "sí",
"additionalParticipants": "participants addicionals",
"channel_one": "canal",
"channel_many": "canals",
"channel_other": "canals",
"filter_one": "filtre",
"filter_many": "filtres",
"filter_other": "filtres",
"saveAs": "desar com",
"action_one": "acció",
"action_many": "accions",
"action_other": "accions",
"newVersion": "s'ha instal·lat una nova versió ({{version}})",
"viewReleaseNotes": "veure les notes de la versió",
"currentSong": "$t(entity.track, {\"count\": 1}) actual",
"limit": "límit",
"previousSong": "$t(entity.track, {\"count\": 1}) anterior",
"trackNumber": "pista",
"albumGain": "guany de l'àlbum",
"albumPeak": "pic de l'àlbum",
"areYouSure": "estàs segur?",
"backward": "enrere",
"clear": "neteja",
"collapse": "col·lapsa",
"comingSoon": "aviat disponible…",
"favorite": "preferit",
"forceRestartRequired": "reinicia per aplicar els canvis… tanca la notificació per reiniciar",
"owner": "propietari",
"refresh": "actualitzar",
"resetToDefault": "restablir els valors predeterminats",
"saveAndReplace": "desar i substituir",
"bitDepth": "profunditat de bits",
"forward": "endavant",
"manage": "gestiona",
"mbid": "ID de MusicBrainz",
"noResultsFromQuery": "la petició no ha produït resultats",
"path": "ruta",
"playerMustBePaused": "cal pausar el reproductor",
"restartRequired": "cal reiniciar",
"sampleRate": "freqüència de mostreig",
"setting_one": "configuració",
"setting_many": "configuracions",
"setting_other": "configuracions",
"trackGain": "guany de pista",
"trackPeak": "pic de pista",
"gap": "espera",
"explicitStatus": "estat explícit",
"explicit": "explícit",
"clean": "net",
"private": "privat",
"public": "públic",
"recordLabel": "segell discogràfic",
"releaseType": "tipus de llançament",
"doNotShowAgain": "no tornis a mostrar això",
"view": "mostra",
"externalLinks": "enllaços externs",
"faster": "més ràpid",
"noFilters": "cap filtre configurat",
"slower": "més lent",
"sort": "ordre",
"gridRows": "files de la quadrícula",
"tableColumns": "columnes de la taula",
"itemsMore": "{{count}} més",
"countSelected": "{{count}} seleccionats",
"retry": "reintenta",
"example": "exemple",
"mood": "estat d'ànim",
"filter_single": "senzill",
"filter_multiple": "multi",
"rename": "reanomena",
"newVersionAvailable": "hi ha una nova versió disponible"
},
"entity": {
"album_one": "àlbum",
"album_many": "àlbums",
"album_other": "àlbums",
"albumWithCount_one": "{{count}} àlbum",
"albumWithCount_many": "{{count}} àlbums",
"albumWithCount_other": "{{count}} àlbums",
"albumArtist_one": "artista de l'àlbum",
"albumArtist_many": "artistes de l'àlbum",
"albumArtist_other": "artistes de l'àlbum",
"albumArtistCount_one": "{{count}} artista de l'àlbum",
"albumArtistCount_many": "{{count}} artistes de l'àlbum",
"albumArtistCount_other": "{{count}} artistes de l'àlbum",
"artist_one": "artista",
"artist_many": "artistes",
"artist_other": "artistes",
"artistWithCount_one": "{{count}} artista",
"artistWithCount_many": "{{count}} artistes",
"artistWithCount_other": "{{count}} artistes",
"playlist_one": "llista de reproducció",
"playlist_many": "llistes de reproducció",
"playlist_other": "llistes de reproducció",
"playlistWithCount_one": "{{count}} llista de reproducció",
"playlistWithCount_many": "{{count}} llistes de reproducció",
"playlistWithCount_other": "{{count}} llistes de reproducció",
"smartPlaylist": "$t(entity.playlist, {\"count\": 1}) intel·ligent",
"play_one": "{{count}} reproducció",
"play_many": "{{count}} reproduccions",
"play_other": "{{count}} reproduccions",
"folderWithCount_one": "{{count}} carpeta",
"folderWithCount_many": "{{count}} carpetes",
"folderWithCount_other": "{{count}} carpetes",
"genreWithCount_one": "{{count}} gènere",
"genreWithCount_many": "{{count}} gèneres",
"genreWithCount_other": "{{count}} gèneres",
"track_one": "pista",
"track_many": "pistes",
"track_other": "pistes",
"trackWithCount_one": "{{count}} pista",
"trackWithCount_many": "{{count}} pistes",
"trackWithCount_other": "{{count}} pistes",
"folder_one": "carpeta",
"folder_many": "carpetes",
"folder_other": "carpetes",
"genre_one": "gènere",
"genre_many": "gèneres",
"genre_other": "gèneres",
"song_one": "cançó",
"song_many": "cançons",
"song_other": "cançons",
"favorite_one": "preferit",
"favorite_many": "preferits",
"favorite_other": "preferits",
"radioStation_one": "emissora de ràdio",
"radioStation_many": "emissores de ràdio",
"radioStation_other": "emissores de ràdio",
"radioStationWithCount_one": "{{count}} emissora de ràdio",
"radioStationWithCount_many": "{{count}} emissores de ràdio",
"radioStationWithCount_other": "{{count}} emissores de ràdio"
},
"form": {
"addToPlaylist": {
"input_playlists": "$t(entity.playlist, {\"count\": 2})",
"title": "afegir a una $t(entity.playlist, {\"count\": 1})",
"input_skipDuplicates": "salta't els duplicats",
"success": "s'ha afegit $t(entity.trackWithCount, {\"count\": {{message}} }) a $t(entity.playlistWithCount, {\"count\": {{numOfPlaylists}} })",
"create": "crea $t(entity.playlist, {\"count\": 1}) {{playlist}}",
"searchOrCreate": "cerca $t(entity.playlist, {\"count\": 2}) o escriu per crear-ne una de nova"
},
"createPlaylist": {
"input_description": "$t(common.description)",
"input_name": "$t(common.name)",
"input_owner": "$t(common.owner)",
"success": "$t(entity.playlist, {\"count\": 1}) s'ha creat amb èxit",
"title": "crea una $t(entity.playlist, {\"count\": 1})",
"input_public": "públic"
},
"deletePlaylist": {
"success": "$t(entity.playlist, {\"count\": 1}) s'ha eliminat amb èxit",
"title": "elimina la $t(entity.playlist, {\"count\": 1})",
"input_confirm": "escriviu el nom de la $t(entity.playlist, {\"count\": 1}) per confirmar"
},
"editPlaylist": {
"success": "$t(entity.playlist, {\"count\": 1}) s'ha actualitzat amb èxit",
"title": "editar la $t(entity.playlist, {\"count\": 1})",
"publicJellyfinNote": "Per algun motiu, Jellyfin no exposa si una llista de reproducció és pública o no. Si voleu que es mantingui pública, seleccioneu la següent entrada",
"editNote": "es recomana no editar manualment les llistes de reproducció grans. segur que accepteu el risc de perdre dades si sobreescriviu la llista de reproducció existent?"
},
"lyricSearch": {
"input_artist": "$t(entity.artist, {\"count\": 1})",
"input_name": "$t(common.name)",
"title": "cerca de lletres"
},
"addServer": {
"input_password": "contrasenya",
"input_username": "nom d'usuari",
"error_savePassword": "hi ha hagut un error en intentar desar la contrasenya",
"ignoreCors": "ignora el cors ($t(common.restartRequired))",
"ignoreSsl": "ignora l'ssl ($t(common.restartRequired))",
"input_legacyAuthentication": "activa l'autenticació antiga",
"input_name": "nom del servidor",
"input_savePassword": "desa la contrasenya",
"input_url": "url",
"success": "servidor afegit correctament",
"title": "afegeix un servidor",
"input_preferInstantMix": "prefereix el mix instantani",
"input_preferInstantMixDescription": "utilitza només el mix instantani per obtenir cançons similars. útil si teniu complements que modifiquin aquest comportament",
"input_preferRemoteUrl": "prefereix l'url públic",
"input_remoteUrl": "url públic",
"input_remoteUrlPlaceholder": "opcional: url públic per característiques externes"
},
"shareItem": {
"description": "descripció",
"allowDownloading": "permetre descàrrega",
"setExpiration": "estableix expiració",
"success": "s'ha copiat l'enllaç de compartició al porta-retalls (o feu clic aquí per obrir-lo)",
"expireInvalid": "la data d'expiració ha de ser al futur",
"createFailed": "no s'ha pogut crear el recurs compartit (està habilitat, l'ús compartit?)",
"copyToClipboard": "Copiar al porta-retalls: Ctrl+C, Enter",
"successMustClick": "Compartició creada correctament. Feu clic aquí per obrir-la."
},
"updateServer": {
"success": "s'ha actualitzat el servidor amb èxit",
"title": "actualitzar el servidor"
},
"queryEditor": {
"title": "editor de consultes",
"input_optionMatchAll": "coincidències totals",
"input_optionMatchAny": "coincidències parcials",
"addRuleGroup": "afegeix el grup de regles",
"removeRuleGroup": "elimina el grup de regles",
"resetToDefault": "reestableix als valors predeterminats",
"clearFilters": "neteja els filtres"
},
"privateMode": {
"enabled": "mode privat actiu; l'estat de reproducció ara està ocult d'integracions externes",
"disabled": "mode privat inactiu; l'estat de reproducció ara és visible per les integracions externes",
"title": "mode privat"
},
"largeFetchConfirmation": {
"title": "afegeix elements a la cua",
"description": "Aquesta acció afegirà tots els elements a la vista filtrada actual"
},
"shuffleAll": {
"title": "reprodueix a l'atzar",
"input_genre": "$t(entity.genre, {\"count\": 1})",
"input_limit": "quantes cançons?",
"input_minYear": "de l'any",
"input_maxYear": "fins a l'any",
"input_played": "reprodueix el filtre",
"input_played_optionAll": "totes les pistes",
"input_played_optionUnplayed": "només les pistes sense reproduir",
"input_played_optionPlayed": "només les pistes reproduïdes"
},
"createRadioStation": {
"success": "emissora de ràdio creada amb èxit",
"title": "crea una emissora de ràdio",
"input_homepageUrl": "URL de la pàgina d'inici",
"input_name": "nom",
"input_streamUrl": "URL de transmissió"
},
"saveQueue": {
"success": "cua de reproducció desada al servidor"
},
"lyricsExport": {
"export": "exporta la lletra",
"input_synced": "exporta la lletra sincronitzada",
"input_offset": "$t(setting.lyricOffset)"
}
},
"action": {
"addToFavorites": "afegeix a $t(entity.favorite, {\"count\": 2})",
"addToPlaylist": "afegeix a $t(entity.playlist, {\"count\": 1})",
"createPlaylist": "crea $t(entity.playlist, {\"count\": 1})",
"deletePlaylist": "elimina la $t(entity.playlist, {\"count\": 1})",
"editPlaylist": "edita $t(entity.playlist, {\"count\": 1})",
"refresh": "$t(common.refresh)",
"removeFromFavorites": "elimina dels $t(entity.favorite, {\"count\": 2})",
"removeFromPlaylist": "elimina de $t(entity.playlist, {\"count\": 1})",
"clearQueue": "buida la cua",
"removeFromQueue": "treure de la cua",
"goToPage": "anar a la pàgina",
"openIn": {
"lastfm": "Obrir a Last.fm",
"musicbrainz": "Obrir a MusicBrainz"
},
"deselectAll": "deselecciona-ho tot",
"viewPlaylists": "veure $t(entity.playlist, {\"count\": 2})",
"moveToNext": "passar al següent",
"moveToBottom": "anar al final",
"moveToTop": "anar al principi",
"setRating": "Qualifica",
"toggleSmartPlaylistEditor": "canvia l'editor $t(entity.smartPlaylist)",
"downloadStarted": "s'ha iniciat la descàrrega de {{count}} elements",
"moveUp": "mou amunt",
"moveDown": "mou avall",
"holdToMoveToTop": "mantingueu premut per moure al capdamunt",
"holdToMoveToBottom": "mantingueu premut per moure al capdavall",
"moveItems": "mou elements",
"shuffle": "mescla",
"shuffleAll": "mescla-ho tot",
"shuffleSelected": "mescla els seleccionats",
"viewMore": "mostra'n més",
"createRadioStation": "crea $t(entity.radioStation, {\"count\": 1})",
"deleteRadioStation": "elimina $t(entity.radioStation, {\"count\": 1})",
"ad
gitextract_6koal0cm/ ├── .dockerignore ├── .editorconfig ├── .gitattributes ├── .github/ │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── 01-feature_request.yml │ │ ├── 02-bug_report.yml │ │ └── config.yml │ ├── config.yml │ ├── stale.yml │ └── workflows/ │ ├── publish-alpha.yml │ ├── publish-beta.yml │ ├── publish-docker-auto.yml │ ├── publish-docker.yml │ ├── publish-linux.yml │ ├── publish-macos.yml │ ├── publish-pr-comment.yml │ ├── publish-pr.yml │ ├── publish-windows.yml │ ├── publish-winget.yml │ ├── publish.yml │ ├── stale.yml │ └── test.yml ├── .gitignore ├── .npmrc ├── .prettierignore ├── .prettierrc.yaml ├── .stylelintrc.json ├── .vscode/ │ ├── extensions.json │ ├── launch.json │ └── settings.json ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── README.md ├── assets/ │ ├── assets.d.ts │ ├── entitlements.mac.plist │ └── icons/ │ └── icon.icns ├── dev-app-update.yml ├── docker-compose.yaml ├── docs/ │ └── ENV_SETTINGS.md ├── electron-builder-alpha.yml ├── electron-builder-beta.yml ├── electron-builder.yml ├── electron.vite.config.ts ├── eslint.config.mjs ├── feishin.desktop.tmpl ├── install-feishin-appimage ├── ng.conf.template ├── org.jeffvli.feishin.metainfo.xml ├── package.json ├── postcss.config.cjs ├── remote.vite.config.ts ├── scripts/ │ ├── after-all-artifact-build.mjs │ └── update-app-stream.mjs ├── settings.js.template ├── src/ │ ├── i18n/ │ │ ├── i18n.ts │ │ ├── i18next-parser.config.js │ │ └── locales/ │ │ ├── ar.json │ │ ├── ca.json │ │ ├── cs.json │ │ ├── da.json │ │ ├── de.json │ │ ├── en.json │ │ ├── es.json │ │ ├── eu.json │ │ ├── fa.json │ │ ├── fi.json │ │ ├── fr.json │ │ ├── hu.json │ │ ├── id.json │ │ ├── it.json │ │ ├── ja.json │ │ ├── ko.json │ │ ├── nb-NO.json │ │ ├── nl.json │ │ ├── pl.json │ │ ├── pt-BR.json │ │ ├── pt.json │ │ ├── ro.json │ │ ├── ru.json │ │ ├── sk.json │ │ ├── sl.json │ │ ├── sr.json │ │ ├── sv.json │ │ ├── ta.json │ │ ├── tr.json │ │ ├── uk.json │ │ ├── zh-Hans.json │ │ └── zh-Hant.json │ ├── main/ │ │ ├── features/ │ │ │ ├── core/ │ │ │ │ ├── autodiscover/ │ │ │ │ │ └── index.ts │ │ │ │ ├── discord-rpc/ │ │ │ │ │ └── index.ts │ │ │ │ ├── index.ts │ │ │ │ ├── lyrics/ │ │ │ │ │ ├── genius.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── lrclib.ts │ │ │ │ │ ├── netease.ts │ │ │ │ │ ├── shared.ts │ │ │ │ │ └── simpmusic.ts │ │ │ │ ├── player/ │ │ │ │ │ ├── index.ts │ │ │ │ │ └── media-keys.ts │ │ │ │ ├── remote/ │ │ │ │ │ ├── index.ts │ │ │ │ │ └── manifest.json │ │ │ │ └── settings/ │ │ │ │ └── index.ts │ │ │ ├── darwin/ │ │ │ │ ├── dock-menu.ts │ │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ ├── linux/ │ │ │ │ ├── index.ts │ │ │ │ └── mpris.ts │ │ │ └── win32/ │ │ │ └── index.ts │ │ ├── index.ts │ │ ├── menu.ts │ │ └── utils.ts │ ├── preload/ │ │ ├── autodiscover.ts │ │ ├── browser.ts │ │ ├── discord-rpc.ts │ │ ├── index.d.ts │ │ ├── index.ts │ │ ├── ipc.ts │ │ ├── local-settings.ts │ │ ├── lyrics.ts │ │ ├── mpris.ts │ │ ├── mpv-player.ts │ │ ├── remote.ts │ │ └── utils.ts │ ├── remote/ │ │ ├── app.tsx │ │ ├── components/ │ │ │ ├── buttons/ │ │ │ │ ├── image-button.tsx │ │ │ │ ├── reconnect-button.tsx │ │ │ │ └── theme-button.tsx │ │ │ ├── player-image.module.css │ │ │ ├── player-image.tsx │ │ │ ├── remote-container.module.css │ │ │ ├── remote-container.tsx │ │ │ ├── shell.tsx │ │ │ └── wrapped-slider.tsx │ │ ├── index.html │ │ ├── index.tsx │ │ ├── manifest.json │ │ ├── service-worker.ts │ │ ├── store/ │ │ │ └── index.ts │ │ └── worker.js │ ├── renderer/ │ │ ├── api/ │ │ │ ├── controller.ts │ │ │ ├── index.ts │ │ │ ├── jellyfin/ │ │ │ │ ├── jellyfin-api.ts │ │ │ │ └── jellyfin-controller.ts │ │ │ ├── navidrome/ │ │ │ │ ├── navidrome-api.ts │ │ │ │ └── navidrome-controller.ts │ │ │ ├── query-keys.ts │ │ │ ├── subsonic/ │ │ │ │ ├── subsonic-api.ts │ │ │ │ └── subsonic-controller.ts │ │ │ ├── utils-list-count.ts │ │ │ ├── utils-music-folder.ts │ │ │ └── utils.ts │ │ ├── app.tsx │ │ ├── assets/ │ │ │ ├── assets.d.ts │ │ │ ├── entitlements.mac.plist │ │ │ └── icons/ │ │ │ └── icon.icns │ │ ├── components/ │ │ │ ├── drag-preview/ │ │ │ │ ├── drag-preview.module.css │ │ │ │ └── drag-preview.tsx │ │ │ ├── export-import-settings-modal/ │ │ │ │ └── export-import-settings-modal.tsx │ │ │ ├── feature-carousel/ │ │ │ │ ├── feature-carousel.module.css │ │ │ │ ├── feature-carousel.tsx │ │ │ │ └── single-feature-carousel.tsx │ │ │ ├── grid-carousel/ │ │ │ │ ├── grid-carousel-v2.tsx │ │ │ │ └── grid-carousel.module.css │ │ │ ├── item-card/ │ │ │ │ ├── item-card-controls.module.css │ │ │ │ ├── item-card-controls.tsx │ │ │ │ ├── item-card.module.css │ │ │ │ └── item-card.tsx │ │ │ ├── item-image/ │ │ │ │ └── item-image.tsx │ │ │ ├── item-list/ │ │ │ │ ├── expanded-list-container.module.css │ │ │ │ ├── expanded-list-container.tsx │ │ │ │ ├── expanded-list-item.module.css │ │ │ │ ├── expanded-list-item.tsx │ │ │ │ ├── helpers/ │ │ │ │ │ ├── extract-row-id.ts │ │ │ │ │ ├── get-dragged-items.ts │ │ │ │ │ ├── get-title-path.ts │ │ │ │ │ ├── item-list-controls.ts │ │ │ │ │ ├── item-list-infinite-loader.ts │ │ │ │ │ ├── item-list-paginated-loader.ts │ │ │ │ │ ├── item-list-reducer-utils.ts │ │ │ │ │ ├── item-list-state.ts │ │ │ │ │ ├── parse-table-columns.ts │ │ │ │ │ ├── use-grid-rows.ts │ │ │ │ │ ├── use-is-fetching-item-list.ts │ │ │ │ │ ├── use-item-list-column-reorder.ts │ │ │ │ │ ├── use-item-list-column-resize.ts │ │ │ │ │ ├── use-item-list-scroll-persist.ts │ │ │ │ │ └── use-list-hotkeys.ts │ │ │ │ ├── item-detail-list/ │ │ │ │ │ ├── columns/ │ │ │ │ │ │ ├── actions-column.tsx │ │ │ │ │ │ ├── album-artist-column.tsx │ │ │ │ │ │ ├── album-column.tsx │ │ │ │ │ │ ├── artist-column.tsx │ │ │ │ │ │ ├── bit-depth-column.tsx │ │ │ │ │ │ ├── bit-rate-column.tsx │ │ │ │ │ │ ├── bpm-column.tsx │ │ │ │ │ │ ├── channels-column.tsx │ │ │ │ │ │ ├── codec-column.tsx │ │ │ │ │ │ ├── comment-column.tsx │ │ │ │ │ │ ├── composer-column.tsx │ │ │ │ │ │ ├── date-added-column.tsx │ │ │ │ │ │ ├── default-column.tsx │ │ │ │ │ │ ├── disc-number-column.tsx │ │ │ │ │ │ ├── duration-column.tsx │ │ │ │ │ │ ├── favorite-column.tsx │ │ │ │ │ │ ├── genre-badge-column.module.css │ │ │ │ │ │ ├── genre-badge-column.tsx │ │ │ │ │ │ ├── genre-column.tsx │ │ │ │ │ │ ├── image-column.module.css │ │ │ │ │ │ ├── image-column.tsx │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── last-played-column.tsx │ │ │ │ │ │ ├── path-column.tsx │ │ │ │ │ │ ├── play-count-column.tsx │ │ │ │ │ │ ├── rating-column.tsx │ │ │ │ │ │ ├── release-date-column.tsx │ │ │ │ │ │ ├── row-index-column.module.css │ │ │ │ │ │ ├── row-index-column.tsx │ │ │ │ │ │ ├── sample-rate-column.tsx │ │ │ │ │ │ ├── size-column.tsx │ │ │ │ │ │ ├── title-artist-column.tsx │ │ │ │ │ │ ├── title-column.module.css │ │ │ │ │ │ ├── title-column.tsx │ │ │ │ │ │ ├── title-combined-column.tsx │ │ │ │ │ │ ├── track-number-column.tsx │ │ │ │ │ │ ├── types.ts │ │ │ │ │ │ └── year-column.tsx │ │ │ │ │ ├── item-detail-list.module.css │ │ │ │ │ ├── item-detail-list.tsx │ │ │ │ │ └── utils.ts │ │ │ │ ├── item-grid-list/ │ │ │ │ │ ├── item-grid-list.module.css │ │ │ │ │ └── item-grid-list.tsx │ │ │ │ ├── item-list-pagination/ │ │ │ │ │ ├── item-list-pagination.module.css │ │ │ │ │ ├── item-list-pagination.tsx │ │ │ │ │ └── use-item-list-pagination.ts │ │ │ │ ├── item-table-list/ │ │ │ │ │ ├── album-group-header.module.css │ │ │ │ │ ├── album-group-header.tsx │ │ │ │ │ ├── cell-component-factory.tsx │ │ │ │ │ ├── columns/ │ │ │ │ │ │ ├── actions-column.tsx │ │ │ │ │ │ ├── album-artists-column.module.css │ │ │ │ │ │ ├── album-artists-column.tsx │ │ │ │ │ │ ├── album-column.module.css │ │ │ │ │ │ ├── album-column.tsx │ │ │ │ │ │ ├── album-group-column.tsx │ │ │ │ │ │ ├── artists-column.module.css │ │ │ │ │ │ ├── artists-column.tsx │ │ │ │ │ │ ├── composer-column.module.css │ │ │ │ │ │ ├── composer-column.tsx │ │ │ │ │ │ ├── count-column.tsx │ │ │ │ │ │ ├── date-column.tsx │ │ │ │ │ │ ├── default-column.tsx │ │ │ │ │ │ ├── duration-column.tsx │ │ │ │ │ │ ├── favorite-column.tsx │ │ │ │ │ │ ├── genre-badge-column.module.css │ │ │ │ │ │ ├── genre-badge-column.tsx │ │ │ │ │ │ ├── genre-column.module.css │ │ │ │ │ │ ├── genre-column.tsx │ │ │ │ │ │ ├── image-column.module.css │ │ │ │ │ │ ├── image-column.tsx │ │ │ │ │ │ ├── numeric-column.tsx │ │ │ │ │ │ ├── path-column.tsx │ │ │ │ │ │ ├── playlist-reorder-column.module.css │ │ │ │ │ │ ├── playlist-reorder-column.tsx │ │ │ │ │ │ ├── rating-column.tsx │ │ │ │ │ │ ├── row-index-column.module.css │ │ │ │ │ │ ├── row-index-column.tsx │ │ │ │ │ │ ├── size-column.tsx │ │ │ │ │ │ ├── text-column.module.css │ │ │ │ │ │ ├── text-column.tsx │ │ │ │ │ │ ├── title-artist-column.module.css │ │ │ │ │ │ ├── title-artist-column.tsx │ │ │ │ │ │ ├── title-column.module.css │ │ │ │ │ │ ├── title-column.tsx │ │ │ │ │ │ ├── title-combined-column.module.css │ │ │ │ │ │ ├── title-combined-column.tsx │ │ │ │ │ │ └── year-column.tsx │ │ │ │ │ ├── default-columns.ts │ │ │ │ │ ├── hooks/ │ │ │ │ │ │ ├── use-container-width-tracking.ts │ │ │ │ │ │ ├── use-item-drag-drop-state.tsx │ │ │ │ │ │ ├── use-row-interaction-delegate.ts │ │ │ │ │ │ ├── use-sticky-group-row-positioning.ts │ │ │ │ │ │ ├── use-sticky-header-positioning.ts │ │ │ │ │ │ ├── use-sticky-table-group-rows.tsx │ │ │ │ │ │ ├── use-sticky-table-header.tsx │ │ │ │ │ │ ├── use-table-column-model.ts │ │ │ │ │ │ ├── use-table-imperative-handle.ts │ │ │ │ │ │ ├── use-table-initial-scroll.ts │ │ │ │ │ │ ├── use-table-keyboard-navigation.ts │ │ │ │ │ │ ├── use-table-pane-sync.ts │ │ │ │ │ │ ├── use-table-row-model.ts │ │ │ │ │ │ └── use-table-scroll-to-index.ts │ │ │ │ │ ├── item-table-list-column.module.css │ │ │ │ │ ├── item-table-list-column.tsx │ │ │ │ │ ├── item-table-list-context.tsx │ │ │ │ │ ├── item-table-list.module.css │ │ │ │ │ ├── item-table-list.tsx │ │ │ │ │ └── memoized-cell-router.tsx │ │ │ │ ├── selection-dialog.module.css │ │ │ │ ├── selection-dialog.tsx │ │ │ │ └── types.ts │ │ │ ├── motion/ │ │ │ │ └── index.tsx │ │ │ ├── native-scroll-area/ │ │ │ │ ├── native-scroll-area.module.css │ │ │ │ └── native-scroll-area.tsx │ │ │ ├── page-header/ │ │ │ │ ├── page-header.module.css │ │ │ │ └── page-header.tsx │ │ │ ├── query-builder/ │ │ │ │ ├── index.tsx │ │ │ │ └── query-builder-option.tsx │ │ │ ├── select-with-invalid-data/ │ │ │ │ └── index.tsx │ │ │ ├── settings-diff-visualiser/ │ │ │ │ └── settings-diff-visualiser.tsx │ │ │ └── simple-item-table/ │ │ │ ├── simple-item-table.module.css │ │ │ └── simple-item-table.tsx │ │ ├── context/ │ │ │ └── list-context.tsx │ │ ├── env.d.ts │ │ ├── events/ │ │ │ ├── event-emitter.ts │ │ │ ├── events.ts │ │ │ └── types.ts │ │ ├── features/ │ │ │ ├── action-required/ │ │ │ │ ├── components/ │ │ │ │ │ ├── action-required-container.tsx │ │ │ │ │ ├── error-fallback.module.css │ │ │ │ │ ├── error-fallback.tsx │ │ │ │ │ ├── server-credential-required.tsx │ │ │ │ │ └── server-required.tsx │ │ │ │ ├── routes/ │ │ │ │ │ ├── action-required-route.tsx │ │ │ │ │ ├── invalid-route.tsx │ │ │ │ │ └── no-network-route.tsx │ │ │ │ └── utils/ │ │ │ │ └── window-properties.tsx │ │ │ ├── albums/ │ │ │ │ ├── api/ │ │ │ │ │ └── album-api.ts │ │ │ │ ├── components/ │ │ │ │ │ ├── album-detail-content.module.css │ │ │ │ │ ├── album-detail-content.tsx │ │ │ │ │ ├── album-detail-header.module.css │ │ │ │ │ ├── album-detail-header.tsx │ │ │ │ │ ├── album-grid-carousel.tsx │ │ │ │ │ ├── album-infinite-carousel.tsx │ │ │ │ │ ├── album-list-content.tsx │ │ │ │ │ ├── album-list-header-filters.tsx │ │ │ │ │ ├── album-list-header.tsx │ │ │ │ │ ├── album-list-infinite-detail.tsx │ │ │ │ │ ├── album-list-infinite-grid.tsx │ │ │ │ │ ├── album-list-infinite-table.tsx │ │ │ │ │ ├── album-list-paginated-detail.tsx │ │ │ │ │ ├── album-list-paginated-grid.tsx │ │ │ │ │ ├── album-list-paginated-table.tsx │ │ │ │ │ ├── expanded-album-list-item.module.css │ │ │ │ │ ├── expanded-album-list-item.tsx │ │ │ │ │ ├── jellyfin-album-filters.tsx │ │ │ │ │ ├── joined-artists.tsx │ │ │ │ │ ├── navidrome-album-filters.tsx │ │ │ │ │ └── subsonic-album-filters.tsx │ │ │ │ ├── hooks/ │ │ │ │ │ └── use-album-list-filters.ts │ │ │ │ └── routes/ │ │ │ │ ├── album-detail-route.tsx │ │ │ │ ├── album-list-route.tsx │ │ │ │ ├── dummy-album-detail-route.module.css │ │ │ │ └── dummy-album-detail-route.tsx │ │ │ ├── analytics/ │ │ │ │ ├── hooks/ │ │ │ │ │ ├── use-analytics-disabled.ts │ │ │ │ │ ├── use-app-tracker.ts │ │ │ │ │ └── use-page-tracker.ts │ │ │ │ └── utils/ │ │ │ │ └── get-route-pattern.ts │ │ │ ├── artists/ │ │ │ │ ├── api/ │ │ │ │ │ └── artists-api.ts │ │ │ │ ├── components/ │ │ │ │ │ ├── album-artist-detail-content.module.css │ │ │ │ │ ├── album-artist-detail-content.tsx │ │ │ │ │ ├── album-artist-detail-discography-list.tsx │ │ │ │ │ ├── album-artist-detail-favorite-songs-list-header-filters.tsx │ │ │ │ │ ├── album-artist-detail-favorite-songs-list-header.tsx │ │ │ │ │ ├── album-artist-detail-header.module.css │ │ │ │ │ ├── album-artist-detail-header.tsx │ │ │ │ │ ├── album-artist-detail-top-songs-list-header.tsx │ │ │ │ │ ├── album-artist-grid-carousel.tsx │ │ │ │ │ ├── album-artist-infinite-carousel.tsx │ │ │ │ │ ├── album-artist-list-content.tsx │ │ │ │ │ ├── album-artist-list-header-filters.tsx │ │ │ │ │ ├── album-artist-list-header.tsx │ │ │ │ │ ├── album-artist-list-infinite-grid.tsx │ │ │ │ │ ├── album-artist-list-infinite-table.tsx │ │ │ │ │ ├── album-artist-list-paginated-grid.tsx │ │ │ │ │ ├── album-artist-list-paginated-table.tsx │ │ │ │ │ ├── artist-list-content.tsx │ │ │ │ │ ├── artist-list-header-filters.tsx │ │ │ │ │ ├── artist-list-header.tsx │ │ │ │ │ ├── artist-list-infinite-grid.tsx │ │ │ │ │ ├── artist-list-infinite-table.tsx │ │ │ │ │ ├── artist-list-paginated-grid.tsx │ │ │ │ │ └── artist-list-paginated-table.tsx │ │ │ │ ├── hooks/ │ │ │ │ │ ├── use-album-artist-list-filters.ts │ │ │ │ │ ├── use-artist-albums-grouped.ts │ │ │ │ │ └── use-artist-list-filters.ts │ │ │ │ └── routes/ │ │ │ │ ├── album-artist-detail-favorite-songs-list-route.tsx │ │ │ │ ├── album-artist-detail-route.tsx │ │ │ │ ├── album-artist-detail-top-songs-list-route.tsx │ │ │ │ ├── album-artist-list-route.tsx │ │ │ │ └── artist-list-route.tsx │ │ │ ├── context-menu/ │ │ │ │ ├── actions/ │ │ │ │ │ ├── add-to-playlist-action.tsx │ │ │ │ │ ├── delete-playlist-action.tsx │ │ │ │ │ ├── download-action.tsx │ │ │ │ │ ├── edit-playlist-action.tsx │ │ │ │ │ ├── get-info-action.tsx │ │ │ │ │ ├── go-to-action.tsx │ │ │ │ │ ├── move-queue-items-action.tsx │ │ │ │ │ ├── play-action.tsx │ │ │ │ │ ├── play-album-radio-action.tsx │ │ │ │ │ ├── play-artist-radio-action.tsx │ │ │ │ │ ├── play-track-radio-action.tsx │ │ │ │ │ ├── remove-from-playlist-action.tsx │ │ │ │ │ ├── remove-from-queue-action.tsx │ │ │ │ │ ├── set-favorite-action.tsx │ │ │ │ │ ├── set-rating-action.tsx │ │ │ │ │ ├── share-action.tsx │ │ │ │ │ ├── show-in-file-explorer-action.tsx │ │ │ │ │ └── shuffle-items-action.tsx │ │ │ │ ├── components/ │ │ │ │ │ ├── context-menu-preview.module.css │ │ │ │ │ └── context-menu-preview.tsx │ │ │ │ ├── context-menu-controller.tsx │ │ │ │ └── menus/ │ │ │ │ ├── album-artist-context-menu.tsx │ │ │ │ ├── album-context-menu.tsx │ │ │ │ ├── artist-context-menu.tsx │ │ │ │ ├── folder-context-menu.tsx │ │ │ │ ├── genre-context-menu.tsx │ │ │ │ ├── playlist-context-menu.tsx │ │ │ │ ├── playlist-song-context-menu.tsx │ │ │ │ ├── queue-context-menu.tsx │ │ │ │ └── song-context-menu.tsx │ │ │ ├── discord-rpc/ │ │ │ │ └── use-discord-rpc.ts │ │ │ ├── favorites/ │ │ │ │ ├── components/ │ │ │ │ │ ├── favorites-content.tsx │ │ │ │ │ └── favorites-header.tsx │ │ │ │ └── routes/ │ │ │ │ └── favorites-route.tsx │ │ │ ├── folders/ │ │ │ │ ├── api/ │ │ │ │ │ └── folder-api.ts │ │ │ │ ├── components/ │ │ │ │ │ ├── folder-list-content.tsx │ │ │ │ │ ├── folder-list-header-filters.tsx │ │ │ │ │ ├── folder-list-header.tsx │ │ │ │ │ ├── folder-tree-browser.module.css │ │ │ │ │ └── folder-tree-browser.tsx │ │ │ │ ├── hooks/ │ │ │ │ │ └── use-folder-list-filters.ts │ │ │ │ └── routes/ │ │ │ │ └── folder-list-route.tsx │ │ │ ├── genres/ │ │ │ │ ├── api/ │ │ │ │ │ └── genres-api.ts │ │ │ │ ├── components/ │ │ │ │ │ ├── genre-detail-content.tsx │ │ │ │ │ ├── genre-detail-header.tsx │ │ │ │ │ ├── genre-list-content.tsx │ │ │ │ │ ├── genre-list-header-filters.tsx │ │ │ │ │ ├── genre-list-header.tsx │ │ │ │ │ ├── genre-list-infinite-grid.tsx │ │ │ │ │ ├── genre-list-infinite-table.tsx │ │ │ │ │ ├── genre-list-paginated-grid.tsx │ │ │ │ │ └── genre-list-paginated-table.tsx │ │ │ │ ├── hooks/ │ │ │ │ │ └── use-genre-list-filters.ts │ │ │ │ └── routes/ │ │ │ │ ├── genre-detail-route.tsx │ │ │ │ └── genre-list-route.tsx │ │ │ ├── home/ │ │ │ │ ├── api/ │ │ │ │ │ └── home-api.ts │ │ │ │ ├── components/ │ │ │ │ │ ├── album-infinite-feature-carousel.tsx │ │ │ │ │ ├── album-infinite-single-feature-carousel.tsx │ │ │ │ │ ├── featured-genres.module.css │ │ │ │ │ └── featured-genres.tsx │ │ │ │ └── routes/ │ │ │ │ └── home-route.tsx │ │ │ ├── item-details/ │ │ │ │ └── components/ │ │ │ │ ├── item-details-modal.tsx │ │ │ │ └── song-path.tsx │ │ │ ├── login/ │ │ │ │ └── routes/ │ │ │ │ └── login-route.tsx │ │ │ ├── lyrics/ │ │ │ │ ├── api/ │ │ │ │ │ ├── lyric-translate.ts │ │ │ │ │ └── lyrics-api.ts │ │ │ │ ├── components/ │ │ │ │ │ ├── lyrics-export-form.tsx │ │ │ │ │ ├── lyrics-search-form.module.css │ │ │ │ │ ├── lyrics-search-form.tsx │ │ │ │ │ ├── lyrics-settings-form.tsx │ │ │ │ │ └── lyrics-settings-modal.tsx │ │ │ │ ├── lyric-line.module.css │ │ │ │ ├── lyric-line.tsx │ │ │ │ ├── lyrics-actions.tsx │ │ │ │ ├── lyrics.module.css │ │ │ │ ├── lyrics.tsx │ │ │ │ ├── synchronized-lyrics.module.css │ │ │ │ ├── synchronized-lyrics.tsx │ │ │ │ ├── unsynchronized-lyrics.module.css │ │ │ │ ├── unsynchronized-lyrics.tsx │ │ │ │ └── utils/ │ │ │ │ └── open-lyrics-settings-modal.ts │ │ │ ├── now-playing/ │ │ │ │ ├── components/ │ │ │ │ │ ├── drawer-play-queue.tsx │ │ │ │ │ ├── now-playing-header.tsx │ │ │ │ │ ├── play-queue-list-controls.tsx │ │ │ │ │ ├── play-queue.module.css │ │ │ │ │ ├── play-queue.tsx │ │ │ │ │ ├── popover-play-queue.tsx │ │ │ │ │ ├── sidebar-play-queue.module.css │ │ │ │ │ └── sidebar-play-queue.tsx │ │ │ │ └── routes/ │ │ │ │ └── now-playing-route.tsx │ │ │ ├── player/ │ │ │ │ ├── audio-player/ │ │ │ │ │ ├── engine/ │ │ │ │ │ │ ├── mpv-player-engine.tsx │ │ │ │ │ │ ├── wavesurfer-player-engine.tsx │ │ │ │ │ │ └── web-player-engine.tsx │ │ │ │ │ ├── hooks/ │ │ │ │ │ │ ├── use-main-player-listener.tsx │ │ │ │ │ │ ├── use-player-events.ts │ │ │ │ │ │ └── use-stream-url.tsx │ │ │ │ │ ├── mpv-player.tsx │ │ │ │ │ ├── types.ts │ │ │ │ │ ├── utils/ │ │ │ │ │ │ ├── list-handlers.ts │ │ │ │ │ │ └── player-utils.ts │ │ │ │ │ ├── wavesurfer-player.tsx │ │ │ │ │ └── web-player.tsx │ │ │ │ ├── components/ │ │ │ │ │ ├── audio-players.tsx │ │ │ │ │ ├── center-controls.module.css │ │ │ │ │ ├── center-controls.tsx │ │ │ │ │ ├── full-screen-player-image.module.css │ │ │ │ │ ├── full-screen-player-image.tsx │ │ │ │ │ ├── full-screen-player-queue.module.css │ │ │ │ │ ├── full-screen-player-queue.tsx │ │ │ │ │ ├── full-screen-player.module.css │ │ │ │ │ ├── full-screen-player.tsx │ │ │ │ │ ├── full-screen-similar-songs.tsx │ │ │ │ │ ├── full-screen-visualizer-song-info.tsx │ │ │ │ │ ├── full-screen-visualizer.module.css │ │ │ │ │ ├── full-screen-visualizer.tsx │ │ │ │ │ ├── left-controls.module.css │ │ │ │ │ ├── left-controls.tsx │ │ │ │ │ ├── mobile-fullscreen-player-album-art.tsx │ │ │ │ │ ├── mobile-fullscreen-player-bottom-controls.tsx │ │ │ │ │ ├── mobile-fullscreen-player-controls.tsx │ │ │ │ │ ├── mobile-fullscreen-player-header.tsx │ │ │ │ │ ├── mobile-fullscreen-player-metadata.tsx │ │ │ │ │ ├── mobile-fullscreen-player-progress.tsx │ │ │ │ │ ├── mobile-fullscreen-player.module.css │ │ │ │ │ ├── mobile-fullscreen-player.tsx │ │ │ │ │ ├── mobile-playerbar.module.css │ │ │ │ │ ├── mobile-playerbar.tsx │ │ │ │ │ ├── player-button.module.css │ │ │ │ │ ├── player-button.tsx │ │ │ │ │ ├── player-config.tsx │ │ │ │ │ ├── playerbar-seek-slider.tsx │ │ │ │ │ ├── playerbar-slider.module.css │ │ │ │ │ ├── playerbar-slider.tsx │ │ │ │ │ ├── playerbar-waveform.module.css │ │ │ │ │ ├── playerbar-waveform.tsx │ │ │ │ │ ├── playerbar.module.css │ │ │ │ │ ├── playerbar.tsx │ │ │ │ │ ├── radio-metadata-display.tsx │ │ │ │ │ ├── right-controls.tsx │ │ │ │ │ ├── shuffle-all-modal.tsx │ │ │ │ │ └── sleep-timer-button.tsx │ │ │ │ ├── context/ │ │ │ │ │ ├── player-context.tsx │ │ │ │ │ └── webaudio-context.ts │ │ │ │ ├── hooks/ │ │ │ │ │ ├── use-auto-dj.ts │ │ │ │ │ ├── use-autosave.ts │ │ │ │ │ ├── use-is-current-song.ts │ │ │ │ │ ├── use-media-session.ts │ │ │ │ │ ├── use-mpris.ts │ │ │ │ │ ├── use-playback-hotkeys.ts │ │ │ │ │ ├── use-power-save-blocker.ts │ │ │ │ │ ├── use-queue-restore.ts │ │ │ │ │ ├── use-scrobble.ts │ │ │ │ │ ├── use-update-current-song.ts │ │ │ │ │ └── use-webaudio.ts │ │ │ │ ├── mutations/ │ │ │ │ │ └── scrobble-mutation.ts │ │ │ │ ├── ref/ │ │ │ │ │ └── players-ref.tsx │ │ │ │ ├── update-remote-song.tsx │ │ │ │ ├── utils/ │ │ │ │ │ └── open-visualizer-settings-modal.ts │ │ │ │ └── utils.ts │ │ │ ├── playlists/ │ │ │ │ ├── api/ │ │ │ │ │ └── playlists-api.ts │ │ │ │ ├── components/ │ │ │ │ │ ├── add-to-playlist-context-modal.module.css │ │ │ │ │ ├── add-to-playlist-context-modal.tsx │ │ │ │ │ ├── client-side-song-filters.tsx │ │ │ │ │ ├── create-playlist-form.tsx │ │ │ │ │ ├── playlist-detail-album-view.tsx │ │ │ │ │ ├── playlist-detail-song-list-content.tsx │ │ │ │ │ ├── playlist-detail-song-list-grid.tsx │ │ │ │ │ ├── playlist-detail-song-list-header-filters.tsx │ │ │ │ │ ├── playlist-detail-song-list-header.tsx │ │ │ │ │ ├── playlist-detail-song-list-table.tsx │ │ │ │ │ ├── playlist-list-content.tsx │ │ │ │ │ ├── playlist-list-header-filters.tsx │ │ │ │ │ ├── playlist-list-header.tsx │ │ │ │ │ ├── playlist-list-infinite-grid.tsx │ │ │ │ │ ├── playlist-list-infinite-table.tsx │ │ │ │ │ ├── playlist-list-paginated-grid.tsx │ │ │ │ │ ├── playlist-list-paginated-table.tsx │ │ │ │ │ ├── playlist-query-builder.tsx │ │ │ │ │ ├── playlist-query-editor.tsx │ │ │ │ │ ├── save-and-replace-context-modal.tsx │ │ │ │ │ ├── save-as-playlist-form.tsx │ │ │ │ │ ├── update-playlist-form.tsx │ │ │ │ │ └── update-playlist-modal.ts │ │ │ │ ├── hooks/ │ │ │ │ │ ├── use-playlist-list-filters.ts │ │ │ │ │ ├── use-playlist-song-list-filters.ts │ │ │ │ │ ├── use-playlist-track-list.ts │ │ │ │ │ └── use-recent-playlists.ts │ │ │ │ ├── mutations/ │ │ │ │ │ ├── add-to-playlist-mutation.ts │ │ │ │ │ ├── create-playlist-mutation.ts │ │ │ │ │ ├── delete-playlist-mutation.ts │ │ │ │ │ ├── playlist-optimistic-updates.ts │ │ │ │ │ ├── remove-from-playlist-mutation.ts │ │ │ │ │ └── update-playlist-mutation.ts │ │ │ │ ├── routes/ │ │ │ │ │ ├── playlist-detail-song-list-route.tsx │ │ │ │ │ └── playlist-list-route.tsx │ │ │ │ └── utils.ts │ │ │ ├── radio/ │ │ │ │ ├── api/ │ │ │ │ │ └── radio-api.ts │ │ │ │ ├── components/ │ │ │ │ │ ├── create-radio-station-form.tsx │ │ │ │ │ ├── edit-radio-station-form.tsx │ │ │ │ │ ├── radio-list-content.tsx │ │ │ │ │ ├── radio-list-header-filters.tsx │ │ │ │ │ ├── radio-list-header.tsx │ │ │ │ │ ├── radio-list-items.module.css │ │ │ │ │ ├── radio-list-items.tsx │ │ │ │ │ └── radio-web-player.tsx │ │ │ │ ├── hooks/ │ │ │ │ │ └── use-radio-player.ts │ │ │ │ ├── mutations/ │ │ │ │ │ ├── create-radio-station-mutation.ts │ │ │ │ │ ├── delete-radio-station-mutation.ts │ │ │ │ │ └── update-radio-station-mutation.ts │ │ │ │ ├── routes/ │ │ │ │ │ └── radio-list-route.tsx │ │ │ │ └── store/ │ │ │ │ └── radio-store.ts │ │ │ ├── remote/ │ │ │ │ └── hooks/ │ │ │ │ └── use-remote.tsx │ │ │ ├── search/ │ │ │ │ ├── api/ │ │ │ │ │ └── search-api.ts │ │ │ │ ├── components/ │ │ │ │ │ ├── collapsible-command-group.module.css │ │ │ │ │ ├── collapsible-command-group.tsx │ │ │ │ │ ├── command-item-selectable.tsx │ │ │ │ │ ├── command-palette.tsx │ │ │ │ │ ├── command.css │ │ │ │ │ ├── command.tsx │ │ │ │ │ ├── go-to-commands.tsx │ │ │ │ │ ├── home-commands.tsx │ │ │ │ │ ├── library-command-item.module.css │ │ │ │ │ ├── library-command-item.tsx │ │ │ │ │ ├── search-album-artists-section.tsx │ │ │ │ │ ├── search-albums-section.tsx │ │ │ │ │ ├── search-content.tsx │ │ │ │ │ ├── search-header.tsx │ │ │ │ │ ├── search-songs-section.tsx │ │ │ │ │ └── server-commands.tsx │ │ │ │ └── routes/ │ │ │ │ └── search-route.tsx │ │ │ ├── servers/ │ │ │ │ └── components/ │ │ │ │ ├── add-server-form.tsx │ │ │ │ ├── edit-server-form.tsx │ │ │ │ ├── ignore-cors-ssl-switches.tsx │ │ │ │ ├── server-list-item.tsx │ │ │ │ ├── server-list.tsx │ │ │ │ └── server-section.tsx │ │ │ ├── settings/ │ │ │ │ ├── components/ │ │ │ │ │ ├── advanced/ │ │ │ │ │ │ ├── advanced-tab.tsx │ │ │ │ │ │ ├── analytics-settings.tsx │ │ │ │ │ │ ├── export-import-settings.tsx │ │ │ │ │ │ ├── logger-settings.tsx │ │ │ │ │ │ └── styles-settings.tsx │ │ │ │ │ ├── general/ │ │ │ │ │ │ ├── application-settings.tsx │ │ │ │ │ │ ├── art-resolution-settings.tsx │ │ │ │ │ │ ├── artist-settings.tsx │ │ │ │ │ │ ├── control-settings.tsx │ │ │ │ │ │ ├── draggable-item.tsx │ │ │ │ │ │ ├── draggable-items.tsx │ │ │ │ │ │ ├── external-links-settings.tsx │ │ │ │ │ │ ├── fullscreen-player-settings.tsx │ │ │ │ │ │ ├── general-tab.tsx │ │ │ │ │ │ ├── home-settings.tsx │ │ │ │ │ │ ├── lyric-settings.tsx │ │ │ │ │ │ ├── path-settings.tsx │ │ │ │ │ │ ├── query-builder-settings.tsx │ │ │ │ │ │ ├── scrobble-settings.tsx │ │ │ │ │ │ ├── sidebar-reorder.tsx │ │ │ │ │ │ ├── sidebar-settings.tsx │ │ │ │ │ │ └── theme-settings.tsx │ │ │ │ │ ├── hotkeys/ │ │ │ │ │ │ ├── hotkey-manager-settings.tsx │ │ │ │ │ │ ├── hotkeys-manager-settings.module.css │ │ │ │ │ │ ├── hotkeys-tab.tsx │ │ │ │ │ │ ├── media-session-settings.tsx │ │ │ │ │ │ └── window-hotkey-settings.tsx │ │ │ │ │ ├── playback/ │ │ │ │ │ │ ├── audio-settings.tsx │ │ │ │ │ │ ├── auto-dj-settings.tsx │ │ │ │ │ │ ├── mpv-properties.ts │ │ │ │ │ │ ├── mpv-settings.tsx │ │ │ │ │ │ ├── playback-tab.tsx │ │ │ │ │ │ ├── player-filter-settings.tsx │ │ │ │ │ │ └── transcode-settings.tsx │ │ │ │ │ ├── settings-content.tsx │ │ │ │ │ ├── settings-header.tsx │ │ │ │ │ ├── settings-modal.tsx │ │ │ │ │ ├── settings-option.tsx │ │ │ │ │ ├── settings-section.tsx │ │ │ │ │ └── window/ │ │ │ │ │ ├── cache-settngs.tsx │ │ │ │ │ ├── discord-settings.tsx │ │ │ │ │ ├── password-settings.tsx │ │ │ │ │ ├── remote-settings.tsx │ │ │ │ │ ├── update-settings.tsx │ │ │ │ │ ├── window-settings.tsx │ │ │ │ │ └── window-tab.tsx │ │ │ │ ├── context/ │ │ │ │ │ └── search-context.tsx │ │ │ │ ├── restart-toast.ts │ │ │ │ ├── routes/ │ │ │ │ │ └── settings-route.tsx │ │ │ │ └── utils/ │ │ │ │ └── open-settings-modal.ts │ │ │ ├── shared/ │ │ │ │ ├── api/ │ │ │ │ │ └── shared-api.ts │ │ │ │ ├── components/ │ │ │ │ │ ├── animated-page.module.css │ │ │ │ │ ├── animated-page.tsx │ │ │ │ │ ├── component-error-boundary.tsx │ │ │ │ │ ├── display-type-toggle-button.tsx │ │ │ │ │ ├── filter-bar.module.css │ │ │ │ │ ├── filter-bar.tsx │ │ │ │ │ ├── filter-button.tsx │ │ │ │ │ ├── folder-button.tsx │ │ │ │ │ ├── grid-config.tsx │ │ │ │ │ ├── json-preview.module.css │ │ │ │ │ ├── json-preview.tsx │ │ │ │ │ ├── library-background-overlay.module.css │ │ │ │ │ ├── library-background-overlay.tsx │ │ │ │ │ ├── library-container.module.css │ │ │ │ │ ├── library-container.tsx │ │ │ │ │ ├── library-header-bar.module.css │ │ │ │ │ ├── library-header-bar.tsx │ │ │ │ │ ├── library-header.module.css │ │ │ │ │ ├── library-header.tsx │ │ │ │ │ ├── list-config-menu.tsx │ │ │ │ │ ├── list-display-type-toggle-button.tsx │ │ │ │ │ ├── list-filters.tsx │ │ │ │ │ ├── list-music-folder-dropdown.tsx │ │ │ │ │ ├── list-refresh-button.tsx │ │ │ │ │ ├── list-search-input.tsx │ │ │ │ │ ├── list-select-filter.tsx │ │ │ │ │ ├── list-sort-by-dropdown.tsx │ │ │ │ │ ├── list-sort-order-toggle-button.tsx │ │ │ │ │ ├── list-with-sidebar-container.module.css │ │ │ │ │ ├── list-with-sidebar-container.tsx │ │ │ │ │ ├── more-button.tsx │ │ │ │ │ ├── multi-select-rows.module.css │ │ │ │ │ ├── multi-select-rows.tsx │ │ │ │ │ ├── order-toggle-button.tsx │ │ │ │ │ ├── page-error-boundary.tsx │ │ │ │ │ ├── play-button-group.module.css │ │ │ │ │ ├── play-button-group.tsx │ │ │ │ │ ├── play-button.module.css │ │ │ │ │ ├── play-button.tsx │ │ │ │ │ ├── refresh-button.tsx │ │ │ │ │ ├── resize-handle.module.css │ │ │ │ │ ├── resize-handle.tsx │ │ │ │ │ ├── router-error-boundary.tsx │ │ │ │ │ ├── save-as-collection-button.module.css │ │ │ │ │ ├── save-as-collection-button.tsx │ │ │ │ │ ├── search-input.tsx │ │ │ │ │ ├── settings-button.tsx │ │ │ │ │ ├── table-config.module.css │ │ │ │ │ ├── table-config.tsx │ │ │ │ │ └── tag-filter.tsx │ │ │ │ ├── hooks/ │ │ │ │ │ ├── use-list-filter-persistence.ts │ │ │ │ │ ├── use-music-folder-id-filter.ts │ │ │ │ │ ├── use-play-button-click.ts │ │ │ │ │ ├── use-search-term-filter.ts │ │ │ │ │ ├── use-select-filter.ts │ │ │ │ │ ├── use-set-favorite.ts │ │ │ │ │ ├── use-set-rating.ts │ │ │ │ │ ├── use-sort-by-filter.ts │ │ │ │ │ └── use-sort-order-filter.ts │ │ │ │ ├── mutations/ │ │ │ │ │ ├── create-favorite-mutation.ts │ │ │ │ │ ├── delete-favorite-mutation.ts │ │ │ │ │ ├── favorite-optimistic-updates.ts │ │ │ │ │ ├── rating-optimistic-updates.ts │ │ │ │ │ └── set-rating-mutation.ts │ │ │ │ └── utils.ts │ │ │ ├── sharing/ │ │ │ │ ├── components/ │ │ │ │ │ └── share-item-context-modal.tsx │ │ │ │ └── mutations/ │ │ │ │ └── share-item-mutation.ts │ │ │ ├── sidebar/ │ │ │ │ └── components/ │ │ │ │ ├── action-bar.module.css │ │ │ │ ├── action-bar.tsx │ │ │ │ ├── collapsed-sidebar-button.module.css │ │ │ │ ├── collapsed-sidebar-button.tsx │ │ │ │ ├── collapsed-sidebar-item.module.css │ │ │ │ ├── collapsed-sidebar-item.tsx │ │ │ │ ├── collapsed-sidebar.module.css │ │ │ │ ├── collapsed-sidebar.tsx │ │ │ │ ├── mobile-sidebar.module.css │ │ │ │ ├── mobile-sidebar.tsx │ │ │ │ ├── server-selector-items.tsx │ │ │ │ ├── server-selector.module.css │ │ │ │ ├── server-selector.tsx │ │ │ │ ├── sidebar-collection-list.module.css │ │ │ │ ├── sidebar-collection-list.tsx │ │ │ │ ├── sidebar-icon.module.css │ │ │ │ ├── sidebar-icon.tsx │ │ │ │ ├── sidebar-item.module.css │ │ │ │ ├── sidebar-item.tsx │ │ │ │ ├── sidebar-playlist-list.module.css │ │ │ │ ├── sidebar-playlist-list.tsx │ │ │ │ ├── sidebar.module.css │ │ │ │ └── sidebar.tsx │ │ │ ├── similar-songs/ │ │ │ │ └── components/ │ │ │ │ └── similar-songs-list.tsx │ │ │ ├── songs/ │ │ │ │ ├── api/ │ │ │ │ │ └── songs-api.ts │ │ │ │ ├── components/ │ │ │ │ │ ├── jellyfin-song-filters.tsx │ │ │ │ │ ├── navidrome-song-filters.tsx │ │ │ │ │ ├── song-infinite-carousel.tsx │ │ │ │ │ ├── song-list-content.tsx │ │ │ │ │ ├── song-list-header-filters.tsx │ │ │ │ │ ├── song-list-header.tsx │ │ │ │ │ ├── song-list-infinite-grid.tsx │ │ │ │ │ ├── song-list-infinite-table.tsx │ │ │ │ │ ├── song-list-paginated-grid.tsx │ │ │ │ │ ├── song-list-paginated-table.tsx │ │ │ │ │ └── subsonic-song-filters.tsx │ │ │ │ ├── hooks/ │ │ │ │ │ └── use-song-list-filters.ts │ │ │ │ └── routes/ │ │ │ │ └── song-list-route.tsx │ │ │ ├── titlebar/ │ │ │ │ └── components/ │ │ │ │ ├── app-menu.tsx │ │ │ │ ├── titlebar.module.css │ │ │ │ └── titlebar.tsx │ │ │ ├── visualizer/ │ │ │ │ └── components/ │ │ │ │ ├── audiomotionanalyzer/ │ │ │ │ │ ├── presets.ts │ │ │ │ │ ├── visualizer-settings-form.module.css │ │ │ │ │ ├── visualizer-settings-form.tsx │ │ │ │ │ ├── visualizer-settings-modal.tsx │ │ │ │ │ ├── visualizer.module.css │ │ │ │ │ └── visualizer.tsx │ │ │ │ └── butternchurn/ │ │ │ │ ├── butterchurn.d.ts │ │ │ │ ├── visualizer.module.css │ │ │ │ └── visualizer.tsx │ │ │ └── window-controls/ │ │ │ └── components/ │ │ │ ├── window-controls.module.css │ │ │ └── window-controls.tsx │ │ ├── global.d.ts │ │ ├── hooks/ │ │ │ ├── index.ts │ │ │ ├── use-app-focus.ts │ │ │ ├── use-check-for-updates.ts │ │ │ ├── use-container-query.ts │ │ │ ├── use-drag-drop.tsx │ │ │ ├── use-fast-average-color.tsx │ │ │ ├── use-garbage-collection.ts │ │ │ ├── use-genre-route.ts │ │ │ ├── use-hide-scrollbar.ts │ │ │ ├── use-is-mobile.ts │ │ │ ├── use-is-mounted.ts │ │ │ ├── use-server-authenticated.ts │ │ │ ├── use-should-pad-titlebar.tsx │ │ │ └── use-sync-settings-to-main.ts │ │ ├── index.html │ │ ├── layouts/ │ │ │ ├── auth-layout.module.css │ │ │ ├── auth-layout.tsx │ │ │ ├── authentication-outlet.tsx │ │ │ ├── default-layout/ │ │ │ │ ├── full-screen-overlay.tsx │ │ │ │ ├── full-screen-visualizer-overlay.tsx │ │ │ │ ├── left-sidebar.module.css │ │ │ │ ├── left-sidebar.tsx │ │ │ │ ├── main-content.module.css │ │ │ │ ├── main-content.tsx │ │ │ │ ├── player-bar.module.css │ │ │ │ ├── player-bar.tsx │ │ │ │ ├── right-sidebar.module.css │ │ │ │ ├── right-sidebar.tsx │ │ │ │ ├── side-drawer-queue.module.css │ │ │ │ └── side-drawer-queue.tsx │ │ │ ├── default-layout.module.css │ │ │ ├── default-layout.tsx │ │ │ ├── mobile-layout/ │ │ │ │ ├── mobile-layout.module.css │ │ │ │ └── mobile-layout.tsx │ │ │ ├── responsive-layout.tsx │ │ │ ├── window-bar.module.css │ │ │ └── window-bar.tsx │ │ ├── lib/ │ │ │ ├── react-query.ts │ │ │ └── zustand.ts │ │ ├── main.tsx │ │ ├── release-notes-modal.tsx │ │ ├── router/ │ │ │ ├── app-outlet.tsx │ │ │ ├── app-router.tsx │ │ │ ├── routes.ts │ │ │ ├── titlebar-outlet.module.css │ │ │ └── titlebar-outlet.tsx │ │ ├── store/ │ │ │ ├── app.store.ts │ │ │ ├── auth.store.ts │ │ │ ├── env-settings-overrides.ts │ │ │ ├── full-screen-player.store.ts │ │ │ ├── index.ts │ │ │ ├── player.store.ts │ │ │ ├── scroll.store.ts │ │ │ ├── settings.store.ts │ │ │ ├── sleep-timer.store.ts │ │ │ ├── timestamp.store.ts │ │ │ └── utils.ts │ │ ├── styles/ │ │ │ ├── helpers.ts │ │ │ └── overlayscrollbars.css │ │ ├── themes/ │ │ │ ├── mantine-theme.tsx │ │ │ └── use-app-theme.ts │ │ ├── types/ │ │ │ ├── emotion.d.ts │ │ │ └── fonts.ts │ │ ├── update-available-dialog.tsx │ │ └── utils/ │ │ ├── constrain-sidebar-width.ts │ │ ├── format.tsx │ │ ├── get-header-color.ts │ │ ├── index.ts │ │ ├── linkify.tsx │ │ ├── logger-message.ts │ │ ├── logger.ts │ │ ├── normalize-release-types.tsx │ │ ├── normalize-server-url.ts │ │ ├── parse-search-params.ts │ │ ├── query-params.ts │ │ ├── random-string.ts │ │ ├── rgb-to-rgba.ts │ │ ├── sanitize.ts │ │ ├── sentence-case.ts │ │ ├── set-local-storage-setttings.ts │ │ ├── shuffle.ts │ │ ├── title-case.ts │ │ └── truncate-middle.ts │ ├── shared/ │ │ ├── api/ │ │ │ ├── jellyfin/ │ │ │ │ ├── jellyfin-normalize.ts │ │ │ │ └── jellyfin-types.ts │ │ │ ├── navidrome/ │ │ │ │ ├── navidrome-normalize.ts │ │ │ │ └── navidrome-types.ts │ │ │ ├── subsonic/ │ │ │ │ ├── subsonic-normalize.ts │ │ │ │ └── subsonic-types.ts │ │ │ └── utils.ts │ │ ├── assets.d.ts │ │ ├── components/ │ │ │ ├── accordion/ │ │ │ │ ├── accordion.module.css │ │ │ │ └── accordion.tsx │ │ │ ├── action-icon/ │ │ │ │ ├── action-icon.module.css │ │ │ │ └── action-icon.tsx │ │ │ ├── angle-slider/ │ │ │ │ └── angle-slider.tsx │ │ │ ├── animations/ │ │ │ │ ├── animation-props.ts │ │ │ │ └── animation-variants.ts │ │ │ ├── badge/ │ │ │ │ ├── badge.module.css │ │ │ │ └── badge.tsx │ │ │ ├── box/ │ │ │ │ └── box.tsx │ │ │ ├── breadcrumb/ │ │ │ │ └── breadcrumb.tsx │ │ │ ├── button/ │ │ │ │ ├── button.module.css │ │ │ │ └── button.tsx │ │ │ ├── center/ │ │ │ │ └── center.tsx │ │ │ ├── checkbox/ │ │ │ │ ├── checkbox.module.css │ │ │ │ └── checkbox.tsx │ │ │ ├── checkbox-select/ │ │ │ │ ├── checkbox-select.module.css │ │ │ │ └── checkbox-select.tsx │ │ │ ├── code/ │ │ │ │ ├── code.module.css │ │ │ │ └── code.tsx │ │ │ ├── color-input/ │ │ │ │ ├── color-input.module.css │ │ │ │ └── color-input.tsx │ │ │ ├── context-menu/ │ │ │ │ ├── context-menu.module.css │ │ │ │ └── context-menu.tsx │ │ │ ├── copy-button/ │ │ │ │ └── copy-button.tsx │ │ │ ├── date-picker/ │ │ │ │ ├── date-picker.module.css │ │ │ │ └── date-picker.tsx │ │ │ ├── date-time-picker/ │ │ │ │ ├── date-time-picker.module.css │ │ │ │ └── date-time-picker.tsx │ │ │ ├── dialog/ │ │ │ │ ├── dialog.module.css │ │ │ │ └── dialog.tsx │ │ │ ├── divider/ │ │ │ │ ├── divider.module.css │ │ │ │ └── divider.tsx │ │ │ ├── drag-drop-zone/ │ │ │ │ └── drag-drop-zone.tsx │ │ │ ├── drawer/ │ │ │ │ └── drawer.tsx │ │ │ ├── dropdown-menu/ │ │ │ │ ├── dropdown-menu.module.css │ │ │ │ └── dropdown-menu.tsx │ │ │ ├── explicit-indicator/ │ │ │ │ ├── explicit-indicator.module.css │ │ │ │ └── explicit-indicator.tsx │ │ │ ├── fieldset/ │ │ │ │ ├── fieldset.module.css │ │ │ │ └── fieldset.tsx │ │ │ ├── file-input/ │ │ │ │ ├── file-input.module.css │ │ │ │ └── file-input.tsx │ │ │ ├── flex/ │ │ │ │ └── flex.tsx │ │ │ ├── grid/ │ │ │ │ └── grid.tsx │ │ │ ├── group/ │ │ │ │ └── group.tsx │ │ │ ├── hover-card/ │ │ │ │ ├── hover-card.module.css │ │ │ │ └── hover-card.tsx │ │ │ ├── icon/ │ │ │ │ ├── icon.module.css │ │ │ │ └── icon.tsx │ │ │ ├── image/ │ │ │ │ ├── image.module.css │ │ │ │ ├── image.tsx │ │ │ │ └── use-native-image.ts │ │ │ ├── json-input/ │ │ │ │ ├── json-input.module.css │ │ │ │ └── json-input.tsx │ │ │ ├── kbd/ │ │ │ │ └── kbd.tsx │ │ │ ├── loading-overlay/ │ │ │ │ └── loading-overlay.tsx │ │ │ ├── modal/ │ │ │ │ ├── modal.module.css │ │ │ │ ├── modal.tsx │ │ │ │ └── model-shared.tsx │ │ │ ├── multi-select/ │ │ │ │ ├── multi-select.module.css │ │ │ │ ├── multi-select.tsx │ │ │ │ ├── virtual-multi-select.module.css │ │ │ │ └── virtual-multi-select.tsx │ │ │ ├── number-input/ │ │ │ │ ├── number-input.module.css │ │ │ │ └── number-input.tsx │ │ │ ├── option/ │ │ │ │ ├── option.module.css │ │ │ │ └── option.tsx │ │ │ ├── pagination/ │ │ │ │ ├── pagination.module.css │ │ │ │ └── pagination.tsx │ │ │ ├── paper/ │ │ │ │ ├── paper.module.css │ │ │ │ └── paper.tsx │ │ │ ├── password-input/ │ │ │ │ ├── password-input.module.css │ │ │ │ └── password-input.tsx │ │ │ ├── pill/ │ │ │ │ ├── pill.module.css │ │ │ │ └── pill.tsx │ │ │ ├── popover/ │ │ │ │ ├── popover.module.css │ │ │ │ └── popover.tsx │ │ │ ├── portal/ │ │ │ │ └── portal.tsx │ │ │ ├── rating/ │ │ │ │ ├── rating.module.css │ │ │ │ └── rating.tsx │ │ │ ├── read-only-rating/ │ │ │ │ ├── read-only-rating.module.css │ │ │ │ └── read-only-rating.tsx │ │ │ ├── scroll-area/ │ │ │ │ ├── scroll-area.css │ │ │ │ ├── scroll-area.module.css │ │ │ │ └── scroll-area.tsx │ │ │ ├── segmented-control/ │ │ │ │ ├── segmented-control.module.css │ │ │ │ └── segmented-control.tsx │ │ │ ├── select/ │ │ │ │ ├── select.module.css │ │ │ │ └── select.tsx │ │ │ ├── separator/ │ │ │ │ ├── separator.module.css │ │ │ │ └── separator.tsx │ │ │ ├── skeleton/ │ │ │ │ ├── skeleton.module.css │ │ │ │ └── skeleton.tsx │ │ │ ├── slider/ │ │ │ │ ├── slider.module.css │ │ │ │ └── slider.tsx │ │ │ ├── spinner/ │ │ │ │ ├── spinner.module.css │ │ │ │ └── spinner.tsx │ │ │ ├── spoiler/ │ │ │ │ ├── spoiler.module.css │ │ │ │ └── spoiler.tsx │ │ │ ├── stack/ │ │ │ │ └── stack.tsx │ │ │ ├── switch/ │ │ │ │ ├── switch.module.css │ │ │ │ └── switch.tsx │ │ │ ├── table/ │ │ │ │ ├── table.module.css │ │ │ │ └── table.tsx │ │ │ ├── tabs/ │ │ │ │ ├── tabs.module.css │ │ │ │ └── tabs.tsx │ │ │ ├── text/ │ │ │ │ ├── text.module.css │ │ │ │ └── text.tsx │ │ │ ├── text-input/ │ │ │ │ ├── text-input.module.css │ │ │ │ └── text-input.tsx │ │ │ ├── text-title/ │ │ │ │ ├── text-title.module.css │ │ │ │ └── text-title.tsx │ │ │ ├── textarea/ │ │ │ │ ├── textarea.module.css │ │ │ │ └── textarea.tsx │ │ │ ├── toast/ │ │ │ │ ├── toast.module.css │ │ │ │ └── toast.tsx │ │ │ ├── tooltip/ │ │ │ │ ├── tooltip.module.css │ │ │ │ └── tooltip.tsx │ │ │ └── yes-no-select/ │ │ │ └── yes-no-select.tsx │ │ ├── constants/ │ │ │ └── playback-selectors.ts │ │ ├── hooks/ │ │ │ ├── use-click-outside.ts │ │ │ ├── use-container-query.ts │ │ │ ├── use-debounced-callback.ts │ │ │ ├── use-debounced-state.ts │ │ │ ├── use-debounced-value.ts │ │ │ ├── use-disclosure.ts │ │ │ ├── use-double-click.ts │ │ │ ├── use-element-size.ts │ │ │ ├── use-focus-trap.ts │ │ │ ├── use-focus-within.ts │ │ │ ├── use-form.ts │ │ │ ├── use-hotkeys.ts │ │ │ ├── use-in-viewport.ts │ │ │ ├── use-intersection.ts │ │ │ ├── use-is-overflow.ts │ │ │ ├── use-local-storage.ts │ │ │ ├── use-long-press.ts │ │ │ ├── use-media-query.ts │ │ │ ├── use-merged-ref.ts │ │ │ ├── use-session-storage.ts │ │ │ ├── use-set-state.ts │ │ │ ├── use-throttled-callback.ts │ │ │ ├── use-throttled-value.ts │ │ │ └── use-timeout.ts │ │ ├── styles/ │ │ │ ├── ag-grid.css │ │ │ └── global.css │ │ ├── themes/ │ │ │ ├── app-theme-types.ts │ │ │ ├── app-theme.ts │ │ │ ├── ayu-dark/ │ │ │ │ └── ayu-dark.ts │ │ │ ├── ayu-light/ │ │ │ │ └── ayu-light.ts │ │ │ ├── catppuccin-latte/ │ │ │ │ └── catppuccin-latte.ts │ │ │ ├── catppuccin-mocha/ │ │ │ │ └── catppuccin-mocha.ts │ │ │ ├── default-dark/ │ │ │ │ └── default-dark.ts │ │ │ ├── default-light/ │ │ │ │ └── default-light.ts │ │ │ ├── default.ts │ │ │ ├── dracula/ │ │ │ │ └── dracula.ts │ │ │ ├── github-dark/ │ │ │ │ └── github-dark.ts │ │ │ ├── github-light/ │ │ │ │ └── github-light.ts │ │ │ ├── glassy-dark/ │ │ │ │ ├── glassy-dark.ts │ │ │ │ └── glassy_overrides.css │ │ │ ├── gruvbox-dark/ │ │ │ │ └── gruvbox-dark.ts │ │ │ ├── gruvbox-light/ │ │ │ │ └── gruvbox-light.ts │ │ │ ├── high-contrast-dark/ │ │ │ │ └── high-contrast-dark.ts │ │ │ ├── high-contrast-light/ │ │ │ │ └── high-contrast-light.ts │ │ │ ├── material-dark/ │ │ │ │ └── material-dark.ts │ │ │ ├── material-light/ │ │ │ │ └── material-light.ts │ │ │ ├── monokai/ │ │ │ │ └── monokai.ts │ │ │ ├── night-owl/ │ │ │ │ └── night-owl.ts │ │ │ ├── nord/ │ │ │ │ └── nord.ts │ │ │ ├── one-dark/ │ │ │ │ └── one-dark.ts │ │ │ ├── rose-pine/ │ │ │ │ └── rose-pine.ts │ │ │ ├── rose-pine-dawn/ │ │ │ │ └── rose-pine-dawn.ts │ │ │ ├── rose-pine-moon/ │ │ │ │ └── rose-pine-moon.ts │ │ │ ├── shades-of-purple/ │ │ │ │ └── shades-of-purple.ts │ │ │ ├── solarized-dark/ │ │ │ │ └── solarized-dark.ts │ │ │ ├── solarized-light/ │ │ │ │ └── solarized-light.ts │ │ │ ├── tokyo-night/ │ │ │ │ └── tokyo-night.ts │ │ │ ├── vscode-dark-plus/ │ │ │ │ └── vscode-dark-plus.ts │ │ │ └── vscode-light-plus/ │ │ │ └── vscode-light-plus.ts │ │ ├── types/ │ │ │ ├── css-modules.d.ts │ │ │ ├── domain-types.ts │ │ │ ├── drag-and-drop.ts │ │ │ ├── features-types.ts │ │ │ ├── remote-types.ts │ │ │ └── types.ts │ │ └── utils/ │ │ ├── create-polymorphic-component.ts │ │ ├── create-use-external-events.ts │ │ ├── double-click-handler.ts │ │ ├── is-light-color.ts │ │ └── string-to-color.ts │ └── types/ │ ├── mantine.d.ts │ └── mpris-service.d.ts ├── tsconfig.json ├── tsconfig.node.json ├── tsconfig.web.json └── web.vite.config.ts
SYMBOL INDEX (1469 symbols across 463 files)
FILE: assets/assets.d.ts
type Styles (line 1) | type Styles = Record<string, string>;
FILE: scripts/after-all-artifact-build.mjs
function afterAllArtifactBuild (line 16) | async function afterAllArtifactBuild(buildResult) {
FILE: src/main/features/core/autodiscover/index.ts
type JellyfinResponse (line 6) | type JellyfinResponse = {
function discoverAll (line 12) | function discoverAll(reply: (server: DiscoveredServerItem) => void) {
function discoverJellyfin (line 16) | function discoverJellyfin(reply: (server: DiscoveredServerItem) => void) {
FILE: src/main/features/core/discord-rpc/index.ts
constant FEISHIN_DISCORD_APPLICATION_ID (line 4) | const FEISHIN_DISCORD_APPLICATION_ID = '1165957668758900787';
FILE: src/main/features/core/lyrics/genius.ts
constant SEARCH_URL (line 12) | const SEARCH_URL = 'https://genius.com/api/search/song';
type GeniusResponse (line 16) | interface GeniusResponse {
type Hit (line 21) | interface Hit {
type Meta (line 28) | interface Meta {
type PrimaryArtist (line 32) | interface PrimaryArtist {
type ReleaseDateComponents (line 46) | interface ReleaseDateComponents {
type Response (line 52) | interface Response {
type Result (line 57) | interface Result {
type Section (line 88) | interface Section {
type Stats (line 93) | interface Stats {
function getLyricsBySongId (line 98) | async function getLyricsBySongId(url: string): Promise<null | string> {
function getSearchResults (line 122) | async function getSearchResults(
function query (line 162) | async function query(
function getSongId (line 184) | async function getSongId(
FILE: src/main/features/core/lyrics/index.ts
type LyricSource (line 15) | enum LyricSource {
type FullLyricsMetadata (line 22) | type FullLyricsMetadata = Omit<InternetProviderLyricResponse, 'id' | 'ly...
type InternetProviderLyricResponse (line 28) | type InternetProviderLyricResponse = {
type InternetProviderLyricSearchResponse (line 36) | type InternetProviderLyricSearchResponse = {
type LyricGetQuery (line 45) | type LyricGetQuery = {
type LyricOverride (line 51) | type LyricOverride = Omit<InternetProviderLyricResponse, 'lyrics'>;
type LyricSearchQuery (line 53) | type LyricSearchQuery = {
type LyricsResponse (line 60) | type LyricsResponse = string | SynchronizedLyricsArray;
type SynchronizedLyricsArray (line 62) | type SynchronizedLyricsArray = Array<[number, string]>;
type CachedLyrics (line 64) | type CachedLyrics = Record<LyricSource, InternetProviderLyricResponse>;
type GetFetcher (line 65) | type GetFetcher = (id: string) => Promise<null | string>;
type SearchFetcher (line 66) | type SearchFetcher = (
constant SEARCH_FETCHERS (line 70) | const SEARCH_FETCHERS: Record<LyricSource, SearchFetcher> = {
constant GET_FETCHERS (line 77) | const GET_FETCHERS: Record<LyricSource, GetFetcher> = {
constant MAX_CACHED_ITEMS (line 84) | const MAX_CACHED_ITEMS = 10;
FILE: src/main/features/core/lyrics/lrclib.ts
constant FETCH_URL (line 12) | const FETCH_URL = 'https://lrclib.net/api/get';
constant SEEARCH_URL (line 13) | const SEEARCH_URL = 'https://lrclib.net/api/search';
constant TIMEOUT_MS (line 15) | const TIMEOUT_MS = 5000;
type LrcLibSearchResponse (line 17) | interface LrcLibSearchResponse {
type LrcLibTrackResponse (line 28) | interface LrcLibTrackResponse {
function getLyricsBySongId (line 43) | async function getLyricsBySongId(songId: string): Promise<null | string> {
function getSearchResults (line 56) | async function getSearchResults(
function query (line 91) | async function query(
FILE: src/main/features/core/lyrics/netease.ts
constant SEARCH_URL (line 12) | const SEARCH_URL = 'https://music.163.com/api/search/get';
constant LYRICS_URL (line 13) | const LYRICS_URL = 'https://music.163.com/api/song/lyric';
type Result (line 17) | interface Result {
type Album (line 23) | interface Album {
type Artist (line 36) | interface Artist {
type NetEaseResponse (line 49) | interface NetEaseResponse {
type Song (line 54) | interface Song {
function getLyricsBySongId (line 72) | async function getLyricsBySongId(songId: string): Promise<null | string> {
function getSearchResults (line 96) | async function getSearchResults(
function query (line 140) | async function query(
function getMatchedLyrics (line 162) | async function getMatchedLyrics(
function mergeLyrics (line 176) | function mergeLyrics(original: string | undefined, translated: string | ...
FILE: src/main/features/core/lyrics/simpmusic.ts
constant API_URL (line 11) | const API_URL = 'https://api-lyrics.simpmusic.org/v1';
constant TIMEOUT_MS (line 13) | const TIMEOUT_MS = 5000;
type SimpMusicLyric (line 15) | interface SimpMusicLyric {
type SimpMusicSearchResponse (line 28) | interface SimpMusicSearchResponse {
function getLyricsBySongId (line 33) | async function getLyricsBySongId(songId: string): Promise<null | string> {
function getSearchResults (line 51) | async function getSearchResults(
function query (line 83) | async function query(
FILE: src/main/features/core/player/index.ts
type NodeMpvError (line 42) | type NodeMpvError = {
constant MPV_BINARY_PATH (line 71) | const MPV_BINARY_PATH = store.get('mpv_path') as string | undefined;
type MpvState (line 589) | enum MpvState {
FILE: src/main/features/core/remote/index.ts
function initMpris (line 20) | async function initMpris() {
type MimeType (line 29) | interface MimeType {
type RemoteConfig (line 36) | interface RemoteConfig {
class StatefulWebSocket (line 43) | class StatefulWebSocket extends WebSocket {
type SendData (line 58) | type SendData = ServerEvent & {
function broadcast (line 62) | function broadcast(message: ServerEvent): void {
function send (line 70) | function send({ client, data, event }: SendData): void {
constant MIME_TYPES (line 91) | const MIME_TYPES: MimeType = {
constant PING_TIMEOUT_MS (line 98) | const PING_TIMEOUT_MS = 10000;
constant UP_TIMEOUT_MS (line 99) | const UP_TIMEOUT_MS = 5000;
type Encoding (line 101) | enum Encoding {
constant GZIP_REGEX (line 107) | const GZIP_REGEX = /\bgzip\b/;
constant ZLIB_REGEX (line 108) | const ZLIB_REGEX = /bdeflate\b/;
function authorize (line 129) | function authorize(req: IncomingMessage): boolean {
function serveFile (line 142) | async function serveFile(
function setOk (line 256) | function setOk(
FILE: src/main/features/linux/mpris.ts
constant REPEAT_TO_MPRIS (line 130) | const REPEAT_TO_MPRIS: Record<PlayerRepeat, string> = {
FILE: src/main/index.ts
constant ALPHA_UPDATER_CONFIG (line 46) | const ALPHA_UPDATER_CONFIG: {
constant GITHUB_UPDATER_CONFIG (line 58) | const GITHUB_UPDATER_CONFIG = {
type UpdaterInstance (line 64) | type UpdaterInstance = AppImageUpdater | MacUpdater | NsisUpdater | type...
class AppUpdater (line 66) | class AppUpdater {
method constructor (line 67) | constructor() {
function checkAllChannelsAndGetBest (line 111) | async function checkAllChannelsAndGetBest(): Promise<{
function configureAndGetUpdater (line 176) | function configureAndGetUpdater(): UpdaterInstance {
function configureAutoUpdaterForChannel (line 231) | function configureAutoUpdaterForChannel(channel: 'beta' | 'latest'): void {
function createAlphaUpdaterInstance (line 247) | function createAlphaUpdaterInstance(): AppImageUpdater | MacUpdater | Ns...
constant RESOURCES_PATH (line 329) | const RESOURCES_PATH = app.isPackaged
function createWindow (line 457) | async function createWindow(first = true): Promise<void> {
type BindingActions (line 762) | enum BindingActions {
constant HOTKEY_ACTIONS (line 782) | const HOTKEY_ACTIONS: Record<BindingActions, () => void> = {
constant FONT_HEADERS (line 889) | const FONT_HEADERS = [
FILE: src/main/menu.ts
type DarwinMenuItemConstructorOptions (line 3) | interface DarwinMenuItemConstructorOptions extends MenuItemConstructorOp...
class MenuBuilder (line 8) | class MenuBuilder {
method constructor (line 11) | constructor(mainWindow: BrowserWindow) {
method buildDarwinTemplate (line 15) | buildDarwinTemplate(): MenuItemConstructorOptions[] {
method buildDefaultTemplate (line 162) | buildDefaultTemplate(): MenuItemConstructorOptions[] {
method buildMenu (line 265) | buildMenu(): Menu {
method setupDevelopmentEnvironment (line 281) | setupDevelopmentEnvironment(): void {
FILE: src/preload/autodiscover.ts
type AutoDiscover (line 23) | type AutoDiscover = typeof autodiscover;
FILE: src/preload/browser.ts
type Browser (line 41) | type Browser = typeof browser;
FILE: src/preload/discord-rpc.ts
type DiscordRpc (line 34) | type DiscordRpc = typeof discordRpc;
FILE: src/preload/index.d.ts
type Window (line 6) | interface Window {
FILE: src/preload/index.ts
type PreloadApi (line 30) | type PreloadApi = typeof api;
FILE: src/preload/ipc.ts
type Ipc (line 31) | type Ipc = typeof ipc;
FILE: src/preload/local-settings.ts
constant SERVER_TYPE (line 70) | const SERVER_TYPE = toServerType(process.env.SERVER_TYPE);
type LocalSettings (line 106) | type LocalSettings = typeof localSettings;
FILE: src/preload/lyrics.ts
type Lyrics (line 35) | type Lyrics = typeof lyrics;
FILE: src/preload/mpris.ts
type Mpris (line 73) | type Mpris = typeof mpris;
FILE: src/preload/mpv-player.ts
type MpvPLayer (line 225) | type MpvPLayer = typeof mpvPlayer;
type MpvPlayerListener (line 226) | type MpvPlayerListener = typeof mpvPlayerListener;
FILE: src/preload/remote.ts
type Remote (line 113) | type Remote = typeof remote;
FILE: src/preload/utils.ts
type Utils (line 84) | type Utils = typeof utils;
FILE: src/remote/components/player-image.tsx
type PlayerImageProps (line 5) | interface PlayerImageProps {
FILE: src/remote/components/wrapped-slider.tsx
type WrappedProps (line 52) | interface WrappedProps extends Omit<SliderProps, 'onChangeEnd'> {
FILE: src/remote/store/index.ts
type SettingsSlice (line 11) | interface SettingsSlice extends SettingsState {
type SettingsState (line 20) | interface SettingsState {
type StatefulWebSocket (line 28) | interface StatefulWebSocket extends WebSocket {
FILE: src/renderer/api/controller.ts
type ApiController (line 15) | type ApiController = {
type GeneralController (line 79) | interface GeneralController extends Omit<Required<ControllerEndpoint>, '...
method addToPlaylist (line 88) | addToPlaylist(args) {
method authenticate (line 102) | authenticate(url, body, type) {
method createFavorite (line 105) | createFavorite(args) {
method createInternetRadioStation (line 119) | createInternetRadioStation(args) {
method createPlaylist (line 133) | createPlaylist(args) {
method deleteFavorite (line 147) | deleteFavorite(args) {
method deleteInternetRadioStation (line 161) | deleteInternetRadioStation(args) {
method deletePlaylist (line 175) | deletePlaylist(args) {
method getAlbumArtistDetail (line 189) | getAlbumArtistDetail(args) {
method getAlbumArtistInfo (line 203) | getAlbumArtistInfo(args) {
method getAlbumArtistList (line 215) | getAlbumArtistList(args) {
method getAlbumArtistListCount (line 235) | getAlbumArtistListCount(args) {
method getAlbumDetail (line 255) | getAlbumDetail(args) {
method getAlbumInfo (line 269) | getAlbumInfo(args) {
method getAlbumList (line 283) | getAlbumList(args) {
method getAlbumListCount (line 303) | getAlbumListCount(args) {
method getAlbumRadio (line 323) | getAlbumRadio(args) {
method getArtistList (line 337) | getArtistList(args) {
method getArtistListCount (line 357) | getArtistListCount(args) {
method getArtistRadio (line 377) | getArtistRadio(args) {
method getDownloadUrl (line 391) | getDownloadUrl(args) {
method getFolder (line 405) | getFolder(args) {
method getGenreList (line 425) | getGenreList(args) {
method getImageRequest (line 445) | getImageRequest(args) {
method getImageUrl (line 464) | getImageUrl(args) {
method getInternetRadioStations (line 483) | getInternetRadioStations(args) {
method getLyrics (line 496) | getLyrics(args) {
method getMusicFolderList (line 510) | getMusicFolderList(args) {
method getPlaylistDetail (line 524) | getPlaylistDetail(args) {
method getPlaylistList (line 538) | getPlaylistList(args) {
method getPlaylistListCount (line 552) | getPlaylistListCount(args) {
method getPlaylistSongList (line 566) | getPlaylistSongList(args) {
method getPlayQueue (line 580) | getPlayQueue(args) {
method getRandomSongList (line 594) | getRandomSongList(args) {
method getRoles (line 614) | getRoles(args) {
method getServerInfo (line 628) | getServerInfo(args) {
method getSimilarSongs (line 642) | getSimilarSongs(args) {
method getSongDetail (line 662) | getSongDetail(args) {
method getSongList (line 676) | getSongList(args) {
method getSongListCount (line 696) | getSongListCount(args) {
method getStreamUrl (line 716) | getStreamUrl(args) {
method getStructuredLyrics (line 728) | getStructuredLyrics(args) {
method getTagList (line 742) | getTagList(args) {
method getTopSongs (line 756) | getTopSongs(args) {
method getUserInfo (line 770) | getUserInfo(args) {
method getUserList (line 784) | getUserList(args) {
method movePlaylistItem (line 798) | movePlaylistItem(args) {
method removeFromPlaylist (line 812) | removeFromPlaylist(args) {
method replacePlaylist (line 826) | replacePlaylist(args) {
method savePlayQueue (line 840) | savePlayQueue(args) {
method scrobble (line 854) | scrobble(args) {
method search (line 868) | search(args) {
method setRating (line 888) | setRating(args) {
method shareItem (line 902) | shareItem(args) {
method updateInternetRadioStation (line 916) | updateInternetRadioStation(args) {
method updatePlaylist (line 930) | updatePlaylist(args) {
FILE: src/renderer/api/jellyfin/jellyfin-controller.ts
constant MAX_ITEMS_PER_PLAYLIST_ADD (line 69) | const MAX_ITEMS_PER_PLAYLIST_ADD = 50;
constant VERSION_INFO (line 75) | const VERSION_INFO: VersionInfo = [
constant JF_FIELDS (line 86) | const JF_FIELDS = {
function getLibraryId (line 1848) | function getLibraryId(musicFolderId?: string | string[]) {
FILE: src/renderer/api/navidrome/navidrome-api.ts
constant RETRY_DELAY_MS (line 255) | const RETRY_DELAY_MS = 1000;
constant MAX_RETRIES (line 256) | const MAX_RETRIES = 5;
constant TIMEOUT_ERROR (line 271) | const TIMEOUT_ERROR = Error();
FILE: src/renderer/api/navidrome/navidrome-controller.ts
constant VERSION_INFO (line 30) | const VERSION_INFO: VersionInfo = [
constant NAVIDROME_ROLES (line 41) | const NAVIDROME_ROLES: Array<string | { label: string; value: string }> = [
constant EXCLUDED_TAGS (line 58) | const EXCLUDED_TAGS = new Set<string>([
constant EXCLUDED_ALBUM_TAGS (line 62) | const EXCLUDED_ALBUM_TAGS = new Set<string>([
constant EXCLUDED_SONG_TAGS (line 76) | const EXCLUDED_SONG_TAGS = new Set<string>(['disctotal', 'tracktotal']);
constant ID_TAGS (line 83) | const ID_TAGS = new Set<string>(['albumversion', 'mood']);
FILE: src/renderer/api/query-keys.ts
type QueryPagination (line 48) | type QueryPagination = {
FILE: src/renderer/api/subsonic/subsonic-controller.ts
constant ALBUM_LIST_SORT_MAPPING (line 66) | const ALBUM_LIST_SORT_MAPPING: Record<AlbumListSort, AlbumListSortType |...
constant MAX_SUBSONIC_ITEMS (line 87) | const MAX_SUBSONIC_ITEMS = 500;
constant SUBSONIC_FAST_BATCH_SIZE (line 88) | const SUBSONIC_FAST_BATCH_SIZE = MAX_SUBSONIC_ITEMS * 10;
function sortAndPaginate (line 90) | function sortAndPaginate<T>(
function getLibraryId (line 2175) | function getLibraryId(musicFolderId?: string | string[]) {
FILE: src/renderer/api/utils-list-count.ts
type OptimizedListCountOptions (line 6) | interface OptimizedListCountOptions<TQuery, TListQuery, TResponse> {
FILE: src/renderer/assets/assets.d.ts
type Styles (line 1) | type Styles = Record<string, string>;
FILE: src/renderer/components/drag-preview/drag-preview.tsx
type DragPreviewProps (line 11) | interface DragPreviewProps {
FILE: src/renderer/components/export-import-settings-modal/export-import-settings-modal.tsx
type SCREENS (line 19) | enum SCREENS {
FILE: src/renderer/components/feature-carousel/feature-carousel.tsx
type FeatureCarouselProps (line 54) | interface FeatureCarouselProps {
type CarouselItemProps (line 76) | interface CarouselItemProps {
FILE: src/renderer/components/feature-carousel/single-feature-carousel.tsx
type CarouselItemProps (line 56) | interface CarouselItemProps {
type SingleFeatureCarouselProps (line 60) | interface SingleFeatureCarouselProps {
FILE: src/renderer/components/grid-carousel/grid-carousel-v2.tsx
type Card (line 28) | interface Card {
type GridCarouselProps (line 33) | interface GridCarouselProps {
function BaseGridCarousel (line 65) | function BaseGridCarousel(props: GridCarouselProps) {
type GridCarouselSkeletonProps (line 364) | interface GridCarouselSkeletonProps {
function getCardsToShow (line 437) | function getCardsToShow(breakpoints: {
FILE: src/renderer/components/item-card/item-card-controls.tsx
type ItemCardControlsProps (line 30) | interface ItemCardControlsProps {
type SecondaryButtonProps (line 384) | interface SecondaryButtonProps {
FILE: src/renderer/components/item-card/item-card.tsx
type DataRow (line 50) | type DataRow = {
type ItemCardProps (line 59) | interface ItemCardProps {
type ItemCardDerivativeProps (line 160) | interface ItemCardDerivativeProps extends Omit<ItemCardProps, 'type'> {
FILE: src/renderer/components/item-image/item-image.tsx
type UseItemImageUrlProps (line 81) | interface UseItemImageUrlProps {
function getItemImageRequest (line 162) | function getItemImageRequest(args: UseItemImageUrlProps) {
function getItemImageUrl (line 198) | function getItemImageUrl(args: UseItemImageUrlProps) {
FILE: src/renderer/components/item-list/expanded-list-container.tsx
constant EXPANDED_HEIGHT (line 5) | const EXPANDED_HEIGHT = 300;
type ExpandedListContainerProps (line 7) | interface ExpandedListContainerProps {
FILE: src/renderer/components/item-list/expanded-list-item.tsx
type ExpandedListItemProps (line 10) | interface ExpandedListItemProps {
type SelectedItemProps (line 31) | interface SelectedItemProps {
FILE: src/renderer/components/item-list/helpers/item-list-controls.ts
type UseDefaultItemListControlsArgs (line 15) | interface UseDefaultItemListControlsArgs {
FILE: src/renderer/components/item-list/helpers/item-list-infinite-loader.ts
type InfiniteLoaderCacheData (line 37) | type InfiniteLoaderCacheData = {
type UseItemListInfiniteLoaderProps (line 44) | interface UseItemListInfiniteLoaderProps {
function getInitialData (line 55) | function getInitialData(): InfiniteLoaderCacheData {
FILE: src/renderer/components/item-list/helpers/item-list-paginated-loader.ts
type UseItemListPaginatedLoaderProps (line 36) | interface UseItemListPaginatedLoaderProps {
function getInitialData (line 47) | function getInitialData(itemCount: number) {
FILE: src/renderer/components/item-list/helpers/item-list-state.ts
type ItemListAction (line 34) | type ItemListAction =
type ItemListState (line 65) | interface ItemListState {
type ItemListStateActions (line 75) | interface ItemListStateActions {
type ItemListStateItem (line 108) | interface ItemListStateItem {
type ItemListStateItemWithRequiredProperties (line 115) | type ItemListStateItemWithRequiredProperties = Record<string, unknown> & {
class ItemListStateStore (line 294) | class ItemListStateStore {
method dispatch (line 301) | dispatch(action: ItemListAction): void {
method getExpandedItems (line 309) | getExpandedItems(): unknown[] {
method getState (line 323) | getState(): ItemListState {
method subscribe (line 327) | subscribe(listener: () => void): () => void {
FILE: src/renderer/components/item-list/helpers/use-item-list-column-reorder.ts
type UseItemListColumnReorderProps (line 8) | interface UseItemListColumnReorderProps {
type SetListData (line 92) | type SetListData = Parameters<
FILE: src/renderer/components/item-list/helpers/use-item-list-column-resize.ts
type UseItemListColumnResizeProps (line 6) | interface UseItemListColumnResizeProps {
type SetListData (line 30) | type SetListData = Parameters<
FILE: src/renderer/components/item-list/helpers/use-item-list-scroll-persist.ts
type UseItemListScrollPersistProps (line 6) | interface UseItemListScrollPersistProps {
FILE: src/renderer/components/item-list/item-detail-list/columns/default-column.tsx
type DefaultColumnProps (line 3) | interface DefaultColumnProps extends ItemDetailListCellProps {
FILE: src/renderer/components/item-list/item-detail-list/columns/genre-badge-column.tsx
constant MAX_GENRES (line 12) | const MAX_GENRES = 4;
FILE: src/renderer/components/item-list/item-detail-list/columns/genre-column.tsx
constant TEXT_PROPS (line 8) | const TEXT_PROPS = { isMuted: true, isNoSelect: true, size: 'sm' as cons...
FILE: src/renderer/components/item-list/item-detail-list/columns/index.ts
type CellComponent (line 40) | type CellComponent = (props: ItemDetailListCellProps) => ReactNode;
constant COLUMN_MAP (line 42) | const COLUMN_MAP: Partial<Record<TableColumn, CellComponent>> = {
type DetailListCellComponentProps (line 76) | type DetailListCellComponentProps = ItemDetailListCellProps & { columnId...
function getDetailListCellComponent (line 78) | function getDetailListCellComponent(
FILE: src/renderer/components/item-list/item-detail-list/columns/types.ts
type ItemDetailListCellProps (line 5) | interface ItemDetailListCellProps {
FILE: src/renderer/components/item-list/item-detail-list/item-detail-list.tsx
constant DEFAULT_ROW_HEIGHT (line 81) | const DEFAULT_ROW_HEIGHT = 300;
constant SKELETON_TRACK_ROW_COUNT (line 83) | const SKELETON_TRACK_ROW_COUNT = 6;
type ItemDetailListProps (line 85) | interface ItemDetailListProps {
type RowData (line 114) | interface RowData {
type TrackRowProps (line 137) | interface TrackRowProps {
type MetadataSectionProps (line 415) | interface MetadataSectionProps {
type ItemDetailSkeletonRowProps (line 631) | interface ItemDetailSkeletonRowProps {
type RowContentProps (line 713) | type RowContentProps = Omit<RowComponentProps<RowData>, 'style'>;
type DetailListHeaderCellProps (line 880) | interface DetailListHeaderCellProps {
type DetailListColumnResizeHandleProps (line 1041) | interface DetailListColumnResizeHandleProps {
type DetailListHeaderProps (line 1116) | interface DetailListHeaderProps {
constant SCROLL_END_DEBOUNCE_MS (line 1236) | const SCROLL_END_DEBOUNCE_MS = 150;
constant DEFAULT_DETAIL_TABLE_ID (line 1238) | const DEFAULT_DETAIL_TABLE_ID = 'album-detail';
method initialized (line 1434) | initialized(osInstance) {
FILE: src/renderer/components/item-list/item-detail-list/utils.ts
constant FIXED_TRACK_COLUMN_WIDTHS (line 3) | const FIXED_TRACK_COLUMN_WIDTHS: Partial<Record<TableColumn, number>> = {
constant HOVER_ONLY_COLUMNS (line 21) | const HOVER_ONLY_COLUMNS: TableColumn[] = [
constant NO_HORIZONTAL_PADDING_COLUMNS (line 27) | const NO_HORIZONTAL_PADDING_COLUMNS: TableColumn[] = [
function getTrackColumnFixed (line 33) | function getTrackColumnFixed(columnId: TableColumn): {
function isNoHorizontalPaddingColumn (line 43) | function isNoHorizontalPaddingColumn(columnId: TableColumn): boolean {
function isTrackColumnHoverOnly (line 47) | function isTrackColumnHoverOnly(columnId: TableColumn): boolean {
function shouldShowHoverOnlyColumnContent (line 51) | function shouldShowHoverOnlyColumnContent(
FILE: src/renderer/components/item-list/item-grid-list/item-grid-list.tsx
type VirtualizedGridListProps (line 49) | interface VirtualizedGridListProps {
type GridItemProps (line 311) | interface GridItemProps {
type ItemGridListProps (line 333) | interface ItemGridListProps {
method initialized (line 415) | initialized(osInstance) {
FILE: src/renderer/components/item-list/item-list-pagination/item-list-pagination.tsx
type ItemListWithPaginationProps (line 7) | interface ItemListWithPaginationProps {
FILE: src/renderer/components/item-list/item-table-list/album-group-header.tsx
type AlbumGroupHeaderProps (line 17) | interface AlbumGroupHeaderProps {
FILE: src/renderer/components/item-list/item-table-list/columns/genre-badge-column.tsx
constant MAX_GENRES (line 18) | const MAX_GENRES = 4;
FILE: src/renderer/components/item-list/item-table-list/columns/title-column.tsx
function DefaultTitleColumn (line 34) | function DefaultTitleColumn(props: ItemTableListInnerColumn) {
function QueueSongTitleColumn (line 76) | function QueueSongTitleColumn(props: ItemTableListInnerColumn) {
FILE: src/renderer/components/item-list/item-table-list/default-columns.ts
type DefaultTableColumn (line 5) | type DefaultTableColumn = {
constant SONG_TABLE_COLUMNS (line 15) | const SONG_TABLE_COLUMNS: DefaultTableColumn[] = [
constant PLAYLIST_SONG_TABLE_COLUMNS (line 306) | const PLAYLIST_SONG_TABLE_COLUMNS: DefaultTableColumn[] = SONG_TABLE_COL...
constant ALBUM_TABLE_COLUMNS (line 308) | const ALBUM_TABLE_COLUMNS: DefaultTableColumn[] = [
constant ALBUM_ARTIST_TABLE_COLUMNS (line 491) | const ALBUM_ARTIST_TABLE_COLUMNS: DefaultTableColumn[] = [
constant PLAYLIST_TABLE_COLUMNS (line 611) | const PLAYLIST_TABLE_COLUMNS: DefaultTableColumn[] = [
constant GENRE_TABLE_COLUMNS (line 686) | const GENRE_TABLE_COLUMNS: DefaultTableColumn[] = [
FILE: src/renderer/components/item-list/item-table-list/hooks/use-container-width-tracking.ts
type UseContainerWidthTrackingProps (line 3) | interface UseContainerWidthTrackingProps {
FILE: src/renderer/components/item-list/item-table-list/hooks/use-item-drag-drop-state.tsx
type DragDropState (line 10) | interface DragDropState<TElement extends HTMLElement = HTMLDivElement> {
type UseItemDragDropStateProps (line 16) | interface UseItemDragDropStateProps {
FILE: src/renderer/components/item-list/item-table-list/hooks/use-row-interaction-delegate.ts
type UseRowInteractionDelegateProps (line 3) | interface UseRowInteractionDelegateProps {
FILE: src/renderer/components/item-list/item-table-list/hooks/use-sticky-group-row-positioning.ts
type UseStickyGroupRowPositioningProps (line 3) | interface UseStickyGroupRowPositioningProps {
FILE: src/renderer/components/item-list/item-table-list/hooks/use-sticky-header-positioning.ts
type UseStickyHeaderPositioningProps (line 3) | interface UseStickyHeaderPositioningProps {
FILE: src/renderer/components/item-list/item-table-list/hooks/use-sticky-table-group-rows.tsx
type GroupRowInfo (line 7) | interface GroupRowInfo {
FILE: src/renderer/components/item-list/item-table-list/hooks/use-table-imperative-handle.ts
type UseTableImperativeHandleProps (line 6) | interface UseTableImperativeHandleProps {
FILE: src/renderer/components/item-list/item-table-list/hooks/use-table-initial-scroll.ts
type UseTableInitialScrollProps (line 3) | interface UseTableInitialScrollProps {
FILE: src/renderer/components/item-list/item-table-list/hooks/use-table-keyboard-navigation.ts
type UseTableKeyboardNavigationProps (line 12) | interface UseTableKeyboardNavigationProps {
FILE: src/renderer/components/item-list/item-table-list/hooks/use-table-pane-sync.ts
method initialized (line 47) | initialized(osInstance) {
method initialized (line 77) | initialized(osInstance) {
FILE: src/renderer/components/item-list/item-table-list/item-table-list-column.tsx
type ItemTableListColumn (line 72) | interface ItemTableListColumn extends CellComponentProps<TableItemProps> {
type ItemTableListInnerColumn (line 76) | interface ItemTableListInnerColumn extends ItemTableListColumn {
function isAlbumGroupingActive (line 371) | function isAlbumGroupingActive(columns: { id: string; isEnabled?: boolea...
function isLastInAlbumGroup (line 375) | function isLastInAlbumGroup(
type ColumnResizeHandleProps (line 709) | interface ColumnResizeHandleProps {
FILE: src/renderer/components/item-list/item-table-list/item-table-list-context.tsx
type ItemTableListConfig (line 14) | type ItemTableListConfig = {
type ItemTableListStoreContextValue (line 51) | type ItemTableListStoreContextValue = {
class ActiveRowStore (line 55) | class ActiveRowStore {
method getActiveRowId (line 59) | getActiveRowId(): null | string {
method setActiveRowId (line 63) | setActiveRowId(next: null | string | undefined): void {
method subscribe (line 70) | subscribe(listener: () => void): () => void {
FILE: src/renderer/components/item-list/item-table-list/item-table-list.tsx
type TableItemSize (line 98) | enum TableItemSize {
type VirtualizedTableGridProps (line 104) | interface VirtualizedTableGridProps {
function shallowEqualNumberArrays (line 727) | function shallowEqualNumberArrays(a: number[], b: number[]): boolean {
type TableGroupHeader (line 787) | interface TableGroupHeader {
type TableItemProps (line 798) | interface TableItemProps {
type ItemTableListProps (line 836) | interface ItemTableListProps {
FILE: src/renderer/components/item-list/item-table-list/memoized-cell-router.tsx
type MemoizedCellRouterProps (line 11) | interface MemoizedCellRouterProps extends CellComponentProps<TableItemPr...
FILE: src/renderer/components/item-list/types.ts
type DefaultItemControlProps (line 14) | interface DefaultItemControlProps {
type ItemControls (line 23) | interface ItemControls {
type ItemListComponentProps (line 60) | interface ItemListComponentProps<TQuery> {
type ItemListGridComponentProps (line 67) | interface ItemListGridComponentProps<TQuery> extends ItemListComponentPr...
type ItemListHandle (line 73) | interface ItemListHandle {
type ItemListItem (line 82) | type ItemListItem =
type ItemListTableComponentProps (line 92) | interface ItemListTableComponentProps<TQuery> extends ItemListComponentP...
type ItemTableListColumnConfig (line 104) | interface ItemTableListColumnConfig {
FILE: src/renderer/components/native-scroll-area/native-scroll-area.tsx
type NativeScrollAreaProps (line 12) | interface NativeScrollAreaProps {
FILE: src/renderer/components/page-header/page-header.tsx
type PageHeaderProps (line 14) | interface PageHeaderProps
FILE: src/renderer/components/query-builder/index.tsx
type FilterGroup (line 14) | type FilterGroup = { group: string; items: FilterItem[] };
type FilterItem (line 16) | type FilterItem = { label: string; type: string; value: string };
type Filters (line 18) | type Filters = FilterGroup[] | FilterItem[];
type AddArgs (line 19) | type AddArgs = {
type DeleteArgs (line 23) | type DeleteArgs = {
type QueryBuilderProps (line 29) | interface QueryBuilderProps {
FILE: src/renderer/components/query-builder/query-builder-option.tsx
type DeleteArgs (line 12) | type DeleteArgs = {
type QueryOptionProps (line 18) | interface QueryOptionProps {
FILE: src/renderer/components/settings-diff-visualiser/settings-diff-visualiser.tsx
type DiffVisualiserProps (line 5) | interface DiffVisualiserProps {
FILE: src/renderer/components/simple-item-table/simple-item-table.tsx
type TableItemSize (line 22) | enum TableItemSize {
type SimpleItemTableProps (line 28) | interface SimpleItemTableProps {
type SimpleItemTableRowProps (line 207) | interface SimpleItemTableRowProps {
FILE: src/renderer/context/list-context.tsx
type ListDisplayMode (line 6) | type ListDisplayMode = LibraryItem.ALBUM | LibraryItem.SONG;
type ListContextProps (line 8) | interface ListContextProps {
FILE: src/renderer/events/event-emitter.ts
class TypedEventEmitterImpl (line 5) | class TypedEventEmitterImpl implements TypedEventEmitter<EventMap> {
method emit (line 9) | emit<K extends keyof EventMap>(event: K, payload: EventMap[K]): void {
method off (line 22) | off<K extends keyof EventMap>(event: K, callback: EventCallback<EventM...
method on (line 32) | on<K extends keyof EventMap>(event: K, callback: EventCallback<EventMa...
method removeAllListeners (line 40) | removeAllListeners<K extends keyof EventMap>(event?: K): void {
method setErrorHandler (line 50) | setErrorHandler(handler: ErrorHandler): void {
method handleError (line 54) | private handleError(error: Error, event: string, payload: any): void {
FILE: src/renderer/events/events.ts
type AutoDJQueueAddedEventPayload (line 3) | type AutoDJQueueAddedEventPayload = {
type EventMap (line 7) | type EventMap = {
type ItemListRefreshEventPayload (line 26) | type ItemListRefreshEventPayload = {
type ItemListUpdateItemEventPayload (line 30) | type ItemListUpdateItemEventPayload = {
type MediaNextEventPayload (line 36) | type MediaNextEventPayload = {
type MediaPrevEventPayload (line 41) | type MediaPrevEventPayload = {
type MpvReloadEventPayload (line 46) | type MpvReloadEventPayload = Record<string, never>;
type PlayerPlayEventPayload (line 48) | type PlayerPlayEventPayload = {
type PlayerRepeatedEventPayload (line 53) | type PlayerRepeatedEventPayload = {
type PlaylistMoveEventPayload (line 57) | type PlaylistMoveEventPayload = {
type PlaylistReorderEventPayload (line 62) | type PlaylistReorderEventPayload = {
type QueueRestoredEventPayload (line 69) | type QueueRestoredEventPayload = {
type UserFavoriteEventPayload (line 75) | type UserFavoriteEventPayload = {
type UserRatingEventPayload (line 82) | type UserRatingEventPayload = {
FILE: src/renderer/events/types.ts
type ErrorHandler (line 1) | type ErrorHandler = (error: Error, event: string, payload: any) => void;
type EventCallback (line 3) | type EventCallback<T = any> = (payload: T) => void;
type TypedEventEmitter (line 5) | interface TypedEventEmitter<T extends Record<string, any>> {
FILE: src/renderer/features/action-required/components/action-required-container.tsx
type ActionRequiredContainerProps (line 7) | interface ActionRequiredContainerProps {
FILE: src/renderer/features/action-required/components/server-required.tsx
function ServerSelector (line 51) | function ServerSelector() {
FILE: src/renderer/features/albums/components/album-detail-content.tsx
type AlbumMetadataTagsProps (line 92) | interface AlbumMetadataTagsProps {
constant MOOD_TAG (line 96) | const MOOD_TAG = 'mood';
constant RELEASE_COUNTRY_TAG (line 97) | const RELEASE_COUNTRY_TAG = 'releasecountry';
constant RELEASE_STATUS_TAG (line 98) | const RELEASE_STATUS_TAG = 'releasestatus';
type AlbumMetadataGenresProps (line 228) | interface AlbumMetadataGenresProps {
type AlbumMetadataExternalLinksProps (line 294) | interface AlbumMetadataExternalLinksProps {
type AlbumDetailSongsTableProps (line 522) | interface AlbumDetailSongsTableProps {
type DiscGroupRowProps (line 526) | interface DiscGroupRowProps {
function AlbumDetailCarousels (line 600) | function AlbumDetailCarousels({ data }: { data: Album }) {
FILE: src/renderer/features/albums/components/album-grid-carousel.tsx
type AlbumGridCarouselProps (line 10) | interface AlbumGridCarouselProps {
function AlbumGridCarousel (line 17) | function AlbumGridCarousel(props: AlbumGridCarouselProps) {
FILE: src/renderer/features/albums/components/album-infinite-carousel.tsx
type AlbumCarouselProps (line 25) | interface AlbumCarouselProps {
function useAlbumListInfinite (line 138) | function useAlbumListInfinite(
FILE: src/renderer/features/albums/components/album-list-content.tsx
type OverrideAlbumListQuery (line 98) | type OverrideAlbumListQuery = Omit<Partial<AlbumListQuery>, 'limit' | 's...
FILE: src/renderer/features/albums/components/album-list-header.tsx
type AlbumListHeaderProps (line 21) | interface AlbumListHeaderProps {
FILE: src/renderer/features/albums/components/album-list-infinite-detail.tsx
type AlbumListInfiniteDetailProps (line 19) | interface AlbumListInfiniteDetailProps extends ItemListComponentProps<Al...
FILE: src/renderer/features/albums/components/album-list-infinite-grid.tsx
type AlbumListInfiniteGridProps (line 20) | interface AlbumListInfiniteGridProps extends ItemListGridComponentProps<...
FILE: src/renderer/features/albums/components/album-list-infinite-table.tsx
type AlbumListInfiniteTableProps (line 21) | interface AlbumListInfiniteTableProps extends ItemListTableComponentProp...
FILE: src/renderer/features/albums/components/album-list-paginated-detail.tsx
type AlbumListPaginatedDetailProps (line 21) | interface AlbumListPaginatedDetailProps extends ItemListComponentProps<A...
FILE: src/renderer/features/albums/components/album-list-paginated-grid.tsx
type AlbumListPaginatedGridProps (line 22) | interface AlbumListPaginatedGridProps extends ItemListGridComponentProps...
FILE: src/renderer/features/albums/components/album-list-paginated-table.tsx
type AlbumListPaginatedTableProps (line 23) | interface AlbumListPaginatedTableProps extends ItemListTableComponentPro...
FILE: src/renderer/features/albums/components/expanded-album-list-item.tsx
type ExpandedAlbumData (line 38) | interface ExpandedAlbumData {
type ExpandedAlbumListItemProps (line 47) | interface ExpandedAlbumListItemProps {
type AlbumTracksTableProps (line 52) | interface AlbumTracksTableProps {
type TrackRowProps (line 64) | interface TrackRowProps {
type ExpandedAlbumListItemContentProps (line 218) | interface ExpandedAlbumListItemContentProps {
function itemToExpandedAlbumData (line 339) | function itemToExpandedAlbumData(
FILE: src/renderer/features/albums/components/jellyfin-album-filters.tsx
type JellyfinAlbumFiltersProps (line 32) | interface JellyfinAlbumFiltersProps {
FILE: src/renderer/features/albums/components/joined-artists.tsx
constant JOINED_ARTISTS_MUTED_PROPS (line 8) | const JOINED_ARTISTS_MUTED_PROPS = {
type JoinedArtistsProps (line 13) | interface JoinedArtistsProps {
function escapeRegex (line 221) | function escapeRegex(str: string): string {
FILE: src/renderer/features/albums/components/navidrome-album-filters.tsx
type NavidromeAlbumFiltersProps (line 32) | interface NavidromeAlbumFiltersProps {
FILE: src/renderer/features/albums/components/subsonic-album-filters.tsx
type SubsonicAlbumFiltersProps (line 31) | interface SubsonicAlbumFiltersProps {
FILE: src/renderer/features/analytics/hooks/use-app-tracker.ts
type AppTrackerProperties (line 52) | type AppTrackerProperties = PlayerProperties &
type PlayerProperties (line 59) | type PlayerProperties = {
type SettingsProperties (line 67) | type SettingsProperties = {
function ignoreWeb (line 131) | function ignoreWeb<T>(value: T): T | undefined {
FILE: src/renderer/features/artists/components/album-artist-detail-content.tsx
type AlbumArtistActionButtonsProps (line 95) | interface AlbumArtistActionButtonsProps {
type AlbumArtistMetadataGenresProps (line 157) | interface AlbumArtistMetadataGenresProps {
type AlbumArtistMetadataBiographyProps (line 202) | interface AlbumArtistMetadataBiographyProps {
constant TABLE_ROW_HEIGHT (line 276) | const TABLE_ROW_HEIGHT = {
constant TABLE_HEADER_HEIGHT (line 282) | const TABLE_HEADER_HEIGHT = 40;
type SongTableListContainerProps (line 284) | interface SongTableListContainerProps {
function getTableRowHeight (line 292) | function getTableRowHeight(size: 'compact' | 'default' | 'large' | undef...
type AlbumArtistMetadataTopSongsProps (line 309) | interface AlbumArtistMetadataTopSongsProps {
type AlbumArtistMetadataFavoriteSongsProps (line 608) | interface AlbumArtistMetadataFavoriteSongsProps {
type AlbumArtistMetadataExternalLinksProps (line 887) | interface AlbumArtistMetadataExternalLinksProps {
type AlbumArtistMetadataSimilarArtistsProps (line 1039) | interface AlbumArtistMetadataSimilarArtistsProps {
type AlbumArtistDetailContentProps (line 1124) | interface AlbumArtistDetailContentProps {
type AlbumSectionProps (line 1270) | interface AlbumSectionProps {
constant MAX_SECTION_CARDS (line 1280) | const MAX_SECTION_CARDS = 100;
type ArtistAlbumsProps (line 1445) | interface ArtistAlbumsProps {
function GroupingTypeSelector (line 1571) | function GroupingTypeSelector() {
FILE: src/renderer/features/artists/components/album-artist-detail-favorite-songs-list-header.tsx
type AlbumArtistDetailFavoriteSongsListHeaderProps (line 9) | interface AlbumArtistDetailFavoriteSongsListHeaderProps {
FILE: src/renderer/features/artists/components/album-artist-detail-header.tsx
type AlbumArtistDetailHeaderProps (line 30) | interface AlbumArtistDetailHeaderProps {
FILE: src/renderer/features/artists/components/album-artist-detail-top-songs-list-header.tsx
type AlbumArtistDetailTopSongsListHeaderProps (line 9) | interface AlbumArtistDetailTopSongsListHeaderProps {
FILE: src/renderer/features/artists/components/album-artist-grid-carousel.tsx
type AlbumArtistGridCarouselProps (line 13) | interface AlbumArtistGridCarouselProps {
function AlbumArtistGridCarousel (line 21) | function AlbumArtistGridCarousel(props: AlbumArtistGridCarouselProps) {
FILE: src/renderer/features/artists/components/album-artist-infinite-carousel.tsx
type AlbumArtistCarouselProps (line 25) | interface AlbumArtistCarouselProps {
function useAlbumArtistListInfinite (line 138) | function useAlbumArtistListInfinite(
FILE: src/renderer/features/artists/components/album-artist-list-content.tsx
type OverrideAlbumArtistListQuery (line 59) | type OverrideAlbumArtistListQuery = Omit<AlbumArtistListQuery, 'limit' |...
FILE: src/renderer/features/artists/components/album-artist-list-header.tsx
type AlbumArtistListHeaderProps (line 15) | interface AlbumArtistListHeaderProps {
FILE: src/renderer/features/artists/components/album-artist-list-infinite-grid.tsx
type AlbumArtistListInfiniteGridProps (line 19) | interface AlbumArtistListInfiniteGridProps
FILE: src/renderer/features/artists/components/album-artist-list-infinite-table.tsx
type AlbumArtistListInfiniteTableProps (line 20) | interface AlbumArtistListInfiniteTableProps
FILE: src/renderer/features/artists/components/album-artist-list-paginated-grid.tsx
type AlbumArtistListPaginatedGridProps (line 21) | interface AlbumArtistListPaginatedGridProps
FILE: src/renderer/features/artists/components/album-artist-list-paginated-table.tsx
type AlbumArtistListPaginatedTableProps (line 22) | interface AlbumArtistListPaginatedTableProps
FILE: src/renderer/features/artists/components/artist-list-content.tsx
type OverrideArtistListQuery (line 51) | type OverrideArtistListQuery = Omit<ArtistListQuery, 'limit' | 'startInd...
FILE: src/renderer/features/artists/components/artist-list-header.tsx
type ArtistListHeaderProps (line 15) | interface ArtistListHeaderProps {
FILE: src/renderer/features/artists/components/artist-list-infinite-grid.tsx
type ArtistListInfiniteGridProps (line 19) | interface ArtistListInfiniteGridProps extends ItemListGridComponentProps...
FILE: src/renderer/features/artists/components/artist-list-infinite-table.tsx
type ArtistListInfiniteTableProps (line 20) | interface ArtistListInfiniteTableProps extends ItemListTableComponentPro...
FILE: src/renderer/features/artists/components/artist-list-paginated-grid.tsx
type ArtistListPaginatedGridProps (line 21) | interface ArtistListPaginatedGridProps extends ItemListGridComponentProp...
FILE: src/renderer/features/artists/components/artist-list-paginated-table.tsx
type ArtistListPaginatedTableProps (line 22) | interface ArtistListPaginatedTableProps extends ItemListTableComponentPr...
FILE: src/renderer/features/artists/hooks/use-artist-albums-grouped.ts
type GroupingType (line 12) | type GroupingType = 'all' | 'primary';
constant PRIMARY_RELEASE_TYPES (line 14) | const PRIMARY_RELEASE_TYPES = ['album', 'broadcast', 'ep', 'other', 'sin...
FILE: src/renderer/features/context-menu/actions/add-to-playlist-action.tsx
type AddToPlaylistActionProps (line 30) | interface AddToPlaylistActionProps {
FILE: src/renderer/features/context-menu/actions/delete-playlist-action.tsx
type DeletePlaylistActionProps (line 15) | interface DeletePlaylistActionProps {
FILE: src/renderer/features/context-menu/actions/download-action.tsx
type DownloadActionProps (line 9) | interface DownloadActionProps {
FILE: src/renderer/features/context-menu/actions/edit-playlist-action.tsx
type EditPlaylistActionProps (line 8) | interface EditPlaylistActionProps {
FILE: src/renderer/features/context-menu/actions/get-info-action.tsx
type GetInfoActionProps (line 12) | interface GetInfoActionProps {
FILE: src/renderer/features/context-menu/actions/go-to-action.tsx
type GoToActionProps (line 16) | interface GoToActionProps {
FILE: src/renderer/features/context-menu/actions/move-queue-items-action.tsx
type MoveQueueItemsActionProps (line 8) | interface MoveQueueItemsActionProps {
FILE: src/renderer/features/context-menu/actions/play-action.tsx
type PlayActionProps (line 10) | interface PlayActionProps {
FILE: src/renderer/features/context-menu/actions/play-album-radio-action.tsx
type PlayAlbumRadioActionProps (line 13) | interface PlayAlbumRadioActionProps {
FILE: src/renderer/features/context-menu/actions/play-artist-radio-action.tsx
type PlayArtistRadioActionProps (line 13) | interface PlayArtistRadioActionProps {
FILE: src/renderer/features/context-menu/actions/play-track-radio-action.tsx
type PlayTrackRadioActionProps (line 13) | interface PlayTrackRadioActionProps {
FILE: src/renderer/features/context-menu/actions/remove-from-playlist-action.tsx
type RemoveFromPlaylistActionProps (line 14) | interface RemoveFromPlaylistActionProps {
FILE: src/renderer/features/context-menu/actions/remove-from-queue-action.tsx
type RemoveFromQueueActionProps (line 8) | interface RemoveFromQueueActionProps {
FILE: src/renderer/features/context-menu/actions/set-favorite-action.tsx
type SetFavoriteActionProps (line 10) | interface SetFavoriteActionProps {
FILE: src/renderer/features/context-menu/actions/set-rating-action.tsx
type SetRatingActionProps (line 11) | interface SetRatingActionProps {
FILE: src/renderer/features/context-menu/actions/share-action.tsx
type ShareActionProps (line 8) | interface ShareActionProps {
FILE: src/renderer/features/context-menu/actions/show-in-file-explorer-action.tsx
type ShowInFileExplorerActionProps (line 9) | interface ShowInFileExplorerActionProps {
FILE: src/renderer/features/context-menu/actions/shuffle-items-action.tsx
type ShuffleItemsActionProps (line 8) | interface ShuffleItemsActionProps {
FILE: src/renderer/features/context-menu/components/context-menu-preview.tsx
type ContextMenuPreviewProps (line 10) | interface ContextMenuPreviewProps {
FILE: src/renderer/features/context-menu/context-menu-controller.tsx
type ContextMenuControllerProps (line 28) | interface ContextMenuControllerProps {
type ContextMenuCommand (line 97) | type ContextMenuCommand =
type AlbumArtistContextMenuProps (line 108) | type AlbumArtistContextMenuProps = {
type AlbumContextMenuProps (line 113) | type AlbumContextMenuProps = {
type ArtistContextMenuProps (line 118) | type ArtistContextMenuProps = {
type FolderContextMenuProps (line 123) | type FolderContextMenuProps = {
type GenreContextMenuProps (line 128) | type GenreContextMenuProps = {
type PlaylistContextMenuProps (line 133) | type PlaylistContextMenuProps = {
type PlaylistSongContextMenuProps (line 138) | type PlaylistSongContextMenuProps = {
type QueueSongContextMenuProps (line 143) | type QueueSongContextMenuProps = {
type SongContextMenuProps (line 148) | type SongContextMenuProps = {
FILE: src/renderer/features/context-menu/menus/album-artist-context-menu.tsx
type AlbumArtistContextMenuProps (line 16) | interface AlbumArtistContextMenuProps {
FILE: src/renderer/features/context-menu/menus/album-context-menu.tsx
type AlbumContextMenuProps (line 16) | interface AlbumContextMenuProps {
FILE: src/renderer/features/context-menu/menus/artist-context-menu.tsx
type ArtistContextMenuProps (line 16) | interface ArtistContextMenuProps {
FILE: src/renderer/features/context-menu/menus/folder-context-menu.tsx
type FolderContextMenuProps (line 11) | interface FolderContextMenuProps {
FILE: src/renderer/features/context-menu/menus/genre-context-menu.tsx
type GenreContextMenuProps (line 9) | interface GenreContextMenuProps {
FILE: src/renderer/features/context-menu/menus/playlist-context-menu.tsx
type PlaylistContextMenuProps (line 13) | interface PlaylistContextMenuProps {
FILE: src/renderer/features/context-menu/menus/playlist-song-context-menu.tsx
type PlaylistSongContextMenuProps (line 18) | interface PlaylistSongContextMenuProps {
FILE: src/renderer/features/context-menu/menus/queue-context-menu.tsx
type QueueContextMenuProps (line 19) | interface QueueContextMenuProps {
FILE: src/renderer/features/context-menu/menus/song-context-menu.tsx
type SongContextMenuProps (line 17) | interface SongContextMenuProps {
FILE: src/renderer/features/discord-rpc/use-discord-rpc.ts
type ActivityState (line 30) | type ActivityState = [QueueSong | undefined, number, PlayerStatus];
constant MAX_FIELD_LENGTH (line 32) | const MAX_FIELD_LENGTH = 127;
constant MAX_URL_LENGTH (line 33) | const MAX_URL_LENGTH = 256;
FILE: src/renderer/features/favorites/components/favorites-content.tsx
type FavoritesContentProps (line 22) | interface FavoritesContentProps {
FILE: src/renderer/features/favorites/components/favorites-header.tsx
type FavoritesHeaderProps (line 25) | interface FavoritesHeaderProps {
FILE: src/renderer/features/folders/components/folder-list-content.tsx
type FolderListViewProps (line 82) | interface FolderListViewProps {
FILE: src/renderer/features/folders/components/folder-list-header-filters.tsx
constant MAX_BREADCRUMB_TEXT_LENGTH (line 26) | const MAX_BREADCRUMB_TEXT_LENGTH = 26;
FILE: src/renderer/features/folders/components/folder-list-header.tsx
type FolderListHeaderProps (line 14) | interface FolderListHeaderProps {
FILE: src/renderer/features/folders/components/folder-tree-browser.tsx
type FlattenedNode (line 18) | interface FlattenedNode {
type TreeNode (line 26) | interface TreeNode {
constant ITEM_HEIGHT (line 34) | const ITEM_HEIGHT = 32;
constant INDENT_SIZE (line 35) | const INDENT_SIZE = 16;
type FolderTreeBrowserProps (line 37) | interface FolderTreeBrowserProps {
method initialized (line 300) | initialized(osInstance) {
FILE: src/renderer/features/folders/hooks/use-folder-list-filters.ts
type FolderPathItem (line 12) | type FolderPathItem = {
FILE: src/renderer/features/genres/components/genre-detail-content.tsx
function GenreDetailContentAlbums (line 66) | function GenreDetailContentAlbums() {
function GenreDetailContentSongs (line 90) | function GenreDetailContentSongs() {
FILE: src/renderer/features/genres/components/genre-detail-header.tsx
type GenreDetailHeaderProps (line 18) | interface GenreDetailHeaderProps {
FILE: src/renderer/features/genres/components/genre-list-header.tsx
type GenreListHeaderProps (line 15) | interface GenreListHeaderProps {
FILE: src/renderer/features/genres/components/genre-list-infinite-grid.tsx
type GenreListInfiniteGridProps (line 19) | interface GenreListInfiniteGridProps extends ItemListGridComponentProps<...
FILE: src/renderer/features/genres/components/genre-list-infinite-table.tsx
type GenreListInfiniteTableProps (line 20) | interface GenreListInfiniteTableProps extends ItemListTableComponentProp...
FILE: src/renderer/features/genres/components/genre-list-paginated-grid.tsx
type GenreListPaginatedGridProps (line 21) | interface GenreListPaginatedGridProps extends ItemListGridComponentProps...
FILE: src/renderer/features/genres/components/genre-list-paginated-table.tsx
type GenreListPaginatedTableProps (line 22) | interface GenreListPaginatedTableProps extends ItemListTableComponentPro...
FILE: src/renderer/features/home/components/album-infinite-feature-carousel.tsx
type InfiniteAlbumFeatureCarouselProps (line 10) | interface InfiniteAlbumFeatureCarouselProps {
FILE: src/renderer/features/home/components/album-infinite-single-feature-carousel.tsx
type InfiniteAlbumSingleFeatureCarouselProps (line 10) | interface InfiniteAlbumSingleFeatureCarouselProps {
FILE: src/renderer/features/home/components/featured-genres.tsx
function getGenresToShow (line 24) | function getGenresToShow(breakpoints: {
FILE: src/renderer/features/item-details/components/item-details-modal.tsx
type ItemDetailsModalProps (line 33) | type ItemDetailsModalProps = {
type ItemDetailRow (line 38) | type ItemDetailRow<T> = {
FILE: src/renderer/features/item-details/components/song-path.tsx
type SongPathProps (line 14) | type SongPathProps = {
FILE: src/renderer/features/login/routes/login-route.tsx
constant SERVER_ICONS (line 42) | const SERVER_ICONS: Record<ServerType, string> = {
constant SERVER_NAMES (line 48) | const SERVER_NAMES: Record<ServerType, string> = {
FILE: src/renderer/features/lyrics/api/lyrics-api.ts
type LyricsQueryResult (line 28) | type LyricsQueryResult = {
function computeSelectedFromResult (line 83) | function computeSelectedFromResult(
function fetchLocalLyrics (line 150) | async function fetchLocalLyrics(params: {
function fetchRemoteLyricsAuto (line 195) | async function fetchRemoteLyricsAuto(song: QueueSong): Promise<FullLyric...
function fetchRemoteLyricsById (line 211) | async function fetchRemoteLyricsById(params: {
function getDisplayOffset (line 221) | function getDisplayOffset(
FILE: src/renderer/features/lyrics/components/lyrics-export-form.tsx
type LyricsExportFormProps (line 17) | interface LyricsExportFormProps {
FILE: src/renderer/features/lyrics/components/lyrics-search-form.tsx
type SearchResultProps (line 41) | interface SearchResultProps {
type LyricSearchFormProps (line 98) | interface LyricSearchFormProps {
FILE: src/renderer/features/lyrics/components/lyrics-settings-form.tsx
type LyricsSettingsFormProps (line 28) | interface LyricsSettingsFormProps {
FILE: src/renderer/features/lyrics/lyric-line.tsx
type LyricLineProps (line 9) | interface LyricLineProps extends ComponentPropsWithoutRef<'div'> {
FILE: src/renderer/features/lyrics/lyrics-actions.tsx
type LyricsActionsProps (line 15) | interface LyricsActionsProps {
FILE: src/renderer/features/lyrics/lyrics.tsx
type LyricsProps (line 38) | type LyricsProps = {
FILE: src/renderer/features/lyrics/synchronized-lyrics.tsx
type SynchronizedLyricsProps (line 23) | interface SynchronizedLyricsProps extends Omit<FullLyricsMetadata, 'lyri...
FILE: src/renderer/features/lyrics/unsynchronized-lyrics.tsx
type UnsynchronizedLyricsProps (line 9) | interface UnsynchronizedLyricsProps extends Omit<FullLyricsMetadata, 'ly...
FILE: src/renderer/features/now-playing/components/play-queue-list-controls.tsx
type PlayQueueListOptionsProps (line 23) | interface PlayQueueListOptionsProps {
FILE: src/renderer/features/now-playing/components/play-queue.tsx
type QueueProps (line 39) | type QueueProps = {
FILE: src/renderer/features/now-playing/components/popover-play-queue.tsx
type PopoverPlayQueueProps (line 13) | interface PopoverPlayQueueProps {
FILE: src/renderer/features/now-playing/components/sidebar-play-queue.tsx
type SidebarPanelType (line 32) | type SidebarPanelType = 'lyrics' | 'queue' | 'visualizer';
FILE: src/renderer/features/player/audio-player/engine/mpv-player-engine.tsx
type MpvPlayerEngineHandle (line 21) | interface MpvPlayerEngineHandle extends AudioPlayer {}
type MpvPlayerEngineProps (line 23) | interface MpvPlayerEngineProps {
constant PROGRESS_UPDATE_INTERVAL (line 38) | const PROGRESS_UPDATE_INTERVAL = 250;
method decreaseVolume (line 299) | decreaseVolume(by: number) {
method increaseVolume (line 306) | increaseVolume(by: number) {
method pause (line 313) | pause() {
method play (line 318) | play() {
method seekTo (line 323) | seekTo(seekTo: number) {
method setVolume (line 328) | setVolume(vol: number) {
function handleMpvAutoNext (line 342) | function handleMpvAutoNext(transcode: {
function replaceMpvQueue (line 354) | function replaceMpvQueue(transcode: {
FILE: src/renderer/features/player/audio-player/engine/wavesurfer-player-engine.tsx
type WaveSurferPlayerEngineHandle (line 11) | interface WaveSurferPlayerEngineHandle extends AudioPlayer {
type WaveSurferPlayerEngineProps (line 22) | interface WaveSurferPlayerEngineProps {
constant EMPTY_SOURCE (line 43) | const EMPTY_SOURCE =
method decreaseVolume (line 212) | decreaseVolume(by: number) {
method increaseVolume (line 216) | increaseVolume(by: number) {
method pause (line 220) | pause() {
method play (line 224) | play() {
method player1 (line 231) | player1() {
method player2 (line 237) | player2() {
method seekTo (line 243) | seekTo(seekTo: number) {
method setVolume (line 250) | setVolume(volume: number) {
method setVolume1 (line 254) | setVolume1(volume: number) {
method setVolume2 (line 257) | setVolume2(volume: number) {
FILE: src/renderer/features/player/audio-player/engine/web-player-engine.tsx
type WebPlayerEngineHandle (line 12) | interface WebPlayerEngineHandle extends AudioPlayer {
type WebPlayerEngineProps (line 23) | interface WebPlayerEngineProps {
constant MAX_NETWORK_RETRIES (line 43) | const MAX_NETWORK_RETRIES = 5;
constant NETWORK_RETRY_DELAY_MS (line 44) | const NETWORK_RETRY_DELAY_MS = 2000;
constant EMPTY_SOURCE (line 51) | const EMPTY_SOURCE =
method decreaseVolume (line 109) | decreaseVolume(by: number) {
method increaseVolume (line 113) | increaseVolume(by: number) {
method pause (line 117) | pause() {
method play (line 121) | play() {
method player1 (line 130) | player1() {
method player2 (line 136) | player2() {
method seekTo (line 142) | seekTo(seekTo: number) {
method setVolume (line 147) | setVolume(volume: number) {
method setVolume1 (line 151) | setVolume1(volume: number) {
method setVolume2 (line 154) | setVolume2(volume: number) {
FILE: src/renderer/features/player/audio-player/hooks/use-player-events.ts
type PlayerEvents (line 21) | interface PlayerEvents {
type PlayerEventsCallbacks (line 25) | interface PlayerEventsCallbacks {
function usePlayerEvents (line 67) | function usePlayerEvents(callbacks: PlayerEventsCallbacks, deps: React.D...
function createPlayerEvents (line 78) | function createPlayerEvents(callbacks: PlayerEventsCallbacks): PlayerEve...
FILE: src/renderer/features/player/audio-player/hooks/use-stream-url.tsx
function useSongUrl (line 7) | function useSongUrl(
FILE: src/renderer/features/player/audio-player/mpv-player.tsx
constant PLAY_PAUSE_FADE_DURATION (line 19) | const PLAY_PAUSE_FADE_DURATION = 300;
constant PLAY_PAUSE_FADE_INTERVAL (line 20) | const PLAY_PAUSE_FADE_INTERVAL = 10;
function MpvPlayer (line 24) | function MpvPlayer() {
FILE: src/renderer/features/player/audio-player/types.ts
type AudioPlayer (line 1) | interface AudioPlayer {
type PlayerOnProgressProps (line 10) | interface PlayerOnProgressProps {
FILE: src/renderer/features/player/audio-player/wavesurfer-player.tsx
constant PLAY_PAUSE_FADE_DURATION (line 23) | const PLAY_PAUSE_FADE_DURATION = 300;
constant PLAY_PAUSE_FADE_INTERVAL (line 24) | const PLAY_PAUSE_FADE_INTERVAL = 10;
function WaveSurferPlayer (line 26) | function WaveSurferPlayer() {
function crossfadeHandler (line 256) | function crossfadeHandler(args: {
function gaplessHandler (line 327) | function gaplessHandler(args: {
function getDuration (line 357) | function getDuration(ref: null | undefined | WaveSurfer) {
function getDurationPadding (line 361) | function getDurationPadding(isFlac: boolean) {
FILE: src/renderer/features/player/audio-player/web-player.tsx
constant PLAY_PAUSE_FADE_DURATION (line 30) | const PLAY_PAUSE_FADE_DURATION = 300;
constant PLAY_PAUSE_FADE_INTERVAL (line 31) | const PLAY_PAUSE_FADE_INTERVAL = 10;
function WebPlayer (line 33) | function WebPlayer() {
function crossfadeHandler (line 494) | function crossfadeHandler(args: {
function equalPowerEaseIn (line 584) | function equalPowerEaseIn(t: number): number {
function equalPowerEaseOut (line 589) | function equalPowerEaseOut(t: number): number {
function exponentialEaseIn (line 599) | function exponentialEaseIn(t: number): number {
function exponentialEaseOut (line 605) | function exponentialEaseOut(t: number): number {
function gaplessHandler (line 615) | function gaplessHandler(args: {
function getCrossfadeEasing (line 653) | function getCrossfadeEasing(style: CrossfadeStyle): {
function getDuration (line 687) | function getDuration(ref: null | ReactPlayer | undefined) {
function getDurationPadding (line 691) | function getDurationPadding(isFlac: boolean) {
function linearEase (line 703) | function linearEase(t: number): number {
function sCurveEase (line 711) | function sCurveEase(t: number): number {
FILE: src/renderer/features/player/components/full-screen-player.tsx
type BackgroundImageProps (line 78) | interface BackgroundImageProps {
type BackgroundImageOverlayProps (line 204) | interface BackgroundImageOverlayProps {
type PlayerContainerProps (line 621) | interface PlayerContainerProps {
FILE: src/renderer/features/player/components/full-screen-visualizer.tsx
type VisualizerContainerProps (line 105) | interface VisualizerContainerProps {
FILE: src/renderer/features/player/components/mobile-fullscreen-player-bottom-controls.tsx
type MobileFullscreenPlayerBottomControlsProps (line 11) | interface MobileFullscreenPlayerBottomControlsProps {
FILE: src/renderer/features/player/components/mobile-fullscreen-player-controls.tsx
type MobileFullscreenPlayerControlsProps (line 13) | interface MobileFullscreenPlayerControlsProps {
FILE: src/renderer/features/player/components/mobile-fullscreen-player-header.tsx
type MobileFullscreenPlayerHeaderProps (line 31) | interface MobileFullscreenPlayerHeaderProps {
FILE: src/renderer/features/player/components/mobile-fullscreen-player-metadata.tsx
type MobileFullscreenPlayerMetadataProps (line 15) | interface MobileFullscreenPlayerMetadataProps {
FILE: src/renderer/features/player/components/mobile-fullscreen-player-progress.tsx
type MobileFullscreenPlayerProgressProps (line 20) | interface MobileFullscreenPlayerProgressProps {
FILE: src/renderer/features/player/components/mobile-fullscreen-player.tsx
type BackgroundImageProps (line 73) | interface BackgroundImageProps {
type BackgroundImageOverlayProps (line 216) | interface BackgroundImageOverlayProps {
type MobilePlayerContainerProps (line 300) | interface MobilePlayerContainerProps {
FILE: src/renderer/features/player/components/player-button.tsx
type PlayerButtonProps (line 11) | interface PlayerButtonProps extends Omit<ActionIconProps, 'icon' | 'vari...
type PlayButtonProps (line 60) | interface PlayButtonProps extends Omit<ActionIconProps, 'icon' | 'varian...
FILE: src/renderer/features/player/components/playerbar-seek-slider.tsx
type PlayerbarSeekSliderProps (line 9) | interface PlayerbarSeekSliderProps {
FILE: src/renderer/features/player/components/radio-metadata-display.tsx
type RadioMetadataDisplayProps (line 14) | interface RadioMetadataDisplayProps {
FILE: src/renderer/features/player/components/shuffle-all-modal.tsx
type ShuffleAllSlice (line 26) | interface ShuffleAllSlice extends RandomSongListQuery {
constant PLAYED_DATA (line 59) | const PLAYED_DATA: { label: string; value: Played }[] = [
FILE: src/renderer/features/player/components/sleep-timer-button.tsx
constant PRESET_OPTIONS (line 26) | const PRESET_OPTIONS = [
function formatRemaining (line 39) | function formatRemaining(totalSeconds: number): string {
FILE: src/renderer/features/player/context/player-context.tsx
type PlayerContext (line 41) | interface PlayerContext {
function fetchSongsByItemType (line 893) | async function fetchSongsByItemType(
FILE: src/renderer/features/playlists/components/client-side-song-filters.tsx
type BooleanSegmentFilterProps (line 32) | interface BooleanSegmentFilterProps {
function booleanToSegmentValue (line 39) | function booleanToSegmentValue(value: boolean | null | undefined): string {
function segmentValueToBoolean (line 45) | function segmentValueToBoolean(value: string): boolean | null {
type MultiSelectFilterOption (line 71) | interface MultiSelectFilterOption {
type MultiSelectFilterProps (line 79) | interface MultiSelectFilterProps {
type MultiSelectRowContext (line 90) | type MultiSelectRowContext = {
type YearRangeFilterProps (line 121) | interface YearRangeFilterProps {
type MultiSelectFilterLabelProps (line 160) | interface MultiSelectFilterLabelProps {
FILE: src/renderer/features/playlists/components/create-playlist-form.tsx
type CreatePlaylistFormProps (line 31) | interface CreatePlaylistFormProps {
FILE: src/renderer/features/playlists/components/playlist-detail-song-list-content.tsx
type OverridePlaylistSongListQuery (line 99) | type OverridePlaylistSongListQuery = Omit<Partial<PlaylistSongListQuery>...
type PlaylistDetailSongListViewProps (line 101) | interface PlaylistDetailSongListViewProps {
FILE: src/renderer/features/playlists/components/playlist-detail-song-list-grid.tsx
type PlaylistDetailSongListGridProps (line 23) | interface PlaylistDetailSongListGridProps
FILE: src/renderer/features/playlists/components/playlist-detail-song-list-header-filters.tsx
type PlaylistDetailSongListHeaderFiltersProps (line 56) | interface PlaylistDetailSongListHeaderFiltersProps {
FILE: src/renderer/features/playlists/components/playlist-detail-song-list-header.tsx
type PlaylistDetailSongListHeaderProps (line 26) | interface PlaylistDetailSongListHeaderProps {
FILE: src/renderer/features/playlists/components/playlist-detail-song-list-table.tsx
type PlaylistDetailSongListTableProps (line 26) | interface PlaylistDetailSongListTableProps
FILE: src/renderer/features/playlists/components/playlist-list-header.tsx
type PlaylistListHeaderProps (line 15) | interface PlaylistListHeaderProps {
FILE: src/renderer/features/playlists/components/playlist-list-infinite-grid.tsx
type PlaylistListInfiniteGridProps (line 19) | interface PlaylistListInfiniteGridProps extends ItemListGridComponentPro...
FILE: src/renderer/features/playlists/components/playlist-list-infinite-table.tsx
type PlaylistListInfiniteTableProps (line 20) | interface PlaylistListInfiniteTableProps extends ItemListTableComponentP...
FILE: src/renderer/features/playlists/components/playlist-list-paginated-grid.tsx
type PlaylistListPaginatedGridProps (line 21) | interface PlaylistListPaginatedGridProps extends ItemListGridComponentPr...
FILE: src/renderer/features/playlists/components/playlist-list-paginated-table.tsx
type PlaylistListPaginatedTableProps (line 22) | interface PlaylistListPaginatedTableProps extends ItemListTableComponent...
FILE: src/renderer/features/playlists/components/playlist-query-builder.tsx
type AddArgs (line 41) | type AddArgs = {
type DeleteArgs (line 46) | type DeleteArgs = {
type PlaylistQueryBuilderProps (line 52) | interface PlaylistQueryBuilderProps {
type SortEntry (line 60) | type SortEntry = {
constant DEFAULT_QUERY (line 65) | const DEFAULT_QUERY: QueryBuilderGroup = {
type PlaylistQueryBuilderRef (line 154) | type PlaylistQueryBuilderRef = {
type FilterGroup (line 431) | type FilterGroup = {
FILE: src/renderer/features/playlists/components/playlist-query-editor.tsx
type PlaylistQueryEditorProps (line 27) | interface PlaylistQueryEditorProps {
type AppliedJsonState (line 44) | type AppliedJsonState = {
type EditorMode (line 50) | type EditorMode = 'builder' | 'json';
FILE: src/renderer/features/playlists/components/save-as-playlist-form.tsx
type SaveAsPlaylistFormProps (line 20) | interface SaveAsPlaylistFormProps {
FILE: src/renderer/features/playlists/hooks/use-playlist-track-list.ts
function applyClientSideSongFilters (line 17) | function applyClientSideSongFilters(songs: Song[], query: Record<string,...
function usePlaylistTrackList (line 89) | function usePlaylistTrackList(data: PlaylistSongListResponse | undefined...
FILE: src/renderer/features/playlists/hooks/use-recent-playlists.ts
type RecentPlaylists (line 5) | interface RecentPlaylists {
constant RECENT_PLAYLISTS_KEY (line 9) | const RECENT_PLAYLISTS_KEY = 'recent-playlists';
constant DEFAULT_VALUE (line 10) | const DEFAULT_VALUE: RecentPlaylists = {};
FILE: src/renderer/features/playlists/mutations/playlist-optimistic-updates.ts
type PreviousQueryData (line 12) | interface PreviousQueryData {
FILE: src/renderer/features/playlists/utils.ts
type PlaylistAlbumRow (line 7) | type PlaylistAlbumRow = Album & { _playlistSongs?: Song[] };
function playlistSongsToAlbums (line 9) | function playlistSongsToAlbums(songs: Song[]): PlaylistAlbumRow[] {
constant DATE_STRING_REGEX (line 201) | const DATE_STRING_REGEX = /^\d{4}-\d{2}-\d{2}$/;
function isDateLikeValue (line 203) | function isDateLikeValue(value: unknown): boolean {
function isDateRangeValue (line 209) | function isDateRangeValue(value: unknown): value is [null | string, null...
function mapApiOperatorToDatePicker (line 215) | function mapApiOperatorToDatePicker(operator: string, value: unknown): s...
function mapDatePickerOperatorToApi (line 222) | function mapDatePickerOperatorToApi(operator: string): string {
FILE: src/renderer/features/radio/components/create-radio-station-form.tsx
type CreateRadioStationFormProps (line 16) | interface CreateRadioStationFormProps {
FILE: src/renderer/features/radio/components/edit-radio-station-form.tsx
type EditRadioStationFormProps (line 22) | interface EditRadioStationFormProps {
FILE: src/renderer/features/radio/components/radio-list-header.tsx
type RadioListHeaderProps (line 12) | interface RadioListHeaderProps {
FILE: src/renderer/features/radio/components/radio-list-items.tsx
type RadioListItemProps (line 25) | interface RadioListItemProps {
type RadioListItemsProps (line 29) | interface RadioListItemsProps {
FILE: src/renderer/features/radio/components/radio-web-player.tsx
function RadioWebPlayer (line 19) | function RadioWebPlayer() {
FILE: src/renderer/features/radio/hooks/use-radio-player.ts
type RadioMetadata (line 10) | interface RadioMetadata {
type RadioStore (line 15) | interface RadioStore {
FILE: src/renderer/features/radio/store/radio-store.ts
type RadioStoreSlice (line 9) | interface RadioStoreSlice extends RadioStoreState {
type RadioStoreState (line 26) | interface RadioStoreState {
FILE: src/renderer/features/search/api/search-api.ts
constant SEARCH_PAGE_SIZE (line 8) | const SEARCH_PAGE_SIZE = 4;
FILE: src/renderer/features/search/components/collapsible-command-group.tsx
type CollapsibleCommandGroupProps (line 9) | interface CollapsibleCommandGroupProps {
function CollapsibleCommandGroup (line 18) | function CollapsibleCommandGroup({
FILE: src/renderer/features/search/components/command-item-selectable.tsx
type CommandItemSelectableProps (line 4) | interface CommandItemSelectableProps
function CommandItemSelectable (line 9) | function CommandItemSelectable({ children, ...itemProps }: CommandItemSe...
FILE: src/renderer/features/search/components/command-palette.tsx
type CommandPaletteProps (line 24) | interface CommandPaletteProps {
constant SEARCH_SECTION_IDS (line 28) | const SEARCH_SECTION_IDS = {
type CommandPaletteSearchProps (line 34) | interface CommandPaletteSearchProps {
function CommandPaletteSearch (line 43) | function CommandPaletteSearch({
FILE: src/renderer/features/search/components/command.tsx
type CommandPalettePages (line 5) | enum CommandPalettePages {
FILE: src/renderer/features/search/components/go-to-commands.tsx
type GoToCommandsProps (line 9) | interface GoToCommandsProps {
FILE: src/renderer/features/search/components/home-commands.tsx
type HomeCommandsProps (line 12) | interface HomeCommandsProps {
FILE: src/renderer/features/search/components/library-command-item.tsx
type LibraryCommandItemProps (line 37) | interface LibraryCommandItemProps {
FILE: src/renderer/features/search/components/search-album-artists-section.tsx
type SearchAlbumArtistsSectionProps (line 20) | interface SearchAlbumArtistsSectionProps {
function SearchAlbumArtistsSection (line 29) | function SearchAlbumArtistsSection({
FILE: src/renderer/features/search/components/search-albums-section.tsx
type SearchAlbumsSectionProps (line 20) | interface SearchAlbumsSectionProps {
function SearchAlbumsSection (line 29) | function SearchAlbumsSection({
FILE: src/renderer/features/search/components/search-header.tsx
type SearchHeaderProps (line 27) | interface SearchHeaderProps {
FILE: src/renderer/features/search/components/search-songs-section.tsx
type SearchSongsSectionProps (line 20) | interface SearchSongsSectionProps {
function SearchSongsSection (line 29) | function SearchSongsSection({
FILE: src/renderer/features/search/components/server-commands.tsx
type ServerCommandsProps (line 13) | interface ServerCommandsProps {
FILE: src/renderer/features/servers/components/add-server-form.tsx
type AddServerFormProps (line 36) | interface AddServerFormProps {
type ServerDetails (line 40) | interface ServerDetails {
function ServerIconWithLabel (line 45) | function ServerIconWithLabel({ icon, label }: { icon: string; label: str...
function useAutodiscovery (line 54) | function useAutodiscovery() {
constant SERVER_TYPES (line 73) | const SERVER_TYPES: Record<ServerType, ServerDetails> = {
constant ALL_SERVERS (line 88) | const ALL_SERVERS = Object.keys(SERVER_TYPES).map((serverType) => {
FILE: src/renderer/features/servers/components/edit-server-form.tsx
type EditServerFormProps (line 30) | interface EditServerFormProps {
FILE: src/renderer/features/servers/components/ignore-cors-ssl-switches.tsx
function IgnoreCorsSslSwitches (line 11) | function IgnoreCorsSslSwitches() {
FILE: src/renderer/features/servers/components/server-list-item.tsx
type ServerListItemProps (line 19) | interface ServerListItemProps {
FILE: src/renderer/features/servers/components/server-section.tsx
type ServerSectionProps (line 5) | interface ServerSectionProps {
FILE: src/renderer/features/settings/components/advanced/logger-settings.tsx
constant DEFAULT_LOG_LEVEL (line 11) | const DEFAULT_LOG_LEVEL: LogLevel = process.env.NODE_ENV === 'production...
FILE: src/renderer/features/settings/components/general/application-settings.tsx
constant HOME_FEATURE_STYLE_OPTIONS (line 44) | const HOME_FEATURE_STYLE_OPTIONS = [
constant SIDE_QUEUE_OPTIONS (line 61) | const SIDE_QUEUE_OPTIONS = [
constant SIDE_QUEUE_LAYOUT_OPTIONS (line 78) | const SIDE_QUEUE_LAYOUT_OPTIONS = [
constant FONT_TYPES (line 95) | const FONT_TYPES: Font[] = [
FILE: src/renderer/features/settings/components/general/artist-settings.tsx
constant ARTIST_ITEMS (line 12) | const ARTIST_ITEMS: Array<[ArtistItem, string]> = [
constant ARTIST_RELEASE_TYPE_ITEMS (line 35) | const ARTIST_RELEASE_TYPE_ITEMS: Array<[ArtistReleaseTypeItem, string]> = [
FILE: src/renderer/features/settings/components/general/draggable-item.tsx
type DraggableItemProps (line 23) | interface DraggableItemProps {
type SidebarItem (line 29) | interface SidebarItem {
FILE: src/renderer/features/settings/components/general/draggable-items.tsx
type DraggableItemsProps (line 12) | type DraggableItemsProps<K, T> = {
FILE: src/renderer/features/settings/components/general/fullscreen-player-settings.tsx
constant PLAYER_ITEMS (line 11) | const PLAYER_ITEMS: Array<[PlayerItem, string]> = [
FILE: src/renderer/features/settings/components/general/home-settings.tsx
constant HOME_ITEMS (line 11) | const HOME_ITEMS: Array<[string, string]> = [
FILE: src/renderer/features/settings/components/general/query-builder-settings.tsx
constant QUERY_VALUE_INPUT_TYPES (line 16) | const QUERY_VALUE_INPUT_TYPES = [
FILE: src/renderer/features/settings/components/general/sidebar-reorder.tsx
constant SIDEBAR_ITEMS (line 12) | const SIDEBAR_ITEMS: Array<[string, string]> = [
FILE: src/renderer/features/settings/components/hotkeys/hotkey-manager-settings.tsx
constant BINDINGS_MAP (line 24) | const BINDINGS_MAP: Record<BindingActions, string> = {
FILE: src/renderer/features/settings/components/hotkeys/media-session-settings.tsx
function handleMediaSessionChange (line 23) | function handleMediaSessionChange(e: boolean) {
FILE: src/renderer/features/settings/components/playback/audio-settings.tsx
type AudioDeviceOption (line 39) | type AudioDeviceOption = { label: string; value: string };
FILE: src/renderer/features/settings/components/playback/player-filter-settings.tsx
type FilterFieldConfig (line 32) | type FilterFieldConfig = {
FILE: src/renderer/features/settings/components/settings-header.tsx
type SettingsHeaderProps (line 15) | type SettingsHeaderProps = {
FILE: src/renderer/features/settings/components/settings-option.tsx
type SettingsOptionProps (line 9) | interface SettingsOptionProps {
FILE: src/renderer/features/settings/components/settings-section.tsx
type SettingOption (line 8) | type SettingOption = {
type SettingsSectionProps (line 16) | interface SettingsSectionProps {
FILE: src/renderer/features/settings/components/window/password-settings.tsx
constant PASSWORD_SETTINGS (line 14) | const PASSWORD_SETTINGS: { label: string; value: string }[] = [
FILE: src/renderer/features/settings/components/window/update-settings.tsx
function disableAutoUpdates (line 16) | function disableAutoUpdates(): boolean {
FILE: src/renderer/features/settings/components/window/window-settings.tsx
constant WINDOW_BAR_OPTIONS (line 15) | const WINDOW_BAR_OPTIONS = [
FILE: src/renderer/features/shared/components/animated-page.tsx
type AnimatedPageProps (line 10) | interface AnimatedPageProps {
FILE: src/renderer/features/shared/components/component-error-boundary.tsx
type ComponentErrorFallbackProps (line 12) | interface ComponentErrorFallbackProps {
type ComponentErrorBoundaryProps (line 41) | interface ComponentErrorBoundaryProps {
FILE: src/renderer/features/shared/components/display-type-toggle-button.tsx
type DisplayTypeToggleButtonProps (line 6) | interface DisplayTypeToggleButtonProps {
FILE: src/renderer/features/shared/components/filter-button.tsx
type FilterButtonProps (line 5) | interface FilterButtonProps extends ActionIconProps {
FILE: src/renderer/features/shared/components/folder-button.tsx
type FolderButtonProps (line 5) | interface FolderButtonProps extends ActionIconProps {
FILE: src/renderer/features/shared/components/grid-config.tsx
type GridConfigProps (line 41) | type GridConfigProps = {
FILE: src/renderer/features/shared/components/json-preview.tsx
type JsonPreviewProps (line 5) | interface JsonPreviewProps {
FILE: src/renderer/features/shared/components/library-background-overlay.tsx
type LibraryBackgroundOverlayProps (line 9) | interface LibraryBackgroundOverlayProps {
type BackgroundOverlayProps (line 34) | interface BackgroundOverlayProps {
type LibraryBackgroundProps (line 69) | interface LibraryBackgroundProps {
FILE: src/renderer/features/shared/components/library-container.tsx
type LibraryContainerProps (line 5) | interface LibraryContainerProps {
FILE: src/renderer/features/shared/components/library-header-bar.tsx
type LibraryHeaderBarProps (line 17) | interface LibraryHeaderBarProps {
type HeaderPlayButtonProps (line 33) | interface HeaderPlayButtonProps {
type TitleProps (line 42) | interface TitleProps {
type HeaderBadgeProps (line 112) | interface HeaderBadgeProps extends BadgeProps {
FILE: src/renderer/features/shared/components/library-header.tsx
type LibraryHeaderProps (line 34) | interface LibraryHeaderProps {
type LibraryHeaderMenuProps (line 280) | interface LibraryHeaderMenuProps {
FILE: src/renderer/features/shared/components/list-config-menu.tsx
constant SONG_DISPLAY_TYPES (line 21) | const SONG_DISPLAY_TYPES: ListConfigMenuDisplayTypeConfig[] = [
constant DISPLAY_TYPES (line 25) | const DISPLAY_TYPES = [
type ListConfigMenuDetailConfig (line 79) | interface ListConfigMenuDetailConfig {
type ListConfigMenuDisplayTypeConfig (line 85) | interface ListConfigMenuDisplayTypeConfig {
type ListConfigMenuOptionConfig (line 91) | interface ListConfigMenuOptionConfig {
type ListConfigMenuOptionsConfig (line 96) | interface ListConfigMenuOptionsConfig {
type ListConfigMenuProps (line 108) | interface ListConfigMenuProps {
FILE: src/renderer/features/shared/components/list-display-type-toggle-button.tsx
type ListDisplayTypeToggleButtonProps (line 5) | interface ListDisplayTypeToggleButtonProps {
FILE: src/renderer/features/shared/components/list-filters.tsx
type ListFiltersProps (line 28) | interface ListFiltersProps {
type ListFiltersTitleProps (line 141) | interface ListFiltersTitleProps {
constant FILTERS (line 181) | const FILTERS = {
FILE: src/renderer/features/shared/components/list-music-folder-dropdown.tsx
type ListMusicFolderDropdownProps (line 10) | interface ListMusicFolderDropdownProps {
FILE: src/renderer/features/shared/components/list-refresh-button.tsx
type ListRefreshButtonProps (line 8) | interface ListRefreshButtonProps {
constant LIST_REFRESH_MUTATION_KEY (line 23) | const LIST_REFRESH_MUTATION_KEY = 'item-list-refresh';
FILE: src/renderer/features/shared/components/list-search-input.tsx
function navigationIdFromState (line 6) | function navigationIdFromState(state: unknown): string | undefined {
FILE: src/renderer/features/shared/components/list-select-filter.tsx
type SelectOption (line 7) | type SelectOption = string | { label: string; value: string };
type ListSelectFilterProps (line 9) | interface ListSelectFilterProps {
FILE: src/renderer/features/shared/components/list-sort-by-dropdown.tsx
type ListSortByDropdownProps (line 22) | interface ListSortByDropdownProps {
type ListSortByDropdownControlledProps (line 79) | interface ListSortByDropdownControlledProps {
constant CLIENT_SIDE_SONG_FILTERS (line 133) | const CLIENT_SIDE_SONG_FILTERS = [
constant CLIENT_SIDE_ALBUM_FILTERS (line 221) | const CLIENT_SIDE_ALBUM_FILTERS = [
constant ALBUM_LIST_FILTERS (line 294) | const ALBUM_LIST_FILTERS: Partial<
constant SONG_LIST_FILTERS (line 460) | const SONG_LIST_FILTERS: Partial<
constant FOLDER_LIST_FILTERS (line 606) | const FOLDER_LIST_FILTERS: Partial<
constant PLAYLIST_SONG_LIST_FILTERS (line 635) | const PLAYLIST_SONG_LIST_FILTERS: Partial<
constant ALBUM_ARTIST_LIST_FILTERS (line 643) | const ALBUM_ARTIST_LIST_FILTERS: Partial<
constant ARTIST_LIST_FILTERS (line 729) | const ARTIST_LIST_FILTERS: Partial<
constant GENRE_LIST_FILTERS (line 815) | const GENRE_LIST_FILTERS: Partial<
constant PLAYLIST_LIST_FILTERS (line 841) | const PLAYLIST_LIST_FILTERS: Partial<
constant RADIO_LIST_FILTERS (line 902) | const RADIO_LIST_FILTERS: Partial<
constant FILTERS (line 943) | const FILTERS: Partial<Record<LibraryItem, any>> = {
FILE: src/renderer/features/shared/components/list-sort-order-toggle-button.tsx
type ListSortOrderToggleButtonProps (line 6) | interface ListSortOrderToggleButtonProps {
type ListSortOrderToggleButtonControlledProps (line 33) | interface ListSortOrderToggleButtonControlledProps {
FILE: src/renderer/features/shared/components/list-with-sidebar-container.tsx
type ListWithSidebarContainerContextValue (line 10) | interface ListWithSidebarContainerContextValue {
type ListWithSidebarContainerProps (line 18) | interface ListWithSidebarContainerProps {
type SidebarPortalProps (line 24) | interface SidebarPortalProps {
type SidebarProps (line 28) | interface SidebarProps {
function Sidebar (line 32) | function Sidebar({ children }: SidebarProps) {
function SidebarPortal (line 52) | function SidebarPortal({ children }: SidebarPortalProps) {
FILE: src/renderer/features/shared/components/more-button.tsx
type MoreButtonProps (line 3) | interface MoreButtonProps extends ActionIconProps {}
FILE: src/renderer/features/shared/components/multi-select-rows.tsx
function ArtistMultiSelectRow (line 13) | function ArtistMultiSelectRow({
function GenreMultiSelectRow (line 76) | function GenreMultiSelectRow({
FILE: src/renderer/features/shared/components/order-toggle-button.tsx
type OrderToggleButtonProps (line 6) | interface OrderToggleButtonProps {
FILE: src/renderer/features/shared/components/page-error-boundary.tsx
type PageErrorFallbackProps (line 15) | interface PageErrorFallbackProps {
type PageErrorBoundaryProps (line 79) | interface PageErrorBoundaryProps {
FILE: src/renderer/features/shared/components/play-button-group.tsx
constant LONG_PRESS_PLAY_BEHAVIOR (line 70) | const LONG_PRESS_PLAY_BEHAVIOR = {
constant PLAY_BEHAVIOR_TO_LABEL (line 76) | const PLAY_BEHAVIOR_TO_LABEL = {
type PlayButtonGroupPopoverProps (line 114) | interface PlayButtonGroupPopoverProps extends PlayButtonGroupProps {
type PlayButtonGroupProps (line 120) | interface PlayButtonGroupProps {
type PopoverPosition (line 125) | type PopoverPosition = 'bottom' | 'left' | 'right' | 'top';
FILE: src/renderer/features/shared/components/play-button.tsx
type DefaultPlayButtonProps (line 16) | interface DefaultPlayButtonProps extends ActionIconProps {
type TextPlayButtonProps (line 41) | interface TextPlayButtonProps extends ButtonProps {
type PlayButtonProps (line 137) | interface PlayButtonProps {
FILE: src/renderer/features/shared/components/refresh-button.tsx
type RefreshButtonProps (line 5) | interface RefreshButtonProps extends ActionIconProps {
FILE: src/renderer/features/shared/components/resize-handle.tsx
type ResizeHandleProps (line 6) | interface ResizeHandleProps extends HTMLAttributes<HTMLDivElement> {
FILE: src/renderer/features/shared/components/router-error-boundary.tsx
type RouterErrorFallbackProps (line 15) | interface RouterErrorFallbackProps {
type RouterErrorBoundaryProps (line 85) | interface RouterErrorBoundaryProps {
FILE: src/renderer/features/shared/components/save-as-collection-button.tsx
type SaveAsCollectionButtonProps (line 23) | interface SaveAsCollectionButtonProps {
FILE: src/renderer/features/shared/components/search-input.tsx
type SearchInputProps (line 19) | interface SearchInputProps extends TextInputProps {
FILE: src/renderer/features/shared/components/settings-button.tsx
type SettingsButtonProps (line 5) | interface SettingsButtonProps extends ActionIconProps {}
FILE: src/renderer/features/shared/components/table-config.tsx
type TableConfigProps (line 46) | interface TableConfigProps {
FILE: src/renderer/features/shared/components/tag-filter.tsx
type TagFilterItemProps (line 11) | interface TagFilterItemProps {
type TagFiltersProps (line 61) | interface TagFiltersProps {
FILE: src/renderer/features/shared/hooks/use-list-filter-persistence.ts
type ListFilterPersistence (line 4) | interface ListFilterPersistence {
FILE: src/renderer/features/shared/hooks/use-play-button-click.ts
type UsePlayButtonClickOptions (line 6) | interface UsePlayButtonClickOptions {
type UsePlayButtonClickReturn (line 13) | interface UsePlayButtonClickReturn {
FILE: src/renderer/features/shared/mutations/favorite-optimistic-updates.ts
type PreviousQueryData (line 23) | interface PreviousQueryData {
type PendingUpdate (line 28) | interface PendingUpdate {
function collectAndApplyUpdates (line 34) | function collectAndApplyUpdates(
function updateItemInArray (line 48) | function updateItemInArray<T extends { id: string }>(
function updateItemsInPages (line 65) | function updateItemsInPages<T extends { id: string }, P extends { items:...
FILE: src/renderer/features/shared/mutations/rating-optimistic-updates.ts
type PendingUpdate (line 23) | interface PendingUpdate {
function collectAndApplyUpdates (line 29) | function collectAndApplyUpdates(
function updateItemInArray (line 44) | function updateItemInArray<T extends { id: string }>(
function updateItemsInPages (line 61) | function updateItemsInPages<T extends { id: string }, P extends { items:...
FILE: src/renderer/features/shared/utils.ts
constant PLAY_TYPES (line 18) | const PLAY_TYPES = [
type AlbumFilterKeys (line 39) | enum AlbumFilterKeys {
type ArtistFilterKeys (line 51) | enum ArtistFilterKeys {
type SharedFilterKeys (line 55) | enum SharedFilterKeys {
type SongFilterKeys (line 62) | enum SongFilterKeys {
type FolderFilterKeys (line 81) | enum FolderFilterKeys {
type PlaylistFilterKeys (line 85) | enum PlaylistFilterKeys {
constant FILTER_KEYS (line 89) | const FILTER_KEYS = {
type CreateFuseOptions (line 99) | interface CreateFuseOptions {
type FuseSearchableItem (line 105) | type FuseSearchableItem =
FILE: src/renderer/features/sidebar/components/collapsed-sidebar-button.tsx
type CollapsedSidebarButtonProps (line 7) | interface CollapsedSidebarButtonProps extends ActionIconProps {}
FILE: src/renderer/features/sidebar/components/collapsed-sidebar-item.tsx
type CollapsedSidebarItemProps (line 11) | interface CollapsedSidebarItemProps {
FILE: src/renderer/features/sidebar/components/sidebar-icon.tsx
type SidebarIconProps (line 34) | interface SidebarIconProps {
FILE: src/renderer/features/sidebar/components/sidebar-item.tsx
type SidebarItemProps (line 9) | interface SidebarItemProps extends ButtonProps {
FILE: src/renderer/features/sidebar/components/sidebar-playlist-list.tsx
type PlaylistRowButtonProps (line 53) | interface PlaylistRowButtonProps extends Omit<ButtonProps, 'onContextMen...
FILE: src/renderer/features/similar-songs/components/similar-songs-list.tsx
type SimilarSongsListProps (line 16) | type SimilarSongsListProps = {
FILE: src/renderer/features/songs/components/jellyfin-song-filters.tsx
type JellyfinSongFiltersProps (line 32) | interface JellyfinSongFiltersProps {
FILE: src/renderer/features/songs/components/navidrome-song-filters.tsx
type NavidromeSongFiltersProps (line 33) | interface NavidromeSongFiltersProps {
FILE: src/renderer/features/songs/components/song-infinite-carousel.tsx
type SongCarouselProps (line 27) | interface SongCarouselProps {
function useSongListInfinite (line 154) | function useSongListInfinite(
FILE: src/renderer/features/songs/components/song-list-content.tsx
type OverrideSongListQuery (line 80) | type OverrideSongListQuery = Omit<Partial<SongListQuery>, 'limit' | 'sta...
FILE: src/renderer/features/songs/components/song-list-header.tsx
type SongListHeaderProps (line 22) | interface SongListHeaderProps {
FILE: src/renderer/features/songs/components/song-list-infinite-grid.tsx
type SongListInfiniteGridProps (line 15) | interface SongListInfiniteGridProps extends ItemListGridComponentProps<S...
FILE: src/renderer/features/songs/components/song-list-infinite-table.tsx
type SongListInfiniteTableProps (line 17) | interface SongListInfiniteTableProps extends ItemListTableComponentProps...
FILE: src/renderer/features/songs/components/song-list-paginated-grid.tsx
type SongListPaginatedGridProps (line 16) | interface SongListPaginatedGridProps extends ItemListGridComponentProps<...
FILE: src/renderer/features/songs/components/song-list-paginated-table.tsx
type SongListPaginatedTableProps (line 19) | interface SongListPaginatedTableProps extends ItemListTableComponentProp...
FILE: src/renderer/features/songs/components/subsonic-song-filters.tsx
type SubsonicSongFiltersProps (line 22) | interface SubsonicSongFiltersProps {
FILE: src/renderer/features/titlebar/components/app-menu.tsx
type BaseMenuItem (line 28) | interface BaseMenuItem {
type ConditionalGroupItem (line 33) | interface ConditionalGroupItem extends BaseMenuItem {
type ConditionalItem (line 39) | interface ConditionalItem extends BaseMenuItem {
type CustomItem (line 45) | interface CustomItem extends BaseMenuItem {
type DividerItem (line 50) | interface DividerItem extends BaseMenuItem {
type MenuItem (line 54) | type MenuItem = ConditionalGroupItem | ConditionalItem | CustomItem | Di...
type RegularMenuItem (line 56) | interface RegularMenuItem extends BaseMenuItem {
FILE: src/renderer/features/titlebar/components/titlebar.tsx
type TitlebarProps (line 8) | interface TitlebarProps {
FILE: src/renderer/features/visualizer/components/audiomotionanalyzer/visualizer-settings-form.tsx
type ButterchurnPresetOption (line 28) | type ButterchurnPresetOption = { label: string; value: string };
type CustomGradient (line 1123) | type CustomGradient = {
type StoredColorStop (line 1129) | type StoredColorStop = {
FILE: src/renderer/features/visualizer/components/butternchurn/visualizer.tsx
constant IGNORED_PRESETS (line 25) | const IGNORED_PRESETS = ['Flexi + Martin - astral projection'];
type ButterchurnVisualizer (line 27) | type ButterchurnVisualizer = {
function getButterchurnPresetOptions (line 34) | function getButterchurnPresetOptions(presets: Record<string, string>) {
function initializeVisualizer (line 161) | async function initializeVisualizer(width: number, height: number) {
FILE: src/renderer/global.d.ts
type Window (line 2) | interface Window {
FILE: src/renderer/hooks/use-check-for-updates.ts
constant CHECK_FOR_UPDATES_INTERVAL_MS (line 5) | const CHECK_FOR_UPDATES_INTERVAL_MS = 6 * 60 * 60 * 1000;
FILE: src/renderer/hooks/use-container-query.ts
type UseContainerQueryProps (line 3) | interface UseContainerQueryProps {
FILE: src/renderer/hooks/use-drag-drop.tsx
type UseDraggableProps (line 25) | interface UseDraggableProps {
FILE: src/renderer/hooks/use-garbage-collection.ts
constant GARBAGE_COLLECTION_INTERVAL (line 5) | const GARBAGE_COLLECTION_INTERVAL = 1000 * 60 * 5;
FILE: src/renderer/hooks/use-genre-route.ts
constant ALBUM_REGEX (line 6) | const ALBUM_REGEX = /albums$/;
constant SONG_REGEX (line 7) | const SONG_REGEX = /songs$/;
FILE: src/renderer/hooks/use-server-authenticated.ts
constant MIN_AUTH_DELAY_MS (line 19) | const MIN_AUTH_DELAY_MS = 1000;
constant MAX_NETWORK_RETRIES (line 20) | const MAX_NETWORK_RETRIES = 1;
constant NETWORK_RETRY_DELAY_MS (line 21) | const NETWORK_RETRY_DELAY_MS = 500;
FILE: src/renderer/layouts/default-layout.tsx
type DefaultLayoutProps (line 27) | interface DefaultLayoutProps {
FILE: src/renderer/layouts/default-layout/left-sidebar.tsx
type LeftSidebarProps (line 20) | interface LeftSidebarProps {
FILE: src/renderer/layouts/default-layout/main-content.tsx
constant MINIMUM_SIDEBAR_WIDTH (line 25) | const MINIMUM_SIDEBAR_WIDTH = 260;
function GlobalExpandedPanel (line 218) | function GlobalExpandedPanel() {
function MainContentBody (line 230) | function MainContentBody() {
FILE: src/renderer/layouts/default-layout/right-sidebar.tsx
type RightSidebarProps (line 48) | interface RightSidebarProps {
FILE: src/renderer/layouts/mobile-layout/mobile-layout.tsx
type MobileLayoutProps (line 26) | interface MobileLayoutProps {
FILE: src/renderer/layouts/responsive-layout.tsx
type ResponsiveLayoutProps (line 19) | interface ResponsiveLayoutProps {
FILE: src/renderer/layouts/window-bar.tsx
type WindowBarControlsProps (line 29) | interface WindowBarControlsProps {
FILE: src/renderer/lib/react-query.ts
type InfiniteQueryHookArgs (line 42) | type InfiniteQueryHookArgs<T> = {
type MutationHookArgs (line 48) | type MutationHookArgs = {
type MutationOptions (line 52) | type MutationOptions = {
type QueryHookArgs (line 62) | type QueryHookArgs<T> = {
type UseQueryHookOptions (line 68) | type UseQueryHookOptions = {
FILE: src/renderer/lib/zustand.ts
type WithSelectors (line 3) | type WithSelectors<S> = S extends { getState: () => infer T }
FILE: src/renderer/main.tsx
function createIDBPersister (line 12) | function createIDBPersister(idbValidKey: IDBValidKey = 'reactQuery') {
FILE: src/renderer/release-notes-modal.tsx
constant GITHUB_RELEASES_URL (line 22) | const GITHUB_RELEASES_URL = 'https://api.github.com/repos/jeffvli/feishi...
constant GITHUB_COMPARE_URL (line 23) | const GITHUB_COMPARE_URL = 'https://api.github.com/repos/jeffvli/feishin...
constant RELEASES_TO_FETCH (line 24) | const RELEASES_TO_FETCH = 30;
type GitHubCompareCommit (line 26) | interface GitHubCompareCommit {
type GitHubCompareResponse (line 35) | interface GitHubCompareResponse {
type GitHubRelease (line 40) | interface GitHubRelease {
type ReleaseNotesContentProps (line 48) | interface ReleaseNotesContentProps {
function isAlphaVersion (line 53) | function isAlphaVersion(version: string): boolean {
function parseVersionFromTag (line 57) | function parseVersionFromTag(tagName: string): string {
function toTag (line 61) | function toTag(version: string): string {
constant WAIT_FOR_LOCAL_STORAGE (line 398) | const WAIT_FOR_LOCAL_STORAGE = 1000 * 2;
type ReleaseNotesModalContentWrapperProps (line 400) | interface ReleaseNotesModalContentWrapperProps {
FILE: src/renderer/router/routes.ts
type AppRoute (line 1) | enum AppRoute {
FILE: src/renderer/store/app.store.ts
type AppSlice (line 12) | interface AppSlice extends AppState {
type AppState (line 34) | interface AppState {
type GlobalExpandedState (line 62) | interface GlobalExpandedState {
type CommandPaletteProps (line 67) | type CommandPaletteProps = {
type SidebarProps (line 74) | type SidebarProps = {
type TitlebarProps (line 84) | type TitlebarProps = {
FILE: src/renderer/store/auth.store.ts
type AuthSlice (line 10) | interface AuthSlice extends AuthState {
type AuthState (line 21) | interface AuthState {
FILE: src/renderer/store/env-settings-overrides.ts
constant APP_THEMES (line 5) | const APP_THEMES = new Set([
constant DISCORD_DISPLAY_TYPES (line 37) | const DISCORD_DISPLAY_TYPES = new Set(['artist', 'feishin', 'song']);
constant DISCORD_LINK_TYPES (line 38) | const DISCORD_LINK_TYPES = new Set(['last_fm', 'musicbrainz', 'musicbrai...
constant LYRICS_ALIGNMENTS (line 39) | const LYRICS_ALIGNMENTS = new Set(['center', 'left', 'right']);
constant FONT_TYPES (line 40) | const FONT_TYPES = new Set(['builtIn', 'custom', 'system']);
constant HOME_FEATURE_STYLES (line 41) | const HOME_FEATURE_STYLES = new Set(['multiple', 'single']);
constant SIDE_QUEUE_TYPES (line 42) | const SIDE_QUEUE_TYPES = new Set(['sideDrawerQueue', 'sideQueue']);
constant SIDE_QUEUE_LAYOUTS (line 43) | const SIDE_QUEUE_LAYOUTS = new Set(['horizontal', 'vertical']);
type EnvSettingsOverrides (line 45) | type EnvSettingsOverrides = DeepPartial<
type DeepPartial (line 49) | type DeepPartial<T> = {
type EnvSettingSpec (line 53) | interface EnvSettingSpec {
function setAtPath (line 62) | function setAtPath(
constant RGB_ACCENT_REGEX (line 80) | const RGB_ACCENT_REGEX = /^rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1...
constant ENV_SETTING_SPECS (line 82) | const ENV_SETTING_SPECS: EnvSettingSpec[] = [
function getEnvSettingsOverrides (line 320) | function getEnvSettingsOverrides(): EnvSettingsOverrides {
function getWin (line 342) | function getWin(): Record<string, unknown> & Window {
function isUnsubstitutedPlaceholder (line 347) | function isUnsubstitutedPlaceholder(s: string): boolean {
function parseBool (line 351) | function parseBool(s: string | undefined): boolean | undefined {
function parseEnum (line 359) | function parseEnum<T extends string>(s: string | undefined, allowed: Set...
function parseNum (line 365) | function parseNum(s: string | undefined): number | undefined {
function parseValue (line 371) | function parseValue(raw: string | undefined, spec: EnvSettingSpec): unkn...
FILE: src/renderer/store/full-screen-player.store.ts
type FullScreenPlayerSlice (line 6) | interface FullScreenPlayerSlice extends FullScreenPlayerState {
type FullScreenPlayerState (line 12) | interface FullScreenPlayerState {
FILE: src/renderer/store/player.store.ts
type PlayerState (line 27) | interface PlayerState extends Actions, State {}
type QueueGroupingProperty (line 29) | type QueueGroupingProperty = keyof QueueSong;
type Actions (line 31) | interface Actions {
type GroupedQueue (line 84) | interface GroupedQueue {
type State (line 89) | interface State {
function calculateNextSong (line 109) | function calculateNextSong(
function isShuffleEnabled (line 136) | function isShuffleEnabled(state: {
function mapShuffledToQueueIndex (line 144) | function mapShuffledToQueueIndex(shuffledIndex: number, shuffled: number...
function addIndexesToShuffled (line 152) | function addIndexesToShuffled(
function adjustShuffledIndexesForInsertion (line 166) | function adjustShuffledIndexesForInsertion(
function calculateNextIndex (line 180) | function calculateNextIndex(
function emitPlayerPlayEvent (line 207) | function emitPlayerPlayEvent(
function findShuffledPositionForQueueIndex (line 270) | function findShuffledPositionForQueueIndex(
function generateShuffledIndexes (line 279) | function generateShuffledIndexes(length: number): number[] {
function regenerateShuffledIndexesIfNeeded (line 285) | function regenerateShuffledIndexesIfNeeded(state: {
type AddToQueueByPlayType (line 1671) | type AddToQueueByPlayType = Play;
type AddToQueueByUniqueId (line 1673) | type AddToQueueByUniqueId = {
type AddToQueueType (line 1678) | type AddToQueueType = AddToQueueByPlayType | AddToQueueByUniqueId;
function addToQueueByData (line 1680) | async function addToQueueByData(type: AddToQueueType, data: Song[]) {
function cleanupOrphanedSongs (line 2098) | function cleanupOrphanedSongs(state: any): boolean {
function parseUniqueSeekToTimestamp (line 2129) | function parseUniqueSeekToTimestamp(timestamp: string) {
function recalculatePlayerIndex (line 2133) | function recalculatePlayerIndex(state: any, queue: string[]) {
function toQueueSong (line 2144) | function toQueueSong(item: Song): QueueSong {
function uniqueSeekToTimestamp (line 2152) | function uniqueSeekToTimestamp(timestamp: number) {
FILE: src/renderer/store/scroll.store.ts
type ScrollState (line 3) | type ScrollState = {
FILE: src/renderer/store/settings.store.ts
type DeepPartial (line 46) | type DeepPartial<T> = {
type ItemTableListColumnConfig (line 207) | type ItemTableListColumnConfig = z.infer<typeof ItemTableListColumnConfi...
type ItemGridListRowConfig (line 215) | type ItemGridListRowConfig = z.infer<typeof ItemGridListRowConfigSchema>;
type HomeFeatureStyle (line 429) | enum HomeFeatureStyle {
type ArtistItem (line 704) | enum ArtistItem {
type ArtistReleaseTypeItem (line 712) | enum ArtistReleaseTypeItem {
type BarAlign (line 733) | enum BarAlign {
type BindingActions (line 739) | enum BindingActions {
type DiscordDisplayType (line 781) | enum DiscordDisplayType {
type DiscordLinkType (line 787) | enum DiscordLinkType {
type GenreTarget (line 794) | enum GenreTarget {
type HomeItem (line 799) | enum HomeItem {
type PlayerbarSliderType (line 808) | enum PlayerbarSliderType {
type PlayerItem (line 813) | enum PlayerItem {
type PlaylistTarget (line 827) | enum PlaylistTarget {
type SidebarItem (line 832) | enum SidebarItem {
type DataGridProps (line 849) | type DataGridProps = {
type DataTableProps (line 857) | type DataTableProps = z.infer<typeof ItemTableListPropsSchema>;
type ItemDetailListProps (line 858) | type ItemDetailListProps = z.infer<typeof ItemDetailListPropsSchema>;
type ItemListSettings (line 859) | type ItemListSettings = {
type PlayerFilter (line 868) | type PlayerFilter = z.infer<typeof PlayerFilterSchema>;
type PlayerFilterField (line 870) | type PlayerFilterField = z.infer<typeof PlayerFilterFieldSchema>;
type PlayerFilterOperator (line 872) | type PlayerFilterOperator = z.infer<typeof PlayerFilterOperatorSchema>;
type SettingsSlice (line 874) | interface SettingsSlice extends z.infer<typeof SettingsStateSchema> {
type SettingsState (line 897) | interface SettingsState extends z.infer<typeof SettingsStateSchema> {}
type SidebarItemType (line 898) | type SidebarItemType = z.infer<typeof SidebarItemTypeSchema>;
type SideQueueLayout (line 900) | type SideQueueLayout = z.infer<typeof SideQueueLayoutSchema>;
type SideQueueType (line 901) | type SideQueueType = z.infer<typeof SideQueueTypeSchema>;
type SortableItem (line 903) | type SortableItem<T extends string> = {
type TranscodingConfig (line 908) | type TranscodingConfig = z.infer<typeof TranscodingConfigSchema>;
type VersionedSettings (line 910) | type VersionedSettings = SettingsState & { version: number };
method migrate (line 2074) | migrate(persistedState, version) {
FILE: src/renderer/store/sleep-timer.store.ts
type SleepTimerMode (line 4) | type SleepTimerMode = 'endOfSong' | 'timed';
type SleepTimerActions (line 6) | interface SleepTimerActions {
type SleepTimerState (line 13) | interface SleepTimerState {
FILE: src/renderer/store/timestamp.store.ts
type TimestampState (line 4) | interface TimestampState {
FILE: src/renderer/themes/mantine-theme.tsx
function createMantineTheme (line 140) | function createMantineTheme(theme: AppThemeConfiguration): MantineThemeO...
FILE: src/renderer/themes/use-app-theme.ts
constant THEME_DATA (line 18) | const THEME_DATA = [
FILE: src/renderer/types/emotion.d.ts
type GREY (line 5) | interface GREY extends MantineTheme {}
FILE: src/renderer/types/fonts.ts
type Font (line 3) | type Font = {
constant FONT_OPTIONS (line 8) | const FONT_OPTIONS: Font[] = [
FILE: src/renderer/utils/format.tsx
constant SIZES (line 145) | const SIZES = ['B', 'KiB', 'MiB', 'GiB', 'TiB'];
FILE: src/renderer/utils/linkify.tsx
constant URL_REGEX (line 4) | const URL_REGEX =
FILE: src/renderer/utils/logger.ts
type LogCategory (line 3) | enum LogCategory {
type LogLevel (line 15) | type LogLevel = 'debug' | 'error' | 'info' | 'warn';
type LogFn (line 17) | interface LogFn {
type Logger (line 27) | interface Logger {
constant DEFAULT_LOG_LEVEL (line 35) | const DEFAULT_LOG_LEVEL = process.env.NODE_ENV === 'production' ? 'info'...
constant DEBOUNCE_INTERVAL (line 48) | const DEBOUNCE_INTERVAL = 200;
constant DEBOUNCE_MAP (line 49) | const DEBOUNCE_MAP = new Map<string, { count: number; lastLog: number }>();
class ConsoleLogger (line 77) | class ConsoleLogger implements Logger {
method constructor (line 84) | constructor() {
method initializeLoggers (line 92) | private initializeLoggers(level: LogLevel) {
FILE: src/renderer/utils/normalize-release-types.tsx
constant PRIMARY_MAPPING (line 6) | const PRIMARY_MAPPING = {
constant SECONDARY_MAPPING (line 14) | const SECONDARY_MAPPING = {
FILE: src/renderer/utils/query-params.ts
constant PAGINATION_KEYS (line 182) | const PAGINATION_KEYS = ['currentPage', 'scrollOffset'];
FILE: src/renderer/utils/sanitize.ts
constant SANITIZE_OPTIONS (line 3) | const SANITIZE_OPTIONS: Config = {
FILE: src/renderer/utils/shuffle.ts
function shuffle (line 1) | function shuffle<T>(array: T[]): T[] {
function shuffleInPlace (line 16) | function shuffleInPlace<T>(array: T[]): T[] {
FILE: src/shared/api/jellyfin/jellyfin-normalize.ts
constant TICKS_PER_MS (line 18) | const TICKS_PER_MS = 10000;
type AlbumOrSong (line 20) | type AlbumOrSong = z.infer<typeof jfType._response.album> | z.infer<type...
constant KEYS_TO_OMIT (line 22) | const KEYS_TO_OMIT = new Set(['AlbumArtist', 'Artist']);
FILE: src/shared/api/jellyfin/jellyfin-types.ts
type JFAlbumArtistListSort (line 3) | enum JFAlbumArtistListSort {
type JFAlbumListSort (line 12) | enum JFAlbumListSort {
type JFArtistListSort (line 23) | enum JFArtistListSort {
type JFGenreListSort (line 32) | enum JFGenreListSort {
type JFPlaylistListSort (line 36) | enum JFPlaylistListSort {
type JFSongListSort (line 44) | enum JFSongListSort {
type JFSortOrder (line 58) | enum JFSortOrder {
type JellyfinExtensions (line 769) | enum JellyfinExtensions {
FILE: src/shared/api/navidrome/navidrome-normalize.ts
type WithDate (line 28) | interface WithDate {
FILE: src/shared/api/navidrome/navidrome-types.ts
type NDAlbumArtistListSort (line 4) | enum NDAlbumArtistListSort {
type NDAlbumListSort (line 13) | enum NDAlbumListSort {
type NDGenreListSort (line 29) | enum NDGenreListSort {
type NDPlaylistListSort (line 33) | enum NDPlaylistListSort {
type NDSongListSort (line 42) | enum NDSongListSort {
type NDSortOrder (line 65) | enum NDSortOrder {
type NDUserListSort (line 315) | enum NDUserListSort {
type NDTagListSort (line 688) | enum NDTagListSort {
FILE: src/shared/api/subsonic/subsonic-normalize.ts
constant PRIMARY_RELEASE_TYPES (line 258) | const PRIMARY_RELEASE_TYPES = ['album', 'ep', 'single', 'broadcast', 'ot...
FILE: src/shared/api/subsonic/subsonic-types.ts
type SubsonicExtensions (line 380) | enum SubsonicExtensions {
type AlbumListSortType (line 533) | enum AlbumListSortType {
FILE: src/shared/api/utils.ts
type VersionInfo (line 66) | type VersionInfo = ReadonlyArray<
constant SEPARATOR_STRING (line 142) | const SEPARATOR_STRING = ' • ';
FILE: src/shared/components/accordion/accordion.tsx
type AccordionProps (line 10) | interface AccordionProps
FILE: src/shared/components/action-icon/action-icon.tsx
constant COMPACT_SIZES (line 14) | const COMPACT_SIZES = ['compact-xs', 'compact-sm', 'compact-md'] as const;
type ActionIconProps (line 20) | interface ActionIconProps
FILE: src/shared/components/angle-slider/angle-slider.tsx
type AngleSliderProps (line 7) | interface AngleSliderProps extends MantineAngleSliderProps {}
FILE: src/shared/components/animations/animation-variants.ts
function combine (line 80) | function combine(...variants: Variants[]) {
function stagger (line 86) | function stagger(variants: Variants, delay?: number): Variants {
FILE: src/shared/components/badge/badge.tsx
type BadgeProps (line 12) | interface BadgeProps
FILE: src/shared/components/box/box.tsx
type BoxProps (line 4) | interface BoxProps extends ElementProps<'div', keyof MantineBoxProps>, M...
FILE: src/shared/components/breadcrumb/breadcrumb.tsx
type BreadcrumbProps (line 6) | interface BreadcrumbProps extends MantineBreadcrumbsProps {}
FILE: src/shared/components/button/button.tsx
type ButtonProps (line 13) | interface ButtonProps
type ExtendedButtonVariant (line 22) | type ExtendedButtonVariant =
type TimeoutButtonProps (line 99) | interface TimeoutButtonProps extends ButtonProps {
FILE: src/shared/components/center/center.tsx
type CenterProps (line 4) | interface CenterProps extends MantineCenterProps {
FILE: src/shared/components/checkbox-select/checkbox-select.tsx
type CheckboxSelectProps (line 22) | interface CheckboxSelectProps {
type CheckboxSelectItemProps (line 49) | interface CheckboxSelectItemProps {
function CheckboxSelectItem (line 56) | function CheckboxSelectItem({ enableDrag, onChange, option, values }: Ch...
FILE: src/shared/components/checkbox/checkbox.tsx
type CheckboxProps (line 6) | interface CheckboxProps extends MantineCheckboxProps {}
FILE: src/shared/components/code/code.tsx
type CodeProps (line 5) | interface CodeProps extends MantineCodeProps {}
FILE: src/shared/components/color-input/color-input.tsx
type ColorInputProps (line 8) | interface ColorInputProps extends MantineColorInputProps {}
FILE: src/shared/components/context-menu/context-menu.tsx
type ContextMenuContext (line 23) | interface ContextMenuContext {
type ContentProps (line 30) | interface ContentProps {
type ContextMenuProps (line 40) | interface ContextMenuProps {
type DividerProps (line 44) | interface DividerProps {}
type ItemProps (line 46) | interface ItemProps {
type LabelProps (line 56) | interface LabelProps extends React.ComponentPropsWithoutRef<'div'> {
type SubmenuContext (line 60) | interface SubmenuContext {
type TargetProps (line 69) | interface TargetProps {
function ContextMenu (line 73) | function ContextMenu(props: ContextMenuProps) {
function Content (line 86) | function Content(props: ContentProps) {
function Divider (line 112) | function Divider(props: DividerProps) {
function Item (line 116) | function Item(props: ItemProps) {
function Label (line 137) | function Label(props: LabelProps) {
function Target (line 147) | function Target(props: TargetProps) {
type SubmenuContentProps (line 159) | interface SubmenuContentProps {
type SubmenuProps (line 164) | interface SubmenuProps {
type SubmenuTargetProps (line 171) | interface SubmenuTargetProps {
function Submenu (line 175) | function Submenu(props: SubmenuProps) {
function SubmenuContent (line 218) | function SubmenuContent(props: SubmenuContentProps) {
function SubmenuTarget (line 265) | function SubmenuTarget(props: SubmenuTargetProps) {
FILE: src/shared/components/copy-button/copy-button.tsx
type CopyButtonProps (line 6) | interface CopyButtonProps extends MantineCopyButtonProps {}
FILE: src/shared/components/date-picker/date-picker.tsx
type DateInputProps (line 13) | interface DateInputProps extends MantineDateInputProps {
type DateTimeInputProps (line 43) | interface DateTimeInputProps extends MantineDateTimeInputProps {
FILE: src/shared/components/date-time-picker/date-time-picker.tsx
type DateTimePickerProps (line 7) | interface DateTimePickerProps extends MantineDateTimePickerProps {
FILE: src/shared/components/dialog/dialog.tsx
type DialogProps (line 7) | interface DialogProps extends MantineDialogProps {}
FILE: src/shared/components/divider/divider.tsx
type DividerProps (line 6) | interface DividerProps extends MantineDividerProps {}
FILE: src/shared/components/drag-drop-zone/drag-drop-zone.tsx
type DragDropZoneProps (line 8) | interface DragDropZoneProps {
FILE: src/shared/components/drawer/drawer.tsx
type DrawerProps (line 4) | interface DrawerProps extends MantineDrawerProps {
FILE: src/shared/components/dropdown-menu/dropdown-menu.tsx
type MenuItemProps (line 17) | interface MenuItemProps extends MantineMenuItemProps {
type MenuDividerProps (line 22) | type MenuDividerProps = MantineMenuDividerProps;
type MenuDropdownProps (line 23) | type MenuDropdownProps = MantineMenuDropdownProps;
type MenuLabelProps (line 24) | type MenuLabelProps = MantineMenuLabelProps;
type MenuProps (line 25) | type MenuProps = MantineMenuProps;
FILE: src/shared/components/explicit-indicator/explicit-indicator.tsx
constant EXPLICIT_SYMBOL (line 8) | const EXPLICIT_SYMBOL = '🅴';
constant CLEAN_SYMBOL (line 9) | const CLEAN_SYMBOL = '🅲';
type ExplicitIndicatorProps (line 11) | interface ExplicitIndicatorProps extends ComponentPropsWithoutRef<'span'> {
type ExplicitIndicatorSize (line 17) | type ExplicitIndicatorSize = '2xl' | '3xl' | '4xl' | 'lg' | 'md' | 'sm' ...
FILE: src/shared/components/fieldset/fieldset.tsx
type FieldsetProps (line 6) | interface FieldsetProps extends MantineFieldsetProps {
FILE: src/shared/components/file-input/file-input.tsx
type FileInputProps (line 9) | interface FileInputProps extends MantineFileInputProps {
FILE: src/shared/components/flex/flex.tsx
type FlexProps (line 4) | interface FlexProps extends MantineFlexProps {}
FILE: src/shared/components/grid/grid.tsx
type GridProps (line 4) | interface GridProps extends MantineGridProps {}
FILE: src/shared/components/group/group.tsx
type GroupProps (line 4) | interface GroupProps extends MantineGroupProps {}
FILE: src/shared/components/hover-card/hover-card.tsx
type HoverCardProps (line 8) | interface HoverCardProps extends MantineHoverCardProps {}
FILE: src/shared/components/icon/icon.tsx
type AppIconSelection (line 144) | type AppIconSelection = keyof typeof AppIcon;
type LogoImgProps (line 146) | type LogoImgProps = ImgHTMLAttributes<HTMLImageElement> & { size?: numbe...
function logoImgStyle (line 148) | function logoImgStyle(size: number | string | undefined): CSSProperties ...
type IconProps (line 368) | interface IconProps extends Omit<IconBaseProps, 'color' | 'fill' | 'size...
type IconColor (line 375) | type IconColor =
function isPredefinedSize (line 423) | function isPredefinedSize(size: IconProps['size']) {
FILE: src/shared/components/image/image.tsx
type ImageProps (line 25) | interface ImageProps extends Omit<ImgHTMLAttributes<HTMLImageElement>, '...
type ImageContainerProps (line 40) | interface ImageContainerProps extends HTMLAttributes<HTMLDivElement> {
type ImageLoaderProps (line 45) | interface ImageLoaderProps {
type ImageUnloaderProps (line 49) | interface ImageUnloaderProps {
constant FALLBACK_SVG (line 54) | const FALLBACK_SVG =
function BaseImage (line 57) | function BaseImage({
function ImageLoader (line 176) | function ImageLoader({ className }: ImageLoaderProps) {
function ImageUnloader (line 185) | function ImageUnloader({ className, icon = 'emptyImage' }: ImageUnloader...
FILE: src/shared/components/image/use-native-image.ts
type FetchPriority (line 5) | type FetchPriority = 'auto' | 'high' | 'low';
type NativeImageState (line 7) | interface NativeImageState {
type UseNativeImageArgs (line 12) | interface UseNativeImageArgs {
function useNativeImage (line 19) | function useNativeImage({
FILE: src/shared/components/json-input/json-input.tsx
type JsonInputProps (line 9) | interface JsonInputProps extends MantineJsonInputProps {
FILE: src/shared/components/kbd/kbd.tsx
type KbdProps (line 3) | interface KbdProps extends MantineKbdProps {}
FILE: src/shared/components/loading-overlay/loading-overlay.tsx
type LoadingOverlayProps (line 8) | interface LoadingOverlayProps extends MantineLoadingOverlayProps {
FILE: src/shared/components/modal/modal.tsx
type ModalProps (line 24) | interface ModalProps extends Omit<MantineModalProps, 'onClose'> {
type ContextModalVars (line 70) | type ContextModalVars = {
type ConfirmModalProps (line 83) | interface ConfirmModalProps {
type ModalsProviderProps (line 132) | interface ModalsProviderProps extends MantineModalsProviderProps {}
FILE: src/shared/components/multi-select/multi-select.tsx
type MultiSelectProps (line 9) | interface MultiSelectProps extends MantineMultiSelectProps {
FILE: src/shared/components/multi-select/virtual-multi-select.tsx
type VirtualMultiSelectOption (line 18) | type VirtualMultiSelectOption<T> = T & { label: string; value: string };
type VirtualMultiSelectProps (line 20) | interface VirtualMultiSelectProps<T> {
function VirtualMultiSelect (line 42) | function VirtualMultiSelect<T>({
FILE: src/shared/components/number-input/number-input.tsx
type NumberInputProps (line 9) | interface NumberInputProps extends MantineNumberInputProps {
FILE: src/shared/components/option/option.tsx
type OptionProps (line 9) | interface OptionProps extends GroupProps {
type LabelProps (line 30) | interface LabelProps {
type ControlProps (line 38) | interface ControlProps {
FILE: src/shared/components/pagination/pagination.tsx
type PaginationProps (line 19) | interface PaginationProps extends MantinePaginationProps {
FILE: src/shared/components/paper/paper.tsx
type PaperProps (line 8) | interface PaperProps extends MantinePaperProps {
FILE: src/shared/components/password-input/password-input.tsx
type PasswordInputProps (line 9) | interface PasswordInputProps extends MantinePasswordInputProps {
FILE: src/shared/components/pill/pill.tsx
type PillProps (line 12) | interface PillProps extends MantinePillProps {}
type PillGroupProps (line 39) | interface PillGroupProps extends MantinePillGroupProps {}
type PillLinkProps (line 64) | interface PillLinkProps
FILE: src/shared/components/popover/popover.tsx
type PopoverDropdownProps (line 10) | interface PopoverDropdownProps extends MantinePopoverDropdownProps {}
type PopoverProps (line 11) | interface PopoverProps extends MantinePopoverProps {}
FILE: src/shared/components/portal/portal.tsx
type PortalProps (line 3) | interface PortalProps extends MantinePortalProps {}
FILE: src/shared/components/rating/rating.tsx
type RatingProps (line 8) | interface RatingProps extends MantineRatingProps {
FILE: src/shared/components/read-only-rating/read-only-rating.tsx
constant MAX_STARS (line 6) | const MAX_STARS = 5;
type ReadOnlyRatingProps (line 8) | interface ReadOnlyRatingProps {
function ReadOnlyRatingComponent (line 15) | function ReadOnlyRatingComponent({ className, onChange, size = 'sm', val...
FILE: src/shared/components/scroll-area/scroll-area.tsx
type ScrollAreaProps (line 12) | interface ScrollAreaProps extends React.ComponentPropsWithoutRef<'div'> {
FILE: src/shared/components/segmented-control/segmented-control.tsx
type SegmentedControlProps (line 8) | type SegmentedControlProps = MantineSegmentedControlProps;
FILE: src/shared/components/select/select.tsx
type SelectProps (line 8) | interface SelectProps extends MantineSelectProps {
FILE: src/shared/components/skeleton/skeleton.tsx
type SkeletonProps (line 6) | interface SkeletonProps {
function BaseSkeleton (line 21) | function BaseSkeleton({
FILE: src/shared/components/slider/slider.tsx
type SliderProps (line 8) | interface SliderProps extends MantineSliderProps {}
FILE: src/shared/components/spinner/spinner.tsx
type SpinnerProps (line 8) | interface SpinnerProps extends IconBaseProps {
FILE: src/shared/components/spoiler/spoiler.tsx
type SpoilerProps (line 8) | interface SpoilerProps extends Omit<MantineSpoilerProps, 'hideLabel' | '...
FILE: src/shared/components/stack/stack.tsx
type StackProps (line 4) | interface StackProps extends MantineStackProps {}
FILE: src/shared/components/switch/switch.tsx
type SwitchProps (line 8) | type SwitchProps = MantineSwitchProps;
FILE: src/shared/components/table/table.tsx
type TableProps (line 5) | interface TableProps extends MantineTableProps {}
FILE: src/shared/components/tabs/tabs.tsx
type TabsProps (line 6) | type TabsProps = MantineTabsProps;
FILE: src/shared/components/text-input/text-input.tsx
type TextInputProps (line 9) | interface TextInputProps extends MantineTextInputProps {
FILE: src/shared/components/text-title/text-title.tsx
type MantineTextTitleDivProps (line 9) | type MantineTextTitleDivProps = ComponentPropsWithoutRef<'div'> & Mantin...
type TextTitleProps (line 11) | interface TextTitleProps extends MantineTextTitleDivProps {
FILE: src/shared/components/text/text.tsx
type TextProps (line 9) | interface TextProps extends MantineTextDivProps {
type Font (line 20) | type Font = 'Epilogue' | 'Gotham' | 'Inter' | 'Poppins';
type MantineTextDivProps (line 22) | type MantineTextDivProps = ComponentPropsWithoutRef<'div'> & MantineText...
FILE: src/shared/components/textarea/textarea.tsx
type TextareaProps (line 6) | interface TextareaProps extends MantineTextareaProps {
FILE: src/shared/components/toast/toast.tsx
type NotificationProps (line 14) | interface NotificationProps extends Omit<NotificationData, 'message'> {
FILE: src/shared/components/tooltip/tooltip.tsx
type TooltipProps (line 7) | interface TooltipProps extends MantineTooltipProps {}
constant DEFAULT_TRANSITION_PROPS (line 9) | const DEFAULT_TRANSITION_PROPS = {
FILE: src/shared/components/yes-no-select/yes-no-select.tsx
type YesNoSelectProps (line 5) | interface YesNoSelectProps extends SelectProps {}
FILE: src/shared/hooks/use-container-query.ts
type UseContainerQueryProps (line 3) | interface UseContainerQueryProps {
FILE: src/shared/hooks/use-debounced-value.ts
type UseDebouncedValueOptions (line 3) | interface UseDebouncedValueOptions {
function useDebouncedValue (line 7) | function useDebouncedValue<T>(
FILE: src/shared/hooks/use-hotkeys.ts
type HotkeyItem (line 8) | type HotkeyItem = MantineHotkeyItem;
FILE: src/shared/hooks/use-long-press.ts
type UseLongPressOptions (line 3) | interface UseLongPressOptions<T extends HTMLElement = HTMLElement> {
type UseLongPressReturn (line 11) | interface UseLongPressReturn {
FILE: src/shared/themes/app-theme-types.ts
type AppTheme (line 5) | enum AppTheme {
type AppThemeConfiguration (line 37) | type AppThemeConfiguration = Partial<BaseAppThemeConfiguration>;
type BaseAppThemeConfiguration (line 39) | interface BaseAppThemeConfiguration {
FILE: src/shared/types/domain-types.ts
type LibraryItem (line 23) | enum LibraryItem {
type ServerType (line 36) | enum ServerType {
type SortOrder (line 42) | enum SortOrder {
type AnyLibraryItem (line 47) | type AnyLibraryItem = Album | AlbumArtist | Artist | Playlist | QueueSon...
type AnyLibraryItems (line 49) | type AnyLibraryItems =
type PlayerData (line 57) | interface PlayerData {
type QueueData (line 69) | interface QueueData {
type QueueSong (line 75) | type QueueSong = Song & {
type SavedCollection (line 79) | interface SavedCollection {
type ServerListItem (line 86) | type ServerListItem = {
type ServerListItemWithCredential (line 103) | type ServerListItemWithCredential = ServerListItem & {
type User (line 108) | type User = {
type SortOrderMap (line 118) | type SortOrderMap = {
type ExplicitStatus (line 139) | enum ExplicitStatus {
type ExternalSource (line 144) | enum ExternalSource {
type ExternalType (line 151) | enum ExternalType {
type GenreListSort (line 156) | enum GenreListSort {
type ImageType (line 160) | enum ImageType {
type TagListSort (line 167) | enum TagListSort {
type Album (line 171) | type Album = {
type AlbumArtist (line 211) | type AlbumArtist = {
type Artist (line 232) | type Artist = Omit<AlbumArtist, '_itemType'> & {
type AuthenticationResponse (line 236) | type AuthenticationResponse = {
type BasePaginatedResponse (line 244) | interface BasePaginatedResponse<T> {
type BaseQuery (line 251) | interface BaseQuery<T> {
type EndpointDetails (line 256) | type EndpointDetails = {
type Folder (line 260) | type Folder = {
type FolderArgs (line 275) | type FolderArgs = BaseEndpointArgs & { query: FolderQuery };
type FolderQuery (line 277) | interface FolderQuery extends BaseQuery<SongListSort> {
type FolderResponse (line 283) | type FolderResponse = Folder;
type GainInfo (line 285) | type GainInfo = {
type Genre (line 290) | type Genre = {
type GenreListArgs (line 302) | type GenreListArgs = BaseEndpointArgs & { query: GenreListQuery };
type GenreListQuery (line 304) | interface GenreListQuery extends BaseQuery<GenreListSort> {
type GenreListResponse (line 316) | type GenreListResponse = BasePaginatedResponse<Genre[]>;
type GenresResponse (line 318) | type GenresResponse = Genre[];
type ListSortOrder (line 320) | type ListSortOrder = 'asc' | 'desc';
type MusicFolder (line 322) | type MusicFolder = {
type MusicFoldersResponse (line 327) | type MusicFoldersResponse = MusicFolder[];
type Playlist (line 329) | type Playlist = {
type RelatedAlbumArtist (line 349) | type RelatedAlbumArtist = {
type RelatedArtist (line 354) | type RelatedArtist = {
type Song (line 363) | type Song = {
type BaseEndpointArgs (line 413) | type BaseEndpointArgs = {
type GenreListSortMap (line 425) | type GenreListSortMap = {
type TagListSortMap (line 443) | type TagListSortMap = {
type AlbumListSort (line 461) | enum AlbumListSort {
type AlbumListArgs (line 482) | type AlbumListArgs = BaseEndpointArgs & { query: AlbumListQuery };
type AlbumListCountArgs (line 484) | type AlbumListCountArgs = BaseEndpointArgs & { query: ListCountQuery<Alb...
type AlbumListQuery (line 486) | interface AlbumListQuery extends AlbumListNavidromeQuery, BaseQuery<Albu...
type AlbumListResponse (line 501) | type AlbumListResponse = BasePaginatedResponse<Album[]>;
type ListCountQuery (line 503) | type ListCountQuery<TQuery> = Omit<TQuery, 'startIndex'>;
type AlbumListNavidromeQuery (line 505) | interface AlbumListNavidromeQuery {
type AlbumListSortMap (line 510) | type AlbumListSortMap = {
type SongListSort (line 580) | enum SongListSort {
type AlbumDetailArgs (line 603) | type AlbumDetailArgs = BaseEndpointArgs & { query: AlbumDetailQuery };
type AlbumDetailQuery (line 605) | type AlbumDetailQuery = { id: string };
type AlbumDetailResponse (line 608) | type AlbumDetailResponse = Album;
type AlbumInfo (line 610) | type AlbumInfo = {
type SongListArgs (line 615) | type SongListArgs = BaseEndpointArgs & { query: SongListQuery };
type SongListCountArgs (line 617) | type SongListCountArgs = BaseEndpointArgs & { query: ListCountQuery<Song...
type SongListQuery (line 619) | interface SongListQuery extends BaseQuery<SongListSort> {
type SongListResponse (line 637) | type SongListResponse = BasePaginatedResponse<Song[]>;
type SongListSortMap (line 639) | type SongListSortMap = {
type AlbumArtistListSort (line 714) | enum AlbumArtistListSort {
type AlbumArtistListArgs (line 728) | type AlbumArtistListArgs = BaseEndpointArgs & { query: AlbumArtistListQu...
type AlbumArtistListCountArgs (line 730) | type AlbumArtistListCountArgs = BaseEndpointArgs & {
type AlbumArtistListQuery (line 734) | interface AlbumArtistListQuery extends BaseQuery<AlbumArtistListSort> {
type AlbumArtistListResponse (line 744) | type AlbumArtistListResponse = BasePaginatedResponse<AlbumArtist[]>;
type SongDetailArgs (line 746) | type SongDetailArgs = BaseEndpointArgs & { query: SongDetailQuery };
type SongDetailQuery (line 748) | type SongDetailQuery = { id: string };
type SongDetailResponse (line 751) | type SongDetailResponse = Song;
type AlbumArtistListSortMap (line 753) | type AlbumArtistListSortMap = {
type ArtistListSort (line 803) | enum ArtistListSort {
type AlbumArtistDetailArgs (line 817) | type AlbumArtistDetailArgs = BaseEndpointArgs & { query: AlbumArtistDeta...
type AlbumArtistDetailQuery (line 819) | type AlbumArtistDetailQuery = { id: string };
type AlbumArtistDetailResponse (line 821) | type AlbumArtistDetailResponse = AlbumArtist | null;
type AlbumArtistInfoArgs (line 823) | type AlbumArtistInfoArgs = BaseEndpointArgs & { query: AlbumArtistInfoQu...
type AlbumArtistInfoQuery (line 825) | type AlbumArtistInfoQuery = { id: string; limit?: number };
type AlbumArtistInfoResponse (line 827) | type AlbumArtistInfoResponse = {
type ArtistListArgs (line 833) | type ArtistListArgs = BaseEndpointArgs & { query: ArtistListQuery };
type ArtistListCountArgs (line 835) | type ArtistListCountArgs = BaseEndpointArgs & { query: ListCountQuery<Ar...
type ArtistListQuery (line 837) | interface ArtistListQuery extends BaseQuery<ArtistListSort> {
type ArtistListResponse (line 848) | type ArtistListResponse = BasePaginatedResponse<AlbumArtist[]>;
type ArtistListSortMap (line 850) | type ArtistListSortMap = {
type PlaylistListSort (line 898) | enum PlaylistListSort {
type RadioListSort (line 907) | enum RadioListSort {
type AddToPlaylistArgs (line 912) | type AddToPlaylistArgs = BaseEndpointArgs & {
type AddToPlaylistBody (line 917) | type AddToPlaylistBody = {
type AddToPlaylistQuery (line 921) | type AddToPlaylistQuery = {
type AddToPlaylistResponse (line 926) | type AddToPlaylistResponse = null | undefined;
type CreateInternetRadioStationArgs (line 928) | type CreateInternetRadioStationArgs = BaseEndpointArgs & {
type CreateInternetRadioStationBody (line 932) | type CreateInternetRadioStationBody = {
type CreateInternetRadioStationResponse (line 938) | type CreateInternetRadioStationResponse = null | undefined;
type CreatePlaylistArgs (line 940) | type CreatePlaylistArgs = BaseEndpointArgs & { body: CreatePlaylistBody };
type CreatePlaylistBody (line 942) | type CreatePlaylistBody = {
type CreatePlaylistResponse (line 953) | type CreatePlaylistResponse = undefined | { id: string };
type DeleteInternetRadioStationArgs (line 955) | type DeleteInternetRadioStationArgs = BaseEndpointArgs & {
type DeleteInternetRadioStationQuery (line 959) | type DeleteInternetRadioStationQuery = {
type DeleteInternetRadioStationResponse (line 963) | type DeleteInternetRadioStationResponse = null | undefined;
type DeletePlaylistArgs (line 965) | type DeletePlaylistArgs = BaseEndpointArgs & {
type DeletePlaylistQuery (line 969) | type DeletePlaylistQuery = { id: string };
type DeletePlaylistResponse (line 972) | type DeletePlaylistResponse = null | undefined;
type FavoriteArgs (line 974) | type FavoriteArgs = BaseEndpointArgs & { query: FavoriteQuery };
type FavoriteQuery (line 976) | type FavoriteQuery = {
type FavoriteResponse (line 982) | type FavoriteResponse = null | undefined;
type GetInternetRadioStationsArgs (line 984) | type GetInternetRadioStationsArgs = BaseEndpointArgs;
type GetInternetRadioStationsResponse (line 986) | type GetInternetRadioStationsResponse = InternetRadioStation[];
type InternetRadioStation (line 988) | type InternetRadioStation = {
type PlaylistListArgs (line 995) | type PlaylistListArgs = BaseEndpointArgs & { query: PlaylistListQuery };
type PlaylistListCountArgs (line 997) | type PlaylistListCountArgs = BaseEndpointArgs & { query: ListCountQuery<...
type PlaylistListQuery (line 999) | interface PlaylistListQuery extends BaseQuery<PlaylistListSort> {
type PlaylistListResponse (line 1008) | type PlaylistListResponse = BasePaginatedResponse<Playlist[]>;
type RatingQuery (line 1010) | type RatingQuery = {
type RatingResponse (line 1017) | type RatingResponse = null | undefined;
type RemoveFromPlaylistArgs (line 1019) | type RemoveFromPlaylistArgs = BaseEndpointArgs & {
type RemoveFromPlaylistQuery (line 1023) | type RemoveFromPlaylistQuery = {
type RemoveFromPlaylistResponse (line 1029) | type RemoveFromPlaylistResponse = null | undefined;
type ReplacePlaylistArgs (line 1031) | type ReplacePlaylistArgs = BaseEndpointArgs & {
type ReplacePlaylistBody (line 1036) | type ReplacePlaylistBody = {
type ReplacePlaylistQuery (line 1040) | type ReplacePlaylistQuery = {
type ReplacePlaylistResponse (line 1045) | type ReplacePlaylistResponse = null | undefined;
type SetRatingArgs (line 1047) | type SetRatingArgs = BaseEndpointArgs & { query: RatingQuery };
type ShareItemArgs (line 1049) | type ShareItemArgs = BaseEndpointArgs & { body: ShareItemBody };
type ShareItemBody (line 1051) | type ShareItemBody = {
type ShareItemResponse (line 1060) | type ShareItemResponse = undefined | { id: string };
type UpdateInternetRadioStationArgs (line 1062) | type UpdateInternetRadioStationArgs = BaseEndpointArgs & {
type UpdateInternetRadioStationBody (line 1067) | type UpdateInternetRadioStationBody = {
type UpdateInternetRadioStationQuery (line 1073) | type UpdateInternetRadioStationQuery = {
type UpdateInternetRadioStationResponse (line 1077) | type UpdateInternetRadioStationResponse = null | undefined;
type UpdatePlaylistArgs (line 1079) | type UpdatePlaylistArgs = BaseEndpointArgs & {
type UpdatePlaylistBody (line 1084) | type UpdatePlaylistBody = {
type UpdatePlaylistQuery (line 1095) | type UpdatePlaylistQuery = {
type UpdatePlaylistResponse (line 1100) | type UpdatePlaylistResponse = null | undefined;
type PlaylistListSortMap (line 1102) | type PlaylistListSortMap = {
type UserListSort (line 1135) | enum UserListSort {
type MusicFolderListArgs (line 1139) | type MusicFolderListArgs = BaseEndpointArgs;
type MusicFolderListQuery (line 1141) | type MusicFolderListQuery = null;
type MusicFolderListResponse (line 1144) | type MusicFolderListResponse = BasePaginatedResponse<MusicFolder[]>;
type PlaylistDetailArgs (line 1146) | type PlaylistDetailArgs = BaseEndpointArgs & { query: PlaylistDetailQuer...
type PlaylistDetailQuery (line 1148) | type PlaylistDetailQuery = {
type PlaylistDetailResponse (line 1153) | type PlaylistDetailResponse = Playlist;
type PlaylistSongListArgs (line 1155) | type PlaylistSongListArgs = BaseEndpointArgs & { query: PlaylistSongList...
type PlaylistSongListCountArgs (line 1157) | type PlaylistSongListCountArgs = BaseEndpointArgs & {
type PlaylistSongListQuery (line 1161) | type PlaylistSongListQuery = {
type PlaylistSongListQueryClientSide (line 1165) | type PlaylistSongListQueryClientSide = {
type PlaylistSongListResponse (line 1171) | type PlaylistSongListResponse = BasePaginatedResponse<Song[]>;
type UserListArgs (line 1173) | type UserListArgs = BaseEndpointArgs & { query: UserListQuery };
type UserListQuery (line 1175) | interface UserListQuery extends BaseQuery<UserListSort> {
type UserListResponse (line 1184) | type UserListResponse = BasePaginatedResponse<User[]>;
type UserListSortMap (line 1186) | type UserListSortMap = {
type Played (line 1204) | enum Played {
type ArtistInfoArgs (line 1210) | type ArtistInfoArgs = BaseEndpointArgs & { query: ArtistInfoQuery };
type ArtistInfoQuery (line 1213) | type ArtistInfoQuery = {
type FullLyricsMetadata (line 1219) | type FullLyricsMetadata = Omit<InternetProviderLyricResponse, 'id' | 'ly...
type InternetProviderLyricResponse (line 1226) | type InternetProviderLyricResponse = {
type InternetProviderLyricSearchResponse (line 1234) | type InternetProviderLyricSearchResponse = {
type LyricOverride (line 1243) | type LyricOverride = Omit<InternetProviderLyricResponse, 'lyrics'>;
type LyricsArgs (line 1245) | type LyricsArgs = BaseEndpointArgs & {
type LyricsQuery (line 1249) | type LyricsQuery = {
type LyricsResponse (line 1253) | type LyricsResponse = string | SynchronizedLyricsArray;
type RandomSongListArgs (line 1255) | type RandomSongListArgs = BaseEndpointArgs & {
type RandomSongListQuery (line 1259) | type RandomSongListQuery = {
type RandomSongListResponse (line 1268) | type RandomSongListResponse = SongListResponse;
type ScrobbleArgs (line 1270) | type ScrobbleArgs = BaseEndpointArgs & {
type ScrobbleQuery (line 1274) | type ScrobbleQuery = {
type ScrobbleResponse (line 1283) | type ScrobbleResponse = null;
type SearchAlbumArtistsQuery (line 1285) | type SearchAlbumArtistsQuery = {
type SearchAlbumsQuery (line 1292) | type SearchAlbumsQuery = {
type SearchArgs (line 1299) | type SearchArgs = BaseEndpointArgs & {
type SearchQuery (line 1303) | type SearchQuery = {
type SearchResponse (line 1314) | type SearchResponse = {
type SearchSongsQuery (line 1320) | type SearchSongsQuery = {
type SynchronizedLyricsArray (line 1327) | type SynchronizedLyricsArray = Array<[number, string]>;
type TopSongListArgs (line 1329) | type TopSongListArgs = BaseEndpointArgs & { query: TopSongListQuery };
type TopSongListQuery (line 1331) | type TopSongListQuery = {
type TopSongListResponse (line 1339) | type TopSongListResponse = BasePaginatedResponse<Song[]>;
type LyricSource (line 1345) | enum LyricSource {
type AlbumRadioArgs (line 1352) | type AlbumRadioArgs = BaseEndpointArgs & {
type AlbumRadioQuery (line 1356) | type AlbumRadioQuery = {
type ArtistRadioArgs (line 1361) | type ArtistRadioArgs = BaseEndpointArgs & {
type ArtistRadioQuery (line 1365) | type ArtistRadioQuery = {
type ControllerEndpoint (line 1370) | type ControllerEndpoint = {
type DownloadArgs (line 1441) | type DownloadArgs = BaseEndpointArgs & {
type DownloadQuery (line 1445) | type DownloadQuery = {
type FontData (line 1451) | type FontData = {
type GetQueueArgs (line 1458) | type GetQueueArgs = BaseEndpointArgs;
type GetQueueQuery (line 1460) | interface GetQueueQuery {}
type GetQueueResponse (line 1462) | type GetQueueResponse = {
type ImageArgs (line 1471) | type ImageArgs = BaseEndpointArgs & {
type ImageQuery (line 1476) | type ImageQuery = {
type ImageRequest (line 1482) | type ImageRequest = {
type InternalControllerEndpoint (line 1489) | type InternalControllerEndpoint = {
type LyricGetQuery (line 1594) | type LyricGetQuery = {
type LyricSearchQuery (line 1600) | type LyricSearchQuery = {
type LyricsOverride (line 1607) | type LyricsOverride = Omit<FullLyricsMetadata, 'lyrics'> & { id: string };
type MoveItemArgs (line 1609) | type MoveItemArgs = BaseEndpointArgs & {
type MoveItemQuery (line 1613) | type MoveItemQuery = {
type ReplaceApiClientProps (line 1620) | type ReplaceApiClientProps<T> = BaseEndpointArgsWithServer & Omit<T, 'ap...
type SaveQueueArgs (line 1622) | type SaveQueueArgs = BaseEndpointArgs & {
type SaveQueueQuery (line 1626) | type SaveQueueQuery = {
type ServerInfo (line 1632) | type ServerInfo = {
type ServerInfoArgs (line 1638) | type ServerInfoArgs = BaseEndpointArgs;
type SimilarSongsArgs (line 1640) | type SimilarSongsArgs = BaseEndpointArgs & {
type SimilarSongsQuery (line 1644) | type SimilarSongsQuery = {
type StreamArgs (line 1650) | type StreamArgs = BaseEndpointArgs & {
type StreamQuery (line 1654) | type StreamQuery = {
type StructuredLyric (line 1661) | type StructuredLyric = (StructuredSyncedLyric | StructuredUnsyncedLyric)...
type StructuredLyricsArgs (line 1665) | type StructuredLyricsArgs = BaseEndpointArgs & {
type StructuredSyncedLyric (line 1669) | type StructuredSyncedLyric = Omit<FullLyricsMetadata, 'lyrics'> & {
type StructuredUnsyncedLyric (line 1674) | type StructuredUnsyncedLyric = Omit<FullLyricsMetadata, 'lyrics'> & {
type Tag (line 1679) | type Tag = {
type TagListArgs (line 1684) | type TagListArgs = BaseEndpointArgs & {
type TagListQuery (line 1688) | type TagListQuery = {
type TagListResponse (line 1694) | type TagListResponse = {
type UserInfoArgs (line 1702) | type UserInfoArgs = BaseEndpointArgs & { query: UserInfoQuery };
type UserInfoQuery (line 1704) | type UserInfoQuery = {
type UserInfoResponse (line 1709) | type UserInfoResponse = {
type BaseEndpointArgsWithServer (line 1715) | type BaseEndpointArgsWithServer = {
FILE: src/shared/types/drag-and-drop.ts
type DragTarget (line 5) | enum DragTarget {
type DragOperation (line 31) | enum DragOperation {
type AlbumDragMetadata (line 36) | interface AlbumDragMetadata {
type DragData (line 41) | interface DragData<
FILE: src/shared/types/features-types.ts
type ServerFeature (line 3) | enum ServerFeature {
type ServerFeatures (line 20) | type ServerFeatures = Partial<Record<ServerFeature, number[]>>;
FILE: src/shared/types/remote-types.ts
type ClientAuth (line 4) | interface ClientAuth {
type ClientEvent (line 9) | type ClientEvent =
type ClientFavorite (line 17) | interface ClientFavorite {
type ClientPosition (line 23) | interface ClientPosition {
type ClientRating (line 28) | interface ClientRating {
type ClientSimpleEvent (line 33) | interface ClientSimpleEvent {
type ClientVolume (line 37) | interface ClientVolume {
type ServerError (line 42) | interface ServerError {
type ServerEvent (line 47) | type ServerEvent =
type ServerFavorite (line 60) | interface ServerFavorite {
type ServerPlayStatus (line 65) | interface ServerPlayStatus {
type ServerPosition (line 70) | interface ServerPosition {
type ServerProxy (line 75) | interface ServerProxy {
type ServerRating (line 80) | interface ServerRating {
type ServerRepeat (line 85) | interface ServerRepeat {
type ServerShuffle (line 90) | interface ServerShuffle {
type ServerSong (line 95) | interface ServerSong {
type ServerState (line 100) | interface ServerState {
type ServerVolume (line 105) | interface ServerVolume {
type SongUpdateSocket (line 110) | interface SongUpdateSocket extends Omit<SongState, 'song'> {
FILE: src/shared/types/types.ts
type ItemListKey (line 16) | enum ItemListKey {
type ListDisplayType (line 37) | enum ListDisplayType {
type ListPaginationType (line 44) | enum ListPaginationType {
type Platform (line 49) | enum Platform {
type ServerType (line 56) | enum ServerType {
type CardRoute (line 62) | type CardRoute = {
type CardRow (line 67) | type CardRow<T> = {
type ListPagination (line 74) | type ListPagination = {
type RouteSlug (line 81) | type RouteSlug = {
type AuthState (line 99) | enum AuthState {
type CrossfadeStyle (line 105) | enum CrossfadeStyle {
type FontType (line 116) | enum FontType {
type Play (line 122) | enum Play {
type PlayerQueueType (line 132) | enum PlayerQueueType {
type PlayerRepeat (line 137) | enum PlayerRepeat {
type PlayerShuffle (line 143) | enum PlayerShuffle {
type PlayerStatus (line 149) | enum PlayerStatus {
type PlayerStyle (line 154) | enum PlayerStyle {
type PlayerType (line 159) | enum PlayerType {
type TableColumn (line 164) | enum TableColumn {
type DiscoveredServerItem (line 206) | type DiscoveredServerItem = {
type GridCardData (line 212) | type GridCardData = {
type PlayQueueAddOptions (line 230) | type PlayQueueAddOptions = {
type QueryBuilderGroup (line 242) | type QueryBuilderGroup = {
type QueryBuilderRule (line 249) | type QueryBuilderRule = {
type ServerListItem (line 256) | type ServerListItem = {
type SongState (line 272) | type SongState = {
type TitleTheme (line 282) | type TitleTheme = 'dark' | 'light' | 'system';
type UniqueId (line 284) | interface UniqueId {
type WebAudio (line 288) | type WebAudio = {
FILE: src/shared/utils/double-click-handler.ts
type DoubleClickHandlerOptions (line 3) | interface DoubleClickHandlerOptions<T extends HTMLElement = HTMLElement> {
FILE: src/types/mantine.d.ts
type ExtendedActionIconSize (line 3) | type ExtendedActionIconSize = 'compact-md' | 'compact-sm' | 'compact-xs'...
type ExtendedPillVariant (line 4) | type ExtendedPillVariant = 'outline' | PillVariant;
type ActionIconProps (line 7) | interface ActionIconProps {
type PillProps (line 11) | interface PillProps {
Condensed preview — 1083 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (6,845K chars).
[
{
"path": ".dockerignore",
"chars": 41,
"preview": "node_modules\nDockerfile\ndocker-compose.*\n"
},
{
"path": ".editorconfig",
"chars": 147,
"preview": "root = true\n\n[*]\ncharset = utf-8\nindent_style = space\nindent_size = 4\nend_of_line = lf\ninsert_final_newline = true\ntrim_"
},
{
"path": ".gitattributes",
"chars": 203,
"preview": "* text eol=lf\n*.exe binary\n*.png binary\n*.jpg binary\n*.jpeg binary\n*.ico binary\n*.icns binary\n*.webp "
},
{
"path": ".github/FUNDING.yml",
"chars": 62,
"preview": "# These are supported funding model platforms\n\nko_fi: jeffvli\n"
},
{
"path": ".github/ISSUE_TEMPLATE/01-feature_request.yml",
"chars": 892,
"preview": "name: Feature request\ndescription: Request a feature to be added to Feishin\ntitle: '[Feature]: '\nlabels: ['enhancement']"
},
{
"path": ".github/ISSUE_TEMPLATE/02-bug_report.yml",
"chars": 2342,
"preview": "name: Bug report\ndescription: You're having technical issues.\ntitle: '[Bug]: '\nlabels: ['bug']\nbody:\n - type: checkbo"
},
{
"path": ".github/ISSUE_TEMPLATE/config.yml",
"chars": 525,
"preview": "blank_issues_enabled: false\ncontact_links:\n - name: Questions or help\n url: https://github.com/jeffvli/feishin/d"
},
{
"path": ".github/config.yml",
"chars": 131,
"preview": "requiredHeaders:\n - Prerequisites\n - Expected Behavior\n - Current Behavior\n - Possible Solution\n - Your E"
},
{
"path": ".github/stale.yml",
"chars": 692,
"preview": "# Number of days of inactivity before an issue becomes stale\ndaysUntilStale: 60\n# Number of days of inactivity before a "
},
{
"path": ".github/workflows/publish-alpha.yml",
"chars": 7549,
"preview": "# Alpha builds published to Cloudflare R2 with date versioning (e.g. 1.0.0-alpha-20260205).\n# Required repo secrets: R2_"
},
{
"path": ".github/workflows/publish-beta.yml",
"chars": 17774,
"preview": "name: Publish Beta (Manual)\n\non:\n workflow_dispatch:\n inputs:\n version:\n description"
},
{
"path": ".github/workflows/publish-docker-auto.yml",
"chars": 2018,
"preview": "# Referenced from: https://docs.github.com/en/actions/publishing-packages/publishing-docker-images#introduction\nname: Pu"
},
{
"path": ".github/workflows/publish-docker.yml",
"chars": 1636,
"preview": "# Referenced from: https://docs.github.com/en/actions/publishing-packages/publishing-docker-images#introduction\nname: Pu"
},
{
"path": ".github/workflows/publish-linux.yml",
"chars": 1345,
"preview": "name: Publish Linux (Manual)\n\non: workflow_dispatch\n\njobs:\n publish:\n runs-on: ${{ matrix.os }}\n\n strat"
},
{
"path": ".github/workflows/publish-macos.yml",
"chars": 904,
"preview": "name: Publish macOS (Manual)\n\non: workflow_dispatch\n\njobs:\n publish:\n runs-on: ${{ matrix.os }}\n\n strat"
},
{
"path": ".github/workflows/publish-pr-comment.yml",
"chars": 2938,
"preview": "name: Comment on pull request\non:\n workflow_run:\n workflows: ['Publish (PR)']\n types: [completed]\njobs:"
},
{
"path": ".github/workflows/publish-pr.yml",
"chars": 3666,
"preview": "name: Publish (PR)\n\non:\n workflow_dispatch:\n pull_request:\n branches:\n - development\n pat"
},
{
"path": ".github/workflows/publish-windows.yml",
"chars": 908,
"preview": "name: Publish Windows (Manual)\n\non: workflow_dispatch\n\njobs:\n publish:\n runs-on: ${{ matrix.os }}\n\n str"
},
{
"path": ".github/workflows/publish-winget.yml",
"chars": 472,
"preview": "name: Publish release to WinGet\non:\n release:\n types: [released]\n workflow_dispatch:\n inputs:\n tag_name:\n "
},
{
"path": ".github/workflows/publish.yml",
"chars": 2433,
"preview": "name: Publish (Manual)\n\non: workflow_dispatch\n\njobs:\n publish:\n runs-on: ${{ matrix.os }}\n\n strategy:\n "
},
{
"path": ".github/workflows/stale.yml",
"chars": 2074,
"preview": "name: 'Close stale issues and PRs'\non:\n workflow_dispatch:\n schedule:\n - cron: '30 1 * * *'\npermissions:\n "
},
{
"path": ".github/workflows/test.yml",
"chars": 462,
"preview": "name: Test\n\non: [push, pull_request]\n\njobs:\n lint:\n runs-on: ubuntu-latest\n\n steps:\n - name:"
},
{
"path": ".gitignore",
"chars": 60,
"preview": "node_modules\ndist\nout\n.DS_Store\n.eslintcache\n*.log*\nrelease\n"
},
{
"path": ".npmrc",
"chars": 22,
"preview": "legacy-peer-deps=true\n"
},
{
"path": ".prettierignore",
"chars": 65,
"preview": "out\ndist\npnpm-lock.yaml\nLICENSE.md\ntsconfig.json\ntsconfig.*.json\n"
},
{
"path": ".prettierrc.yaml",
"chars": 270,
"preview": "singleQuote: true\nsemi: true\nprintWidth: 100\ntabWidth: 4\ntrailingComma: all\nuseTabs: false\narrowParens: always\nproseWrap"
},
{
"path": ".stylelintrc.json",
"chars": 810,
"preview": "{\n \"extends\": [\n \"stylelint-config-standard\",\n \"stylelint-config-css-modules\",\n \"stylelint-confi"
},
{
"path": ".vscode/extensions.json",
"chars": 52,
"preview": "{\n \"recommendations\": [\"dbaeumer.vscode-eslint\"]\n}\n"
},
{
"path": ".vscode/launch.json",
"chars": 915,
"preview": "{\n \"version\": \"0.2.0\",\n \"configurations\": [\n {\n \"name\": \"Debug Main Process\",\n \"type\": \"node\",\n \"req"
},
{
"path": ".vscode/settings.json",
"chars": 2979,
"preview": "{\n \"[typescript]\": {\n \"editor.defaultFormatter\": \"esbenp.prettier-vscode\"\n },\n \"[javascript]\": {\n "
},
{
"path": "CHANGELOG.md",
"chars": 252,
"preview": "# Changelog\n\nAll notable changes to this project will be documented in this file.\n\nThe format is based on [Keep a Change"
},
{
"path": "Dockerfile",
"chars": 782,
"preview": "# --- Builder stage\nFROM node:23-alpine AS builder\nWORKDIR /app\n\n# Copy package.json first to cache node_modules\nCOPY pa"
},
{
"path": "LICENSE",
"chars": 35147,
"preview": " GNU GENERAL PUBLIC LICENSE\n Version 3, 29 June 2007\n\n Copyright (C) 2007 Free "
},
{
"path": "README.md",
"chars": 12401,
"preview": "<img src=\"assets/icons/icon.png\" alt=\"logo\" title=\"feishin\" align=\"right\" height=\"60px\" width=\"60px\" />\n\n# Feishin\n\n <p"
},
{
"path": "assets/assets.d.ts",
"chars": 538,
"preview": "type Styles = Record<string, string>;\n\ndeclare module '*.svg' {\n const content: string;\n export default content;\n}"
},
{
"path": "assets/entitlements.mac.plist",
"chars": 415,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "dev-app-update.yml",
"chars": 93,
"preview": "provider: generic\nurl: https://example.com/auto-updates\nupdaterCacheDirName: feishin-updater\n"
},
{
"path": "docker-compose.yaml",
"chars": 1036,
"preview": "services:\n feishin:\n container_name: feishin\n image: \"ghcr.io/jeffvli/feishin:latest\"\n restart: "
},
{
"path": "docs/ENV_SETTINGS.md",
"chars": 10327,
"preview": "# Environment variables for settings (web / Docker)\n\nThese variables override app settings **on first run** when no pers"
},
{
"path": "electron-builder-alpha.yml",
"chars": 1493,
"preview": "appId: org.jeffvli.feishin\nproductName: Feishin\nartifactName: ${productName}-${version}-${os}-${arch}.${ext}\nelectronVer"
},
{
"path": "electron-builder-beta.yml",
"chars": 1446,
"preview": "appId: org.jeffvli.feishin\nproductName: Feishin\nartifactName: ${productName}-${version}-${os}-${arch}.${ext}\nelectronVer"
},
{
"path": "electron-builder.yml",
"chars": 1508,
"preview": "appId: org.jeffvli.feishin\nproductName: Feishin\nartifactName: ${productName}-${version}-${os}-${arch}.${ext}\nelectronVer"
},
{
"path": "electron.vite.config.ts",
"chars": 2384,
"preview": "import react from '@vitejs/plugin-react';\nimport { externalizeDepsPlugin, UserConfig } from 'electron-vite';\nimport { re"
},
{
"path": "eslint.config.mjs",
"chars": 2031,
"preview": "import eslintConfigPrettier from '@electron-toolkit/eslint-config-prettier';\nimport tseslint from '@electron-toolkit/esl"
},
{
"path": "feishin.desktop.tmpl",
"chars": 396,
"preview": "[Desktop Entry]\nName=Feishin\nGenericName=Music player\nExec=${FEISHIN_DESKTOP_EXECUTABLE} ${FEISHIN_DESKTOP_ARGS}\nTryExec"
},
{
"path": "install-feishin-appimage",
"chars": 3278,
"preview": "#!/bin/sh\n\nset -eu\n\nif [ \"$#\" -lt 1 ]; then\n echo \"Usage: $0 <installation-directory> <option>\"\n echo \"Options:\"\n "
},
{
"path": "ng.conf.template",
"chars": 837,
"preview": "server {\n listen 9180;\n listen [::]:9180;\n sendfile on;\n default_type application/octet-stream;\n\n gzip on;\n gzip_h"
},
{
"path": "org.jeffvli.feishin.metainfo.xml",
"chars": 5589,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<component type=\"desktop-application\">\n <id>org.jeffvli.feishin</id>\n <name>Fei"
},
{
"path": "package.json",
"chars": 9034,
"preview": "{\n \"name\": \"feishin\",\n \"version\": \"1.9.0\",\n \"description\": \"A modern self-hosted music player.\",\n \"keywords\""
},
{
"path": "postcss.config.cjs",
"chars": 513,
"preview": "module.exports = {\n plugins: {\n 'postcss-preset-mantine': {},\n 'postcss-simple-vars': {\n var"
},
{
"path": "remote.vite.config.ts",
"chars": 1932,
"preview": "import react from '@vitejs/plugin-react';\nimport path from 'path';\nimport { defineConfig, normalizePath } from 'vite';\ni"
},
{
"path": "scripts/after-all-artifact-build.mjs",
"chars": 1649,
"preview": "import { execSync } from 'child_process';\nimport path from 'path';\nimport { fileURLToPath } from 'url';\n\nconst __filenam"
},
{
"path": "scripts/update-app-stream.mjs",
"chars": 2054,
"preview": "import { XMLBuilder, XMLParser } from 'fast-xml-parser';\nimport fs from 'fs';\nimport path from 'path';\n\nconst args = pro"
},
{
"path": "settings.js.template",
"chars": 5397,
"preview": "\"use strict\";\n\nwindow.SERVER_URL = \"${SERVER_URL}\";\nwindow.REMOTE_URL = \"${REMOTE_URL}\";\nwindow.SERVER_NAME = \"${SERVER_"
},
{
"path": "src/i18n/i18n.ts",
"chars": 5961,
"preview": "import { PostProcessorModule, TOptions } from 'i18next';\nimport i18n from 'i18next';\nimport { initReactI18next } from 'r"
},
{
"path": "src/i18n/i18next-parser.config.js",
"chars": 1256,
"preview": "// Reference: https://github.com/i18next/i18next-parser#options\n\nmodule.exports = {\n contextSeparator: '_',\n creat"
},
{
"path": "src/i18n/locales/ar.json",
"chars": 5698,
"preview": "{\n \"action\": {\n \"addToFavorites\": \"إضافة الى $t(entity.favorite, {\\\"count\\\": 2})\",\n \"addToPlaylist\": \"إ"
},
{
"path": "src/i18n/locales/ca.json",
"chars": 76305,
"preview": "{\n \"page\": {\n \"sidebar\": {\n \"myLibrary\": \"La meva llibreria\",\n \"albumArtists\": \"$t(entit"
},
{
"path": "src/i18n/locales/cs.json",
"chars": 73886,
"preview": "{\n \"player\": {\n \"repeat_all\": \"opakovat vše\",\n \"stop\": \"zastavit\",\n \"repeat\": \"opakovat\",\n "
},
{
"path": "src/i18n/locales/da.json",
"chars": 68325,
"preview": "{\n \"action\": {\n \"addToFavorites\": \"tilføj til $t(entity.favorite, {\\\"count\\\": 2})\",\n \"addToPlaylist\": \""
},
{
"path": "src/i18n/locales/de.json",
"chars": 71353,
"preview": "{\n \"action\": {\n \"editPlaylist\": \"$t(entity.playlist, {\\\"count\\\": 1}) bearbeiten\",\n \"clearQueue\": \"Wiede"
},
{
"path": "src/i18n/locales/en.json",
"chars": 69879,
"preview": "{\n \"action\": {\n \"addToFavorites\": \"add to $t(entity.favorite, {\\\"count\\\": 2})\",\n \"addToPlaylist\": \"add "
},
{
"path": "src/i18n/locales/es.json",
"chars": 78246,
"preview": "{\n \"player\": {\n \"repeat_all\": \"repetir todo\",\n \"stop\": \"detener\",\n \"repeat\": \"repetir\",\n "
},
{
"path": "src/i18n/locales/eu.json",
"chars": 59679,
"preview": "{\n \"action\": {\n \"deselectAll\": \"deshautatu dena\",\n \"editPlaylist\": \"editatu $t(entity.playlist, {\\\"coun"
},
{
"path": "src/i18n/locales/fa.json",
"chars": 29065,
"preview": "{\n \"player\": {\n \"repeat_all\": \"تکرار همه\",\n \"stop\": \"توقف\",\n \"repeat\": \"تکرار\",\n \"skip\": "
},
{
"path": "src/i18n/locales/fi.json",
"chars": 53590,
"preview": "{\n \"common\": {\n \"size\": \"koko\",\n \"search\": \"etsi\",\n \"sortOrder\": \"järjestys\",\n \"setting_o"
},
{
"path": "src/i18n/locales/fr.json",
"chars": 77496,
"preview": "{\n \"player\": {\n \"repeat_all\": \"répèter tout\",\n \"stop\": \"stop\",\n \"repeat\": \"répéter\",\n \"qu"
},
{
"path": "src/i18n/locales/hu.json",
"chars": 59116,
"preview": "{\n \"action\": {\n \"moveToNext\": \"ugrás a következőre\",\n \"deletePlaylist\": \"$t(entity.playlist, {\\\"count\\\""
},
{
"path": "src/i18n/locales/id.json",
"chars": 69711,
"preview": "{\n \"action\": {\n \"createPlaylist\": \"buat $t(entity.playlist, {\\\"count\\\": 1})\",\n \"toggleSmartPlaylistEdit"
},
{
"path": "src/i18n/locales/it.json",
"chars": 56917,
"preview": "{\n \"action\": {\n \"editPlaylist\": \"modifica $t(entity.playlist, {\\\"count\\\": 1})\",\n \"goToPage\": \"vai alla "
},
{
"path": "src/i18n/locales/ja.json",
"chars": 58566,
"preview": "{\n \"player\": {\n \"repeat_all\": \"全曲リピート\",\n \"stop\": \"停止\",\n \"repeat\": \"リピート\",\n \"queue_remove\""
},
{
"path": "src/i18n/locales/ko.json",
"chars": 19132,
"preview": "{\n \"action\": {\n \"createPlaylist\": \"$t(entity.playlist, {\\\"count\\\": 1}) 생성\",\n \"addToFavorites\": \"$t(enti"
},
{
"path": "src/i18n/locales/nb-NO.json",
"chars": 28895,
"preview": "{\n \"action\": {\n \"openIn\": {\n \"lastfm\": \"Åpne i Last.fm\",\n \"musicbrainz\": \"Åpne i MusicBr"
},
{
"path": "src/i18n/locales/nl.json",
"chars": 71683,
"preview": "{\n \"action\": {\n \"editPlaylist\": \"pas $t(entity.playlist, {\\\"count\\\": 1}) aan\",\n \"goToPage\": \"ga naar pa"
},
{
"path": "src/i18n/locales/pl.json",
"chars": 74080,
"preview": "{\n \"action\": {\n \"editPlaylist\": \"edytuj $t(entity.playlist, {\\\"count\\\": 1})\",\n \"goToPage\": \"idź do stro"
},
{
"path": "src/i18n/locales/pt-BR.json",
"chars": 46660,
"preview": "{\n \"common\": {\n \"backward\": \"para trás\",\n \"areYouSure\": \"tem certeza?\",\n \"add\": \"adicionar\",\n "
},
{
"path": "src/i18n/locales/pt.json",
"chars": 25455,
"preview": "{\n \"action\": {\n \"addToFavorites\": \"adicionar a $t(entity.favorite, {\\\"count\\\": 2})\",\n \"addToPlaylist\": "
},
{
"path": "src/i18n/locales/ro.json",
"chars": 400,
"preview": "{\n \"common\": {\n \"confirm\": \"confirmă\",\n \"create\": \"creează\",\n \"biography\": \"biografie\",\n "
},
{
"path": "src/i18n/locales/ru.json",
"chars": 63103,
"preview": "{\n \"action\": {\n \"editPlaylist\": \"редактировать $t(entity.playlist, {\\\"count\\\": 1})\",\n \"goToPage\": \"пере"
},
{
"path": "src/i18n/locales/sk.json",
"chars": 44329,
"preview": "{\n \"action\": {\n \"addToFavorites\": \"pridať do $t(entity.favorite, {\\\"count\\\": 2})\",\n \"addToPlaylist\": \"p"
},
{
"path": "src/i18n/locales/sl.json",
"chars": 32188,
"preview": "{\n \"action\": {\n \"addToFavorites\": \"dodaj na $t(entity.favorite, {\\\"count\\\": 2})\",\n \"addToPlaylist\": \"do"
},
{
"path": "src/i18n/locales/sr.json",
"chars": 31120,
"preview": "{\n \"player\": {\n \"repeat_all\": \"ponavljaj sve\",\n \"stop\": \"zaustavi\",\n \"repeat\": \"ponavljaj jednu\""
},
{
"path": "src/i18n/locales/sv.json",
"chars": 20491,
"preview": "{\n \"action\": {\n \"editPlaylist\": \"redigera $t(entity.playlist, {\\\"count\\\": 1})\",\n \"goToPage\": \"gå till s"
},
{
"path": "src/i18n/locales/ta.json",
"chars": 74120,
"preview": "{\n \"action\": {\n \"addToFavorites\": \"$t(entity.favorite, {\\\"count\\\": 2}) இல் சேர்க்கவும்\",\n \"clearQueue\":"
},
{
"path": "src/i18n/locales/tr.json",
"chars": 44558,
"preview": "{\n \"action\": {\n \"moveToBottom\": \"alttakine geç\",\n \"moveToTop\": \"başa dön\",\n \"removeFromFavorites"
},
{
"path": "src/i18n/locales/uk.json",
"chars": 24566,
"preview": "{\n \"action\": {\n \"addToFavorites\": \"додати до $t(entity.favorite, {\\\"count\\\": 2})\",\n \"addOrRemoveFromSel"
},
{
"path": "src/i18n/locales/zh-Hans.json",
"chars": 52487,
"preview": "{\n \"action\": {\n \"editPlaylist\": \"编辑 $t(entity.playlist, {\\\"count\\\": 1})\",\n \"moveToTop\": \"移至顶部\",\n "
},
{
"path": "src/i18n/locales/zh-Hant.json",
"chars": 53489,
"preview": "{\n \"common\": {\n \"backward\": \"返回\",\n \"biography\": \"簡介\",\n \"bitrate\": \"位元率\",\n \"bpm\": \"bpm\",\n "
},
{
"path": "src/main/features/core/autodiscover/index.ts",
"chars": 1656,
"preview": "import { createSocket } from 'dgram';\nimport { ipcMain } from 'electron';\n\nimport { DiscoveredServerItem, ServerType } f"
},
{
"path": "src/main/features/core/discord-rpc/index.ts",
"chars": 1399,
"preview": "import { Client, SetActivity } from '@xhayper/discord-rpc';\nimport { ipcMain } from 'electron';\n\nconst FEISHIN_DISCORD_A"
},
{
"path": "src/main/features/core/index.ts",
"chars": 127,
"preview": "import './autodiscover';\nimport './lyrics';\nimport './player';\nimport './remote';\nimport './settings';\nimport './discord"
},
{
"path": "src/main/features/core/lyrics/genius.ts",
"chars": 5204,
"preview": "import axios, { AxiosResponse } from 'axios';\nimport { load } from 'cheerio';\n\nimport {\n InternetProviderLyricRespons"
},
{
"path": "src/main/features/core/lyrics/index.ts",
"chars": 6695,
"preview": "import { ipcMain } from 'electron';\n\nimport { store } from '../settings';\nimport { getLyricsBySongId as getGenius, getSe"
},
{
"path": "src/main/features/core/lyrics/lrclib.ts",
"chars": 3449,
"preview": "// Credits to https://github.com/tranxuanthang/lrcget for API implementation\nimport axios, { AxiosResponse } from 'axios"
},
{
"path": "src/main/features/core/lyrics/netease.ts",
"chars": 5818,
"preview": "import axios, { AxiosResponse } from 'axios';\n\nimport {\n InternetProviderLyricResponse,\n InternetProviderLyricSear"
},
{
"path": "src/main/features/core/lyrics/shared.ts",
"chars": 3231,
"preview": "import Fuse, { FuseResult, IFuseOptions } from 'fuse.js';\n\nimport {\n InternetProviderLyricSearchResponse,\n LyricSe"
},
{
"path": "src/main/features/core/lyrics/simpmusic.ts",
"chars": 3373,
"preview": "import axios, { AxiosResponse } from 'axios';\n\nimport {\n InternetProviderLyricResponse,\n InternetProviderLyricSear"
},
{
"path": "src/main/features/core/player/index.ts",
"chars": 22180,
"preview": "import console from 'console';\nimport { app, ipcMain } from 'electron';\nimport { rm } from 'fs/promises';\nimport uniq fr"
},
{
"path": "src/main/features/core/player/media-keys.ts",
"chars": 2073,
"preview": "import { BrowserWindow, globalShortcut, systemPreferences } from 'electron';\n\nimport { isLinux, isMacOS } from '../../.."
},
{
"path": "src/main/features/core/remote/index.ts",
"chars": 22686,
"preview": "import axios from 'axios';\nimport { app, ipcMain } from 'electron';\nimport { promises, Stats } from 'fs';\nimport { readF"
},
{
"path": "src/main/features/core/remote/manifest.json",
"chars": 393,
"preview": "{\n \"name\": \"Feishin Remote\",\n \"short_name\": \"Feishin Remote\",\n \"start_url\": \"/\",\n \"background_color\": \"#0001"
},
{
"path": "src/main/features/core/settings/index.ts",
"chars": 3498,
"preview": "import type { TitleTheme } from '/@/shared/types/types';\n\nimport { app, dialog, ipcMain, nativeTheme, OpenDialogOptions,"
},
{
"path": "src/main/features/darwin/dock-menu.ts",
"chars": 1438,
"preview": "import { app, ipcMain, Menu } from 'electron';\n\nimport { getMainWindow } from '/@/main/index';\nimport { PlayerStatus } f"
},
{
"path": "src/main/features/darwin/index.ts",
"chars": 22,
"preview": "import './dock-menu';\n"
},
{
"path": "src/main/features/index.ts",
"chars": 50,
"preview": "import './core';\nimport(`./${process.platform}`);\n"
},
{
"path": "src/main/features/linux/index.ts",
"chars": 18,
"preview": "import './mpris';\n"
},
{
"path": "src/main/features/linux/mpris.ts",
"chars": 5829,
"preview": "import { ipcMain } from 'electron';\nimport Player from 'mpris-service';\n\nimport { getMainWindow } from '/@/main/index';\n"
},
{
"path": "src/main/features/win32/index.ts",
"chars": 0,
"preview": ""
},
{
"path": "src/main/index.ts",
"chars": 31381,
"preview": "import type { UpdateCheckResult } from 'electron-updater';\n\nimport { is } from '@electron-toolkit/utils';\nimport {\n a"
},
{
"path": "src/main/menu.ts",
"chars": 10906,
"preview": "import { app, BrowserWindow, Menu, MenuItemConstructorOptions, shell } from 'electron';\n\ninterface DarwinMenuItemConstru"
},
{
"path": "src/main/utils.ts",
"chars": 2604,
"preview": "import log from 'electron-log/main';\nimport path from 'path';\nimport process from 'process';\nimport { URL } from 'url';\n"
},
{
"path": "src/preload/autodiscover.ts",
"chars": 599,
"preview": "import { ipcRenderer } from 'electron';\n\nimport { DiscoveredServerItem } from '../shared/types/types';\n\nconst discover ="
},
{
"path": "src/preload/browser.ts",
"chars": 711,
"preview": "import { ipcRenderer } from 'electron';\n\nconst exit = () => {\n ipcRenderer.send('window-close');\n};\n\nconst maximize ="
},
{
"path": "src/preload/discord-rpc.ts",
"chars": 789,
"preview": "import { SetActivity } from '@xhayper/discord-rpc';\nimport { ipcRenderer } from 'electron';\n\nconst initialize = (clientI"
},
{
"path": "src/preload/index.d.ts",
"chars": 444,
"preview": "import { ElectronAPI } from '@electron-toolkit/preload';\n\nimport { PreloadApi } from './index';\n\ndeclare global {\n in"
},
{
"path": "src/preload/index.ts",
"chars": 1248,
"preview": "import { electronAPI } from '@electron-toolkit/preload';\nimport { contextBridge } from 'electron';\n\nimport { autodiscove"
},
{
"path": "src/preload/ipc.ts",
"chars": 746,
"preview": "import { ipcRenderer } from 'electron';\n\nconst removeAllListeners = (channel: string) => {\n ipcRenderer.removeAllList"
},
{
"path": "src/preload/local-settings.ts",
"chars": 2814,
"preview": "import { ipcRenderer, IpcRendererEvent, OpenDialogOptions, webFrame } from 'electron';\n\nimport { TitleTheme } from '/@/s"
},
{
"path": "src/preload/lyrics.ts",
"chars": 900,
"preview": "import { ipcRenderer } from 'electron';\n\nimport {\n InternetProviderLyricSearchResponse,\n LyricGetQuery,\n LyricS"
},
{
"path": "src/preload/mpris.ts",
"chars": 1963,
"preview": "import { ipcRenderer, IpcRendererEvent } from 'electron';\n\nimport { QueueSong } from '/@/shared/types/domain-types';\nimp"
},
{
"path": "src/preload/mpv-player.ts",
"chars": 5858,
"preview": "import { ipcRenderer, IpcRendererEvent } from 'electron';\n\nimport { PlayerData } from '/@/shared/types/domain-types';\n\nc"
},
{
"path": "src/preload/remote.ts",
"chars": 3050,
"preview": "import { ipcRenderer, IpcRendererEvent } from 'electron';\n\nimport { QueueSong } from '/@/shared/types/domain-types';\nimp"
},
{
"path": "src/preload/utils.ts",
"chars": 2074,
"preview": "import { ipcRenderer, IpcRendererEvent, webFrame } from 'electron';\n\nimport { disableAutoUpdates, isLinux, isMacOS, isWi"
},
{
"path": "src/remote/app.tsx",
"chars": 841,
"preview": "import { MantineProvider } from '@mantine/core';\nimport '@mantine/core/styles.css';\nimport '@mantine/notifications/style"
},
{
"path": "src/remote/components/buttons/image-button.tsx",
"chars": 647,
"preview": "import { CiImageOff, CiImageOn } from 'react-icons/ci';\n\nimport { useShowImage, useToggleShowImage } from '/@/remote/sto"
},
{
"path": "src/remote/components/buttons/reconnect-button.tsx",
"chars": 733,
"preview": "import { RiRestartLine } from 'react-icons/ri';\n\nimport { useConnected, useReconnect } from '/@/remote/store';\nimport { "
},
{
"path": "src/remote/components/buttons/theme-button.tsx",
"chars": 687,
"preview": "import { useIsDark, useToggleDark } from '/@/remote/store';\nimport { ActionIcon } from '/@/shared/components/action-icon"
},
{
"path": "src/remote/components/player-image.module.css",
"chars": 156,
"preview": ".container {\n width: 100%;\n height: 40vh;\n aspect-ratio: 1/1;\n object-fit: var(--theme-image-fit);\n borde"
},
{
"path": "src/remote/components/player-image.tsx",
"chars": 446,
"preview": "import styles from './player-image.module.css';\n\nimport { useSend } from '/@/remote/store';\n\ninterface PlayerImageProps "
},
{
"path": "src/remote/components/remote-container.module.css",
"chars": 0,
"preview": ""
},
{
"path": "src/remote/components/remote-container.tsx",
"chars": 8763,
"preview": "import formatDuration from 'format-duration';\nimport debounce from 'lodash/debounce';\nimport { useCallback } from 'react"
},
{
"path": "src/remote/components/shell.tsx",
"chars": 2098,
"preview": "import { AppShell, Flex, Grid, Image } from '@mantine/core';\n\nimport { ImageButton } from '/@/remote/components/buttons/"
},
{
"path": "src/remote/components/wrapped-slider.tsx",
"chars": 2658,
"preview": "import { rem, Slider, SliderProps } from '@mantine/core';\nimport { ReactNode, useState } from 'react';\n\nimport { Group }"
},
{
"path": "src/remote/index.html",
"chars": 1005,
"preview": "<!DOCTYPE html>\n<html>\n\n<head>\n <meta charset=\"utf-8\" />\n <meta http-equiv=\"Content-Security-Policy\" />\n <meta "
},
{
"path": "src/remote/index.tsx",
"chars": 212,
"preview": "import { createRoot } from 'react-dom/client';\n\nimport { App } from '/@/remote/app';\n\nconst container = document.getElem"
},
{
"path": "src/remote/manifest.json",
"chars": 395,
"preview": "{\n \"name\": \"Feishin Remote\",\n \"short_name\": \"Feishin Remote\",\n \"start_url\": \"/\",\n \"background_color\": \"#FFDC"
},
{
"path": "src/remote/service-worker.ts",
"chars": 1179,
"preview": "/// <reference lib=\"WebWorker\" />\n\nexport type {};\n\ndeclare const self: ServiceWorkerGlobalScope;\n\nconst url = new URL(l"
},
{
"path": "src/remote/store/index.ts",
"chars": 19641,
"preview": "import merge from 'lodash/merge';\nimport { devtools, persist } from 'zustand/middleware';\nimport { immer } from 'zustand"
},
{
"path": "src/remote/worker.js",
"chars": 0,
"preview": ""
},
{
"path": "src/renderer/api/controller.ts",
"chars": 29754,
"preview": "import i18n from '/@/i18n/i18n';\nimport { JellyfinController } from '/@/renderer/api/jellyfin/jellyfin-controller';\nimpo"
},
{
"path": "src/renderer/api/index.ts",
"chars": 98,
"preview": "import { controller } from '/@/renderer/api/controller';\n\nexport const api = {\n controller,\n};\n"
},
{
"path": "src/renderer/api/jellyfin/jellyfin-api.ts",
"chars": 13763,
"preview": "import { initClient, initContract } from '@ts-rest/core';\nimport axios, { AxiosError, AxiosResponse, isAxiosError, Metho"
},
{
"path": "src/renderer/api/jellyfin/jellyfin-controller.ts",
"chars": 62288,
"preview": "import { set } from 'idb-keyval';\nimport chunk from 'lodash/chunk';\nimport filter from 'lodash/filter';\nimport orderBy f"
},
{
"path": "src/renderer/api/navidrome/navidrome-api.ts",
"chars": 16015,
"preview": "import { initClient, initContract } from '@ts-rest/core';\nimport axios, { AxiosError, AxiosResponse, isAxiosError, Metho"
},
{
"path": "src/renderer/api/navidrome/navidrome-controller.ts",
"chars": 39982,
"preview": "import { set } from 'idb-keyval';\nimport orderBy from 'lodash/orderBy';\n\nimport { ndApiClient } from '/@/renderer/api/na"
},
{
"path": "src/renderer/api/query-keys.ts",
"chars": 16222,
"preview": "import type {\n AlbumArtistDetailQuery,\n AlbumArtistInfoQuery,\n AlbumArtistListQuery,\n AlbumDetailQuery,\n "
},
{
"path": "src/renderer/api/subsonic/subsonic-api.ts",
"chars": 14021,
"preview": "import { initClient, initContract } from '@ts-rest/core';\nimport axios, { AxiosError, AxiosRequestConfig, AxiosResponse,"
},
{
"path": "src/renderer/api/subsonic/subsonic-controller.ts",
"chars": 69942,
"preview": "import type { ServerInferResponses } from '@ts-rest/core';\n\nimport dayjs from 'dayjs';\nimport { set } from 'idb-keyval';"
},
{
"path": "src/renderer/api/utils-list-count.ts",
"chars": 2320,
"preview": "import { QueryClient } from '@tanstack/react-query';\n\nimport { getServerById } from '/@/renderer/store';\nimport { Server"
},
{
"path": "src/renderer/api/utils-music-folder.ts",
"chars": 668,
"preview": "import { ServerListItemWithCredential } from '/@/shared/types/domain-types';\n\nexport const mergeMusicFolderId = <T exten"
},
{
"path": "src/renderer/api/utils.ts",
"chars": 656,
"preview": "import { useAuthStore } from '/@/renderer/store';\nimport { toast } from '/@/shared/components/toast/toast';\nimport { Ser"
},
{
"path": "src/renderer/app.tsx",
"chars": 4282,
"preview": "/* eslint-disable perfectionist/sort-imports */\nimport { MantineProvider } from '@mantine/core';\nimport { Notifications "
},
{
"path": "src/renderer/assets/assets.d.ts",
"chars": 538,
"preview": "type Styles = Record<string, string>;\n\ndeclare module '*.svg' {\n const content: string;\n export default content;\n}"
},
{
"path": "src/renderer/assets/entitlements.mac.plist",
"chars": 333,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "src/renderer/components/drag-preview/drag-preview.module.css",
"chars": 2320,
"preview": ".container {\n position: relative;\n pointer-events: none;\n user-select: none;\n transform-style: preserve-3d;\n"
},
{
"path": "src/renderer/components/drag-preview/drag-preview.tsx",
"chars": 3157,
"preview": "import { memo } from 'react';\nimport { useTranslation } from 'react-i18next';\n\nimport styles from './drag-preview.module"
},
{
"path": "src/renderer/components/export-import-settings-modal/export-import-settings-modal.tsx",
"chars": 4576,
"preview": "import { t } from 'i18next';\nimport { useCallback, useState } from 'react';\nimport { ZodError } from 'zod';\n\nimport { Di"
},
{
"path": "src/renderer/components/feature-carousel/feature-carousel.module.css",
"chars": 8474,
"preview": ".carousel-container {\n position: relative;\n width: 100%;\n margin-bottom: var(--theme-spacing-md);\n container"
},
{
"path": "src/renderer/components/feature-carousel/feature-carousel.tsx",
"chars": 11800,
"preview": "import type { MouseEvent } from 'react';\n\nimport { AnimatePresence, motion } from 'motion/react';\nimport { useCallback, "
},
{
"path": "src/renderer/components/feature-carousel/single-feature-carousel.tsx",
"chars": 12337,
"preview": "import type { MouseEvent } from 'react';\n\nimport { AnimatePresence, motion } from 'motion/react';\nimport { useCallback, "
},
{
"path": "src/renderer/components/grid-carousel/grid-carousel-v2.tsx",
"chars": 16270,
"preview": "import type { Variants } from 'motion/react';\nimport type { ReactNode } from 'react';\n\nimport { AnimatePresence, motion "
},
{
"path": "src/renderer/components/grid-carousel/grid-carousel.module.css",
"chars": 1538,
"preview": ".grid-carousel {\n display: flex;\n flex-direction: column;\n gap: var(--theme-spacing-md);\n width: 100%;\n m"
},
{
"path": "src/renderer/components/item-card/item-card-controls.module.css",
"chars": 2346,
"preview": ".container {\n position: absolute;\n top: 0;\n left: 0;\n z-index: 100;\n display: grid;\n grid-template-row"
},
{
"path": "src/renderer/components/item-card/item-card-controls.tsx",
"chars": 13871,
"preview": "import clsx from 'clsx';\nimport { motion } from 'motion/react';\nimport { memo, MouseEvent, useMemo } from 'react';\n\nimpo"
},
{
"path": "src/renderer/components/item-card/item-card.module.css",
"chars": 4433,
"preview": ".container {\n position: relative;\n display: flex;\n flex-direction: column;\n width: 100%;\n height: 100%;\n "
},
{
"path": "src/renderer/components/item-card/item-card.tsx",
"chars": 50104,
"preview": "import clsx from 'clsx';\nimport { AnimatePresence } from 'motion/react';\nimport { Fragment, memo, ReactNode, useCallback"
},
{
"path": "src/renderer/components/item-image/item-image.tsx",
"chars": 6682,
"preview": "import { memo, useMemo } from 'react';\nimport z from 'zod';\n\nimport { api } from '/@/renderer/api';\nimport {\n General"
},
{
"path": "src/renderer/components/item-list/expanded-list-container.module.css",
"chars": 49,
"preview": ".list-expanded-container {\n overflow: auto;\n}\n"
},
{
"path": "src/renderer/components/item-list/expanded-list-container.tsx",
"chars": 533,
"preview": "import { ReactNode } from 'react';\n\nimport styles from './expanded-list-container.module.css';\n\nconst EXPANDED_HEIGHT = "
},
{
"path": "src/renderer/components/item-list/expanded-list-item.module.css",
"chars": 229,
"preview": ".container {\n width: 100%;\n height: 100%;\n padding: var(--theme-spacing-sm);\n}\n\n.inner {\n width: 100%;\n h"
},
{
"path": "src/renderer/components/item-list/expanded-list-item.tsx",
"chars": 1255,
"preview": "import { Suspense } from 'react';\n\nimport styles from './expanded-list-item.module.css';\n\nimport { ItemListStateItem } f"
},
{
"path": "src/renderer/components/item-list/helpers/extract-row-id.ts",
"chars": 922,
"preview": "/**\n * Creates a function to extract row ID from an item based on the getRowId configuration.\n *\n * @param getRowId - Ei"
},
{
"path": "src/renderer/components/item-list/helpers/get-dragged-items.ts",
"chars": 2887,
"preview": "import {\n ItemListStateActions,\n ItemListStateItemWithRequiredProperties,\n} from '/@/renderer/components/item-list"
},
{
"path": "src/renderer/components/item-list/helpers/get-title-path.ts",
"chars": 905,
"preview": "import { generatePath } from 'react-router';\n\nimport { AppRoute } from '/@/renderer/router/routes';\nimport { LibraryItem"
},
{
"path": "src/renderer/components/item-list/helpers/item-list-controls.ts",
"chars": 19005,
"preview": "import { useEffect, useMemo, useRef } from 'react';\nimport { useNavigate } from 'react-router';\n\nimport { getTitlePath }"
},
{
"path": "src/renderer/components/item-list/helpers/item-list-infinite-loader.ts",
"chars": 16336,
"preview": "import {\n useMutation,\n useQuery,\n useQueryClient,\n useSuspenseQuery,\n UseSuspenseQueryOptions,\n} from '@"
},
{
"path": "src/renderer/components/item-list/helpers/item-list-paginated-loader.ts",
"chars": 7385,
"preview": "import {\n useMutation,\n useQuery,\n useQueryClient,\n useSuspenseQuery,\n UseSuspenseQueryOptions,\n} from '@"
},
{
"path": "src/renderer/components/item-list/helpers/item-list-reducer-utils.ts",
"chars": 5654,
"preview": "import {\n ItemListAction,\n ItemListState,\n ItemListStateItemWithRequiredProperties,\n} from './item-list-state';"
},
{
"path": "src/renderer/components/item-list/helpers/item-list-state.ts",
"chars": 22384,
"preview": "import { useCallback, useMemo, useRef, useSyncExternalStore } from 'react';\n\nimport { itemListSelectors } from '/@/rende"
},
{
"path": "src/renderer/components/item-list/helpers/parse-table-columns.ts",
"chars": 1433,
"preview": "import { ItemTableListColumnConfig } from '/@/renderer/components/item-list/types';\n\n/**\n * Sorts table columns by their"
},
{
"path": "src/renderer/components/item-list/helpers/use-grid-rows.ts",
"chars": 4912,
"preview": "import { useMemo } from 'react';\n\nimport { type DataRow, getDataRows } from '/@/renderer/components/item-card/item-card'"
},
{
"path": "src/renderer/components/item-list/helpers/use-is-fetching-item-list.ts",
"chars": 904,
"preview": "import { useIsFetching } from '@tanstack/react-query';\n\nimport { queryKeys } from '/@/renderer/api/query-keys';\nimport {"
},
{
"path": "src/renderer/components/item-list/helpers/use-item-list-column-reorder.ts",
"chars": 4357,
"preview": "import type { Edge } from '@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge';\n\nimport { useCallback } from 'react';"
},
{
"path": "src/renderer/components/item-list/helpers/use-item-list-column-resize.ts",
"chars": 1515,
"preview": "import { useCallback } from 'react';\n\nimport { useSettingsStore, useSettingsStoreActions } from '/@/renderer/store';\nimp"
},
{
"path": "src/renderer/components/item-list/helpers/use-item-list-scroll-persist.ts",
"chars": 989,
"preview": "import { useCallback, useMemo } from 'react';\nimport { useLocation, useNavigationType } from 'react-router';\n\nimport { u"
},
{
"path": "src/renderer/components/item-list/helpers/use-list-hotkeys.ts",
"chars": 4367,
"preview": "import { useNavigate } from 'react-router';\n\nimport { getTitlePath } from '/@/renderer/components/item-list/helpers/get-"
},
{
"path": "src/renderer/components/item-list/item-detail-list/columns/actions-column.tsx",
"chars": 1171,
"preview": "import { ItemDetailListCellProps } from './types';\n\nimport { ActionIcon } from '/@/shared/components/action-icon/action-"
},
{
"path": "src/renderer/components/item-list/item-detail-list/columns/album-artist-column.tsx",
"chars": 763,
"preview": "import { ItemDetailListCellProps } from './types';\n\nimport {\n JOINED_ARTISTS_MUTED_PROPS,\n JoinedArtists,\n} from '"
},
{
"path": "src/renderer/components/item-list/item-detail-list/columns/album-column.tsx",
"chars": 145,
"preview": "import { ItemDetailListCellProps } from './types';\n\nexport const AlbumColumn = ({ song }: ItemDetailListCellProps) => so"
},
{
"path": "src/renderer/components/item-list/item-detail-list/columns/artist-column.tsx",
"chars": 738,
"preview": "import { ItemDetailListCellProps } from './types';\n\nimport {\n JOINED_ARTISTS_MUTED_PROPS,\n JoinedArtists,\n} from '"
},
{
"path": "src/renderer/components/item-list/item-detail-list/columns/bit-depth-column.tsx",
"chars": 136,
"preview": "import { ItemDetailListCellProps } from './types';\n\nexport const BitDepthColumn = ({ song }: ItemDetailListCellProps) =>"
},
{
"path": "src/renderer/components/item-list/item-detail-list/columns/bit-rate-column.tsx",
"chars": 185,
"preview": "import { ItemDetailListCellProps } from './types';\n\nexport const BitRateColumn = ({ song }: ItemDetailListCellProps) =>\n"
},
{
"path": "src/renderer/components/item-list/item-detail-list/columns/bpm-column.tsx",
"chars": 141,
"preview": "import { ItemDetailListCellProps } from './types';\n\nexport const BpmColumn = ({ song }: ItemDetailListCellProps) => song"
},
{
"path": "src/renderer/components/item-list/item-detail-list/columns/channels-column.tsx",
"chars": 186,
"preview": "import { ItemDetailListCellProps } from './types';\n\nexport const ChannelsColumn = ({ song }: ItemDetailListCellProps) =>"
},
{
"path": "src/renderer/components/item-list/item-detail-list/columns/codec-column.tsx",
"chars": 149,
"preview": "import { ItemDetailListCellProps } from './types';\n\nexport const CodecColumn = ({ song }: ItemDetailListCellProps) => so"
},
{
"path": "src/renderer/components/item-list/item-detail-list/columns/comment-column.tsx",
"chars": 149,
"preview": "import { ItemDetailListCellProps } from './types';\n\nexport const CommentColumn = ({ song }: ItemDetailListCellProps) => "
},
{
"path": "src/renderer/components/item-list/item-detail-list/columns/composer-column.tsx",
"chars": 277,
"preview": "import { ItemDetailListCellProps } from './types';\n\nexport const ComposerColumn = ({ song }: ItemDetailListCellProps) =>"
},
{
"path": "src/renderer/components/item-list/item-detail-list/columns/date-added-column.tsx",
"chars": 257,
"preview": "import { ItemDetailListCellProps } from './types';\n\nimport { formatDateAbsolute } from '/@/renderer/utils/format';\n\nexpo"
},
{
"path": "src/renderer/components/item-list/item-detail-list/columns/default-column.tsx",
"chars": 393,
"preview": "import { ItemDetailListCellProps } from './types';\n\ninterface DefaultColumnProps extends ItemDetailListCellProps {\n c"
},
{
"path": "src/renderer/components/item-list/item-detail-list/columns/disc-number-column.tsx",
"chars": 153,
"preview": "import { ItemDetailListCellProps } from './types';\n\nexport const DiscNumberColumn = ({ song }: ItemDetailListCellProps) "
}
]
// ... and 883 more files (download for full content)
About this extraction
This page contains the full source code of the jeffvli/feishin GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 1083 files (6.2 MB), approximately 1.7M tokens, and a symbol index with 1469 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.