refactor(updater+contextMenu): co-locate updater feature (AppUpdater+Changelog) and the context-menu subsystem into features/

This commit is contained in:
Psychotoxical
2026-06-30 19:12:26 +02:00
parent 7d2645171b
commit 15a6590e77
24 changed files with 72 additions and 59 deletions
+2 -2
View File
@@ -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 {
@@ -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,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;
@@ -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,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 {
@@ -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';
@@ -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) {
@@ -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,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 {
@@ -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[];
@@ -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[];
@@ -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,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 };
@@ -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 {
@@ -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,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';
+8
View File
@@ -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();
+5
View File
@@ -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 {};