diff --git a/core/settings.py b/core/settings.py index e9c405b..89acaee 100644 --- a/core/settings.py +++ b/core/settings.py @@ -68,10 +68,11 @@ DATABASES = { } # 静态文件设置 +STATIC_EXTRA_DIR = 'static' STATIC_URL = '/static/' STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles') STATICFILES_DIRS = [ - os.path.join(BASE_DIR, 'static'), + os.path.join(BASE_DIR, STATIC_EXTRA_DIR), ] # 默认自动字段类型 @@ -124,6 +125,8 @@ SIMPLEMDE_OPTIONS = { MEDIA_URL = '/media/' MEDIA_ROOT = os.path.join(BASE_DIR, 'data', 'media') +MUSIC_ROOT = os.path.join(BASE_DIR, 'data', 'music') + LOGGING = { 'version': 1, 'disable_existing_loggers': False, diff --git a/core/urls.py b/core/urls.py index 1b337c5..9668e9f 100644 --- a/core/urls.py +++ b/core/urls.py @@ -15,6 +15,10 @@ urlpatterns = [ path('media/', serve, { 'document_root': settings.MEDIA_ROOT, }), + # Music files + path('music/', serve, { + 'document_root': settings.MUSIC_ROOT, + }), path('i18n/', include('django.conf.urls.i18n')), path('search/', include('links.search_urls')), path('link//', LinkDetailView.as_view(), name='link_detail'), diff --git a/docker.sh b/docker.sh index 6ca6af8..31bbde6 100755 --- a/docker.sh +++ b/docker.sh @@ -32,7 +32,7 @@ case "$1" in docker-compose exec web bash ;; "static") - docker-compose exec web uv run manage.py makemigrations + docker-compose exec web uv run manage.py collectstatic ;; "migrate") docker-compose exec web uv run manage.py makemigrations diff --git a/links/api_urls.py b/links/api_urls.py index d41a928..4be69cf 100644 --- a/links/api_urls.py +++ b/links/api_urls.py @@ -2,11 +2,13 @@ from django.urls import path, include from rest_framework.routers import DefaultRouter from . import page_views from . import newsletter_views +from . import api_views # Create a router and register our viewsets with it router = DefaultRouter(trailing_slash=False) router.register('pages', page_views.PageViewSet, basename='api-pages') router.register('newsletters', newsletter_views.NewsletterViewSet, basename='api-newsletters') +router.register('music', api_views.MusicViewSet, basename='api-music') # The API URLs are determined automatically by the router urlpatterns = [ diff --git a/links/api_views.py b/links/api_views.py index 9a36f8c..5e60446 100644 --- a/links/api_views.py +++ b/links/api_views.py @@ -7,6 +7,9 @@ from .serializers import ImageCollectionSerializer, ImageSerializer from .storage import R2Storage import uuid import logging +from django.conf import settings +import os + logger = logging.getLogger(__name__) @@ -138,3 +141,20 @@ class ImageViewSet(viewsets.ModelViewSet): except Exception as e: logger.error(f"Failed to delete from storage: {e}") instance.delete() + +class MusicViewSet(viewsets.ViewSet): + def list(self, request): + static_music = [ + {'title': 'Purple Planet', 'src': '/static/music/Purple-Planet.mp3'}, + {'title': 'Just Kidding', 'src': '/static/music/mixkit-just-kidding.mp3'}, + {'title': 'Thomas', 'src': '/static/music/Thomas-the-Tank-Engine.mp3'} + ] + + data_music_dir = os.path.join(settings.BASE_DIR, 'data', 'music') + data_music = [] + for file in os.listdir(data_music_dir): + if file.endswith('.mp3'): + title = os.path.splitext(file)[0].replace('-', ' ').replace('_', ' ') + data_music.append({'title': title, 'src': f'/music/{file}'}) + + return Response(static_music + data_music) \ No newline at end of file diff --git a/links/templates/links/collection_slideshow.html b/links/templates/links/collection_slideshow.html index b405af3..2576cc3 100644 --- a/links/templates/links/collection_slideshow.html +++ b/links/templates/links/collection_slideshow.html @@ -46,36 +46,50 @@ background: linear-gradient(to top, rgba(0,0,0,0.8), transparent); display: flex; justify-content: center; - gap: 1rem; - opacity: 0; - transition: opacity 0.3s; + flex-wrap: wrap; + gap: 0.5rem; + opacity: 1; + transition: opacity 0.5s ease-in-out; + pointer-events: auto; } - .slideshow-container:hover .controls { + .controls[style*="opacity: 0"] { + pointer-events: none; + } + .slideshow-container:hover .controls, + .controls:focus-within { opacity: 1; } .control-button { background: rgba(255,255,255,0.1); border: none; color: white; - padding: 0.5rem 1rem; + padding: 0.5rem; border-radius: 0.375rem; cursor: pointer; display: flex; align-items: center; gap: 0.5rem; transition: background-color 0.2s; + font-size: 0.9rem; + min-width: 44px; + min-height: 44px; + justify-content: center; } .control-button:hover { background: rgba(255,255,255,0.2); } .progress-circle { position: fixed; - bottom: 2rem; - right: 2rem; - width: 32px; - height: 32px; + top: 1rem; + right: 1rem; + width: 24px; + height: 24px; z-index: 50; opacity: 0.8; + transition: opacity 0.3s; + } + .slideshow-container:hover .progress-circle { + opacity: 1; } .progress-circle-bg { fill: none; @@ -90,6 +104,118 @@ transform: rotate(-90deg); transform-origin: center; } + .music-playlist { + position: fixed; + bottom: 5rem; + left: 50%; + transform: translateX(-50%); + background-color: rgba(0, 0, 0, 0.9); + padding: 1rem; + border-radius: 0.5rem; + display: none; + width: 90%; + max-width: 400px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3); + } + .playlist-content { + max-height: 60vh; + overflow-y: auto; + -webkit-overflow-scrolling: touch; + } + .playlist-items { + margin-top: 1rem; + } + .playlist-item { + padding: 0.75rem; + cursor: pointer; + color: #fff; + border-radius: 0.375rem; + transition: background-color 0.2s; + font-size: 1rem; + min-height: 44px; + display: flex; + align-items: center; + gap: 0.5rem; + } + .playlist-item:hover { + background-color: rgba(255, 255, 255, 0.1); + } + .playlist-item.active { + background-color: rgba(59, 130, 246, 0.5); + font-weight: 500; + } + .playlist-item input[type="checkbox"] { + width: 18px; + height: 18px; + margin: 0; + } + .playlist-controls { + display: flex; + justify-content: center; + margin-top: 1rem; + padding-top: 1rem; + border-top: 1px solid rgba(255, 255, 255, 0.1); + } + .playlist-controls button { + background: rgba(59, 130, 246, 0.5); + color: white; + border: none; + padding: 0.5rem 1rem; + border-radius: 0.375rem; + cursor: pointer; + transition: background-color 0.2s; + font-size: 0.9rem; + } + .playlist-controls button:hover { + background: rgba(59, 130, 246, 0.7); + } + .playlist-content h3 { + color: #fff; + margin: 0; + font-size: 1.1rem; + font-weight: 500; + text-align: center; + } + + @media (max-width: 768px) { + .controls { + padding: 0.75rem; + gap: 0.25rem; + } + .control-button { + padding: 0.5rem; + font-size: 0.8rem; + } + .control-button span { + display: none; + } + .control-button svg { + width: 20px; + height: 20px; + } + .music-playlist { + bottom: 4rem; + } + #musicText { + display: inline !important; + font-size: 0.8rem; + max-width: 120px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + } + + @media (max-width: 480px) { + .music-playlist { + width: 95%; + max-height: 50vh; + } + .playlist-item { + padding: 0.5rem; + font-size: 0.9rem; + } + } {% endblock %} @@ -131,6 +257,18 @@ {% trans "Next" %} + + + + + +
+
+

