feat: update onboarding logic, enhance Navidrome connection checks, and improve WhatsNewPopup functionality

This commit is contained in:
2025-07-01 17:04:42 +00:00
committed by GitHub
parent f0f3d5adb1
commit bd764fd9e1
6 changed files with 307 additions and 36 deletions

View File

@@ -14,19 +14,56 @@ import Image from "next/image";
function NavidromeErrorBoundary({ children }: { children: React.ReactNode }) {
const { error } = useNavidrome();
if (error) {
// Check if this is a first-time user
const hasCompletedOnboarding = typeof window !== 'undefined'
? localStorage.getItem('onboarding-completed')
: false;
// Simple check: has config in localStorage or environment
const hasAnyConfig = React.useMemo(() => {
if (typeof window === 'undefined') return false;
// Check localStorage config
const savedConfig = localStorage.getItem('navidrome-config');
if (savedConfig) {
try {
const config = JSON.parse(savedConfig);
if (config.serverUrl && config.username && config.password) {
return true;
}
} catch (e) {
// Invalid config, continue to env check
}
}
// Check environment variables (visible on client side with NEXT_PUBLIC_)
if (process.env.NEXT_PUBLIC_NAVIDROME_URL &&
process.env.NEXT_PUBLIC_NAVIDROME_USERNAME &&
process.env.NEXT_PUBLIC_NAVIDROME_PASSWORD) {
return true;
}
return false;
}, []);
// Show start screen ONLY if:
// 1. First-time user (no onboarding completed), OR
// 2. User has completed onboarding BUT there's an error AND no config exists
const shouldShowStartScreen = !hasCompletedOnboarding || (hasCompletedOnboarding && error && !hasAnyConfig);
if (shouldShowStartScreen) {
return (
<div className="flex min-h-svh w-full items-center justify-center p-6 md:p-10">
{/* top right add the logo located in /icon-192.png here and the word mice */}
<div className="absolute top-4 left-4 flex items-center space-x-2">
<Image src="/icon-192.png" alt="Logo" width={32} height={32} className="h-8 w-8" />
<span className="text-xl font-semibold">mice | navidrome client</span>
</div>
<div className="w-full max-w-sm">
<LoginForm />
<LoginForm />
</div>
</div>
</div>
);
}
return <>{children}</>;

View File

@@ -14,17 +14,19 @@ const APP_VERSION = '1.0.0';
const CHANGELOG = [
{
version: '1.0.0',
date: '2024-01-10',
date: '2025-07-01',
title: 'Initial Release',
changes: [
'Complete redesign with modern UI',
'Added Favorites functionality for albums, songs, and artists',
'Integrated standalone Last.fm scrobbling support',
'Added collapsible sidebar with icon-only mode',
'Improved search and browsing experience',
'Added history tracking for played songs',
'New Library Artist Page',
'Enhanced audio player with better controls',
'Added settings page for customization options'
'Added settings page for customization options',
'Introduced Whats New popup for version updates',
'Improved UI consistency with new Badge component',
],
breaking: [],
fixes: []
@@ -35,6 +37,10 @@ export function WhatsNewPopup() {
const [isOpen, setIsOpen] = useState(false);
useEffect(() => {
// Only show for users who have completed onboarding
const hasCompletedOnboarding = localStorage.getItem('onboarding-completed');
if (!hasCompletedOnboarding) return;
// Check if we've shown the popup for this version
const lastShownVersion = localStorage.getItem('whats-new-last-shown');
@@ -61,7 +67,7 @@ export function WhatsNewPopup() {
<DialogHeader className="flex flex-row items-center justify-between space-y-0 pb-4">
<div>
<DialogTitle className="text-2xl font-bold flex items-center gap-2">
What's New in mice
What&apos;s New in mice
<Badge variant="outline">{currentVersionChangelog.version}</Badge>
</DialogTitle>
<p className="text-sm text-muted-foreground mt-1">

View File

@@ -1,6 +1,6 @@
'use client';
import React, { useState } from 'react';
import React, { useState, useEffect } from 'react';
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import {
@@ -13,16 +13,18 @@ import {
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Select, SelectTrigger, SelectContent, SelectItem, SelectValue } from '@/components/ui/select';
import { Badge } from '@/components/ui/badge';
import { useNavidromeConfig } from '@/app/components/NavidromeConfigContext';
import { useTheme } from '@/app/components/ThemeProvider';
import { useToast } from '@/hooks/use-toast';
import { FaServer, FaUser, FaLock, FaCheck, FaTimes, FaPalette, FaLastfm } from 'react-icons/fa';
import { FaServer, FaUser, FaLock, FaCheck, FaTimes, FaPalette, FaLastfm, FaBars } from 'react-icons/fa';
export function LoginForm({
className,
...props
}: React.ComponentProps<"div">) {
const [step, setStep] = useState<'login' | 'settings'>('login');
const [canSkipNavidrome, setCanSkipNavidrome] = useState(false);
const { config, updateConfig, testConnection } = useNavidromeConfig();
const { theme, setTheme } = useTheme();
const { toast } = useToast();
@@ -43,6 +45,85 @@ export function LoginForm({
return true;
});
// New settings
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
const hasEnvConfig = React.useMemo(() => {
return !!(process.env.NEXT_PUBLIC_NAVIDROME_URL &&
process.env.NEXT_PUBLIC_NAVIDROME_USERNAME &&
process.env.NEXT_PUBLIC_NAVIDROME_PASSWORD);
}, []);
// Check if Navidrome is already working on component mount
useEffect(() => {
checkNavidromeConnection();
}, []);
const checkNavidromeConnection = async () => {
try {
// First check if there's a working API instance
const { getNavidromeAPI } = await import('@/lib/navidrome');
const api = getNavidromeAPI();
if (api) {
// Test the existing API
const success = await api.ping();
if (success) {
setCanSkipNavidrome(true);
// Get the current config to populate form
if (config.serverUrl && config.username && config.password) {
setFormData({
serverUrl: config.serverUrl,
username: config.username,
password: config.password
});
}
// If this is first-time setup and Navidrome is working, skip to settings
const hasCompletedOnboarding = localStorage.getItem('onboarding-completed');
if (!hasCompletedOnboarding) {
setStep('settings');
}
return;
}
}
// If no working API, check if we have config that just needs testing
if (config.serverUrl && config.username && config.password) {
const success = await testConnection(config);
if (success) {
setCanSkipNavidrome(true);
setFormData({
serverUrl: config.serverUrl,
username: config.username,
password: config.password
});
const hasCompletedOnboarding = localStorage.getItem('onboarding-completed');
if (!hasCompletedOnboarding) {
setStep('settings');
}
}
}
} catch (error) {
console.log('Navidrome connection check failed, will show config step');
}
};
const handleInputChange = (field: string, value: string) => {
setFormData(prev => ({ ...prev, [field]: value }));
};
@@ -104,8 +185,13 @@ export function LoginForm({
};
const handleFinishSetup = () => {
// Save scrobbling preference
// Save all settings
localStorage.setItem('lastfm-scrobbling-enabled', scrobblingEnabled.toString());
localStorage.setItem('sidebar-collapsed', sidebarCollapsed.toString());
localStorage.setItem('standalone-lastfm-enabled', standaloneLastfmEnabled.toString());
// Mark onboarding as complete
localStorage.setItem('onboarding-completed', '1.1.0');
toast({
title: "Setup Complete",
@@ -126,7 +212,9 @@ export function LoginForm({
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<FaPalette className="w-5 h-5" />
Customize Your Experience
{canSkipNavidrome && <Badge variant="outline">Step 1 of 1</Badge>}
</CardTitle>
<CardDescription>
Configure your preferences to get started
@@ -155,6 +243,29 @@ export function LoginForm({
</Select>
</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 */}
<div className="grid gap-3">
<Label className="flex items-center gap-2">
@@ -180,18 +291,45 @@ export function LoginForm({
</p>
</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">
<Button onClick={handleFinishSetup} className="w-full">
<FaCheck className="w-4 h-4 mr-2" />
Complete Setup
</Button>
<Button
variant="outline"
className="w-full"
onClick={() => setStep('login')}
>
Back to Connection Settings
</Button>
{!hasEnvConfig && (
<Button
variant="outline"
className="w-full"
onClick={() => setStep('login')}
>
{canSkipNavidrome ? "Review Connection Settings" : "Back to Connection Settings"}
</Button>
)}
</div>
</div>
</CardContent>
@@ -205,10 +343,17 @@ export function LoginForm({
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<FaServer className="w-5 h-5" />
Connect to Navidrome
{canSkipNavidrome && <Badge variant="outline">{hasEnvConfig ? "Configured via .env" : "Already Connected"}</Badge>}
</CardTitle>
<CardDescription>
Enter your Navidrome server details to get started
{canSkipNavidrome
? hasEnvConfig
? "Your Navidrome connection is configured via environment variables."
: "Your Navidrome connection is working. You can proceed to customize your settings."
: "Enter your Navidrome server details to get started"
}
</CardDescription>
</CardHeader>
<CardContent>
@@ -269,6 +414,17 @@ export function LoginForm({
</>
)}
</Button>
{canSkipNavidrome && (
<Button
type="button"
variant="outline"
className="w-full"
onClick={() => setStep('settings')}
>
Skip to Settings
</Button>
)}
</div>
</div>
</form>

View File

@@ -50,6 +50,13 @@ const SettingsPage = () => {
username: ''
});
// Check if Navidrome is configured via environment variables
const hasEnvConfig = React.useMemo(() => {
return !!(process.env.NEXT_PUBLIC_NAVIDROME_URL &&
process.env.NEXT_PUBLIC_NAVIDROME_USERNAME &&
process.env.NEXT_PUBLIC_NAVIDROME_PASSWORD);
}, []);
// Sidebar settings
const [sidebarCollapsed, setSidebarCollapsed] = useState(() => {
if (typeof window !== 'undefined') {
@@ -272,16 +279,17 @@ const SettingsPage = () => {
<p className="text-muted-foreground">Customize your music experience</p>
</div>
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<FaServer className="w-5 h-5" />
Navidrome Server
</CardTitle>
<CardDescription>
Configure connection to your Navidrome music server
</CardDescription>
</CardHeader>
{!hasEnvConfig && (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<FaServer className="w-5 h-5" />
Navidrome Server
</CardTitle>
<CardDescription>
Configure connection to your Navidrome music server
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="space-y-2">
<Label htmlFor="server-url">Server URL</Label>
@@ -358,6 +366,35 @@ const SettingsPage = () => {
</div>
</CardContent>
</Card>
)}
{hasEnvConfig && (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<FaServer className="w-5 h-5" />
Navidrome Server
</CardTitle>
<CardDescription>
Using environment variables configuration
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center gap-3 p-3 rounded-lg bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800">
<FaCheck className="w-4 h-4 text-green-600" />
<div className="text-sm">
<p className="text-green-600 font-medium">Configured via Environment Variables</p>
<p className="text-green-600">Server: {process.env.NEXT_PUBLIC_NAVIDROME_URL}</p>
<p className="text-green-600">Username: {process.env.NEXT_PUBLIC_NAVIDROME_USERNAME}</p>
</div>
</div>
<p className="text-sm text-muted-foreground">
Your Navidrome connection is configured through environment variables.
Contact your administrator to change these settings.
</p>
</CardContent>
</Card>
)}
<Card>
<CardHeader>
@@ -406,6 +443,37 @@ const SettingsPage = () => {
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<FaCog className="w-5 h-5" />
Application Settings
</CardTitle>
<CardDescription>
General application preferences and setup
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="space-y-3">
<Label>First-Time Setup</Label>
<Button
variant="outline"
onClick={() => {
localStorage.removeItem('onboarding-completed');
window.location.reload();
}}
className="w-full sm:w-auto"
>
<Settings className="w-4 h-4 mr-2" />
Run Setup Wizard Again
</Button>
<p className="text-sm text-muted-foreground">
Re-run the initial setup wizard to configure your preferences from scratch
</p>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">