mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-21 22:15:40 +00:00
feat: queue improvements, system browser links (v1.4.5)
- Queue: year added to now-playing meta, cover enlarged to 90px + top-aligned, default width 340px - Artist pages: Last.fm/Wikipedia open in system browser with button label feedback - README: AUR badge + install section, Nord theme mention, in-app browser bullet removed Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,18 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [1.4.5] - 2026-03-17
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
#### Artist Pages — External Links
|
||||||
|
- Last.fm and Wikipedia buttons now open in the **system browser** instead of an in-app window. The button label temporarily changes to "Opened in browser" / "Im Browser geöffnet" for 2.5 seconds as visual confirmation.
|
||||||
|
|
||||||
|
#### Queue Panel
|
||||||
|
- **Release year** added to the now-playing meta box, shown below the album name (when available).
|
||||||
|
- **Cover art enlarged** from 72 × 72 px to 90 × 90 px, aligned to the top of the meta block so it lines up with the song title.
|
||||||
|
- **Default width increased** from 300 px to 340 px.
|
||||||
|
|
||||||
## [1.4.4] - 2026-03-17
|
## [1.4.4] - 2026-03-17
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -181,4 +181,4 @@ The workflow is split into three jobs: `create-release` (creates the GitHub Rele
|
|||||||
- **RandomAlbums**: No auto-refresh timer — loads once on mount, manual refresh button only. `loadingRef` guards against concurrent fetches.
|
- **RandomAlbums**: No auto-refresh timer — loads once on mount, manual refresh button only. `loadingRef` guards against concurrent fetches.
|
||||||
- **Queue shuffle**: `shuffleQueue()` in playerStore keeps current track at index 0, Fisher-Yates shuffles the rest.
|
- **Queue shuffle**: `shuffleQueue()` in playerStore keeps current track at index 0, Fisher-Yates shuffles the rest.
|
||||||
- **Tooltip z-index**: `.main-content` has `z-index: 1` so tooltips in the content area render above the queue panel (which has no z-index but appears later in DOM order). Multi-line tooltips: add `data-tooltip-wrap` attribute + use `\n` in the string; CSS rule `[data-tooltip-wrap]::after { white-space: pre-line; max-width: 220px }`.
|
- **Tooltip z-index**: `.main-content` has `z-index: 1` so tooltips in the content area render above the queue panel (which has no z-index but appears later in DOM order). Multi-line tooltips: add `data-tooltip-wrap` attribute + use `\n` in the string; CSS rule `[data-tooltip-wrap]::after { white-space: pre-line; max-width: 220px }`.
|
||||||
- **Version**: 1.4.4
|
- **Version**: 1.4.5
|
||||||
|
|||||||
@@ -7,12 +7,13 @@
|
|||||||
<a href="https://github.com/Psychotoxical/psysonic/releases/latest"><img alt="Latest Release" src="https://img.shields.io/github/v/release/Psychotoxical/psysonic?style=flat-square&color=8839ef"></a>
|
<a href="https://github.com/Psychotoxical/psysonic/releases/latest"><img alt="Latest Release" src="https://img.shields.io/github/v/release/Psychotoxical/psysonic?style=flat-square&color=8839ef"></a>
|
||||||
<a href="https://github.com/Psychotoxical/psysonic/blob/main/LICENSE"><img alt="License: GPL v3" src="https://img.shields.io/badge/License-GPLv3-cba6f7?style=flat-square"></a>
|
<a href="https://github.com/Psychotoxical/psysonic/blob/main/LICENSE"><img alt="License: GPL v3" src="https://img.shields.io/badge/License-GPLv3-cba6f7?style=flat-square"></a>
|
||||||
<a href="https://tauri.app/"><img alt="Built with Tauri" src="https://img.shields.io/badge/Built%20with-Tauri-242938?style=flat-square&logo=tauri"></a>
|
<a href="https://tauri.app/"><img alt="Built with Tauri" src="https://img.shields.io/badge/Built%20with-Tauri-242938?style=flat-square&logo=tauri"></a>
|
||||||
|
<a href="https://aur.archlinux.org/packages/psysonic"><img alt="AUR" src="https://img.shields.io/aur/version/psysonic?style=flat-square&color=1793d1"></a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
Psysonic is a beautiful desktop music player built completely from the ground up for the modern era. Utilizing **Tauri v2** and **React**, it offers a native-feeling, lightweight, and incredibly fast experience with a stunning UI inspired by the [Catppuccin](https://github.com/catppuccin/catppuccin) aesthetic.
|
Psysonic is a beautiful desktop music player built completely from the ground up for the modern era. Utilizing **Tauri v2** and **React**, it offers a native-feeling, lightweight, and incredibly fast experience with a stunning UI inspired by the [Catppuccin](https://github.com/catppuccin/catppuccin) and [Nord](https://www.nordtheme.com/) aesthetics.
|
||||||
|
|
||||||
Designed specifically for users hosting their own music via Navidrome or other Subsonic API servers, Psysonic aims to be the best way to interact with your personal library.
|
Designed specifically for users hosting their own music via Navidrome or other Subsonic API servers, Psysonic aims to be the best way to interact with your personal library.
|
||||||
|
|
||||||
@@ -33,7 +34,6 @@ Designed specifically for users hosting their own music via Navidrome or other S
|
|||||||
- 🌊 **MilkDrop Visualizer**: Full-screen Butterchurn/MilkDrop visualizer in the Ambient Stage with hundreds of presets and smooth transitions.
|
- 🌊 **MilkDrop Visualizer**: Full-screen Butterchurn/MilkDrop visualizer in the Ambient Stage with hundreds of presets and smooth transitions.
|
||||||
- 🎛️ **Queue Management**: Drag & drop reordering, shuffle, playlist saving/loading, and server-side queue synchronization.
|
- 🎛️ **Queue Management**: Drag & drop reordering, shuffle, playlist saving/loading, and server-side queue synchronization.
|
||||||
- 🎼 **Random Mix**: Generate a random playlist from your entire library. Filter by keyword or pick a Super Genre (Metal, Rock, Electronic, Jazz…) for a focused mix with progressive loading.
|
- 🎼 **Random Mix**: Generate a random playlist from your entire library. Filter by keyword or pick a Super Genre (Metal, Rock, Electronic, Jazz…) for a focused mix with progressive loading.
|
||||||
- 🔗 **In-App Browser**: Last.fm and Wikipedia links on artist pages open in a dedicated native window — no need to leave the app.
|
|
||||||
- 🔄 **Update Notifications**: Built-in update checker (on startup + every 10 minutes) that notifies you when a new version is available on GitHub.
|
- 🔄 **Update Notifications**: Built-in update checker (on startup + every 10 minutes) that notifies you when a new version is available on GitHub.
|
||||||
- 🖥️ **Cross-Platform**: Available natively for Windows, macOS, and Linux (including Wayland support).
|
- 🖥️ **Cross-Platform**: Available natively for Windows, macOS, and Linux (including Wayland support).
|
||||||
|
|
||||||
@@ -47,7 +47,11 @@ Navigate to the [Releases](https://github.com/Psychotoxical/psysonic/releases) p
|
|||||||
|
|
||||||
- **Windows**: `.exe` or `.msi`
|
- **Windows**: `.exe` or `.msi`
|
||||||
- **macOS**: `.dmg` (Universal or Apple Silicon)
|
- **macOS**: `.dmg` (Universal or Apple Silicon)
|
||||||
- **Linux**: `.AppImage` or `.deb`
|
- **Linux (Ubuntu/Debian)**: `.deb` from GitHub Releases
|
||||||
|
- **Linux (Fedora/RHEL)**: `.rpm` from GitHub Releases
|
||||||
|
- **Linux (Arch/CachyOS)**: AUR — `yay -S psysonic` or `paru -S psysonic`
|
||||||
|
|
||||||
|
> The AUR package builds from source using your system's own WebKitGTK — no bundled libs, no EGL/Mesa compatibility issues.
|
||||||
|
|
||||||
## 🚀 Getting Started
|
## 🚀 Getting Started
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "psysonic",
|
"name": "psysonic",
|
||||||
"version": "1.4.4",
|
"version": "1.4.5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Maintainer: stelle <stelle@psychotoxical.dev>
|
# Maintainer: stelle <stelle@psychotoxical.dev>
|
||||||
pkgname=psysonic
|
pkgname=psysonic
|
||||||
pkgver=1.4.3
|
pkgver=1.4.5
|
||||||
pkgrel=1
|
pkgrel=1
|
||||||
pkgdesc="Desktop music player for Subsonic API-compatible servers (Navidrome, Gonic, etc.)"
|
pkgdesc="Desktop music player for Subsonic API-compatible servers (Navidrome, Gonic, etc.)"
|
||||||
arch=('x86_64')
|
arch=('x86_64')
|
||||||
|
|||||||
Generated
+1
-1
@@ -3118,7 +3118,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "psysonic"
|
name = "psysonic"
|
||||||
version = "1.4.2"
|
version = "1.4.5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"reqwest 0.12.28",
|
"reqwest 0.12.28",
|
||||||
"rodio",
|
"rodio",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "psysonic"
|
name = "psysonic"
|
||||||
version = "1.4.4"
|
version = "1.4.5"
|
||||||
description = "Psysonic Desktop Music Player"
|
description = "Psysonic Desktop Music Player"
|
||||||
authors = []
|
authors = []
|
||||||
license = ""
|
license = ""
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ fn exit_app(app_handle: tauri::AppHandle) {
|
|||||||
app_handle.exit(0);
|
app_handle.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
pub fn run() {
|
pub fn run() {
|
||||||
let (audio_engine, _audio_thread) = audio::create_engine();
|
let (audio_engine, _audio_thread) = audio::create_engine();
|
||||||
|
|
||||||
@@ -107,9 +108,11 @@ pub fn run() {
|
|||||||
})
|
})
|
||||||
.on_window_event(|window, event| {
|
.on_window_event(|window, event| {
|
||||||
if let tauri::WindowEvent::CloseRequested { .. } = event {
|
if let tauri::WindowEvent::CloseRequested { .. } = event {
|
||||||
// Emit event so JS can decide: hide to tray or allow close.
|
// Only intercept close for the main window (hide to tray).
|
||||||
// JS handles prevent_close via onCloseRequested() in App.tsx.
|
// Browser popup windows (browser_*) close normally.
|
||||||
let _ = window.emit("window:close-requested", ());
|
if window.label() == "main" {
|
||||||
|
let _ = window.emit("window:close-requested", ());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "Psysonic",
|
"productName": "Psysonic",
|
||||||
"version": "1.4.4",
|
"version": "1.4.5",
|
||||||
"identifier": "dev.psysonic.app",
|
"identifier": "dev.psysonic.app",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "npm run dev",
|
"beforeDevCommand": "npm run dev",
|
||||||
|
|||||||
+1
-1
@@ -73,7 +73,7 @@ function AppShell() {
|
|||||||
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(() => {
|
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(() => {
|
||||||
return localStorage.getItem('psysonic_sidebar_collapsed') === 'true';
|
return localStorage.getItem('psysonic_sidebar_collapsed') === 'true';
|
||||||
});
|
});
|
||||||
const [queueWidth, setQueueWidth] = useState(300);
|
const [queueWidth, setQueueWidth] = useState(340);
|
||||||
const [isDraggingQueue, setIsDraggingQueue] = useState(false);
|
const [isDraggingQueue, setIsDraggingQueue] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -316,7 +316,9 @@ export default function QueuePanel() {
|
|||||||
style={{ cursor: currentTrack.albumId ? 'pointer' : 'default' }}
|
style={{ cursor: currentTrack.albumId ? 'pointer' : 'default' }}
|
||||||
onClick={() => currentTrack.albumId && navigate(`/album/${currentTrack.albumId}`)}
|
onClick={() => currentTrack.albumId && navigate(`/album/${currentTrack.albumId}`)}
|
||||||
>{currentTrack.album}</div>
|
>{currentTrack.album}</div>
|
||||||
|
{currentTrack.year && (
|
||||||
|
<div className="queue-current-sub">{currentTrack.year}</div>
|
||||||
|
)}
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginTop: '6px' }}>
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginTop: '6px' }}>
|
||||||
<div className="queue-current-tech">
|
<div className="queue-current-tech">
|
||||||
{currentTrack.bitRate && currentTrack.suffix ? (
|
{currentTrack.bitRate && currentTrack.suffix ? (
|
||||||
|
|||||||
@@ -120,6 +120,7 @@ const enTranslation = {
|
|||||||
favorite: 'Favorite',
|
favorite: 'Favorite',
|
||||||
albumCount_one: '{{count}} Album',
|
albumCount_one: '{{count}} Album',
|
||||||
albumCount_other: '{{count}} Albums',
|
albumCount_other: '{{count}} Albums',
|
||||||
|
openedInBrowser: 'Opened in browser',
|
||||||
},
|
},
|
||||||
favorites: {
|
favorites: {
|
||||||
title: 'Favorites',
|
title: 'Favorites',
|
||||||
@@ -539,6 +540,7 @@ const deTranslation = {
|
|||||||
favorite: 'Als Favorit',
|
favorite: 'Als Favorit',
|
||||||
albumCount_one: '{{count}} Album',
|
albumCount_one: '{{count}} Album',
|
||||||
albumCount_other: '{{count}} Alben',
|
albumCount_other: '{{count}} Alben',
|
||||||
|
openedInBrowser: 'Im Browser geöffnet',
|
||||||
},
|
},
|
||||||
favorites: {
|
favorites: {
|
||||||
title: 'Favoriten',
|
title: 'Favoriten',
|
||||||
|
|||||||
+11
-15
@@ -1,10 +1,10 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useParams, useNavigate } from 'react-router-dom';
|
import { useParams, useNavigate } from 'react-router-dom';
|
||||||
import { getArtist, getArtistInfo, getTopSongs, getSimilarSongs2, SubsonicArtist, SubsonicAlbum, SubsonicSong, SubsonicArtistInfo, buildCoverArtUrl, coverArtCacheKey, star, unstar } from '../api/subsonic';
|
import { getArtist, getArtistInfo, getTopSongs, getSimilarSongs2, SubsonicArtist, SubsonicAlbum, SubsonicSong, SubsonicArtistInfo, buildCoverArtUrl, coverArtCacheKey, star, unstar } from '../api/subsonic';
|
||||||
import AlbumCard from '../components/AlbumCard';
|
import AlbumCard from '../components/AlbumCard';
|
||||||
import CachedImage from '../components/CachedImage';
|
import CachedImage from '../components/CachedImage';
|
||||||
import { ArrowLeft, Users, ExternalLink, Star, Play, Shuffle, Radio } from 'lucide-react';
|
import { ArrowLeft, Users, ExternalLink, Star, Play, Shuffle, Radio } from 'lucide-react';
|
||||||
import { WebviewWindow } from '@tauri-apps/api/webviewWindow';
|
import { open } from '@tauri-apps/plugin-shell';
|
||||||
import { usePlayerStore } from '../store/playerStore';
|
import { usePlayerStore } from '../store/playerStore';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
@@ -50,6 +50,7 @@ export default function ArtistDetail() {
|
|||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [radioLoading, setRadioLoading] = useState(false);
|
const [radioLoading, setRadioLoading] = useState(false);
|
||||||
const [isStarred, setIsStarred] = useState(false);
|
const [isStarred, setIsStarred] = useState(false);
|
||||||
|
const [openedLink, setOpenedLink] = useState<string | null>(null);
|
||||||
|
|
||||||
const playTrack = usePlayerStore(state => state.playTrack);
|
const playTrack = usePlayerStore(state => state.playTrack);
|
||||||
const enqueue = usePlayerStore(state => state.enqueue);
|
const enqueue = usePlayerStore(state => state.enqueue);
|
||||||
@@ -77,15 +78,10 @@ export default function ArtistDetail() {
|
|||||||
});
|
});
|
||||||
}, [id]);
|
}, [id]);
|
||||||
|
|
||||||
const openLink = (url: string, title: string) => {
|
const openLink = (url: string, key: string) => {
|
||||||
const label = `browser_${Date.now()}`;
|
open(url);
|
||||||
new WebviewWindow(label, {
|
setOpenedLink(key);
|
||||||
url,
|
setTimeout(() => setOpenedLink(null), 2500);
|
||||||
title,
|
|
||||||
width: 1100,
|
|
||||||
height: 780,
|
|
||||||
center: true,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleStar = async () => {
|
const toggleStar = async () => {
|
||||||
@@ -192,14 +188,14 @@ export default function ArtistDetail() {
|
|||||||
{(info?.lastFmUrl || artist.name) && (
|
{(info?.lastFmUrl || artist.name) && (
|
||||||
<div className="artist-detail-links">
|
<div className="artist-detail-links">
|
||||||
{info?.lastFmUrl && (
|
{info?.lastFmUrl && (
|
||||||
<button className="artist-ext-link" onClick={() => openLink(info.lastFmUrl!, `${artist.name} — Last.fm`)}>
|
<button className="artist-ext-link" onClick={() => openLink(info.lastFmUrl!, 'lastfm')}>
|
||||||
<LastfmIcon size={14} />
|
<LastfmIcon size={14} />
|
||||||
Last.fm
|
{openedLink === 'lastfm' ? t('artistDetail.openedInBrowser') : 'Last.fm'}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<button className="artist-ext-link" onClick={() => openLink(wikiUrl, `${artist.name} — Wikipedia`)}>
|
<button className="artist-ext-link" onClick={() => openLink(wikiUrl, 'wiki')}>
|
||||||
<ExternalLink size={14} />
|
<ExternalLink size={14} />
|
||||||
Wikipedia
|
{openedLink === 'wiki' ? t('artistDetail.openedInBrowser') : 'Wikipedia'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -478,15 +478,15 @@
|
|||||||
padding: var(--space-3) var(--space-4);
|
padding: var(--space-3) var(--space-4);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: flex-start;
|
||||||
gap: var(--space-3);
|
gap: var(--space-3);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
border-bottom: 1px solid var(--border-subtle);
|
border-bottom: 1px solid var(--border-subtle);
|
||||||
}
|
}
|
||||||
|
|
||||||
.queue-current-cover {
|
.queue-current-cover {
|
||||||
width: 72px;
|
width: 90px;
|
||||||
height: 72px;
|
height: 90px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-md);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|||||||
Reference in New Issue
Block a user