27 Commits
master ... dev

Author SHA1 Message Date
00ce124a3e fix: update tag pattern in release workflow and set default tag to 'latest'
Some checks failed
Docker Image (Nightly) / check_changes (push) Successful in 5s
Docker Image (Nightly) / push_to_registry (push) Failing after 11s
2026-01-25 03:39:16 +00:00
7262305343 fix: update workflow name and remove unnecessary triggers in nightly build 2026-01-25 03:34:21 +00:00
8009609825 chore: fix
Some checks failed
GitHub Release / Create Release (push) Failing after 10s
2026-01-25 03:18:55 +00:00
b6342ded21 fix: update tag pattern in cliff.toml to match date format 2026-01-25 03:18:45 +00:00
09de406890 fix: implement proper crossfade with fade-in on new tracks and fade-out on track end 2026-01-25 03:01:28 +00:00
477b172c6c feat: add sidebar customization to start screen with default items (home,queue,artists,albums,playlists,favorites,settings) and playlists-only shortcuts 2026-01-25 02:57:10 +00:00
ed41ad6671 refactor: change album cover size from 600x600 to 300x300 for better performance 2026-01-25 02:54:17 +00:00
b59deee486 feat: add shuffle button to album page for both mobile and desktop layouts 2026-01-25 02:50:25 +00:00
f1957c7d91 feat: add default theme option and fix toast z-index, add error styling to start screen inputs 2026-01-25 02:48:33 +00:00
88c31c5082 feat: add sidebar customization to start screen with default shortcuts 2026-01-25 02:06:15 +00:00
4721c058ae fix: increase toast z-index and set theme dropdown default value 2026-01-25 02:04:17 +00:00
e5bd7209eb doc: update readme's git url 2026-01-25 01:52:55 +00:00
d8b03ec702 chore: merge offline-support branch into dev 2026-01-25 01:43:41 +00:00
ae288cc4e4 Merge pull request 'chore(deps): bump @radix-ui/react-toast from 1.2.4 to 1.2.14' (#41) from dependabot/npm_and_yarn/radix-ui/react-toast-1.2.14 into dev
Reviewed-on: #41
2025-08-16 16:05:27 -05:00
666722056b Merge pull request 'chore(deps-dev): bump chalk from 5.4.1 to 5.5.0' (#42) from dependabot/npm_and_yarn/chalk-5.5.0 into dev
Reviewed-on: #42
2025-08-16 16:05:16 -05:00
06aaa8cf74 Merge pull request 'chore(deps): bump react-resizable-panels from 3.0.3 to 3.0.4' (#43) from dependabot/npm_and_yarn/react-resizable-panels-3.0.4 into dev
Reviewed-on: #43
2025-08-16 16:05:00 -05:00
394bdaca89 Merge branch 'dev' into dependabot/npm_and_yarn/react-resizable-panels-3.0.4 2025-08-16 16:04:06 -05:00
5dda540a16 Merge pull request 'chore(deps): bump sonner from 2.0.5 to 2.0.7' (#44) from dependabot/npm_and_yarn/sonner-2.0.7 into dev
Reviewed-on: #44
2025-08-16 16:03:36 -05:00
7a146e9e4f Merge pull request 'chore(deps): bump @hookform/resolvers from 5.2.0 to 5.2.1' (#45) from dependabot/npm_and_yarn/hookform/resolvers-5.2.1 into dev
Reviewed-on: #45
2025-08-16 16:03:04 -05:00
7ac5eb89ce style: update README formatting and improve content clarity 2025-08-06 02:15:29 +00:00
dependabot[bot]
4652689aec chore(deps): bump @hookform/resolvers from 5.2.0 to 5.2.1
Bumps [@hookform/resolvers](https://github.com/react-hook-form/resolvers) from 5.2.0 to 5.2.1.
- [Release notes](https://github.com/react-hook-form/resolvers/releases)
- [Commits](https://github.com/react-hook-form/resolvers/compare/v5.2.0...v5.2.1)

---
updated-dependencies:
- dependency-name: "@hookform/resolvers"
  dependency-version: 5.2.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-04 00:55:56 +00:00
dependabot[bot]
a3dcfc043d chore(deps): bump sonner from 2.0.5 to 2.0.7
Bumps [sonner](https://github.com/emilkowalski/sonner) from 2.0.5 to 2.0.7.
- [Release notes](https://github.com/emilkowalski/sonner/releases)
- [Commits](https://github.com/emilkowalski/sonner/compare/v2.0.5...v2.0.7)

---
updated-dependencies:
- dependency-name: sonner
  dependency-version: 2.0.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-04 00:55:37 +00:00
dependabot[bot]
efb4e5aef5 chore(deps): bump react-resizable-panels from 3.0.3 to 3.0.4
Bumps [react-resizable-panels](https://github.com/bvaughn/react-resizable-panels) from 3.0.3 to 3.0.4.
- [Release notes](https://github.com/bvaughn/react-resizable-panels/releases)
- [Commits](https://github.com/bvaughn/react-resizable-panels/compare/3.0.3...3.0.4)

---
updated-dependencies:
- dependency-name: react-resizable-panels
  dependency-version: 3.0.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-04 00:55:11 +00:00
dependabot[bot]
b9e75622d1 chore(deps-dev): bump chalk from 5.4.1 to 5.5.0
Bumps [chalk](https://github.com/chalk/chalk) from 5.4.1 to 5.5.0.
- [Release notes](https://github.com/chalk/chalk/releases)
- [Commits](https://github.com/chalk/chalk/compare/v5.4.1...v5.5.0)

---
updated-dependencies:
- dependency-name: chalk
  dependency-version: 5.5.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-04 00:54:30 +00:00
dependabot[bot]
b82ba0749b chore(deps): bump @radix-ui/react-toast from 1.2.4 to 1.2.14
Bumps [@radix-ui/react-toast](https://github.com/radix-ui/primitives) from 1.2.4 to 1.2.14.
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

---
updated-dependencies:
- dependency-name: "@radix-ui/react-toast"
  dependency-version: 1.2.14
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-04 00:53:41 +00:00
8886302809 Merge pull request #39 from sillyangel/dependabot/npm_and_yarn/dev-99ea30e4b7
chore(deps-dev): bump the dev group across 1 directory with 2 updates
2025-08-03 09:39:18 -05:00
dependabot[bot]
b5669cf831 chore(deps-dev): bump the dev group across 1 directory with 2 updates
Bumps the dev group with 2 updates in the / directory: [eslint-config-next](https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next) and [typescript](https://github.com/microsoft/TypeScript).


Updates `eslint-config-next` from 15.4.4 to 15.4.5
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/commits/v15.4.5/packages/eslint-config-next)

Updates `typescript` from 5.8.3 to 5.9.2
- [Release notes](https://github.com/microsoft/TypeScript/releases)
- [Changelog](https://github.com/microsoft/TypeScript/blob/main/azure-pipelines.release-publish.yml)
- [Commits](https://github.com/microsoft/TypeScript/compare/v5.8.3...v5.9.2)

---
updated-dependencies:
- dependency-name: eslint-config-next
  dependency-version: 15.4.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev
- dependency-name: typescript
  dependency-version: 5.9.2
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-01 18:32:37 +00:00
11 changed files with 109 additions and 32 deletions

View File

@@ -1 +1 @@
NEXT_PUBLIC_COMMIT_SHA=b5fc053
NEXT_PUBLIC_COMMIT_SHA=477b172

View File

@@ -3,7 +3,8 @@ name: GitHub Release
on:
push:
tags:
- 'v*'
- '[0-9][0-9][0-9][0-9].[0-9][0-9].[0-9][0-9]'
permissions:
contents: write

View File

@@ -1,13 +1,9 @@
name: Development Docker Image (Nightly)
name: Docker Image (Nightly)
on:
schedule:
# Run every night at 5:00 UTC
- cron: '0 5 * * *'
push:
branches:
- dev
workflow_dispatch: # Allow manual triggering
env:
REGISTRY: docker.io
@@ -89,7 +85,6 @@ jobs:
tags: |
type=raw,value=nightly
type=raw,value=dev-latest
type=sha,prefix=dev-
labels: |
org.opencontainers.image.created=${{ github.event.head_commit.timestamp }}
org.opencontainers.image.licenses=MIT

View File

@@ -68,7 +68,7 @@ jobs:
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
tags: ${{ steps.meta.outputs.tags || 'latest' }}
labels: |
${{ steps.meta.outputs.labels }}
org.opencontainers.image.description=$(cat README.md | head -20 | tr '\n' ' ')
@@ -81,10 +81,3 @@ jobs:
cache-to: |
type=gha,mode=max,scope=deps-only
# - name: Docker Hub Description
# uses: peter-evans/dockerhub-description@v4
# with:
# username: ${{ vars.DOCKERHUB_USERNAME }}
# password: ${{ secrets.DOCKERHUB_TOKEN }}
# repository: sillyangel/mice

View File

@@ -18,7 +18,7 @@ This is a "Modern" Navidrome (or Subsonic) client built with [Next.js](https://n
- **Search** - Find music across your entire library
- **Audio Player** with queue management
- **Scrobbling** - Track your listening history
- **Playlist Management** - Create and manage playlists
<!-- - **Playlist Management** - Create and manage playlists -->
### Preview
![preview](https://github.com/sillyangel/mice/blob/main/public/home-preview.png?raw=true)
@@ -34,8 +34,8 @@ This is a "Modern" Navidrome (or Subsonic) client built with [Next.js](https://n
1. **Clone and install the required dependencies**
```bash
git clone https://github.com/sillyangel/project-still.git
cd project-still/
git clone https://github.com/sillyangel/mice.git
cd mice/
pnpm install
# or npm
@@ -113,7 +113,7 @@ docker run -p 3000:3000 \
sillyangel/mice:latest
```
**For detailed Docker configuration, environment variables, troubleshooting, and advanced setups, see [DOCKER.md](./DOCKER.md)**
**For detailed Docker configuration, environment variables, troubleshooting, and advanced setups, see [DOCKER.md](./docs/DOCKER.md)**
## Tech Stack

View File

@@ -4,7 +4,7 @@ import { useParams } from 'next/navigation';
import Image from 'next/image';
import { Album, Song } from '@/lib/navidrome';
import { useNavidrome } from '@/app/components/NavidromeContext';
import { Play, Heart } from 'lucide-react';
import { Play, Heart, Shuffle } from 'lucide-react';
import { Button } from '@/components/ui/button';
import Link from 'next/link';
import { useAudioPlayer } from '@/app/components/AudioPlayerContext'
@@ -111,6 +111,19 @@ export default function AlbumPage() {
}
};
const handleShuffleAlbum = async (): Promise<void> => {
if (!album || !tracklist.length) return;
try {
// Shuffle the tracklist
const shuffled = [...tracklist].sort(() => Math.random() - 0.5);
// Play the first shuffled track
await playAlbumFromTrack(album.id, shuffled[0].id);
} catch (error) {
console.error('Failed to shuffle album:', error);
}
};
const isCurrentlyPlaying = (song: Song): boolean => {
return currentTrack?.id === song.id;
};
@@ -124,13 +137,13 @@ export default function AlbumPage() {
// Dynamic cover art URLs based on image size
const getMobileCoverArtUrl = () => {
return album.coverArt && api
? api.getCoverArtUrl(album.coverArt, 600)
? api.getCoverArtUrl(album.coverArt, 300)
: '/default-user.jpg';
};
const getDesktopCoverArtUrl = () => {
return album.coverArt && api
? api.getCoverArtUrl(album.coverArt, 600)
? api.getCoverArtUrl(album.coverArt, 300)
: '/default-user.jpg';
};
@@ -146,8 +159,8 @@ export default function AlbumPage() {
<Image
src={getMobileCoverArtUrl()}
alt={album.name}
width={600}
height={600}
width={300}
height={300}
className="rounded-md shadow-lg"
/>
</div>
@@ -173,6 +186,14 @@ export default function AlbumPage() {
>
<Play className="w-6 h-6" />
</Button>
<Button
variant="outline"
className="w-12 h-12 rounded-full p-0"
onClick={handleShuffleAlbum}
title="Shuffle Album"
>
<Shuffle className="w-5 h-5" />
</Button>
</div>
</div>
</div>
@@ -182,8 +203,8 @@ export default function AlbumPage() {
<Image
src={getDesktopCoverArtUrl()}
alt={album.name}
width={600}
height={600}
width={300}
height={300}
className="rounded-md"
/>
<div className="space-y-2">
@@ -200,8 +221,13 @@ export default function AlbumPage() {
{/* Controls row */}
<div className="flex items-center gap-3">
<Button className="px-5" onClick={() => playAlbum(album.id)}>
<Play className="w-4 h-4 mr-2" />
Play
</Button>
<Button variant="outline" className="px-5" onClick={handleShuffleAlbum}>
<Shuffle className="w-4 h-4 mr-2" />
Shuffle
</Button>
</div>
{/* Album info */}

View File

@@ -388,6 +388,11 @@ export const AudioPlayer: React.FC = () => {
// Auto-play only if the track has the autoPlay flag and audio is initialized
if (currentTrack.autoPlay && (!isMobile || audioInitialized)) {
// Start crossfade fade-in if enabled
if (audioSettings.crossfadeDuration > 0 && audioEffects) {
audioEffects.startCrossfade();
}
// Add a small delay for iOS compatibility
const playPromise = isMobile ?
new Promise(resolve => setTimeout(resolve, 100)).then(() => audioCurrent.play()) :
@@ -410,7 +415,7 @@ export const AudioPlayer: React.FC = () => {
setIsPlaying(false);
}
}
}, [currentTrack, onTrackStart, onTrackPlay, isMobile, audioInitialized, audioEffects, audioSettings.gaplessPlayback, audioSettings.replayGainEnabled, queue]);
}, [currentTrack, onTrackStart, onTrackPlay, isMobile, audioInitialized, audioEffects, audioSettings.gaplessPlayback, audioSettings.replayGainEnabled, audioSettings.crossfadeDuration, queue]);
useEffect(() => {
const audioCurrent = audioRef.current;

View File

@@ -18,6 +18,7 @@ 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 type { SidebarItem, SidebarLayoutSettings } from '@/hooks/use-sidebar-layout';
export function LoginForm({
className,
@@ -36,6 +37,7 @@ export function LoginForm({
});
const [isTesting, setIsTesting] = useState(false);
const [hasError, setHasError] = useState(false);
// Settings for step 2
const [scrobblingEnabled, setScrobblingEnabled] = useState(() => {
@@ -45,7 +47,8 @@ export function LoginForm({
return true;
});
// New settings - removed sidebar and standalone lastfm options
// Sidebar settings with new defaults
const [sidebarShortcuts, setSidebarShortcuts] = useState<'albums' | 'playlists' | 'both'>('playlists');
// Check if Navidrome is configured via environment variables
const hasEnvConfig = React.useMemo(() => {
@@ -119,6 +122,7 @@ export function LoginForm({
e.preventDefault();
if (!formData.serverUrl || !formData.username || !formData.password) {
setHasError(true);
toast({
title: "Missing Information",
description: "Please fill in all fields before proceeding.",
@@ -128,6 +132,7 @@ export function LoginForm({
}
setIsTesting(true);
setHasError(false);
try {
// Strip trailing slash from server URL before testing
const cleanServerUrl = formData.serverUrl.replace(/\/+$/, '');
@@ -154,6 +159,7 @@ export function LoginForm({
// Move to settings step
setStep('settings');
} else {
setHasError(true);
toast({
title: "Connection Failed",
description: "Could not connect to the server. Please check your settings.",
@@ -161,6 +167,7 @@ export function LoginForm({
});
}
} catch (error) {
setHasError(true);
toast({
title: "Connection Error",
description: "An error occurred while testing the connection.",
@@ -175,6 +182,30 @@ export function LoginForm({
// Save all settings
localStorage.setItem('lastfm-scrobbling-enabled', scrobblingEnabled.toString());
// Save sidebar settings with new defaults
const defaultSidebarItems: SidebarItem[] = [
{ id: 'home', label: 'Home', visible: true, icon: 'home', href: '/' },
{ id: 'queue', label: 'Queue', visible: true, icon: 'queue', href: '/queue' },
{ id: 'artists', label: 'Artists', visible: true, icon: 'artists', href: '/library/artists' },
{ id: 'albums', label: 'Albums', visible: true, icon: 'albums', href: '/library/albums' },
{ id: 'playlists', label: 'Playlists', visible: true, icon: 'playlists', href: '/library/playlists' },
{ id: 'favorites', label: 'Favorites', visible: true, icon: 'favorites', href: '/favorites' },
{ id: 'settings', label: 'Settings', visible: true, icon: 'settings', href: '/settings' },
// Hidden by default
{ id: 'search', label: 'Search', visible: false, icon: 'search', href: '/search' },
{ id: 'radio', label: 'Radio', visible: false, icon: 'radio', href: '/radio' },
{ id: 'browse', label: 'Browse', visible: false, icon: 'browse', href: '/browse' },
{ id: 'songs', label: 'Songs', visible: false, icon: 'songs', href: '/library/songs' },
{ id: 'history', label: 'History', visible: false, icon: 'history', href: '/history' },
];
const sidebarSettings: SidebarLayoutSettings = {
items: defaultSidebarItems,
shortcuts: sidebarShortcuts,
showIcons: true,
};
localStorage.setItem('sidebar-layout-settings', JSON.stringify(sidebarSettings));
// Mark onboarding as complete
localStorage.setItem('onboarding-completed', '1.1.0');
@@ -260,6 +291,7 @@ export function LoginForm({
<SelectValue placeholder="Select a theme" />
</SelectTrigger>
<SelectContent>
<SelectItem value="default">Default</SelectItem>
<SelectItem value="blue">Blue</SelectItem>
<SelectItem value="violet">Violet</SelectItem>
<SelectItem value="red">Red</SelectItem>
@@ -296,6 +328,24 @@ export function LoginForm({
</p>
</div>
{/* Sidebar Shortcuts */}
<div className="grid gap-3">
<Label>Sidebar Shortcuts</Label>
<Select value={sidebarShortcuts} onValueChange={(value: 'albums' | 'playlists' | 'both') => setSidebarShortcuts(value)}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="playlists">Playlists Only</SelectItem>
<SelectItem value="albums">Albums Only</SelectItem>
<SelectItem value="both">Both</SelectItem>
</SelectContent>
</Select>
<p className="text-sm text-muted-foreground">
Choose what shortcuts appear in your sidebar
</p>
</div>
<div className="flex flex-col gap-3">
<Button onClick={handleFinishSetup} className="w-full">
<FaCheck className="w-4 h-4 mr-2" />
@@ -349,6 +399,7 @@ export function LoginForm({
placeholder="https://your-navidrome-server.com"
value={formData.serverUrl}
onChange={(e) => handleInputChange('serverUrl', e.target.value)}
className={hasError ? "border-destructive focus-visible:ring-destructive" : ""}
required
/>
</div>
@@ -363,6 +414,7 @@ export function LoginForm({
placeholder="your-username"
value={formData.username}
onChange={(e) => handleInputChange('username', e.target.value)}
className={hasError ? "border-destructive focus-visible:ring-destructive" : ""}
required
/>
</div>
@@ -376,6 +428,7 @@ export function LoginForm({
type="password"
value={formData.password}
onChange={(e) => handleInputChange('password', e.target.value)}
className={hasError ? "border-destructive focus-visible:ring-destructive" : ""}
required
/>
</div>

View File

@@ -41,6 +41,6 @@ commit_parsers = [
]
protect_breaking_commits = false
filter_commits = false
tag_pattern = "v[0-9].*"
tag_pattern = "[0-9][0-9][0-9][0-9].[0-9][0-9].[0-9][0-9]"
topo_order = false
sort_commits = "oldest"

View File

@@ -16,7 +16,7 @@ const ToastViewport = React.forwardRef<
<ToastPrimitives.Viewport
ref={ref}
className={cn(
"fixed top-0 z-100 flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
"fixed top-0 z-[9999] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
className
)}
{...props}

View File

@@ -126,6 +126,7 @@ export class AudioEffects {
public setCrossfadeTime(seconds: number) {
if (this.crossfadeGainNode) {
const now = this.context.currentTime;
this.crossfadeGainNode.gain.cancelScheduledValues(now);
this.crossfadeGainNode.gain.setValueAtTime(1, now);
this.crossfadeGainNode.gain.linearRampToValueAtTime(0, now + seconds);
}
@@ -133,7 +134,10 @@ export class AudioEffects {
public startCrossfade() {
if (this.crossfadeGainNode) {
this.crossfadeGainNode.gain.value = 1;
const now = this.context.currentTime;
this.crossfadeGainNode.gain.cancelScheduledValues(now);
this.crossfadeGainNode.gain.setValueAtTime(0, now);
this.crossfadeGainNode.gain.linearRampToValueAtTime(1, now + 0.5); // Fast fade in
}
}