mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 07:15:47 +00:00
refactor(updater+contextMenu): co-locate updater feature (AppUpdater+Changelog) and the context-menu subsystem into features/
This commit is contained in:
@@ -15,7 +15,7 @@ import { NowPlayingDropdown } from '@/features/nowPlaying';
|
||||
import QueuePanel from '@/features/queue';
|
||||
import AppRoutes from './AppRoutes';
|
||||
import FullscreenPlayer from '@/features/fullscreenPlayer';
|
||||
import ContextMenu from '../components/ContextMenu';
|
||||
import ContextMenu from '@/features/contextMenu/components/ContextMenu';
|
||||
import SongInfoModal from '../components/SongInfoModal';
|
||||
import { DownloadFolderModal } from '@/features/offline';
|
||||
import GlobalConfirmModal from '../components/GlobalConfirmModal';
|
||||
@@ -30,7 +30,7 @@ import {
|
||||
import ConnectionIndicator from '@/app/ConnectionIndicator';
|
||||
import MusicNetworkIndicator from '../components/MusicNetworkIndicator';
|
||||
import { OfflineBanner } from '@/features/offline';
|
||||
import AppUpdater from '../components/AppUpdater';
|
||||
import AppUpdater from '@/features/updater/components/AppUpdater';
|
||||
import TitleBar from '@/app/TitleBar';
|
||||
import { OrbitSessionBar, OrbitStartTrigger } from '@/features/orbit';
|
||||
import { useOrbitHost } from '@/features/orbit';
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import { ListPlus, Search, X } from 'lucide-react';
|
||||
import type { TFunction } from 'i18next';
|
||||
import { useSelectionStore } from '@/store/selectionStore';
|
||||
import { AddToPlaylistSubmenu } from '@/components/ContextMenu';
|
||||
import { AddToPlaylistSubmenu } from '@/features/contextMenu/components/ContextMenu';
|
||||
import { offlineActionPolicy, type OfflineActionPolicy } from '@/features/offline';
|
||||
|
||||
interface Props {
|
||||
|
||||
+2
-2
@@ -4,11 +4,11 @@ import { ListMusic, Plus } from 'lucide-react';
|
||||
import { getPlaylist, updatePlaylist } from '@/lib/api/subsonicPlaylists';
|
||||
import type { SubsonicPlaylist } from '@/lib/api/subsonicTypes';
|
||||
import { usePlaylistStore } from '@/features/playlist';
|
||||
import { showToast } from '../../utils/ui/toast';
|
||||
import { showToast } from '@/utils/ui/toast';
|
||||
import {
|
||||
confirmAddAllDuplicates,
|
||||
isSmartPlaylistName,
|
||||
} from '../../utils/componentHelpers/contextMenuHelpers';
|
||||
} from '@/utils/componentHelpers/contextMenuHelpers';
|
||||
|
||||
interface Props {
|
||||
songIds: string[];
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { resolveAlbum, resolveArtist, resolveMediaServerId } from '@/features/offline';
|
||||
import { AddToPlaylistSubmenu } from './AddToPlaylistSubmenu';
|
||||
import { AddToPlaylistSubmenu } from '@/features/contextMenu/components/AddToPlaylistSubmenu';
|
||||
|
||||
interface AlbumProps {
|
||||
albumId: string;
|
||||
+3
-3
@@ -6,9 +6,9 @@ import { star, unstar } from '@/lib/api/subsonicStarRating';
|
||||
import type { SubsonicAlbum } from '@/lib/api/subsonicTypes';
|
||||
import { songToTrack } from '@/lib/media/songToTrack';
|
||||
import StarRating from '@/ui/StarRating';
|
||||
import { AlbumToPlaylistSubmenu } from './AlbumArtistToPlaylistSubmenu';
|
||||
import { MultiAlbumToPlaylistSubmenu } from './MultiAlbumToPlaylistSubmenu';
|
||||
import type { ContextMenuItemsProps } from './contextMenuItemTypes';
|
||||
import { AlbumToPlaylistSubmenu } from '@/features/contextMenu/components/AlbumArtistToPlaylistSubmenu';
|
||||
import { MultiAlbumToPlaylistSubmenu } from '@/features/contextMenu/components/MultiAlbumToPlaylistSubmenu';
|
||||
import type { ContextMenuItemsProps } from '@/features/contextMenu/components/contextMenuItemTypes';
|
||||
|
||||
export default function AlbumContextItems(props: ContextMenuItemsProps) {
|
||||
const {
|
||||
+3
-3
@@ -3,9 +3,9 @@ import { Radio, Heart, ChevronRight, ListMusic, Star, Share2 } from 'lucide-reac
|
||||
import { star, unstar } from '@/lib/api/subsonicStarRating';
|
||||
import type { SubsonicArtist } from '@/lib/api/subsonicTypes';
|
||||
import StarRating from '@/ui/StarRating';
|
||||
import { ArtistToPlaylistSubmenu } from './AlbumArtistToPlaylistSubmenu';
|
||||
import { MultiArtistToPlaylistSubmenu } from './MultiArtistToPlaylistSubmenu';
|
||||
import type { ContextMenuItemsProps } from './contextMenuItemTypes';
|
||||
import { ArtistToPlaylistSubmenu } from '@/features/contextMenu/components/AlbumArtistToPlaylistSubmenu';
|
||||
import { MultiArtistToPlaylistSubmenu } from '@/features/contextMenu/components/MultiArtistToPlaylistSubmenu';
|
||||
import type { ContextMenuItemsProps } from '@/features/contextMenu/components/contextMenuItemTypes';
|
||||
|
||||
export default function ArtistContextItems(props: ContextMenuItemsProps) {
|
||||
const {
|
||||
+1
-1
@@ -51,7 +51,7 @@ vi.mock('@/features/offline/hooks/useOfflineBrowseContext', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
import ContextMenu from './ContextMenu';
|
||||
import ContextMenu from '@/features/contextMenu/components/ContextMenu';
|
||||
import { renderWithProviders } from '@/test/helpers/renderWithProviders';
|
||||
import { usePlayerStore } from '@/features/playback/store/playerStore';
|
||||
import { useAuthStore } from '@/store/authStore';
|
||||
+8
-8
@@ -3,26 +3,26 @@ import type { Track } from '@/lib/media/trackTypes';
|
||||
import { useOrbitStore } from '@/features/orbit';
|
||||
import { usePlayerStore } from '@/features/playback/store/playerStore';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { useAuthStore } from '../store/authStore';
|
||||
import { useAuthStore } from '@/store/authStore';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { EntityShareKind } from '../utils/share/shareLink';
|
||||
import { AddToPlaylistSubmenu } from './contextMenu/AddToPlaylistSubmenu';
|
||||
import type { EntityShareKind } from '@/utils/share/shareLink';
|
||||
import { AddToPlaylistSubmenu } from '@/features/contextMenu/components/AddToPlaylistSubmenu';
|
||||
import {
|
||||
copyShareLink as copyShareLinkAction,
|
||||
downloadAlbum as downloadAlbumAction,
|
||||
startInstantMix as startInstantMixAction,
|
||||
startRadio as startRadioAction,
|
||||
} from '../utils/componentHelpers/contextMenuActions';
|
||||
import { useContextMenuKeyboardNav } from '../hooks/useContextMenuKeyboardNav';
|
||||
import { useContextMenuRating } from '../hooks/useContextMenuRating';
|
||||
import { usePlaybackLibraryNavigate } from '../hooks/usePlaybackLibraryNavigate';
|
||||
} from '@/utils/componentHelpers/contextMenuActions';
|
||||
import { useContextMenuKeyboardNav } from '@/hooks/useContextMenuKeyboardNav';
|
||||
import { useContextMenuRating } from '@/hooks/useContextMenuRating';
|
||||
import { usePlaybackLibraryNavigate } from '@/hooks/usePlaybackLibraryNavigate';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useOfflineBrowseContext } from '@/features/offline';
|
||||
import {
|
||||
offlineActionPolicy,
|
||||
type OfflineSurface,
|
||||
} from '@/features/offline';
|
||||
import ContextMenuItems from './contextMenu/ContextMenuItems';
|
||||
import ContextMenuItems from '@/features/contextMenu/components/ContextMenuItems';
|
||||
|
||||
function contextMenuSurfaceForType(type: string | null): OfflineSurface {
|
||||
switch (type) {
|
||||
+6
-6
@@ -1,10 +1,10 @@
|
||||
import React from 'react';
|
||||
import type { ContextMenuItemsProps } from './contextMenuItemTypes';
|
||||
import SongContextItems from './SongContextItems';
|
||||
import QueueItemContextItems from './QueueItemContextItems';
|
||||
import AlbumContextItems from './AlbumContextItems';
|
||||
import ArtistContextItems from './ArtistContextItems';
|
||||
import PlaylistContextItems from './PlaylistContextItems';
|
||||
import type { ContextMenuItemsProps } from '@/features/contextMenu/components/contextMenuItemTypes';
|
||||
import SongContextItems from '@/features/contextMenu/components/SongContextItems';
|
||||
import QueueItemContextItems from '@/features/contextMenu/components/QueueItemContextItems';
|
||||
import AlbumContextItems from '@/features/contextMenu/components/AlbumContextItems';
|
||||
import ArtistContextItems from '@/features/contextMenu/components/ArtistContextItems';
|
||||
import PlaylistContextItems from '@/features/contextMenu/components/PlaylistContextItems';
|
||||
|
||||
export default function ContextMenuItems(props: ContextMenuItemsProps) {
|
||||
const { type } = props;
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Check, Folder, FolderMinus, Plus } from 'lucide-react';
|
||||
import { useAuthStore } from '../../store/authStore';
|
||||
import { useAuthStore } from '@/store/authStore';
|
||||
import { EMPTY_SERVER_FOLDERS, usePlaylistFolderStore } from '@/features/playlist';
|
||||
|
||||
interface Props {
|
||||
+2
-2
@@ -4,11 +4,11 @@ import { ListMusic, Plus } from 'lucide-react';
|
||||
import { resolveAlbum, resolveMediaServerId, resolvePlaylist } from '@/features/offline';
|
||||
import type { SubsonicPlaylist } from '@/lib/api/subsonicTypes';
|
||||
import { usePlaylistStore } from '@/features/playlist';
|
||||
import { showToast } from '../../utils/ui/toast';
|
||||
import { showToast } from '@/utils/ui/toast';
|
||||
import {
|
||||
confirmAddAllDuplicates,
|
||||
isSmartPlaylistName,
|
||||
} from '../../utils/componentHelpers/contextMenuHelpers';
|
||||
} from '@/utils/componentHelpers/contextMenuHelpers';
|
||||
|
||||
interface Props {
|
||||
albumIds: string[];
|
||||
+2
-2
@@ -5,11 +5,11 @@ import { resolveAlbum, resolveArtist, resolveMediaServerId, resolvePlaylist } fr
|
||||
import { getPlaylists } from '@/lib/api/subsonicPlaylists';
|
||||
import type { SubsonicPlaylist } from '@/lib/api/subsonicTypes';
|
||||
import { usePlaylistStore } from '@/features/playlist';
|
||||
import { showToast } from '../../utils/ui/toast';
|
||||
import { showToast } from '@/utils/ui/toast';
|
||||
import {
|
||||
confirmAddAllDuplicates,
|
||||
isSmartPlaylistName,
|
||||
} from '../../utils/componentHelpers/contextMenuHelpers';
|
||||
} from '@/utils/componentHelpers/contextMenuHelpers';
|
||||
|
||||
interface Props {
|
||||
artistIds: string[];
|
||||
+5
-5
@@ -2,9 +2,9 @@ import { useTranslation } from 'react-i18next';
|
||||
import { Play, ChevronRight, FolderTree, ListMusic, Trash2 } from 'lucide-react';
|
||||
import type { SubsonicPlaylist } from '@/lib/api/subsonicTypes';
|
||||
import { usePlaylistStore } from '@/features/playlist';
|
||||
import { MultiPlaylistToPlaylistSubmenu, SinglePlaylistToPlaylistSubmenu } from './PlaylistToPlaylistSubmenus';
|
||||
import MoveToFolderSubmenu from './MoveToFolderSubmenu';
|
||||
import type { ContextMenuItemsProps } from './contextMenuItemTypes';
|
||||
import { MultiPlaylistToPlaylistSubmenu, SinglePlaylistToPlaylistSubmenu } from '@/features/contextMenu/components/PlaylistToPlaylistSubmenus';
|
||||
import MoveToFolderSubmenu from '@/features/contextMenu/components/MoveToFolderSubmenu';
|
||||
import type { ContextMenuItemsProps } from '@/features/contextMenu/components/contextMenuItemTypes';
|
||||
|
||||
export default function PlaylistContextItems(props: ContextMenuItemsProps) {
|
||||
const {
|
||||
@@ -64,7 +64,7 @@ export default function PlaylistContextItems(props: ContextMenuItemsProps) {
|
||||
<>
|
||||
<div className="context-menu-divider" />
|
||||
<div className="context-menu-item" style={{ color: 'var(--danger)' }} onClick={() => handleAction(async () => {
|
||||
const { showToast } = await import('../../utils/ui/toast');
|
||||
const { showToast } = await import('@/utils/ui/toast');
|
||||
const { deletePlaylist } = await import('@/lib/api/subsonicPlaylists');
|
||||
const { removeId } = usePlaylistStore.getState();
|
||||
try {
|
||||
@@ -112,7 +112,7 @@ export default function PlaylistContextItems(props: ContextMenuItemsProps) {
|
||||
)}
|
||||
{offlinePolicy.canEditPlaylist && (
|
||||
<div className="context-menu-item" style={{ color: 'var(--danger)' }} onClick={() => handleAction(async () => {
|
||||
const { showToast } = await import('../../utils/ui/toast');
|
||||
const { showToast } = await import('@/utils/ui/toast');
|
||||
const { deletePlaylist } = await import('@/lib/api/subsonicPlaylists');
|
||||
const { removeId } = usePlaylistStore.getState();
|
||||
const deletedIds: string[] = [];
|
||||
+2
-2
@@ -2,8 +2,8 @@ import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from 're
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ListMusic, Plus } from 'lucide-react';
|
||||
import { usePlaylistStore } from '@/features/playlist';
|
||||
import { showToast } from '../../utils/ui/toast';
|
||||
import { isSmartPlaylistName } from '../../utils/componentHelpers/contextMenuHelpers';
|
||||
import { showToast } from '@/utils/ui/toast';
|
||||
import { isSmartPlaylistName } from '@/utils/componentHelpers/contextMenuHelpers';
|
||||
|
||||
interface SingleProps {
|
||||
playlist: { id: string; name: string };
|
||||
+5
-5
@@ -1,13 +1,13 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Play, Radio, Heart, ChevronRight, User, Disc3, ListMusic, Info, Sparkles, Star, Trash2, Share2 } from 'lucide-react';
|
||||
import { queueSongStar } from '@/features/playback/store/pendingStarSync';
|
||||
import { getMusicNetworkRuntime, useEnrichmentPrimary } from '../../music-network';
|
||||
import { getMusicNetworkRuntime, useEnrichmentPrimary } from '@/music-network';
|
||||
import type { Track } from '@/lib/media/trackTypes';
|
||||
import { useAuthStore } from '../../store/authStore';
|
||||
import { renderPresetIcon } from '../settings/musicNetwork/presetIcon';
|
||||
import { useAuthStore } from '@/store/authStore';
|
||||
import { renderPresetIcon } from '@/components/settings/musicNetwork/presetIcon';
|
||||
import StarRating from '@/ui/StarRating';
|
||||
import { AddToPlaylistSubmenu } from './AddToPlaylistSubmenu';
|
||||
import type { ContextMenuItemsProps } from './contextMenuItemTypes';
|
||||
import { AddToPlaylistSubmenu } from '@/features/contextMenu/components/AddToPlaylistSubmenu';
|
||||
import type { ContextMenuItemsProps } from '@/features/contextMenu/components/contextMenuItemTypes';
|
||||
|
||||
export default function QueueItemContextItems(props: ContextMenuItemsProps) {
|
||||
const {
|
||||
+7
-7
@@ -4,17 +4,17 @@ import { useNavigateToAlbum } from '@/features/album';
|
||||
import { useNavigateToArtist } from '@/features/artist';
|
||||
import { resolveAlbum, resolveMediaServerId, resolvePlaylist } from '@/features/offline';
|
||||
import { queueSongStar } from '@/features/playback/store/pendingStarSync';
|
||||
import { getMusicNetworkRuntime, useEnrichmentPrimary } from '../../music-network';
|
||||
import { getMusicNetworkRuntime, useEnrichmentPrimary } from '@/music-network';
|
||||
import type { Track } from '@/lib/media/trackTypes';
|
||||
import { useAuthStore } from '../../store/authStore';
|
||||
import { useAuthStore } from '@/store/authStore';
|
||||
import { usePlaylistStore } from '@/features/playlist';
|
||||
import { songToTrack } from '@/lib/media/songToTrack';
|
||||
import { showToast } from '../../utils/ui/toast';
|
||||
import { showToast } from '@/utils/ui/toast';
|
||||
import { suggestOrbitTrack, hostEnqueueToOrbit, evaluateOrbitSuggestGate, OrbitSuggestBlockedError } from '@/features/orbit';
|
||||
import { renderPresetIcon } from '../settings/musicNetwork/presetIcon';
|
||||
import { renderPresetIcon } from '@/components/settings/musicNetwork/presetIcon';
|
||||
import StarRating from '@/ui/StarRating';
|
||||
import { AddToPlaylistSubmenu } from './AddToPlaylistSubmenu';
|
||||
import type { ContextMenuItemsProps } from './contextMenuItemTypes';
|
||||
import { AddToPlaylistSubmenu } from '@/features/contextMenu/components/AddToPlaylistSubmenu';
|
||||
import type { ContextMenuItemsProps } from '@/features/contextMenu/components/contextMenuItemTypes';
|
||||
|
||||
export default function SongContextItems(props: ContextMenuItemsProps) {
|
||||
const {
|
||||
@@ -179,7 +179,7 @@ export default function SongContextItems(props: ContextMenuItemsProps) {
|
||||
{offlinePolicy.canEditPlaylist && playlistId && playlistSongIndex !== undefined && (
|
||||
<div className="context-menu-item" style={{ color: 'var(--danger)' }} onClick={() => handleAction(async () => {
|
||||
const { updatePlaylist } = await import('@/lib/api/subsonicPlaylists');
|
||||
const { showToast } = await import('../../utils/ui/toast');
|
||||
const { showToast } = await import('@/utils/ui/toast');
|
||||
const touchPlaylist = usePlaylistStore.getState().touchPlaylist;
|
||||
try {
|
||||
const serverId = resolveMediaServerId();
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import type React from 'react';
|
||||
import type { SubsonicAlbum, SubsonicArtist } from '@/lib/api/subsonicTypes';
|
||||
import type { QueueItemRef, Track } from '@/lib/media/trackTypes';
|
||||
import type { EntityShareKind } from '../../utils/share/shareLink';
|
||||
import type { EntityShareKind } from '@/utils/share/shareLink';
|
||||
import type { OfflineActionPolicy } from '@/features/offline';
|
||||
|
||||
export type RatingKind = 'song' | 'album' | 'artist';
|
||||
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Context-menu feature — the rendered ContextMenu plus the per-entity menu-item
|
||||
* builders (album/artist/playlist/song/queue items + add-to-playlist /
|
||||
* move-to-folder submenus). ContextMenu is deep-imported by its consumers
|
||||
* (app shell + favorites/album/playlist surfaces); the item builders are
|
||||
* internal to the subsystem, so nothing is re-exported here.
|
||||
*/
|
||||
export {};
|
||||
@@ -5,7 +5,7 @@ import type { SubsonicSong } from '@/lib/api/subsonicTypes';
|
||||
import { usePlayerStore } from '@/features/playback/store/playerStore';
|
||||
import { useSelectionStore } from '@/store/selectionStore';
|
||||
import { songToTrack } from '@/lib/media/songToTrack';
|
||||
import { AddToPlaylistSubmenu } from '@/components/ContextMenu';
|
||||
import { AddToPlaylistSubmenu } from '@/features/contextMenu/components/ContextMenu';
|
||||
import GenreFilterBar from '@/ui/GenreFilterBar';
|
||||
|
||||
interface Props {
|
||||
|
||||
@@ -7,7 +7,7 @@ import { songToTrack } from '@/lib/media/songToTrack';
|
||||
import { formatTrackTime } from '@/lib/format/formatDuration';
|
||||
import { AlbumCoverArtImage } from '@/cover/AlbumCoverArtImage';
|
||||
import { COVER_DENSE_SEARCH_CSS_PX } from '@/cover/layoutSizes';
|
||||
import { AddToPlaylistSubmenu } from '@/components/ContextMenu';
|
||||
import { AddToPlaylistSubmenu } from '@/features/contextMenu/components/ContextMenu';
|
||||
|
||||
function PlaylistSearchResultThumb({ albumId, coverArt }: { albumId: string; coverArt: string }) {
|
||||
return (
|
||||
|
||||
@@ -19,7 +19,7 @@ import { useDragDrop } from '@/lib/dnd/DragDropContext';
|
||||
import { useOrbitSongRowBehavior } from '@/features/orbit';
|
||||
import { songToTrack } from '@/lib/media/songToTrack';
|
||||
import type { PlaylistSortKey, PlaylistSortDir } from '@/features/playlist/utils/playlistDisplayedSongs';
|
||||
import { AddToPlaylistSubmenu } from '@/components/ContextMenu';
|
||||
import { AddToPlaylistSubmenu } from '@/features/contextMenu/components/ContextMenu';
|
||||
|
||||
const PL_CENTERED = new Set(['favorite', 'rating', 'duration', 'playCount', 'bpm']);
|
||||
|
||||
|
||||
@@ -2,11 +2,11 @@ import { type ReactNode } from 'react';
|
||||
import { open } from '@tauri-apps/plugin-shell';
|
||||
import { ArrowUpCircle, CheckCircle2, ChevronDown, Download, FolderOpen, RefreshCw, ShieldCheck } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { version as currentVersion } from '../../package.json';
|
||||
import { version as currentVersion } from '@/../package.json';
|
||||
import { formatBytes } from '@/lib/format/formatBytes';
|
||||
import { useAppUpdater } from '../hooks/useAppUpdater';
|
||||
import Modal from './Modal';
|
||||
import Changelog from './appUpdater/Changelog';
|
||||
import { useAppUpdater } from '@/hooks/useAppUpdater';
|
||||
import Modal from '@/components/Modal';
|
||||
import Changelog from '@/features/updater/components/Changelog';
|
||||
|
||||
export default function AppUpdater() {
|
||||
const { t } = useTranslation();
|
||||
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* Updater feature — the in-app update checker/notification (AppUpdater) and its
|
||||
* changelog display. Mounted by the app shell; deep-imported, not re-exported.
|
||||
*/
|
||||
export {};
|
||||
Reference in New Issue
Block a user