Update AudioPlayer and FullScreenPlayer for improved mobile audio handling; refactor WhatsNewPopup for better dialog structure; clean up LoginForm by removing unused settings
This commit is contained in:
@@ -1 +1 @@
|
|||||||
NEXT_PUBLIC_COMMIT_SHA=25e9bd6
|
NEXT_PUBLIC_COMMIT_SHA=1dfb86f
|
||||||
|
|||||||
@@ -129,26 +129,73 @@ export const AudioPlayer: React.FC = () => {
|
|||||||
|
|
||||||
// Mobile-specific audio initialization
|
// Mobile-specific audio initialization
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
|
// Detect if running as PWA
|
||||||
|
const isPWA = window.matchMedia('(display-mode: standalone)').matches ||
|
||||||
|
(window.navigator as Navigator & { standalone?: boolean }).standalone === true;
|
||||||
|
|
||||||
|
console.log('🔍 Audio initialization debug:', {
|
||||||
|
isMobile,
|
||||||
|
isPWA,
|
||||||
|
audioInitialized,
|
||||||
|
userAgent: navigator.userAgent
|
||||||
|
});
|
||||||
|
|
||||||
// Add a document click listener to initialize audio context on first user interaction
|
// Add a document click listener to initialize audio context on first user interaction
|
||||||
const initializeAudioOnMobile = async () => {
|
const initializeAudioOnMobile = async () => {
|
||||||
if (!audioInitialized) {
|
if (!audioInitialized) {
|
||||||
try {
|
try {
|
||||||
|
console.log('🎵 Initializing mobile audio context...', { isPWA });
|
||||||
|
|
||||||
const AudioContextClass = window.AudioContext || (window as typeof window & { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;
|
const AudioContextClass = window.AudioContext || (window as typeof window & { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;
|
||||||
if (AudioContextClass) {
|
if (AudioContextClass) {
|
||||||
const audioContext = new AudioContextClass();
|
const audioContext = new AudioContextClass();
|
||||||
|
console.log('Audio context state:', audioContext.state);
|
||||||
|
|
||||||
if (audioContext.state === 'suspended') {
|
if (audioContext.state === 'suspended') {
|
||||||
|
console.log('Resuming suspended audio context...');
|
||||||
await audioContext.resume();
|
await audioContext.resume();
|
||||||
|
console.log('Audio context resumed, new state:', audioContext.state);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// For PWA, we need to explicitly unlock audio
|
||||||
|
if (isPWA && audioRef.current) {
|
||||||
|
console.log('PWA detected, performing audio unlock...');
|
||||||
|
|
||||||
|
// Create a silent audio buffer to unlock audio
|
||||||
|
const buffer = audioContext.createBuffer(1, 1, 22050);
|
||||||
|
const source = audioContext.createBufferSource();
|
||||||
|
source.buffer = buffer;
|
||||||
|
source.connect(audioContext.destination);
|
||||||
|
source.start(0);
|
||||||
|
|
||||||
|
// Also try to load the audio element
|
||||||
|
try {
|
||||||
|
audioRef.current.volume = 0;
|
||||||
|
const playPromise = audioRef.current.play();
|
||||||
|
if (playPromise) {
|
||||||
|
await playPromise;
|
||||||
|
audioRef.current.pause();
|
||||||
|
audioRef.current.currentTime = 0;
|
||||||
|
}
|
||||||
|
audioRef.current.volume = volume;
|
||||||
|
console.log('✅ PWA audio unlock successful');
|
||||||
|
} catch (unlockError) {
|
||||||
|
console.log('⚠️ PWA audio unlock failed:', unlockError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setAudioInitialized(true);
|
setAudioInitialized(true);
|
||||||
|
console.log('✅ Mobile audio context initialized successfully');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('Mobile audio context initialization failed:', error);
|
console.log('❌ Mobile audio context initialization failed:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Listen for any user interaction to initialize audio
|
// Listen for any user interaction to initialize audio
|
||||||
const handleFirstUserInteraction = () => {
|
const handleFirstUserInteraction = () => {
|
||||||
|
console.log('🎯 First user interaction detected, initializing audio...');
|
||||||
initializeAudioOnMobile();
|
initializeAudioOnMobile();
|
||||||
document.removeEventListener('touchstart', handleFirstUserInteraction);
|
document.removeEventListener('touchstart', handleFirstUserInteraction);
|
||||||
document.removeEventListener('click', handleFirstUserInteraction);
|
document.removeEventListener('click', handleFirstUserInteraction);
|
||||||
@@ -172,7 +219,7 @@ export const AudioPlayer: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
keysToRemove.forEach(key => localStorage.removeItem(key));
|
keysToRemove.forEach(key => localStorage.removeItem(key));
|
||||||
}, [isMobile, audioInitialized]);
|
}, [isMobile, audioInitialized, volume]);
|
||||||
|
|
||||||
// Apply volume to audio element when volume changes
|
// Apply volume to audio element when volume changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -470,22 +517,57 @@ export const AudioPlayer: React.FC = () => {
|
|||||||
|
|
||||||
const togglePlayPause = async () => {
|
const togglePlayPause = async () => {
|
||||||
if (audioCurrent && currentTrack) {
|
if (audioCurrent && currentTrack) {
|
||||||
|
// Detect if running as PWA
|
||||||
|
const isPWA = window.matchMedia('(display-mode: standalone)').matches ||
|
||||||
|
(window.navigator as Navigator & { standalone?: boolean }).standalone === true;
|
||||||
|
|
||||||
|
console.log('🎵 togglePlayPause called:', {
|
||||||
|
isPlaying,
|
||||||
|
isMobile,
|
||||||
|
isPWA,
|
||||||
|
audioInitialized,
|
||||||
|
currentTrackUrl: currentTrack.url,
|
||||||
|
audioSrc: audioCurrent.src,
|
||||||
|
audioReadyState: audioCurrent.readyState
|
||||||
|
});
|
||||||
|
|
||||||
if (isPlaying) {
|
if (isPlaying) {
|
||||||
|
console.log('⏸️ Pausing audio');
|
||||||
audioCurrent.pause();
|
audioCurrent.pause();
|
||||||
setIsPlaying(false);
|
setIsPlaying(false);
|
||||||
onTrackPause(audioCurrent.currentTime);
|
onTrackPause(audioCurrent.currentTime);
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
|
// PWA-specific initialization if needed
|
||||||
|
if (isPWA && !audioInitialized) {
|
||||||
|
console.log('🔧 PWA detected - initializing audio context...');
|
||||||
|
try {
|
||||||
|
const AudioContextClass = window.AudioContext || (window as typeof window & { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;
|
||||||
|
if (AudioContextClass) {
|
||||||
|
const audioContext = new AudioContextClass();
|
||||||
|
if (audioContext.state === 'suspended') {
|
||||||
|
await audioContext.resume();
|
||||||
|
}
|
||||||
|
setAudioInitialized(true);
|
||||||
|
console.log('✅ PWA audio context initialized');
|
||||||
|
}
|
||||||
|
} catch (contextError) {
|
||||||
|
console.log('⚠️ PWA audio context initialization failed:', contextError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// On mobile, ensure audio element is properly loaded before playing
|
// On mobile, ensure audio element is properly loaded before playing
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
// Ensure the audio element has the correct source
|
// Ensure the audio element has the correct source
|
||||||
if (audioCurrent.src !== currentTrack.url) {
|
if (audioCurrent.src !== currentTrack.url) {
|
||||||
|
console.log('🔄 Setting audio source:', currentTrack.url);
|
||||||
audioCurrent.src = currentTrack.url;
|
audioCurrent.src = currentTrack.url;
|
||||||
audioCurrent.load(); // Force reload the audio element
|
audioCurrent.load(); // Force reload the audio element
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for the audio to be ready to play
|
// Wait for the audio to be ready to play
|
||||||
if (audioCurrent.readyState < 3) { // HAVE_FUTURE_DATA
|
if (audioCurrent.readyState < 3) { // HAVE_FUTURE_DATA
|
||||||
|
console.log('⏳ Waiting for audio to be ready...');
|
||||||
await new Promise((resolve, reject) => {
|
await new Promise((resolve, reject) => {
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
audioCurrent.removeEventListener('canplay', handleCanPlay);
|
audioCurrent.removeEventListener('canplay', handleCanPlay);
|
||||||
@@ -494,12 +576,14 @@ export const AudioPlayer: React.FC = () => {
|
|||||||
}, 10000); // 10 second timeout
|
}, 10000); // 10 second timeout
|
||||||
|
|
||||||
const handleCanPlay = () => {
|
const handleCanPlay = () => {
|
||||||
|
console.log('✅ Audio ready to play');
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
audioCurrent.removeEventListener('canplay', handleCanPlay);
|
audioCurrent.removeEventListener('canplay', handleCanPlay);
|
||||||
audioCurrent.removeEventListener('error', handleError);
|
audioCurrent.removeEventListener('error', handleError);
|
||||||
resolve(void 0);
|
resolve(void 0);
|
||||||
};
|
};
|
||||||
const handleError = () => {
|
const handleError = () => {
|
||||||
|
console.log('❌ Audio load error');
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
audioCurrent.removeEventListener('canplay', handleCanPlay);
|
audioCurrent.removeEventListener('canplay', handleCanPlay);
|
||||||
audioCurrent.removeEventListener('error', handleError);
|
audioCurrent.removeEventListener('error', handleError);
|
||||||
@@ -511,16 +595,20 @@ export const AudioPlayer: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('▶️ Attempting to play audio...');
|
||||||
await audioCurrent.play();
|
await audioCurrent.play();
|
||||||
setIsPlaying(true);
|
setIsPlaying(true);
|
||||||
setAudioInitialized(true);
|
setAudioInitialized(true);
|
||||||
onTrackPlay(currentTrack);
|
onTrackPlay(currentTrack);
|
||||||
|
console.log('✅ Audio play successful');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to play audio:', error);
|
console.error('❌ Failed to play audio:', error);
|
||||||
|
|
||||||
// Additional mobile-specific handling
|
// Additional mobile-specific handling
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
try {
|
try {
|
||||||
|
console.log('🔄 Attempting mobile audio recovery...');
|
||||||
|
|
||||||
// Try creating and resuming audio context
|
// Try creating and resuming audio context
|
||||||
const AudioContextClass = window.AudioContext || (window as typeof window & { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;
|
const AudioContextClass = window.AudioContext || (window as typeof window & { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;
|
||||||
if (AudioContextClass) {
|
if (AudioContextClass) {
|
||||||
@@ -534,18 +622,22 @@ export const AudioPlayer: React.FC = () => {
|
|||||||
// Force load and retry
|
// Force load and retry
|
||||||
audioCurrent.load();
|
audioCurrent.load();
|
||||||
await new Promise(resolve => setTimeout(resolve, 200)); // Small delay for iOS
|
await new Promise(resolve => setTimeout(resolve, 200)); // Small delay for iOS
|
||||||
|
console.log('🔄 Retrying audio play...');
|
||||||
await audioCurrent.play();
|
await audioCurrent.play();
|
||||||
setIsPlaying(true);
|
setIsPlaying(true);
|
||||||
onTrackPlay(currentTrack);
|
onTrackPlay(currentTrack);
|
||||||
|
console.log('✅ Audio play retry successful');
|
||||||
} catch (retryError) {
|
} catch (retryError) {
|
||||||
console.error('Audio play retry failed:', retryError);
|
console.error('❌ Audio play retry failed:', retryError);
|
||||||
setIsPlaying(false);
|
setIsPlaying(false);
|
||||||
|
|
||||||
// Show user-friendly error on mobile
|
// Show user-friendly error on mobile
|
||||||
toast({
|
toast({
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
title: "Playback Error",
|
title: "Playback Error",
|
||||||
description: "Unable to play audio. Please try again or check your connection.",
|
description: isPWA
|
||||||
|
? "Unable to play audio in PWA mode. Try refreshing the app or playing in Safari browser."
|
||||||
|
: "Unable to play audio. Please try again or check your connection.",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ export const FullScreenPlayer: React.FC<FullScreenPlayerProps> = ({ isOpen, onCl
|
|||||||
// Only reset scroll on desktop to avoid iOS audio interference
|
// Only reset scroll on desktop to avoid iOS audio interference
|
||||||
const shouldReset = !isMobile && showLyrics && lyrics.length > 0;
|
const shouldReset = !isMobile && showLyrics && lyrics.length > 0;
|
||||||
|
|
||||||
if (currentTrack && shouldReset && lyricsRef.current) {
|
if (currentTrack?.id && shouldReset && lyricsRef.current) {
|
||||||
const resetTimeout = setTimeout(() => {
|
const resetTimeout = setTimeout(() => {
|
||||||
try {
|
try {
|
||||||
const scrollContainer = lyricsRef.current?.querySelector('[data-radix-scroll-area-viewport]') as HTMLElement;
|
const scrollContainer = lyricsRef.current?.querySelector('[data-radix-scroll-area-viewport]') as HTMLElement;
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
|
||||||
import { title } from 'process';
|
|
||||||
|
|
||||||
// Current app version from package.json
|
// Current app version from package.json
|
||||||
const APP_VERSION = '2025.07.31';
|
const APP_VERSION = '2025.07.31';
|
||||||
@@ -209,65 +206,86 @@ export function WhatsNewPopup() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={handleClose}>
|
<>
|
||||||
<DialogContent className="max-w-2xl max-h-[80vh]">
|
{isOpen && (
|
||||||
<DialogHeader className="flex flex-row items-center justify-between space-y-0 pb-4">
|
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||||
<div>
|
{/* Backdrop */}
|
||||||
<DialogTitle className="text-2xl font-bold flex items-center gap-2">
|
<div
|
||||||
What's New in Mice
|
className="fixed inset-0 bg-black/50"
|
||||||
<Badge variant="outline">
|
onClick={handleClose}
|
||||||
{tab === 'latest' ? currentVersionChangelog.version : archiveChangelog?.version}
|
/>
|
||||||
</Badge>
|
|
||||||
</DialogTitle>
|
{/* Dialog content */}
|
||||||
|
<div className="relative bg-background rounded-lg shadow-lg max-w-2xl w-full mx-4 max-h-[80vh] flex flex-col">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex flex-row items-center justify-between space-y-0 p-6 pb-4 shrink-0">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-2xl font-bold flex items-center gap-2">
|
||||||
|
What's New in Mice
|
||||||
|
<Badge variant="outline">
|
||||||
|
{tab === 'latest' ? currentVersionChangelog.version : archiveChangelog?.version}
|
||||||
|
</Badge>
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={handleClose}
|
||||||
|
className="text-muted-foreground hover:text-foreground transition-colors"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tabs */}
|
||||||
|
<div className="flex gap-2 px-6 pt-4 shrink-0">
|
||||||
|
<Button
|
||||||
|
variant={tab === 'latest' ? 'default' : 'outline'}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setTab('latest')}
|
||||||
|
>
|
||||||
|
Latest
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant={tab === 'archive' ? 'default' : 'outline'}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setTab('archive')}
|
||||||
|
disabled={archiveChangelogs.length === 0}
|
||||||
|
>
|
||||||
|
Archive
|
||||||
|
</Button>
|
||||||
|
{tab === 'archive' && archiveChangelogs.length > 0 && (
|
||||||
|
<select
|
||||||
|
className="ml-2 border rounded px-2 py-1 text-sm bg-background"
|
||||||
|
value={selectedArchive}
|
||||||
|
onChange={e => setSelectedArchive(e.target.value)}
|
||||||
|
>
|
||||||
|
{archiveChangelogs.map(entry => (
|
||||||
|
<option key={entry.version} value={entry.version}>
|
||||||
|
{entry.version}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Scrollable content */}
|
||||||
|
<div className="flex-1 overflow-y-auto px-6 py-4 min-h-0">
|
||||||
|
<div className="space-y-6">
|
||||||
|
{tab === 'latest'
|
||||||
|
? renderChangelog(currentVersionChangelog)
|
||||||
|
: archiveChangelog && renderChangelog(archiveChangelog)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer button */}
|
||||||
|
<div className="flex justify-center p-6 pt-4 shrink-0">
|
||||||
|
<Button onClick={handleClose}>
|
||||||
|
Got it!
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</DialogHeader>
|
|
||||||
|
|
||||||
{/* Tabs */}
|
|
||||||
<>
|
|
||||||
<div className="flex gap-2 mb-4">
|
|
||||||
<Button
|
|
||||||
variant={tab === 'latest' ? 'default' : 'outline'}
|
|
||||||
size="sm"
|
|
||||||
onClick={() => setTab('latest')}
|
|
||||||
>
|
|
||||||
Latest
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant={tab === 'archive' ? 'default' : 'outline'}
|
|
||||||
size="sm"
|
|
||||||
onClick={() => setTab('archive')}
|
|
||||||
disabled={archiveChangelogs.length === 0}
|
|
||||||
>
|
|
||||||
Archive
|
|
||||||
</Button>
|
|
||||||
{tab === 'archive' && archiveChangelogs.length > 0 && (
|
|
||||||
<select
|
|
||||||
className="ml-2 border rounded px-2 py-1 text-sm"
|
|
||||||
value={selectedArchive}
|
|
||||||
onChange={e => setSelectedArchive(e.target.value)}
|
|
||||||
>
|
|
||||||
{archiveChangelogs.map(entry => (
|
|
||||||
<option key={entry.version} value={entry.version}>
|
|
||||||
{entry.version}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<ScrollArea className="max-h-[60vh] pr-4">
|
)}
|
||||||
{tab === 'latest'
|
</>
|
||||||
? renderChangelog(currentVersionChangelog)
|
|
||||||
: archiveChangelog && renderChangelog(archiveChangelog)}
|
|
||||||
</ScrollArea>
|
|
||||||
|
|
||||||
<div className="flex justify-center pt-4">
|
|
||||||
<Button onClick={handleClose}>
|
|
||||||
Got it!
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import { Badge } from '@/components/ui/badge';
|
|||||||
import { useNavidromeConfig } from '@/app/components/NavidromeConfigContext';
|
import { useNavidromeConfig } from '@/app/components/NavidromeConfigContext';
|
||||||
import { useTheme } from '@/app/components/ThemeProvider';
|
import { useTheme } from '@/app/components/ThemeProvider';
|
||||||
import { useToast } from '@/hooks/use-toast';
|
import { useToast } from '@/hooks/use-toast';
|
||||||
import { FaServer, FaUser, FaLock, FaCheck, FaTimes, FaPalette, FaLastfm, FaBars } from 'react-icons/fa';
|
import { FaServer, FaUser, FaLock, FaCheck, FaTimes, FaPalette, FaLastfm } from 'react-icons/fa';
|
||||||
|
|
||||||
export function LoginForm({
|
export function LoginForm({
|
||||||
className,
|
className,
|
||||||
@@ -45,20 +45,7 @@ export function LoginForm({
|
|||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
// New settings
|
// New settings - removed sidebar and standalone lastfm options
|
||||||
const [sidebarCollapsed, setSidebarCollapsed] = useState(() => {
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
return localStorage.getItem('sidebar-collapsed') === 'true';
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
const [standaloneLastfmEnabled, setStandaloneLastfmEnabled] = useState(() => {
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
return localStorage.getItem('standalone-lastfm-enabled') === 'true';
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Check if Navidrome is configured via environment variables
|
// Check if Navidrome is configured via environment variables
|
||||||
const hasEnvConfig = React.useMemo(() => {
|
const hasEnvConfig = React.useMemo(() => {
|
||||||
@@ -187,8 +174,6 @@ export function LoginForm({
|
|||||||
const handleFinishSetup = () => {
|
const handleFinishSetup = () => {
|
||||||
// Save all settings
|
// Save all settings
|
||||||
localStorage.setItem('lastfm-scrobbling-enabled', scrobblingEnabled.toString());
|
localStorage.setItem('lastfm-scrobbling-enabled', scrobblingEnabled.toString());
|
||||||
localStorage.setItem('sidebar-collapsed', sidebarCollapsed.toString());
|
|
||||||
localStorage.setItem('standalone-lastfm-enabled', standaloneLastfmEnabled.toString());
|
|
||||||
|
|
||||||
// Mark onboarding as complete
|
// Mark onboarding as complete
|
||||||
localStorage.setItem('onboarding-completed', '1.1.0');
|
localStorage.setItem('onboarding-completed', '1.1.0');
|
||||||
@@ -252,7 +237,7 @@ export function LoginForm({
|
|||||||
if (step === 'settings') {
|
if (step === 'settings') {
|
||||||
return (
|
return (
|
||||||
<div className={cn("flex flex-col gap-6", className)} {...props}>
|
<div className={cn("flex flex-col gap-6", className)} {...props}>
|
||||||
<Card>
|
<Card className='py-5'>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="flex items-center gap-2">
|
<CardTitle className="flex items-center gap-2">
|
||||||
<FaPalette className="w-5 h-5" />
|
<FaPalette className="w-5 h-5" />
|
||||||
@@ -286,29 +271,6 @@ export function LoginForm({
|
|||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Sidebar Settings */}
|
|
||||||
<div className="grid gap-3">
|
|
||||||
<Label className="flex items-center gap-2">
|
|
||||||
<FaBars className="w-4 h-4" />
|
|
||||||
Sidebar Layout
|
|
||||||
</Label>
|
|
||||||
<Select
|
|
||||||
value={sidebarCollapsed ? "collapsed" : "expanded"}
|
|
||||||
onValueChange={(value) => setSidebarCollapsed(value === "collapsed")}
|
|
||||||
>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="expanded">Expanded (with labels)</SelectItem>
|
|
||||||
<SelectItem value="collapsed">Collapsed (icons only)</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
You can always toggle this later using the button in the sidebar
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Last.fm Scrobbling */}
|
{/* Last.fm Scrobbling */}
|
||||||
<div className="grid gap-3">
|
<div className="grid gap-3">
|
||||||
<Label className="flex items-center gap-2">
|
<Label className="flex items-center gap-2">
|
||||||
@@ -334,31 +296,6 @@ export function LoginForm({
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Standalone Last.fm */}
|
|
||||||
<div className="grid gap-3">
|
|
||||||
<Label className="flex items-center gap-2">
|
|
||||||
<FaLastfm className="w-4 h-4" />
|
|
||||||
Standalone Last.fm (Advanced)
|
|
||||||
</Label>
|
|
||||||
<Select
|
|
||||||
value={standaloneLastfmEnabled ? "enabled" : "disabled"}
|
|
||||||
onValueChange={(value) => setStandaloneLastfmEnabled(value === "enabled")}
|
|
||||||
>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="enabled">Enabled</SelectItem>
|
|
||||||
<SelectItem value="disabled">Disabled</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
{standaloneLastfmEnabled
|
|
||||||
? "Direct Last.fm API integration (configure in Settings later)"
|
|
||||||
: "Use only Navidrome's Last.fm integration"}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
<Button onClick={handleFinishSetup} className="w-full">
|
<Button onClick={handleFinishSetup} className="w-full">
|
||||||
<FaCheck className="w-4 h-4 mr-2" />
|
<FaCheck className="w-4 h-4 mr-2" />
|
||||||
@@ -383,7 +320,7 @@ export function LoginForm({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn("flex flex-col gap-6", className)} {...props}>
|
<div className={cn("flex flex-col gap-6", className)} {...props}>
|
||||||
<Card>
|
<Card className="py-5">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="flex items-center gap-2">
|
<CardTitle className="flex items-center gap-2">
|
||||||
<FaServer className="w-5 h-5" />
|
<FaServer className="w-5 h-5" />
|
||||||
|
|||||||
Reference in New Issue
Block a user