diff --git a/Dockerfile b/Dockerfile index 59cdc3d..be1c795 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,6 +13,9 @@ RUN sed -ri -e 's!/var/www/html!${APACHE_DOCUMENT_ROOT}!g' \ /etc/apache2/sites-available/*.conf \ /etc/apache2/apache2.conf +# PHP upload limits +RUN echo "upload_max_filesize = 20M\npost_max_size = 22M" > /usr/local/etc/php/conf.d/uploads.ini + # Allow .htaccess overrides RUN sed -ri -e 's/AllowOverride None/AllowOverride All/g' \ /etc/apache2/apache2.conf diff --git a/public/css/style.css b/public/css/style.css index 4b23c5e..87fbafe 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -111,8 +111,22 @@ body::after { border: 2px solid var(--t-border); box-shadow: 0 2px 8px rgba(0,0,0,0.3); z-index: 2; + transition: width 0.25s cubic-bezier(0.34, 1.56, 0.64, 1), height 0.25s cubic-bezier(0.34, 1.56, 0.64, 1), margin 0.25s cubic-bezier(0.34, 1.56, 0.64, 1), border-color 0.2s ease, box-shadow 0.25s ease; +} +.card-thumb { + transform-origin: center center; +} +.card-thumb:hover { + width: 80px; + height: 80px; + margin-top: -18px; + margin-right: -18px; + border-color: var(--t-muted); + box-shadow: 0 4px 16px rgba(0,0,0,0.4); + z-index: 10; + background-size: cover; + background-repeat: no-repeat; } -.group:hover .card-thumb { border-color: var(--t-muted); } .card-glow:hover { box-shadow: 0 0 16px -3px var(--glow-color, rgba(56, 189, 248, 0.15)); } .card-dragging { opacity: 0.15; transform: scale(0.95); } @@ -444,6 +458,12 @@ body.detail-image-fs-open { overflow: hidden; } } .prose li:has(input[type="checkbox"]) { list-style: none; margin-left: -1.5em; } +.prose audio { + width: 100%; + max-width: 400px; + margin: 0.8em 0; + border-radius: 6px; +} .prose img { max-width: 100%; border-radius: 4px; diff --git a/public/index.php b/public/index.php index 7d14145..472fa9c 100644 --- a/public/index.php +++ b/public/index.php @@ -204,7 +204,7 @@ if (str_starts_with($route, '/api/')) { $dir = __DIR__ . '/uploads'; $files = []; if (is_dir($dir)) { - foreach (glob($dir . '/*.{png,jpg,gif,webp}', GLOB_BRACE) as $f) { + foreach (glob($dir . '/*.{png,jpg,gif,webp,mp3,ogg,wav,m4a}', GLOB_BRACE) as $f) { $name = basename($f); $files[] = ['name' => $name, 'url' => 'uploads/' . $name, 'size' => filesize($f), 'modified' => filemtime($f)]; } @@ -245,14 +245,20 @@ if (str_starts_with($route, '/api/')) { exit; } - // Check real MIME via finfo, not client-supplied type - $allowed = ['image/png' => 'png', 'image/jpeg' => 'jpg', 'image/gif' => 'gif', 'image/webp' => 'webp']; + // Check MIME — finfo first, fallback to extension for audio + $mimeMap = ['image/png' => 'png', 'image/jpeg' => 'jpg', 'image/gif' => 'gif', 'image/webp' => 'webp', 'audio/mpeg' => 'mp3', 'audio/ogg' => 'ogg', 'audio/wav' => 'wav', 'audio/x-wav' => 'wav', 'audio/mp4' => 'm4a', 'application/ogg' => 'ogg']; + $extMap = ['mp3' => 'mp3', 'ogg' => 'ogg', 'wav' => 'wav', 'm4a' => 'm4a']; $finfo = new finfo(FILEINFO_MIME_TYPE); $realMime = $finfo->file($file['tmp_name']); - $ext = $allowed[$realMime] ?? null; + $ext = $mimeMap[$realMime] ?? null; + // Fallback: trust extension for audio when finfo returns octet-stream + if (!$ext && ($realMime === 'application/octet-stream' || str_starts_with($realMime, 'audio/'))) { + $origExt = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION)); + $ext = $extMap[$origExt] ?? null; + } if (!$ext) { http_response_code(400); - echo json_encode(['error' => 'invalid image type']); + echo json_encode(['error' => 'invalid file type', 'detected' => $realMime]); exit; } @@ -470,6 +476,7 @@ if (str_starts_with($route, '/api/')) { +
no cards found