diff --git a/spotizerr-ui/dev-dist/sw.js b/spotizerr-ui/dev-dist/sw.js index b2596b7..89aa4fe 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.ef94id05f1c" + "revision": "0.udbs5r8ifc8" }], {}); workbox.cleanupOutdatedCaches(); workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), { diff --git a/spotizerr-ui/index.html b/spotizerr-ui/index.html index f1da355..937799d 100644 --- a/spotizerr-ui/index.html +++ b/spotizerr-ui/index.html @@ -2,9 +2,19 @@ + + + + + + + + + + + - Spotizerr diff --git a/spotizerr-ui/public/apple-touch-icon-180x180.png b/spotizerr-ui/public/apple-touch-icon-180x180.png index f4c08db..ff73e15 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 acf077f..9bdb0b1 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 3277063..1686296 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 1b3195e..7bf944a 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 02e9591..c3732a2 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 1ce92da..0ddde31 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 3c1762c..2501a45 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 e8cac38..1b677bb 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 f85f81a..a3bd07b 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 f17481e..d9c77b0 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 0e81c1b..2055e18 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 bf2d41d..8fa2c85 100644 Binary files a/spotizerr-ui/public/pwa-512x512.png and b/spotizerr-ui/public/pwa-512x512.png differ diff --git a/spotizerr-ui/scripts/generate-icons.js b/spotizerr-ui/scripts/generate-icons.js index 8ca901f..d809bbe 100644 --- a/spotizerr-ui/scripts/generate-icons.js +++ b/spotizerr-ui/scripts/generate-icons.js @@ -4,6 +4,16 @@ import { join, dirname } from 'path'; import { fileURLToPath } from 'url'; import toIco from 'to-ico'; +// Helper function to create a rounded square mask +async function createRoundedSquareMask(size, radius) { + const svg = ` + + + + `; + return Buffer.from(svg); +} + const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); @@ -80,26 +90,36 @@ async function generateIcons() { size: 16, name: 'favicon-16x16.png', padding: 0.1, // 10% padding for small icons + rounded: true, + cornerRadius: 3, // 3px radius for small icons }, { size: 32, name: 'favicon-32x32.png', padding: 0.1, + rounded: true, + cornerRadius: 6, // 6px radius }, { size: 180, name: 'apple-touch-icon-180x180.png', padding: 0.05, // 5% padding for Apple (they prefer less padding) + rounded: true, + cornerRadius: 32, // ~18% radius for Apple icons }, { size: 192, name: 'pwa-192x192.png', padding: 0.1, + rounded: true, + cornerRadius: 34, // ~18% radius }, { size: 512, name: 'pwa-512x512.png', padding: 0.1, + rounded: true, + cornerRadius: 92, // ~18% radius } ]; @@ -107,7 +127,9 @@ async function generateIcons() { const sourceImage = sharp(processedImage); for (const config of iconConfigs) { - const { size, name, padding } = config; + const { size, name, padding, rounded, cornerRadius } = config; + + let finalIcon; if (padding > 0) { // Create icon with padding by compositing on a background @@ -115,7 +137,7 @@ async function generateIcons() { const offset = Math.round((size - paddedSize) / 2); // Create a pure black background and composite the resized logo on top - await sharp({ + finalIcon = await sharp({ create: { width: size, height: size, @@ -129,25 +151,42 @@ async function generateIcons() { left: offset }]) .png() - .toFile(join(publicDir, name)); + .toBuffer(); } else { // Direct resize without padding - await sourceImage + finalIcon = await sourceImage .resize(size, size) .png() - .toFile(join(publicDir, name)); + .toBuffer(); } - console.log(`✅ Generated ${name} (${size}x${size}) - padding: ${padding * 100}%`); + // Apply rounded corners if specified + if (rounded && cornerRadius > 0) { + const mask = await createRoundedSquareMask(size, cornerRadius); + finalIcon = await sharp(finalIcon) + .composite([{ + input: mask, + blend: 'dest-in' + }]) + .png() + .toBuffer(); + } + + // Write the final icon to file + await sharp(finalIcon).toFile(join(publicDir, name)); + + const roundedText = rounded ? ` - rounded (${cornerRadius}px)` : ''; + console.log(`✅ Generated ${name} (${size}x${size}) - padding: ${padding * 100}%${roundedText}`); } - // Create maskable icon (less padding, solid background) + // Create maskable icon (less padding, solid background, rounded) const maskableSize = 512; const maskablePadding = 0.05; // 5% padding for maskable icons + const maskableRadius = 92; // ~18% radius for consistency const maskablePaddedSize = Math.round(maskableSize * (1 - maskablePadding * 2)); const maskableOffset = Math.round((maskableSize - maskablePaddedSize) / 2); - await sharp({ + let maskableIcon = await sharp({ create: { width: maskableSize, height: maskableSize, @@ -161,47 +200,33 @@ async function generateIcons() { left: maskableOffset }]) .png() - .toFile(join(publicDir, 'pwa-512x512-maskable.png')); + .toBuffer(); - console.log(`✅ Generated pwa-512x512-maskable.png (${maskableSize}x${maskableSize}) - maskable`); + // Apply rounded corners to maskable icon + const maskableMask = await createRoundedSquareMask(maskableSize, maskableRadius); + maskableIcon = await sharp(maskableIcon) + .composite([{ + input: maskableMask, + blend: 'dest-in' + }]) + .png() + .toBuffer(); - // Generate additional favicon sizes for ICO compatibility + await sharp(maskableIcon).toFile(join(publicDir, 'pwa-512x512-maskable.png')); + + console.log(`✅ Generated pwa-512x512-maskable.png (${maskableSize}x${maskableSize}) - maskable, rounded (${maskableRadius}px)`); + + // Generate additional favicon sizes for ICO compatibility (with rounded corners) const additionalSizes = [48, 64, 96, 128, 256]; for (const size of additionalSizes) { const padding = size <= 48 ? 0.05 : 0.1; const paddedSize = Math.round(size * (1 - padding * 2)); const offset = Math.round((size - paddedSize) / 2); + + // Calculate corner radius proportional to size (~18% for larger icons, smaller for tiny ones) + const cornerRadius = size <= 48 ? Math.round(size * 0.125) : Math.round(size * 0.18); - await sharp({ - create: { - width: size, - height: size, - channels: 4, - background: { r: 0, g: 0, b: 0, alpha: 1 } // Pure black background - } - }) - .composite([{ - input: await sourceImage.resize(paddedSize, paddedSize).png().toBuffer(), - top: offset, - left: offset - }]) - .png() - .toFile(join(publicDir, `favicon-${size}x${size}.png`)); - - console.log(`✅ Generated favicon-${size}x${size}.png (${size}x${size}) - padding: ${padding * 100}%`); - } - - // Generate favicon.ico with multiple sizes - console.log('🎯 Generating favicon.ico...'); - const icoSizes = [16, 32, 48]; - const icoBuffers = []; - - for (const size of icoSizes) { - const padding = 0.1; // 10% padding for ICO - const paddedSize = Math.round(size * (1 - padding * 2)); - const offset = Math.round((size - paddedSize) / 2); - - const buffer = await sharp({ + let additionalIcon = await sharp({ create: { width: size, height: size, @@ -217,29 +242,82 @@ async function generateIcons() { .png() .toBuffer(); - icoBuffers.push(buffer); + // Apply rounded corners + const additionalMask = await createRoundedSquareMask(size, cornerRadius); + additionalIcon = await sharp(additionalIcon) + .composite([{ + input: additionalMask, + blend: 'dest-in' + }]) + .png() + .toBuffer(); + + await sharp(additionalIcon).toFile(join(publicDir, `favicon-${size}x${size}.png`)); + + console.log(`✅ Generated favicon-${size}x${size}.png (${size}x${size}) - padding: ${padding * 100}%, rounded (${cornerRadius}px)`); + } + + // Generate favicon.ico with multiple sizes (rounded) + console.log('🎯 Generating favicon.ico with rounded corners...'); + const icoSizes = [16, 32, 48]; + const icoBuffers = []; + + for (const size of icoSizes) { + const padding = 0.1; // 10% padding for ICO + const paddedSize = Math.round(size * (1 - padding * 2)); + const offset = Math.round((size - paddedSize) / 2); + const cornerRadius = size <= 32 ? Math.round(size * 0.125) : Math.round(size * 0.18); + + let icoIcon = await sharp({ + create: { + width: size, + height: size, + channels: 4, + background: { r: 0, g: 0, b: 0, alpha: 1 } // Pure black background + } + }) + .composite([{ + input: await sourceImage.resize(paddedSize, paddedSize).png().toBuffer(), + top: offset, + left: offset + }]) + .png() + .toBuffer(); + + // Apply rounded corners to ICO icon + const icoMask = await createRoundedSquareMask(size, cornerRadius); + const roundedIcoIcon = await sharp(icoIcon) + .composite([{ + input: icoMask, + blend: 'dest-in' + }]) + .png() + .toBuffer(); + + icoBuffers.push(roundedIcoIcon); } // Create the ICO file const icoBuffer = await toIco(icoBuffers); writeFileSync(join(publicDir, 'favicon.ico'), icoBuffer); - console.log(`✅ Generated favicon.ico (${icoSizes.join('x, ')}x sizes) - multi-size ICO`); + console.log(`✅ Generated favicon.ico (${icoSizes.join('x, ')}x sizes) - multi-size ICO, rounded corners`); - console.log('🎉 All PWA icons generated successfully!'); + console.log('🎉 All PWA icons generated successfully with rounded corners!'); console.log(''); console.log('📋 Generated files:'); iconConfigs.forEach(config => { - console.log(` • ${config.name} (${config.size}x${config.size})`); + console.log(` • ${config.name} (${config.size}x${config.size}) - rounded`); }); - console.log(' • pwa-512x512-maskable.png (512x512)'); + console.log(' • pwa-512x512-maskable.png (512x512) - rounded'); additionalSizes.forEach(size => { - console.log(` • favicon-${size}x${size}.png (${size}x${size})`); + console.log(` • favicon-${size}x${size}.png (${size}x${size}) - rounded`); }); - console.log(' • favicon.ico (multi-size: 16x16, 32x32, 48x48)'); + console.log(' • favicon.ico (multi-size: 16x16, 32x32, 48x48) - rounded'); console.log(''); 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('💡 All icons feature rounded corners for a modern, polished appearance.'); + console.log('💡 Corner radius scales proportionally with icon size (~18% for larger icons, ~12.5% for smaller ones).'); console.log('💡 favicon.ico contains multiple sizes for optimal browser compatibility.'); } catch (error) { diff --git a/spotizerr-ui/src/index.css b/spotizerr-ui/src/index.css index 098f919..5b4fd95 100644 --- a/spotizerr-ui/src/index.css +++ b/spotizerr-ui/src/index.css @@ -1,103 +1,110 @@ @import "tailwindcss"; @theme { - /* Background Colors */ + /* Background Colors - Light theme: pure white to warm grays */ --color-surface: #ffffff; - --color-surface-secondary: #f8fafc; - --color-surface-muted: #f1f5f9; - --color-surface-accent: #e2e8f0; - --color-surface-overlay: rgba(255, 255, 255, 0.95); + --color-surface-secondary: #fafbfc; + --color-surface-muted: #f4f6f8; + --color-surface-accent: #e8ecf0; + --color-surface-overlay: rgba(255, 255, 255, 0.96); - /* Dark mode backgrounds */ - --color-surface-dark: #0f172a; - --color-surface-secondary-dark: #1e293b; - --color-surface-muted-dark: #334155; - --color-surface-accent-dark: #475569; - --color-surface-overlay-dark: rgba(15, 23, 42, 0.95); + /* Dark mode backgrounds - Rich blacks with subtle warm undertones */ + --color-surface-dark: #0a0a0a; + --color-surface-secondary-dark: #141414; + --color-surface-muted-dark: #1f1f1f; + --color-surface-accent-dark: #2d2d2d; + --color-surface-overlay-dark: rgba(10, 10, 10, 0.96); - /* Text Colors */ - --color-content-primary: #0f172a; - --color-content-secondary: #475569; - --color-content-muted: #64748b; - --color-content-accent: #94a3b8; + /* Text Colors - Light theme with enhanced contrast */ + --color-content-primary: #0d1117; + --color-content-secondary: #57606a; + --color-content-muted: #768390; + --color-content-accent: #8c959f; --color-content-inverse: #ffffff; - /* Dark mode text */ - --color-content-primary-dark: #f8fafc; - --color-content-secondary-dark: #e2e8f0; - --color-content-muted-dark: #cbd5e1; - --color-content-accent-dark: #94a3b8; - --color-content-inverse-dark: #0f172a; + /* Dark mode text - Crisp whites and refined grays */ + --color-content-primary-dark: #f0f6fc; + --color-content-secondary-dark: #c9d1d9; + --color-content-muted-dark: #8b949e; + --color-content-accent-dark: #6e7681; + --color-content-inverse-dark: #0d1117; - /* Interactive Colors */ - --color-primary: #3b82f6; - --color-primary-hover: #2563eb; - --color-primary-active: #1d4ed8; - --color-primary-muted: #dbeafe; + /* Interactive Colors - Enhanced Spotify green with sophistication */ + --color-primary: #1ed760; + --color-primary-hover: #1fdf64; + --color-primary-active: #1db954; + --color-primary-muted: #d1f7e0; - --color-secondary: #64748b; - --color-secondary-hover: #475569; - --color-secondary-active: #334155; - --color-secondary-muted: #f1f5f9; + /* Secondary colors with purple accent for contrast */ + --color-secondary: #8b5fbf; + --color-secondary-hover: #9d70d1; + --color-secondary-active: #7a4fa3; + --color-secondary-muted: #f0ebff; + + /* Accent purple for complementary design */ + --color-accent: #8b5fbf; + --color-accent-hover: #9d70d1; + --color-accent-active: #7a4fa3; + --color-accent-muted: #f0ebff; - /* Status Colors */ - --color-success: #22c55e; - --color-success-hover: #16a34a; - --color-success-muted: #dcfce7; - --color-success-text: #15803d; + /* Status Colors - Refined and harmonious */ + --color-success: #28a745; + --color-success-hover: #1e7b34; + --color-success-muted: #d4edda; + --color-success-text: #155724; - --color-error: #ef4444; - --color-error-hover: #dc2626; - --color-error-muted: #fef2f2; - --color-error-text: #dc2626; + --color-error: #dc3545; + --color-error-hover: #c82333; + --color-error-muted: #f8d7da; + --color-error-text: #721c24; - --color-warning: #f59e0b; - --color-warning-hover: #d97706; - --color-warning-muted: #fef3c7; - --color-warning-text: #d97706; + --color-warning: #fd7e14; + --color-warning-hover: #e8590c; + --color-warning-muted: #fff3cd; + --color-warning-text: #856404; - --color-info: #3b82f6; - --color-info-hover: #2563eb; - --color-info-muted: #dbeafe; - --color-info-text: #2563eb; + --color-info: #17a2b8; + --color-info-hover: #138496; + --color-info-muted: #d1ecf1; + --color-info-text: #0c5460; - --color-processing: #a855f7; - --color-processing-hover: #9333ea; - --color-processing-muted: #f3e8ff; - --color-processing-text: #9333ea; + --color-processing: #8b5fbf; + --color-processing-hover: #7a4fa3; + --color-processing-muted: #f0ebff; + --color-processing-text: #5d3e7a; - /* Border Colors */ - --color-border: #e2e8f0; - --color-border-muted: #f1f5f9; - --color-border-accent: #cbd5e1; - --color-border-focus: #3b82f6; - --color-border-dark: #475569; - --color-border-muted-dark: #334155; - --color-border-accent-dark: #64748b; + /* Border Colors - Subtle and refined */ + --color-border: #e1e5e9; + --color-border-muted: #f4f6f8; + --color-border-accent: #d0d7de; + --color-border-focus: #1ed760; + --color-border-dark: #30363d; + --color-border-muted-dark: #21262d; + --color-border-accent-dark: #373e47; /* Input Colors */ - --color-input-background: #f1f5f9; - --color-input-border: #e2e8f0; - --color-input-focus: #3b82f6; - --color-input-background-dark: #1e293b; - --color-input-border-dark: #475569; + --color-input-background: #f4f6f8; + --color-input-border: #e1e5e9; + --color-input-focus: #1ed760; + --color-input-background-dark: #21262d; + --color-input-border-dark: #30363d; /* Button Colors */ - --color-button-primary: #3b82f6; - --color-button-primary-hover: #2563eb; + --color-button-primary: #1ed760; + --color-button-primary-hover: #1fdf64; --color-button-primary-text: #ffffff; - --color-button-secondary: #f1f5f9; - --color-button-secondary-hover: #64748b; - --color-button-secondary-text: #64748b; + --color-button-secondary: #f4f6f8; + --color-button-secondary-hover: #8b5fbf; + --color-button-secondary-text: #57606a; --color-button-secondary-text-hover: #ffffff; - --color-button-success: #22c55e; - --color-button-success-hover: #16a34a; + --color-button-success: #28a745; + --color-button-success-hover: #1e7b34; --color-button-success-text: #ffffff; - --color-icon-button-hover: #e5e7eb; - --color-icon-button-hover-dark: #334155; + --color-icon-button-hover: #e8ecf0; + --color-icon-button-hover-dark: #30363d; /* Icon Colors */ --color-icon-primary: #000000; @@ -316,4 +323,20 @@ color: var(--color-icon-inverse-dark); filter: brightness(0); } + + /* Logo Utility Classes - Automatic dark mode detection like Tailwind v4 */ + .logo { + filter: brightness(0); /* Black in light mode */ + } + + @media (prefers-color-scheme: dark) { + .logo { + filter: brightness(0) invert(1); /* White in dark mode */ + } + } + + /* Class-based override for manual toggles */ + .dark .logo { + filter: brightness(0) invert(1); /* White when dark class is present */ + } } diff --git a/spotizerr-ui/src/routes/home.tsx b/spotizerr-ui/src/routes/home.tsx index 41533a6..a075aac 100644 --- a/spotizerr-ui/src/routes/home.tsx +++ b/spotizerr-ui/src/routes/home.tsx @@ -130,7 +130,9 @@ export const Home = () => { return (
-

