Skip to content
452 changes: 452 additions & 0 deletions components/audio/ListeningGuidePrototype.tsx

Large diffs are not rendered by default.

90 changes: 89 additions & 1 deletion components/audio/TagSidebarPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"use client";

import type { MouseEvent as ReactMouseEvent } from "react";
import { useRef, useState } from "react";
import { useEffect, useRef, useState } from "react";
import { Settings } from "lucide-react";
import { cn } from "@/lib/utils";
import AlbumSidebar from "./AlbumSidebar";
Expand All @@ -12,6 +12,8 @@ import { AlbumGroup, TagiumFile } from "./types";
import { Button } from "../ui/button";
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";

const listeningGuideServices = ["spotify?", "apple music?", "spotify?"];

interface TagSidebarPanelProps {
loading: boolean;
files: TagiumFile[];
Expand All @@ -21,6 +23,7 @@ interface TagSidebarPanelProps {
selectedFileId: string | null;
selectedFileIds: Set<string>;
settingsOpen: boolean;
listeningGuideOpen?: boolean;
onAudioUpload: (files: File[]) => void;
onSelectAlbum: (albumId: string, event?: ReactMouseEvent) => void;
onSelectFile: (albumId: string, fileId: string, event?: ReactMouseEvent) => void;
Expand Down Expand Up @@ -48,10 +51,90 @@ interface TagSidebarPanelProps {
playlistDownloadQueue?: PlaylistDownloadQueuePanelState | null;
onDownloadAll: () => void;
onOpenSettings: () => void;
onOpenListeningGuide?: () => void;
onCancelPlaylistDownloadQueue?: () => void;
onRetryPlaylistDownloadQueue?: () => void;
}

function ListeningGuideEntryButton({ active, onClick }: { active: boolean; onClick: () => void }) {
const [serviceIndex, setServiceIndex] = useState(0);
const [transitionEnabled, setTransitionEnabled] = useState(true);
const [prefersReducedMotion, setPrefersReducedMotion] = useState(false);

useEffect(() => {
const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
const updateMotionPreference = () => {
setPrefersReducedMotion(mediaQuery.matches);
setTransitionEnabled(!mediaQuery.matches);
if (mediaQuery.matches) {
setServiceIndex(0);
}
};

updateMotionPreference();
mediaQuery.addEventListener("change", updateMotionPreference);

return () => mediaQuery.removeEventListener("change", updateMotionPreference);
}, []);

useEffect(() => {
if (prefersReducedMotion) return;

const interval = setInterval(() => {
setServiceIndex((current) =>
current >= listeningGuideServices.length - 1 ? 1 : current + 1,
);
}, 3_000);

return () => clearInterval(interval);
}, [prefersReducedMotion]);

return (
<Button
type="button"
variant="outline"
className={cn(
"h-auto w-full flex-col gap-0 py-3 text-center",
active && "border-transparent bg-accent text-accent-foreground shadow-none hover:bg-accent",
)}
onClick={onClick}
aria-label="how do i listen on spotify or apple music?"
>
<span className="text-xs font-normal text-muted-foreground">how do i listen on</span>
<span className="relative h-5 w-full overflow-hidden font-semibold">
<span
aria-hidden="true"
className={cn(
"flex flex-col motion-reduce:transition-none",
transitionEnabled &&
!prefersReducedMotion &&
"transition-transform duration-300 ease-in-out",
)}
style={{ transform: `translateY(-${serviceIndex * 1.25}rem)` }}
onTransitionEnd={() => {
if (serviceIndex !== listeningGuideServices.length - 1) return;

setTransitionEnabled(false);
setServiceIndex(0);
requestAnimationFrame(() => {
requestAnimationFrame(() => setTransitionEnabled(true));
});
}}
>
{listeningGuideServices.map((service, index) => (
<span
key={`${service}-${index}`}
className="flex h-5 shrink-0 items-center justify-center"
>
{service}
</span>
))}
</span>
</span>
</Button>
);
}

export default function TagSidebarPanel({
loading,
files,
Expand All @@ -61,6 +144,7 @@ export default function TagSidebarPanel({
selectedFileId,
selectedFileIds,
settingsOpen,
listeningGuideOpen = false,
onAudioUpload,
onSelectAlbum,
onSelectFile,
Expand All @@ -79,6 +163,7 @@ export default function TagSidebarPanel({
playlistDownloadQueue = null,
onDownloadAll,
onOpenSettings,
onOpenListeningGuide,
onCancelPlaylistDownloadQueue,
onRetryPlaylistDownloadQueue,
}: TagSidebarPanelProps) {
Expand Down Expand Up @@ -210,6 +295,9 @@ export default function TagSidebarPanel({
<Settings />
settings
</Button>
{onOpenListeningGuide && (
<ListeningGuideEntryButton active={listeningGuideOpen} onClick={onOpenListeningGuide} />
)}
</div>
</div>
);
Expand Down
30 changes: 28 additions & 2 deletions components/audio/audioTagger.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import {
type PlaylistDownloadControllerSnapshot,
} from "./playlistDownloadController";
import LandingScreen from "./LandingScreen";
import ListeningGuidePrototype from "./ListeningGuidePrototype";
import TrackMetadataEditor from "./TrackMetadataEditor";
import SettingsPage from "./SettingsPage";
import AudioDownloader from "./AudioDownloader";
Expand All @@ -80,7 +81,7 @@ import {
TagiumFile,
} from "./types";

type ActiveView = "editor" | "settings";
type ActiveView = "editor" | "settings" | "listening-guide";
type ManagedDownloadTrack = QueuedDownloadTrack & { importOperationId?: string };
type PlaylistDownloadQueueState = PlaylistDownloadControllerSnapshot;

Expand Down Expand Up @@ -284,6 +285,9 @@ export default function AudioTagger() {
const [editingAlbumId, setEditingAlbumId] = useState<string | null>(null);
const [createSeedTrackIds, setCreateSeedTrackIds] = useState<string[]>([]);
const [activeView, setActiveView] = useState<ActiveView>("editor");
const [listeningGuidePrototypeAvailable, setListeningGuidePrototypeAvailable] = useState(
import.meta.env.DEV,
);
const [settings, setSettings] = useState<AppSettings>(loadAppSettings);
const [playlistDownloadQueue, setPlaylistDownloadQueue] =
useState<PlaylistDownloadQueueState | null>(null);
Expand Down Expand Up @@ -342,6 +346,14 @@ export default function AudioTagger() {
reset(selectedFile.metadata);
}
}, [selectedFile, reset]);
useEffect(() => {
if (import.meta.env.DEV) return;

fetch("/api/dev/config", { headers: { Accept: "application/json" } })
.then((response) => (response.ok ? response.json() : null))
.then((config) => setListeningGuidePrototypeAvailable(config?.deployEnv === "preview"))
.catch(() => setListeningGuidePrototypeAvailable(false));
}, []);
useEffect(() => {
const fileIdSet = new Set(files.map((file) => file.id));
setLooseTrackIds((prevLooseTrackIds) =>
Expand Down Expand Up @@ -1878,6 +1890,7 @@ export default function AudioTagger() {
selectedFileId={selectedFileId}
selectedFileIds={selectedFileIds}
settingsOpen={activeView === "settings"}
listeningGuideOpen={activeView === "listening-guide"}
onAudioUpload={handleAudioUpload}
onSelectAlbum={handleSelectAlbum}
onSelectFile={handleSelectFile}
Expand All @@ -1899,12 +1912,25 @@ export default function AudioTagger() {
if (isTrackCoverProcessing) return;
setActiveView((currentView) => (currentView === "settings" ? "editor" : "settings"));
}}
onOpenListeningGuide={
listeningGuidePrototypeAvailable
? () => {
if (isTrackCoverProcessing) return;
setActiveView((currentView) =>
currentView === "listening-guide" ? "editor" : "listening-guide",
);
window.scrollTo({ top: 0, left: 0 });
}
: undefined
}
onCancelPlaylistDownloadQueue={handleCancelPlaylistDownloads}
onRetryPlaylistDownloadQueue={handleRetryPlaylistDownloads}
/>
<div className="relative order-1 flex-shrink-0 flex flex-col md:order-none md:min-h-0 md:flex-1">
<div className="h-svh min-h-0 flex flex-col overflow-hidden md:h-auto md:min-h-0 md:flex-1">
{activeView === "settings" ? (
{activeView === "listening-guide" ? (
<ListeningGuidePrototype onBack={() => setActiveView("editor")} />
) : activeView === "settings" ? (
<SettingsPage
settings={settings}
onChange={handleSettingsChange}
Expand Down
1 change: 1 addition & 0 deletions public/brands/applemusic.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions public/brands/soundcloud.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions public/brands/spotify.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions public/brands/youtube.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
22 changes: 22 additions & 0 deletions src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,28 @@
@layer base {
* {
@apply border-border outline-ring/50;
scrollbar-color: var(--muted) transparent;
scrollbar-width: thin;
}

*::-webkit-scrollbar {
width: 10px;
height: 10px;
}

*::-webkit-scrollbar-track {
background: transparent;
}

*::-webkit-scrollbar-thumb {
border: 2px solid transparent;
border-radius: 9999px;
background-color: var(--muted);
background-clip: padding-box;
}

*::-webkit-scrollbar-thumb:hover {
background-color: var(--muted-foreground);
}

html,
Expand Down