Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions src/features/library/TagSidebarFooter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { Settings01Icon } from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";
import { Button } from "@/components/ui/button";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { allTracksReadyForDownload } from "@/features/export/downloadLibrary";
import { isValidFilenameBase } from "@/features/library/filename";
import type { TagSidebarPanelProps } from "@/features/library/TagSidebarPanel";
import { cn } from "@/lib/utils";

type FooterProps = Pick<
TagSidebarPanelProps,
"files" | "loading" | "settingsOpen" | "onDownloadAll" | "onOpenSettings"
>;

export default function TagSidebarFooter({
files,
loading,
settingsOpen,
onDownloadAll,
onOpenSettings,
}: FooterProps) {
const canDownloadAll = files.length > 0 && allTracksReadyForDownload(files);
const hasInvalidFilename = files.some(
(file) => file.metadata && !isValidFilenameBase(file.metadata.filename),
);
const downloadAllReason = loading
? "download in progress"
: files.length === 0
? "add tracks first"
: hasInvalidFilename
? "every track needs a filename"
: "tracks need files and metadata";

return (
<div className="px-3 py-3 border-t flex-shrink-0 flex flex-col gap-2">
{canDownloadAll && !loading ? (
<Button className="w-full [@media(pointer:coarse)]:min-h-11" onClick={onDownloadAll}>
download all
</Button>
) : (
<Tooltip>
<TooltipTrigger asChild>
<span className="block">
<Button
className="w-full [@media(pointer:coarse)]:min-h-11"
onClick={onDownloadAll}
disabled
>
{loading ? "downloading..." : "download all"}
</Button>
</span>
</TooltipTrigger>
<TooltipContent>{downloadAllReason}</TooltipContent>
</Tooltip>
)}
<Button
variant="outline"
data-export-focus-fallback
className={cn(
"h-auto w-full flex-col justify-center gap-1 py-3 text-center",
settingsOpen &&
"border-transparent bg-accent text-accent-foreground shadow-none hover:bg-accent",
)}
onClick={onOpenSettings}
>
<HugeiconsIcon icon={Settings01Icon} strokeWidth={2} />
settings
</Button>
</div>
);
}
62 changes: 62 additions & 0 deletions src/features/library/TagSidebarHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { Cancel01Icon, Moon02Icon, Sun03Icon } from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";
import { Button } from "@/components/ui/button";
import { useTheme } from "@/features/theme/useTheme";
import { cn } from "@/lib/utils";
import type { TagSidebarPanelProps } from "@/features/library/TagSidebarPanel";

export default function TagSidebarHeader({
mobileOpen,
onMobileClose,
onGoHome,
}: Pick<TagSidebarPanelProps, "mobileOpen" | "onMobileClose" | "onGoHome">) {
const { theme, toggleTheme } = useTheme();

return (
<div className="h-14 flex items-center px-5 border-b flex-shrink-0">
<button
type="button"
aria-label="tagium, go to workspace home"
onClick={onGoHome}
className="cursor-pointer font-black text-xl tracking-tight select-none rounded-sm transition-colors hover:text-primary-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
>
tagium
</button>
<button
type="button"
className={cn(
"group ml-auto inline-flex size-11 shrink-0 cursor-pointer items-center justify-center rounded-md outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
!(mobileOpen && onMobileClose) && "-mr-3",
)}
aria-label={`switch to ${theme === "light" ? "dark" : "light"} mode`}
onClick={toggleTheme}
>
{theme === "light" ? (
<HugeiconsIcon
icon={Moon02Icon}
strokeWidth={2}
className="size-4 origin-center transition-transform duration-150 ease-out group-hover:scale-110 motion-reduce:transition-none motion-reduce:group-hover:scale-100"
/>
) : (
<HugeiconsIcon
icon={Sun03Icon}
strokeWidth={2}
className="size-4 origin-center transition-transform duration-150 ease-out group-hover:scale-110 motion-reduce:transition-none motion-reduce:group-hover:scale-100"
/>
)}
</button>
{mobileOpen && onMobileClose ? (
<Button
type="button"
variant="ghost"
size="icon"
className="size-11 md:hidden"
aria-label="close library"
onClick={onMobileClose}
>
<HugeiconsIcon icon={Cancel01Icon} strokeWidth={2} />
</Button>
) : null}
</div>
);
}
109 changes: 10 additions & 99 deletions src/features/library/TagSidebarPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,17 @@

import type { MouseEvent as ReactMouseEvent, RefObject } from "react";
import { useRef, useState } from "react";
import { Cancel01Icon, Moon02Icon, Settings01Icon, Sun03Icon } from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";
import { cn } from "@/lib/utils";
import AlbumSidebar from "@/features/library/AlbumSidebar";
import PlaylistDownloadQueuePanel, {
type PlaylistDownloadQueuePanelState,
} from "@/features/import/PlaylistDownloadQueuePanel";
import { AlbumGroup, TagiumFile } from "@/features/library/types";
import { Button } from "@/components/ui/button";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { allTracksReadyForDownload } from "@/features/export/downloadLibrary";
import { isValidFilenameBase } from "@/features/library/filename";
import type { ShareActionState } from "@/features/share/sharePublication";
import type { ShareLinkSpotlightTarget } from "@/features/share/useShareLinkSpotlight";
import { useTheme } from "@/features/theme/useTheme";
import type { TrackFilenamePreviewStore } from "@/features/library/trackFilenamePreview";
import TagSidebarHeader from "@/features/library/TagSidebarHeader";
import TagSidebarFooter from "@/features/library/TagSidebarFooter";

