From bd764fd9e1a7c8855d5fa035b7b4c5d9b1460203 Mon Sep 17 00:00:00 2001 From: angel Date: Tue, 1 Jul 2025 17:04:42 +0000 Subject: [PATCH] feat: update onboarding logic, enhance Navidrome connection checks, and improve WhatsNewPopup functionality --- .env.local | 2 +- app/components/RootLayoutClient.tsx | 45 ++++++- app/components/WhatsNewPopup.tsx | 14 ++- app/components/start-screen.tsx | 178 ++++++++++++++++++++++++++-- app/settings/page.tsx | 88 ++++++++++++-- components/ui/dialog.tsx | 16 ++- 6 files changed, 307 insertions(+), 36 deletions(-) diff --git a/.env.local b/.env.local index 2ae4e13..fd84c2e 100644 --- a/.env.local +++ b/.env.local @@ -1 +1 @@ -NEXT_PUBLIC_COMMIT_SHA=f721213 +NEXT_PUBLIC_COMMIT_SHA=f0f3d5a diff --git a/app/components/RootLayoutClient.tsx b/app/components/RootLayoutClient.tsx index 69ae609..72a5cf9 100644 --- a/app/components/RootLayoutClient.tsx +++ b/app/components/RootLayoutClient.tsx @@ -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 (
- {/* top right add the logo located in /icon-192.png here and the word mice */}
Logo mice | navidrome client
- +
-
+ ); } return <>{children}; diff --git a/app/components/WhatsNewPopup.tsx b/app/components/WhatsNewPopup.tsx index 89b47ce..17dc71d 100644 --- a/app/components/WhatsNewPopup.tsx +++ b/app/components/WhatsNewPopup.tsx @@ -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() {
- What's New in mice + What's New in mice {currentVersionChangelog.version}

diff --git a/app/components/start-screen.tsx b/app/components/start-screen.tsx index ed71031..6350b32 100644 --- a/app/components/start-screen.tsx +++ b/app/components/start-screen.tsx @@ -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({ + Customize Your Experience + {canSkipNavidrome && Step 1 of 1} Configure your preferences to get started @@ -155,6 +243,29 @@ export function LoginForm({

+ {/* Sidebar Settings */} +
+ + +

+ You can always toggle this later using the button in the sidebar +

+
+ {/* Last.fm Scrobbling */}
+ {/* Standalone Last.fm */} +
+ + +

+ {standaloneLastfmEnabled + ? "Direct Last.fm API integration (configure in Settings later)" + : "Use only Navidrome's Last.fm integration"} +

+
+
- + {!hasEnvConfig && ( + + )}
@@ -205,10 +343,17 @@ export function LoginForm({ + Connect to Navidrome + {canSkipNavidrome && {hasEnvConfig ? "Configured via .env" : "Already Connected"}} - 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" + } @@ -269,6 +414,17 @@ export function LoginForm({ )} + + {canSkipNavidrome && ( + + )} diff --git a/app/settings/page.tsx b/app/settings/page.tsx index fe1f930..081e0fe 100644 --- a/app/settings/page.tsx +++ b/app/settings/page.tsx @@ -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 = () => {

Customize your music experience

- - - - - Navidrome Server - - - Configure connection to your Navidrome music server - - + {!hasEnvConfig && ( + + + + + Navidrome Server + + + Configure connection to your Navidrome music server + +
@@ -358,6 +366,35 @@ const SettingsPage = () => {
+ )} + + {hasEnvConfig && ( + + + + + Navidrome Server + + + Using environment variables configuration + + + +
+ +
+

Configured via Environment Variables

+

Server: {process.env.NEXT_PUBLIC_NAVIDROME_URL}

+

Username: {process.env.NEXT_PUBLIC_NAVIDROME_USERNAME}

+
+
+

+ Your Navidrome connection is configured through environment variables. + Contact your administrator to change these settings. +

+
+
+ )} @@ -406,6 +443,37 @@ const SettingsPage = () => {
+ + + + + Application Settings + + + General application preferences and setup + + + +
+ + +

+ Re-run the initial setup wizard to configure your preferences from scratch +

+
+
+
+ diff --git a/components/ui/dialog.tsx b/components/ui/dialog.tsx index 1647513..9bf4759 100644 --- a/components/ui/dialog.tsx +++ b/components/ui/dialog.tsx @@ -31,8 +31,10 @@ DialogOverlay.displayName = DialogPrimitive.Overlay.displayName const DialogContent = React.forwardRef< React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, children, ...props }, ref) => ( + React.ComponentPropsWithoutRef & { + hideCloseButton?: boolean; + } +>(({ className, children, hideCloseButton = false, ...props }, ref) => ( {children} - - - Close - + {!hideCloseButton && ( + + + Close + + )} ))