diff --git a/spotizerr-ui/dev-dist/sw.js b/spotizerr-ui/dev-dist/sw.js index 186f65d..b2596b7 100644 --- a/spotizerr-ui/dev-dist/sw.js +++ b/spotizerr-ui/dev-dist/sw.js @@ -82,7 +82,7 @@ define(['./workbox-f70c5944'], (function (workbox) { 'use strict'; "revision": "3ca0b8505b4bec776b69afdba2768812" }, { "url": "index.html", - "revision": "0.qqj3c2rpjk8" + "revision": "0.ef94id05f1c" }], {}); workbox.cleanupOutdatedCaches(); workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), { diff --git a/spotizerr-ui/public/apple-touch-icon-180x180.png b/spotizerr-ui/public/apple-touch-icon-180x180.png index 1a80da1..f4c08db 100644 Binary files a/spotizerr-ui/public/apple-touch-icon-180x180.png and b/spotizerr-ui/public/apple-touch-icon-180x180.png differ diff --git a/spotizerr-ui/public/favicon-128x128.png b/spotizerr-ui/public/favicon-128x128.png index 61bb429..acf077f 100644 Binary files a/spotizerr-ui/public/favicon-128x128.png and b/spotizerr-ui/public/favicon-128x128.png differ diff --git a/spotizerr-ui/public/favicon-16x16.png b/spotizerr-ui/public/favicon-16x16.png index 97c1d8d..3277063 100644 Binary files a/spotizerr-ui/public/favicon-16x16.png and b/spotizerr-ui/public/favicon-16x16.png differ diff --git a/spotizerr-ui/public/favicon-256x256.png b/spotizerr-ui/public/favicon-256x256.png index f28e458..1b3195e 100644 Binary files a/spotizerr-ui/public/favicon-256x256.png and b/spotizerr-ui/public/favicon-256x256.png differ diff --git a/spotizerr-ui/public/favicon-32x32.png b/spotizerr-ui/public/favicon-32x32.png index 51c0734..02e9591 100644 Binary files a/spotizerr-ui/public/favicon-32x32.png and b/spotizerr-ui/public/favicon-32x32.png differ diff --git a/spotizerr-ui/public/favicon-48x48.png b/spotizerr-ui/public/favicon-48x48.png index 68d6b27..1ce92da 100644 Binary files a/spotizerr-ui/public/favicon-48x48.png and b/spotizerr-ui/public/favicon-48x48.png differ diff --git a/spotizerr-ui/public/favicon-64x64.png b/spotizerr-ui/public/favicon-64x64.png index 939fcec..3c1762c 100644 Binary files a/spotizerr-ui/public/favicon-64x64.png and b/spotizerr-ui/public/favicon-64x64.png differ diff --git a/spotizerr-ui/public/favicon-96x96.png b/spotizerr-ui/public/favicon-96x96.png index 41d0a36..e8cac38 100644 Binary files a/spotizerr-ui/public/favicon-96x96.png and b/spotizerr-ui/public/favicon-96x96.png differ diff --git a/spotizerr-ui/public/favicon.ico b/spotizerr-ui/public/favicon.ico index 940c2eb..f85f81a 100644 Binary files a/spotizerr-ui/public/favicon.ico and b/spotizerr-ui/public/favicon.ico differ diff --git a/spotizerr-ui/public/pwa-192x192.png b/spotizerr-ui/public/pwa-192x192.png index db1a7a7..f17481e 100644 Binary files a/spotizerr-ui/public/pwa-192x192.png and b/spotizerr-ui/public/pwa-192x192.png differ diff --git a/spotizerr-ui/public/pwa-512x512-maskable.png b/spotizerr-ui/public/pwa-512x512-maskable.png index a42d633..0e81c1b 100644 Binary files a/spotizerr-ui/public/pwa-512x512-maskable.png and b/spotizerr-ui/public/pwa-512x512-maskable.png differ diff --git a/spotizerr-ui/public/pwa-512x512.png b/spotizerr-ui/public/pwa-512x512.png index e159445..bf2d41d 100644 Binary files a/spotizerr-ui/public/pwa-512x512.png and b/spotizerr-ui/public/pwa-512x512.png differ diff --git a/spotizerr-ui/public/spotizerr.png b/spotizerr-ui/public/spotizerr.png index c8a5679..d887aac 100644 Binary files a/spotizerr-ui/public/spotizerr.png and b/spotizerr-ui/public/spotizerr.png differ diff --git a/spotizerr-ui/scripts/generate-icons.js b/spotizerr-ui/scripts/generate-icons.js index 1d8dbe3..8ca901f 100644 --- a/spotizerr-ui/scripts/generate-icons.js +++ b/spotizerr-ui/scripts/generate-icons.js @@ -8,19 +8,71 @@ const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const publicDir = join(__dirname, '../public'); -const pngPath = join(publicDir, 'spotizerr.png'); +const svgPath = join(publicDir, 'spotizerr.svg'); async function generateIcons() { try { - // Check if the PNG file exists - if (!existsSync(pngPath)) { - throw new Error(`PNG file not found at ${pngPath}. Please ensure spotizerr.png exists in the public directory.`); + // Check if the SVG file exists + if (!existsSync(svgPath)) { + throw new Error(`SVG file not found at ${svgPath}. Please ensure spotizerr.svg exists in the public directory.`); } - console.log('🎨 Generating PWA icons from PNG...'); + console.log('🎨 Generating PWA icons from SVG...'); - // Since the source is already 1667x1667 (square), we don't need to worry about aspect ratio - const sourceSize = 1667; + // First, convert SVG to PNG and process it + console.log('📐 Processing SVG: converting to PNG, scaling by 0.67, and centering in black box...'); + + // Create a base canvas size for processing + const baseCanvasSize = 1000; + const scaleFactor = 3; + + // Convert SVG to PNG and get its dimensions + const svgToPng = await sharp(svgPath) + .png() + .toBuffer(); + + const svgMetadata = await sharp(svgToPng).metadata(); + const svgWidth = svgMetadata.width; + const svgHeight = svgMetadata.height; + + // Calculate scaled dimensions + const scaledWidth = Math.round(svgWidth * scaleFactor); + const scaledHeight = Math.round(svgHeight * scaleFactor); + + // Calculate centering offsets + const offsetX = Math.round((baseCanvasSize - scaledWidth) / 2); + const offsetY = Math.round((baseCanvasSize - scaledHeight) / 2); + + // Create the processed base image: scale SVG and center in black box + const processedImage = await sharp({ + create: { + width: baseCanvasSize, + height: baseCanvasSize, + channels: 4, + background: { r: 0, g: 0, b: 0, alpha: 1 } // Pure black background + } + }) + .composite([{ + input: await sharp(svgToPng) + .resize(scaledWidth, scaledHeight) + .png() + .toBuffer(), + top: offsetY, + left: offsetX + }]) + .png() + .toBuffer(); + + console.log(`✅ Processed SVG: ${svgWidth}x${svgHeight} → scaled to ${scaledWidth}x${scaledHeight} → centered in ${baseCanvasSize}x${baseCanvasSize} black box`); + + // Save the processed base image for reference + await sharp(processedImage) + .png() + .toFile(join(publicDir, 'spotizerr.png')); + + console.log(`✅ Saved processed base image as spotizerr.png (${baseCanvasSize}x${baseCanvasSize})`); + + const sourceSize = baseCanvasSize; // Define icon configurations const iconConfigs = [ @@ -51,8 +103,8 @@ async function generateIcons() { } ]; - // Load the source PNG - const sourceImage = sharp(pngPath); + // Use the processed image as source + const sourceImage = sharp(processedImage); for (const config of iconConfigs) { const { size, name, padding } = config; @@ -62,13 +114,13 @@ async function generateIcons() { const paddedSize = Math.round(size * (1 - padding * 2)); const offset = Math.round((size - paddedSize) / 2); - // Create a black background and composite the resized logo on top + // Create a pure black background and composite the resized logo on top await sharp({ create: { width: size, height: size, channels: 4, - background: { r: 15, g: 23, b: 42, alpha: 1 } // #0f172a in RGB + background: { r: 0, g: 0, b: 0, alpha: 1 } // Pure black background } }) .composite([{ @@ -100,7 +152,7 @@ async function generateIcons() { width: maskableSize, height: maskableSize, channels: 4, - background: { r: 15, g: 23, b: 42, alpha: 1 } // Solid background for maskable + background: { r: 0, g: 0, b: 0, alpha: 1 } // Pure black background for maskable } }) .composite([{ @@ -125,7 +177,7 @@ async function generateIcons() { width: size, height: size, channels: 4, - background: { r: 15, g: 23, b: 42, alpha: 1 } + background: { r: 0, g: 0, b: 0, alpha: 1 } // Pure black background } }) .composite([{ @@ -154,7 +206,7 @@ async function generateIcons() { width: size, height: size, channels: 4, - background: { r: 15, g: 23, b: 42, alpha: 1 } + background: { r: 0, g: 0, b: 0, alpha: 1 } // Pure black background } }) .composite([{ @@ -186,8 +238,8 @@ async function generateIcons() { }); console.log(' • favicon.ico (multi-size: 16x16, 32x32, 48x48)'); console.log(''); - console.log('💡 The icons are generated with appropriate padding and the dark theme background.'); - console.log('💡 The source PNG already has the perfect background, so no additional styling needed.'); + console.log('💡 Icons generated from SVG source, scaled by 0.67, and centered on pure black backgrounds.'); + console.log('💡 The SVG logo is automatically processed and optimized for all icon formats.'); console.log('💡 favicon.ico contains multiple sizes for optimal browser compatibility.'); } catch (error) { diff --git a/spotizerr-ui/src/components/Queue.tsx b/spotizerr-ui/src/components/Queue.tsx index c9b8dc0..90c3db3 100644 --- a/spotizerr-ui/src/components/Queue.tsx +++ b/spotizerr-ui/src/components/Queue.tsx @@ -1,4 +1,4 @@ -import { useContext } from "react"; +import { useContext, useState, useRef, useEffect } from "react"; import { FaTimes, FaSync, @@ -127,26 +127,27 @@ const QueueItemCard = ({ item }: { item: QueueItem }) => { const progressText = getProgressText(); return ( -
-
-
-
+
+ {/* Mobile-first layout: stack status and actions on mobile, inline on desktop */} +
+
+
{statusInfo.icon}
{item.type === "track" && ( <>
- -

+ +

{item.name}

-

+

{item.artist}

{item.albumName && ( -

+

{item.albumName}

)} @@ -155,16 +156,16 @@ const QueueItemCard = ({ item }: { item: QueueItem }) => { {item.type === "album" && ( <>
- -

+ +

{item.name}

-

+

{item.artist}

{item.currentTrackTitle && ( -

+

{item.currentTrackNumber}/{item.totalTracks}: {item.currentTrackTitle}

)} @@ -173,16 +174,16 @@ const QueueItemCard = ({ item }: { item: QueueItem }) => { {item.type === "playlist" && ( <>
- -

+ +

{item.name}

-

+

{item.playlistOwner}

{item.currentTrackTitle && ( -

+

{item.currentTrackNumber}/{item.totalTracks}: {item.currentTrackTitle}

)} @@ -190,60 +191,62 @@ const QueueItemCard = ({ item }: { item: QueueItem }) => { )}
-
-
-
+ + {/* Status and actions - stacked on mobile, inline on desktop */} +
+
+
{statusInfo.name}
- {progressText &&

{progressText}

} + {progressText &&

{progressText}

}
-
+
{isTerminal ? ( ) : ( )} {item.canRetry && ( )}
{(item.status === "error" || item.status === "retrying") && item.error && ( -
-

Error: {item.error}

+
+

Error: {item.error}

)} {isTerminal && item.summary && (item.summary.total_failed > 0 || item.summary.total_skipped > 0) && ( -
-
+
+
{item.summary.total_failed > 0 && ( - -
+ +
{item.summary.total_failed} failed
)} {item.summary.total_skipped > 0 && ( - -
+ +
{item.summary.total_skipped} skipped
)} @@ -253,12 +256,12 @@ const QueueItemCard = ({ item }: { item: QueueItem }) => { {(item.status === "downloading" || item.status === "processing") && item.type === "track" && item.progress !== undefined && ( -
-
- Progress - {item.progress.toFixed(0)}% +
+
+ Progress + {item.progress.toFixed(0)}%
-
+
{ export const Queue = () => { const context = useContext(QueueContext); + const [startY, setStartY] = useState(null); + const [currentY, setCurrentY] = useState(null); + const queueRef = useRef(null); if (!context) return null; const { items, isVisible, toggleVisibility, cancelAll, clearCompleted } = context; @@ -283,51 +289,107 @@ export const Queue = () => { const hasActive = items.some((item) => !isTerminalStatus(item.status)); const hasFinished = items.some((item) => isTerminalStatus(item.status)); + // Handle mobile swipe-to-dismiss + const handleTouchStart = (e: React.TouchEvent) => { + setStartY(e.touches[0].clientY); + setCurrentY(e.touches[0].clientY); + }; + + const handleTouchMove = (e: React.TouchEvent) => { + if (startY === null) return; + setCurrentY(e.touches[0].clientY); + + const deltaY = e.touches[0].clientY - startY; + + // Only allow downward swipes to dismiss + if (deltaY > 0) { + if (queueRef.current) { + queueRef.current.style.transform = `translateY(${Math.min(deltaY, 100)}px)`; + queueRef.current.style.opacity = `${Math.max(0.3, 1 - deltaY / 200)}`; + } + } + }; + + const handleTouchEnd = () => { + if (startY === null || currentY === null) return; + + const deltaY = currentY - startY; + + if (queueRef.current) { + queueRef.current.style.transform = ''; + queueRef.current.style.opacity = ''; + } + + // Dismiss if swiped down more than 50px + if (deltaY > 50) { + toggleVisibility(); + } + + setStartY(null); + setCurrentY(null); + }; + return ( -
-
-

- Download Queue ({items.length}) -

-
- - - -
-
-
- {items.length === 0 ? ( -
-
- -
-

The queue is empty.

-

Downloads will appear here

+ <> + {/* Mobile backdrop overlay */} +
+ +
+
+ {/* Add drag indicator for mobile */} +
+

+ Download Queue ({items.length}) +

+
+ + +
- ) : ( - items.map((item) => ) - )} +
+
+ {items.length === 0 ? ( +
+
+ +
+

The queue is empty.

+

Downloads will appear here

+
+ ) : ( + items.map((item) => ) + )} +
-
+ ); };