export interface TagSidebarPanelProps {
mobileOpen?: boolean;
Expand Down Expand Up @@ -125,18 +120,6 @@ export default function TagSidebarPanel({
}: TagSidebarPanelProps) {
const dragCounterRef = useRef(0);
const [isDraggingFile, setIsDraggingFile] = useState(false);
const { theme, toggleTheme } = useTheme();
const canDownloadAll = files.length > 0 && allTracksReadyForDownload(files);
const hasInvalidFilename = files.some(
(file) => file.metadata && !isValidFilenameBase(file.metadata.filename),
);
const downloadAllReason = loading
? "download in progress"
: files.length === 0
? "add tracks first"
: hasInvalidFilename
? "every track needs a filename"
: "tracks need files and metadata";

const handleSidebarDragEnter = (event: React.DragEvent<HTMLDivElement>) => {
if (!isFileDrag(event)) return;
Expand Down Expand Up @@ -197,51 +180,7 @@ export default function TagSidebarPanel({
}}
onDrop={handleSidebarFileDrop}
>
<div className="h-14 flex items-center px-5 border-b flex-shrink-0">
<button
type="button"
aria-label="tagium, go to workspace home"
onClick={onGoHome}
className="cursor-pointer font-black text-xl tracking-tight select-none rounded-sm transition-colors hover:text-primary-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
>
tagium
</button>
<button
type="button"
className={cn(
"group ml-auto inline-flex size-11 shrink-0 cursor-pointer items-center justify-center rounded-md outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
!(mobileOpen && onMobileClose) && "-mr-3",
)}
aria-label={`switch to ${theme === "light" ? "dark" : "light"} mode`}
onClick={toggleTheme}
>
{theme === "light" ? (
<HugeiconsIcon
icon={Moon02Icon}
strokeWidth={2}
className="size-4 origin-center transition-transform duration-150 ease-out group-hover:scale-110 motion-reduce:transition-none motion-reduce:group-hover:scale-100"
/>
) : (
<HugeiconsIcon
icon={Sun03Icon}
strokeWidth={2}
className="size-4 origin-center transition-transform duration-150 ease-out group-hover:scale-110 motion-reduce:transition-none motion-reduce:group-hover:scale-100"
/>
)}
</button>
{mobileOpen && onMobileClose ? (
<Button
type="button"
variant="ghost"
size="icon"
className="size-11 md:hidden"
aria-label="close library"
onClick={onMobileClose}
>
<HugeiconsIcon icon={Cancel01Icon} strokeWidth={2} />
</Button>
) : null}
</div>
<TagSidebarHeader mobileOpen={mobileOpen} onMobileClose={onMobileClose} onGoHome={onGoHome} />

<AlbumSidebar
albums={albums}
Expand Down Expand Up @@ -283,41 +222,13 @@ export default function TagSidebarPanel({
onRetry={onRetryPlaylistDownloadQueue}
/>

<div className="px-3 py-3 border-t flex-shrink-0 flex flex-col gap-2">
{canDownloadAll && !loading ? (
<Button className="w-full [@media(pointer:coarse)]:min-h-11" onClick={onDownloadAll}>
download all
</Button>
) : (
<Tooltip>
<TooltipTrigger asChild>
<span className="block">
<Button
className="w-full [@media(pointer:coarse)]:min-h-11"
onClick={onDownloadAll}
disabled
>
{loading ? "downloading..." : "download all"}
</Button>
</span>
</TooltipTrigger>
<TooltipContent>{downloadAllReason}</TooltipContent>
</Tooltip>
)}
<Button
variant="outline"
data-export-focus-fallback
className={cn(
"h-auto w-full flex-col justify-center gap-1 py-3 text-center",
settingsOpen &&
"border-transparent bg-accent text-accent-foreground shadow-none hover:bg-accent",
)}
onClick={onOpenSettings}
>
<HugeiconsIcon icon={Settings01Icon} strokeWidth={2} />
settings
</Button>
</div>
<TagSidebarFooter
files={files}
loading={loading}
settingsOpen={settingsOpen}
onDownloadAll={onDownloadAll}
onOpenSettings={onOpenSettings}
/>
</div>
);
}
36 changes: 36 additions & 0 deletions src/features/workspace/AudioTaggerDialogs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import AlbumMetadataDialog from "@/features/editor/AlbumMetadataDialog";
import DestructiveActionDialog from "@/features/workspace/DestructiveActionDialog";
import MetadataCleanupDialog from "@/features/library/MetadataCleanupDialog";
import ExportConfirmationDialog from "@/features/export/ExportConfirmationDialog";
import ShareAlbumDialog from "@/features/share/ShareAlbumDialog";
import type { AudioTaggerController } from "@/features/workspace/useAudioTaggerController";

export default function AudioTaggerDialogs({ controller }: { controller: AudioTaggerController }) {
const { sharing, workspace, exporting } = controller;

return (
<>
<ShareAlbumDialog
state={sharing.dialog}
onClose={sharing.closeDialog}
onPublish={sharing.publish}
onStopSharing={sharing.stopDialogShare}
/>
<MetadataCleanupDialog {...workspace.cleanupDialogProps} />
<DestructiveActionDialog {...workspace.removalDialogProps} />
<DestructiveActionDialog {...workspace.albumDeletionDialogProps} />
<AlbumMetadataDialog
key={workspace.albumDialogProps.instanceKey}
{...workspace.albumDialogProps}
/>
<ExportConfirmationDialog
plan={exporting.confirmation}
status={exporting.confirmationStatus}
busy={exporting.exporting}
onCancel={exporting.cancelConfirmation}
onConfirm={() => void exporting.confirmDownload()}
onRestoreFocus={exporting.restoreConfirmationFocus}
/>
</>
);
}
Loading
Loading