Search Spotify

+
+

Spotizerr

+
-
+
+ {/* Desktop Header */} +
- - Logo -

Spotizerr

+ + Spotizerr
- Watchlist + Watchlist - History + History - Settings + Settings
-
+ {/* Mobile Header - Just logo/title */} +
+
+ + Spotizerr + +
+
+ + {/* Main content - flex-1 to push navigation to bottom on mobile */} +
+ {/* Mobile Bottom Navigation */} + +
); diff --git a/spotizerr-ui/src/routes/track.tsx b/spotizerr-ui/src/routes/track.tsx index ccaad61..53ec13e 100644 --- a/spotizerr-ui/src/routes/track.tsx +++ b/spotizerr-ui/src/routes/track.tsx @@ -63,7 +63,7 @@ export const Track = () => { const imageUrl = track.album.images?.[0]?.url; return ( -
+
-
- {imageUrl && ( -
- {track.album.name} + + {/* Hero Section with Cover */} +
+
+ {/* Album Cover */} +
+ {imageUrl ? ( + {track.album.name} + ) : ( +
+ No cover +
+ )}
- )} -
-
-
-

{track.name}

+ + {/* Track Info */} +
+
+

+ {track.name} +

{track.explicit && ( - EXPLICIT + + EXPLICIT + )}
-
+ +
{track.artists.map((artist, index) => ( - + {artist.name} {index < track.artists.length - 1 && ", "} ))}
-

- From the album{" "} - + +

+ From{" "} + {track.album.name}

-
-

Release Date: {track.album.release_date}

-

Duration: {formatDuration(track.duration_ms)}

-
-
-

Popularity:

-
-
+
+
+
+ + {/* Details Section */} +
+
+ {/* Track Details */} +
+

Track Details

+
+
+ Release Date: + {track.album.release_date} +
+
+ Duration: + {formatDuration(track.duration_ms)}
-
- - - - Listen on Spotify - + + {/* Popularity */} +
+

Popularity

+
+
+
+
+ + {track.popularity}% + +
+ + {/* Action Buttons */} +
+ + + + Listen on Spotify + +
);