{% trans "Music Playlist" %}

+
+
+
+ +
+
+
{% endblock %} {% block extra_js %} diff --git a/new_theme/static/css/dist/styles.css b/new_theme/static/css/dist/styles.css index a75ee87..f6def31 100644 --- a/new_theme/static/css/dist/styles.css +++ b/new_theme/static/css/dist/styles.css @@ -629,30 +629,22 @@ video { right: 0px; } -.right-2 { - right: 0.5rem; +.right-1\.5 { + right: 0.375rem; } .top-0 { top: 0px; } -.top-2 { - top: 0.5rem; +.top-1\.5 { + top: 0.375rem; } .top-full { top: 100%; } -.right-1\.5 { - right: 0.375rem; -} - -.top-1\.5 { - top: 0.375rem; -} - .z-0 { z-index: 0; } @@ -739,6 +731,10 @@ video { margin-right: 0.5rem; } +.mt-0\.5 { + margin-top: 0.125rem; +} + .mt-1 { margin-top: 0.25rem; } @@ -775,8 +771,11 @@ video { margin-top: 2rem; } -.mt-0\.5 { - margin-top: 0.125rem; +.line-clamp-1 { + overflow: hidden; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 1; } .line-clamp-2 { @@ -793,13 +792,6 @@ video { -webkit-line-clamp: 3; } -.line-clamp-1 { - overflow: hidden; - display: -webkit-box; - -webkit-box-orient: vertical; - -webkit-line-clamp: 1; -} - .block { display: block; } @@ -808,6 +800,10 @@ video { display: inline-block; } +.inline { + display: inline; +} + .flex { display: flex; } @@ -860,18 +856,6 @@ video { height: 1.5rem; } -.h-auto { - height: auto; -} - -.h-full { - height: 100%; -} - -.h-screen { - height: 100vh; -} - .h-8 { height: 2rem; } @@ -884,6 +868,18 @@ video { height: 280px; } +.h-auto { + height: auto; +} + +.h-full { + height: 100%; +} + +.h-screen { + height: 100vh; +} + .w-12 { width: 3rem; } @@ -916,14 +912,14 @@ video { width: 1.5rem; } -.w-full { - width: 100%; -} - .w-8 { width: 2rem; } +.w-full { + width: 100%; +} + .min-w-0 { min-width: 0px; } @@ -1052,6 +1048,10 @@ video { justify-content: space-between; } +.gap-1\.5 { + gap: 0.375rem; +} + .gap-2 { gap: 0.5rem; } @@ -1060,24 +1060,11 @@ video { gap: 1rem; } -.gap-1\.5 { - gap: 0.375rem; -} - .gap-x-4 { -moz-column-gap: 1rem; column-gap: 1rem; } -.gap-x-8 { - -moz-column-gap: 2rem; - column-gap: 2rem; -} - -.gap-y-10 { - row-gap: 2.5rem; -} - .gap-y-8 { row-gap: 2rem; } @@ -1388,11 +1375,6 @@ video { background-color: rgb(254 249 195 / var(--tw-bg-opacity)); } -.bg-gray-900 { - --tw-bg-opacity: 1; - background-color: rgb(17 24 39 / var(--tw-bg-opacity)); -} - .bg-opacity-50 { --tw-bg-opacity: 0.5; } @@ -1512,6 +1494,11 @@ video { padding-bottom: 0.25rem; } +.py-1\.5 { + padding-top: 0.375rem; + padding-bottom: 0.375rem; +} + .py-12 { padding-top: 3rem; padding-bottom: 3rem; @@ -1542,11 +1529,6 @@ video { padding-bottom: 2rem; } -.py-1\.5 { - padding-top: 0.375rem; - padding-bottom: 0.375rem; -} - .pl-3 { padding-left: 0.75rem; } @@ -1789,11 +1771,6 @@ video { color: rgb(133 77 14 / var(--tw-text-opacity)); } -.text-gray-300 { - --tw-text-opacity: 1; - color: rgb(209 213 219 / var(--tw-text-opacity)); -} - .underline { text-decoration-line: underline; } @@ -2008,11 +1985,6 @@ video { background-color: rgb(185 28 28 / var(--tw-bg-opacity)); } -.hover\:bg-gray-700:hover { - --tw-bg-opacity: 1; - background-color: rgb(55 65 81 / var(--tw-bg-opacity)); -} - .hover\:text-blue-200:hover { --tw-text-opacity: 1; color: rgb(191 219 254 / var(--tw-text-opacity)); @@ -2083,11 +2055,6 @@ video { color: rgb(153 27 27 / var(--tw-text-opacity)); } -.hover\:text-white:hover { - --tw-text-opacity: 1; - color: rgb(255 255 255 / var(--tw-text-opacity)); -} - .hover\:underline:hover { text-decoration-line: underline; } @@ -2210,6 +2177,10 @@ video { margin-right: 0px; } + .sm\:mb-6 { + margin-bottom: 1.5rem; + } + .sm\:ml-3 { margin-left: 0.75rem; } @@ -2218,30 +2189,22 @@ video { margin-left: 1rem; } - .sm\:mt-0 { - margin-top: 0px; - } - - .sm\:mt-4 { - margin-top: 1rem; - } - - .sm\:mb-6 { - margin-bottom: 1.5rem; - } - .sm\:mr-2 { margin-right: 0.5rem; } - .sm\:mt-1 { - margin-top: 0.25rem; + .sm\:mt-0 { + margin-top: 0px; } .sm\:mt-2 { margin-top: 0.5rem; } + .sm\:mt-4 { + margin-top: 1rem; + } + .sm\:block { display: block; } @@ -2282,14 +2245,6 @@ video { width: 2.5rem; } - .sm\:w-64 { - width: 16rem; - } - - .sm\:w-auto { - width: auto; - } - .sm\:w-12 { width: 3rem; } @@ -2298,6 +2253,14 @@ video { width: 1.25rem; } + .sm\:w-64 { + width: 16rem; + } + + .sm\:w-auto { + width: auto; + } + .sm\:grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); } @@ -2360,18 +2323,14 @@ video { border-radius: 0.375rem; } - .sm\:p-12 { - padding: 3rem; - } - - .sm\:p-6 { - padding: 1.5rem; - } - .sm\:p-1\.5 { padding: 0.375rem; } + .sm\:p-12 { + padding: 3rem; + } + .sm\:p-3 { padding: 0.75rem; } @@ -2380,11 +2339,25 @@ video { padding: 1rem; } + .sm\:p-6 { + padding: 1.5rem; + } + + .sm\:px-4 { + padding-left: 1rem; + padding-right: 1rem; + } + .sm\:px-6 { padding-left: 1.5rem; padding-right: 1.5rem; } + .sm\:py-2 { + padding-top: 0.5rem; + padding-bottom: 0.5rem; + } + .sm\:py-3 { padding-top: 0.75rem; padding-bottom: 0.75rem; @@ -2395,16 +2368,6 @@ video { padding-bottom: 1rem; } - .sm\:px-4 { - padding-left: 1rem; - padding-right: 1rem; - } - - .sm\:py-2 { - padding-top: 0.5rem; - padding-bottom: 0.5rem; - } - .sm\:py-8 { padding-top: 2rem; padding-bottom: 2rem; @@ -2414,29 +2377,29 @@ video { text-align: left; } - .sm\:text-base { - font-size: 1rem; - line-height: 1.5rem; - } - - .sm\:text-sm { - font-size: 0.875rem; - line-height: 1.25rem; - } - .sm\:text-2xl { font-size: 1.5rem; line-height: 2rem; } + .sm\:text-3xl { + font-size: 1.875rem; + line-height: 2.25rem; + } + + .sm\:text-base { + font-size: 1rem; + line-height: 1.5rem; + } + .sm\:text-lg { font-size: 1.125rem; line-height: 1.75rem; } - .sm\:text-3xl { - font-size: 1.875rem; - line-height: 2.25rem; + .sm\:text-sm { + font-size: 0.875rem; + line-height: 1.25rem; } } @@ -2445,10 +2408,6 @@ video { display: table-cell; } - .md\:grid-cols-2 { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } - .md\:grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); } diff --git a/static/music/Purple-Planet.mp3 b/static/music/Purple-Planet.mp3 new file mode 100644 index 0000000..0574c38 Binary files /dev/null and b/static/music/Purple-Planet.mp3 differ diff --git a/static/music/Thomas-the-Tank-Engine.mp3 b/static/music/Thomas-the-Tank-Engine.mp3 new file mode 100644 index 0000000..879337a Binary files /dev/null and b/static/music/Thomas-the-Tank-Engine.mp3 differ diff --git a/static/music/mixkit-just-kidding.mp3 b/static/music/mixkit-just-kidding.mp3 new file mode 100644 index 0000000..532856c Binary files /dev/null and b/static/music/mixkit-just-kidding.mp3 differ