mirror of
https://github.com/Alexander-D-Karpov/akarpov
synced 2024-11-21 22:06:33 +03:00
updated gallery and music radio
This commit is contained in:
parent
4ef6021499
commit
e94f90d091
|
@ -2,7 +2,7 @@
|
|||
from rest_framework import serializers
|
||||
|
||||
from akarpov.blog.models import Comment, Post, Tag
|
||||
from akarpov.common.api import RecursiveField
|
||||
from akarpov.common.api.serializers import RecursiveField
|
||||
from akarpov.users.api.serializers import UserPublicInfoSerializer
|
||||
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
)
|
||||
from akarpov.blog.models import Post
|
||||
from akarpov.blog.services import get_main_rating_posts
|
||||
from akarpov.common.api import StandardResultsSetPagination
|
||||
from akarpov.common.api.pagination import StandardResultsSetPagination
|
||||
|
||||
|
||||
class ListMainPostsView(generics.ListAPIView):
|
||||
|
|
|
@ -1,45 +0,0 @@
|
|||
from rest_framework import serializers
|
||||
from rest_framework.pagination import PageNumberPagination
|
||||
from rest_framework.permissions import SAFE_METHODS, BasePermission
|
||||
|
||||
from akarpov.utils.models import get_object_user
|
||||
|
||||
|
||||
class SmallResultsSetPagination(PageNumberPagination):
|
||||
page_size = 10
|
||||
page_size_query_param = "page_size"
|
||||
max_page_size = 100
|
||||
|
||||
|
||||
class StandardResultsSetPagination(PageNumberPagination):
|
||||
page_size = 50
|
||||
page_size_query_param = "page_size"
|
||||
max_page_size = 200
|
||||
|
||||
|
||||
class BigResultsSetPagination(PageNumberPagination):
|
||||
page_size = 100
|
||||
page_size_query_param = "page_size"
|
||||
max_page_size = 1000
|
||||
|
||||
|
||||
class RecursiveField(serializers.Serializer):
|
||||
def to_representation(self, value):
|
||||
serializer = self.parent.parent.__class__(value, context=self.context)
|
||||
return serializer.data
|
||||
|
||||
|
||||
class IsCreatorOrReadOnly(BasePermission):
|
||||
def has_permission(self, request, view):
|
||||
return bool(
|
||||
request.method in SAFE_METHODS
|
||||
or request.user
|
||||
and get_object_user(view.get_object()) == request.user
|
||||
)
|
||||
|
||||
|
||||
class SetUserModelSerializer(serializers.ModelSerializer):
|
||||
def create(self, validated_data):
|
||||
creator = self.context["request"].user
|
||||
obj = self.Meta.model.objects.create(creator=creator, **validated_data)
|
||||
return obj
|
0
akarpov/common/api/__init__.py
Normal file
0
akarpov/common/api/__init__.py
Normal file
19
akarpov/common/api/pagination.py
Normal file
19
akarpov/common/api/pagination.py
Normal file
|
@ -0,0 +1,19 @@
|
|||
from rest_framework.pagination import PageNumberPagination
|
||||
|
||||
|
||||
class SmallResultsSetPagination(PageNumberPagination):
|
||||
page_size = 10
|
||||
page_size_query_param = "page_size"
|
||||
max_page_size = 100
|
||||
|
||||
|
||||
class StandardResultsSetPagination(PageNumberPagination):
|
||||
page_size = 50
|
||||
page_size_query_param = "page_size"
|
||||
max_page_size = 200
|
||||
|
||||
|
||||
class BigResultsSetPagination(PageNumberPagination):
|
||||
page_size = 100
|
||||
page_size_query_param = "page_size"
|
||||
max_page_size = 1000
|
12
akarpov/common/api/permissions.py
Normal file
12
akarpov/common/api/permissions.py
Normal file
|
@ -0,0 +1,12 @@
|
|||
from rest_framework.permissions import SAFE_METHODS, BasePermission
|
||||
|
||||
from akarpov.utils.models import get_object_user
|
||||
|
||||
|
||||
class IsCreatorOrReadOnly(BasePermission):
|
||||
def has_permission(self, request, view):
|
||||
return bool(
|
||||
request.method in SAFE_METHODS
|
||||
or request.user
|
||||
and get_object_user(view.get_object()) == request.user
|
||||
)
|
14
akarpov/common/api/serializers.py
Normal file
14
akarpov/common/api/serializers.py
Normal file
|
@ -0,0 +1,14 @@
|
|||
from rest_framework import serializers
|
||||
|
||||
|
||||
class RecursiveField(serializers.Serializer):
|
||||
def to_representation(self, value):
|
||||
serializer = self.parent.parent.__class__(value, context=self.context)
|
||||
return serializer.data
|
||||
|
||||
|
||||
class SetUserModelSerializer(serializers.ModelSerializer):
|
||||
def create(self, validated_data):
|
||||
creator = self.context["request"].user
|
||||
obj = self.Meta.model.objects.create(creator=creator, **validated_data)
|
||||
return obj
|
0
akarpov/gallery/api/__init__.py
Normal file
0
akarpov/gallery/api/__init__.py
Normal file
24
akarpov/gallery/api/serializers.py
Normal file
24
akarpov/gallery/api/serializers.py
Normal file
|
@ -0,0 +1,24 @@
|
|||
from rest_framework import serializers
|
||||
|
||||
from akarpov.gallery.models import Image
|
||||
|
||||
|
||||
class ImageSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Image
|
||||
fields = (
|
||||
"slug",
|
||||
"image",
|
||||
"collection",
|
||||
"public",
|
||||
"image_cropped",
|
||||
"created",
|
||||
"modified",
|
||||
)
|
||||
extra_kwargs = {
|
||||
"slug": {"read_only": True},
|
||||
"collection": {"write_only": True},
|
||||
"image_cropped": {"read_only": True},
|
||||
"created": {"read_only": True},
|
||||
"modified": {"read_only": True},
|
||||
}
|
9
akarpov/gallery/api/urls.py
Normal file
9
akarpov/gallery/api/urls.py
Normal file
|
@ -0,0 +1,9 @@
|
|||
from django.urls import path
|
||||
|
||||
from . import views
|
||||
|
||||
app_name = "gallery"
|
||||
|
||||
urlpatterns = [
|
||||
path("", views.ListCreateImageAPIView.as_view(), name="list-create-all"),
|
||||
]
|
21
akarpov/gallery/api/views.py
Normal file
21
akarpov/gallery/api/views.py
Normal file
|
@ -0,0 +1,21 @@
|
|||
from rest_framework import generics, parsers
|
||||
from rest_framework.permissions import IsAuthenticatedOrReadOnly
|
||||
|
||||
from akarpov.common.api.pagination import StandardResultsSetPagination
|
||||
from akarpov.gallery.api.serializers import ImageSerializer
|
||||
from akarpov.gallery.models import Image
|
||||
|
||||
|
||||
class ListCreateImageAPIView(generics.ListCreateAPIView):
|
||||
serializer_class = ImageSerializer
|
||||
permission_classes = [IsAuthenticatedOrReadOnly]
|
||||
parser_classes = [parsers.MultiPartParser, parsers.FormParser]
|
||||
pagination_class = StandardResultsSetPagination
|
||||
|
||||
def get_queryset(self):
|
||||
if self.request.user.is_authenticated:
|
||||
return self.request.user.images.all()
|
||||
return Image.objects.filter(public=True)
|
||||
|
||||
def perform_create(self, serializer):
|
||||
serializer.save(user=self.request.user)
|
40
akarpov/gallery/forms.py
Normal file
40
akarpov/gallery/forms.py
Normal file
|
@ -0,0 +1,40 @@
|
|||
from django import forms
|
||||
|
||||
from akarpov.common.forms import MultipleFileField
|
||||
from akarpov.gallery.models import Collection, Image
|
||||
|
||||
|
||||
class ImageUploadForm(forms.ModelForm):
|
||||
image = MultipleFileField
|
||||
|
||||
class Meta:
|
||||
model = Image
|
||||
fields = (
|
||||
"image",
|
||||
"collection",
|
||||
"public",
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
user = kwargs.pop("user", None)
|
||||
super().__init__(*args, **kwargs)
|
||||
if user is not None:
|
||||
self.fields["collection"].queryset = Collection.objects.filter(user=user)
|
||||
|
||||
def save(self, commit=True):
|
||||
files = self.files.getlist("image")
|
||||
instances = []
|
||||
for file in files:
|
||||
instance = self.instance
|
||||
instance.image = file
|
||||
if commit:
|
||||
instance.save()
|
||||
instances.append(instance)
|
||||
return instances
|
||||
|
||||
|
||||
ImageFormSet = forms.modelformset_factory(
|
||||
Image,
|
||||
form=ImageUploadForm,
|
||||
extra=3, # Number of images to upload at once; adjust as needed
|
||||
)
|
17
akarpov/gallery/migrations/0004_image_public.py
Normal file
17
akarpov/gallery/migrations/0004_image_public.py
Normal file
|
@ -0,0 +1,17 @@
|
|||
# Generated by Django 4.2.6 on 2023-11-17 20:34
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("gallery", "0003_alter_collection_options_alter_image_options"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="image",
|
||||
name="public",
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
|
@ -31,6 +31,7 @@ class Image(TimeStampedModel, ShortLinkModel, BaseImageModel, UserHistoryModel):
|
|||
collection = models.ForeignKey(
|
||||
"Collection", related_name="images", on_delete=models.CASCADE
|
||||
)
|
||||
public = models.BooleanField(default=False)
|
||||
user = models.ForeignKey(
|
||||
"users.User", related_name="images", on_delete=models.CASCADE
|
||||
)
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
from akarpov.gallery.views import (
|
||||
collection_view,
|
||||
image_upload_view,
|
||||
image_view,
|
||||
list_collections_view,
|
||||
list_tag_images_view,
|
||||
|
@ -10,6 +11,7 @@
|
|||
app_name = "gallery"
|
||||
urlpatterns = [
|
||||
path("", list_collections_view, name="list"),
|
||||
path("upload/", image_upload_view, name="upload"),
|
||||
path("<str:slug>", collection_view, name="collection"),
|
||||
path("tag/<str:slug>", list_tag_images_view, name="tag"),
|
||||
path("image/<str:slug>", image_view, name="view"),
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.views import generic
|
||||
|
||||
from akarpov.common.views import HasPermissions
|
||||
from akarpov.gallery.forms import ImageUploadForm
|
||||
from akarpov.gallery.models import Collection, Image, Tag
|
||||
|
||||
|
||||
|
@ -46,3 +48,25 @@ class ImageView(generic.DetailView, HasPermissions):
|
|||
|
||||
|
||||
image_view = ImageView.as_view()
|
||||
|
||||
|
||||
class ImageUploadView(LoginRequiredMixin, generic.CreateView):
|
||||
model = Image
|
||||
form_class = ImageUploadForm
|
||||
success_url = "" # Replace with your success URL
|
||||
|
||||
def get_form_kwargs(self):
|
||||
kwargs = super().get_form_kwargs()
|
||||
kwargs["user"] = self.request.user
|
||||
return kwargs
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
form = self.get_form()
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
return self.form_valid(form)
|
||||
else:
|
||||
return self.form_invalid(form)
|
||||
|
||||
|
||||
image_upload_view = ImageUploadView.as_view()
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
from drf_spectacular.utils import extend_schema_field
|
||||
from rest_framework import serializers
|
||||
|
||||
from akarpov.common.api import SetUserModelSerializer
|
||||
from akarpov.common.api.serializers import SetUserModelSerializer
|
||||
from akarpov.music.models import Album, Author, Playlist, Song
|
||||
from akarpov.users.api.serializers import UserPublicInfoSerializer
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
from rest_framework import generics, permissions
|
||||
|
||||
from akarpov.common.api import IsCreatorOrReadOnly
|
||||
from akarpov.common.api.permissions import IsCreatorOrReadOnly
|
||||
from akarpov.music.api.serializers import (
|
||||
FullPlaylistSerializer,
|
||||
ListSongSerializer,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
from rest_framework import generics, permissions
|
||||
|
||||
from akarpov.common.api import StandardResultsSetPagination
|
||||
from akarpov.common.api.pagination import StandardResultsSetPagination
|
||||
from akarpov.notifications.models import Notification
|
||||
from akarpov.notifications.providers.site.api.serializers import (
|
||||
SiteNotificationSerializer,
|
||||
|
|
177
akarpov/static/js/ws_script.js
Normal file
177
akarpov/static/js/ws_script.js
Normal file
|
@ -0,0 +1,177 @@
|
|||
// Determine the correct WebSocket protocol to use
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const host = window.location.host; // Assuming host includes the port if needed
|
||||
const socketPath = `${protocol}//${host}/ws/radio/`;
|
||||
|
||||
let socket = new WebSocket(socketPath);
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
// Your existing timeSince function can remain unchanged
|
||||
|
||||
// Update the onmessage handler here as needed for the music player
|
||||
socket.onmessage = function(event) {
|
||||
let data = JSON.parse(event.data);
|
||||
// Process the data
|
||||
};
|
||||
|
||||
// This function will attempt to reconnect the WebSocket after a 5-second delay
|
||||
async function reconnectSocket() {
|
||||
console.log("Radio socket disconnected, reconnecting...");
|
||||
let socketClosed = true;
|
||||
await sleep(5000); // Wait 5 seconds before attempting to reconnect
|
||||
while (socketClosed) {
|
||||
try {
|
||||
socket = new WebSocket(socketPath);
|
||||
socket.onmessage = onMessageHandler; // Reassign your onmessage handler
|
||||
socket.onclose = onSocketCloseHandler; // Reassign the onclose handler to this function
|
||||
socketClosed = false; // Exit the loop if the connection is successful
|
||||
} catch (e) {
|
||||
console.log("Can't connect to socket, retrying in 1 second...");
|
||||
await sleep(1000); // Wait 1 second before retrying
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Define a separate function for the onclose event
|
||||
const onSocketCloseHandler = async function(event) {
|
||||
if (!event.wasClean) {
|
||||
// Only attempt to reconnect if the socket did not close cleanly
|
||||
await reconnectSocket();
|
||||
}
|
||||
};
|
||||
|
||||
// Assign the onclose handler to the socket
|
||||
socket.onclose = onSocketCloseHandler;
|
||||
let isAdmin = false; // This will be set based on socket connect data for admin TODO: retrieve from server
|
||||
|
||||
|
||||
|
||||
function displaySong(data) {
|
||||
// Display song details using jQuery to manipulate the DOM
|
||||
$('#song-info').html(/* HTML structure for song info */);
|
||||
// Update admin controls if isAdmin is true
|
||||
if (isAdmin) {
|
||||
$('#admin-controls').show();
|
||||
// Add slider and other controls
|
||||
}
|
||||
// Add song to history
|
||||
addToHistory(data);
|
||||
}
|
||||
|
||||
function displayAdminControls(data) {
|
||||
// Display admin controls if user is admin
|
||||
if (isAdmin) {
|
||||
$('#admin-controls').html(/* HTML structure for admin controls */);
|
||||
}
|
||||
}
|
||||
|
||||
function updateSong(data) {
|
||||
// Update the song information on the page
|
||||
displaySong(data);
|
||||
}
|
||||
|
||||
function addToHistory(songData) {
|
||||
var authors = songData.authors.map(author => author.name).join(", ");
|
||||
var historyItemHtml = `
|
||||
<div class="card mb-3" style="max-width: 540px;">
|
||||
<div class="row g-0">
|
||||
<div class="col-md-4">
|
||||
<img src="${songData.image}" class="img-fluid rounded-start" alt="${songData.name}">
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">${songData.name}</h5>
|
||||
<p class="card-text"><small class="text-muted">by ${authors}</small></p>
|
||||
<p class="card-text">${songData.album.name}</p>
|
||||
<!-- Admin controls would go here if needed -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
$('#history').prepend(historyItemHtml); // prepend to add it to the top of the list
|
||||
}
|
||||
|
||||
// Functionality to handle sliding track length and selecting track to play
|
||||
// ...
|
||||
|
||||
// ws_script.js
|
||||
|
||||
$(document).ready(function() {
|
||||
var audioContext;
|
||||
var analyser;
|
||||
var started = false; // Flag to indicate if the audio context has started
|
||||
var audio = document.getElementById('audio-player');
|
||||
var canvas = document.getElementById('audio-visualization');
|
||||
var ctx = canvas.getContext('2d');
|
||||
|
||||
// Function to initialize the AudioContext and analyser
|
||||
function initAudioContext() {
|
||||
audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
||||
analyser = audioContext.createAnalyser();
|
||||
var audioSrc = audioContext.createMediaElementSource(audio);
|
||||
audioSrc.connect(analyser);
|
||||
analyser.connect(audioContext.destination);
|
||||
}
|
||||
|
||||
// Function to start or resume the audio context on user interaction
|
||||
function startOrResumeContext() {
|
||||
if (audioContext.state === 'suspended') {
|
||||
audioContext.resume().then(() => {
|
||||
console.log('Playback resumed successfully');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Volume control listener
|
||||
$('#volume-control').on('input change', function() {
|
||||
if (audio) {
|
||||
audio.volume = $(this).val() / 100;
|
||||
}
|
||||
});
|
||||
|
||||
// Listener for play/pause button
|
||||
$('#play-pause-button').on('click', function() {
|
||||
if (!started) {
|
||||
initAudioContext();
|
||||
started = true;
|
||||
}
|
||||
|
||||
startOrResumeContext(); // Resume the audio context if needed
|
||||
|
||||
if (audio.paused) {
|
||||
audio.play().then(() => {
|
||||
console.log('Audio playing');
|
||||
}).catch((e) => {
|
||||
console.error('Error playing audio:', e);
|
||||
});
|
||||
} else {
|
||||
audio.pause();
|
||||
console.log('Audio paused');
|
||||
}
|
||||
});
|
||||
|
||||
function renderFrame() {
|
||||
if (!analyser) return;
|
||||
|
||||
requestAnimationFrame(renderFrame);
|
||||
var fbc_array = new Uint8Array(analyser.frequencyBinCount);
|
||||
analyser.getByteFrequencyData(fbc_array);
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height); // Clear the canvas
|
||||
// Visualization code...
|
||||
}
|
||||
|
||||
// Error event listener for the audio element
|
||||
audio.addEventListener('error', function(e) {
|
||||
console.error('Error with audio playback:', e);
|
||||
});
|
||||
|
||||
// Attempt to play the audio and start visualization when the audio can play through
|
||||
audio.addEventListener('canplaythrough', function() {
|
||||
startOrResumeContext();
|
||||
renderFrame(); // Start the visualizer
|
||||
});
|
||||
});
|
|
@ -152,11 +152,11 @@
|
|||
{% endblock inline_javascript %}
|
||||
{% if request.user.is_authenticated %}
|
||||
<script>
|
||||
{% if request.is_secure %}
|
||||
let socket = new WebSocket(`wss://{{ request.get_host }}/ws/notifications/`);
|
||||
{% else %}
|
||||
let socket = new WebSocket(`ws://{{ request.get_host }}/ws/notifications/`);
|
||||
{% endif %}
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const host = window.location.host; // Assuming host includes the port if needed
|
||||
const socketPath = `${protocol}//${host}/ws/notifications/`;
|
||||
|
||||
let notification_socket = new WebSocket(socketPath);
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
|
@ -211,18 +211,17 @@
|
|||
toastBootstrap.show()
|
||||
}
|
||||
|
||||
socket.onmessage = fn
|
||||
socket.onclose = async function(event) {
|
||||
notification_socket.onmessage = fn
|
||||
notification_socket.onclose = async function(event) {
|
||||
console.log("Notifications socket disconnected, reconnecting...")
|
||||
let socketClosed = true;
|
||||
await sleep(5000)
|
||||
while (socketClosed) {
|
||||
{# TODO: reconnect socket here #}
|
||||
try {
|
||||
let cl = socket.onclose
|
||||
socket = new WebSocket(`ws://127.0.0.1:8000/ws/notifications/`);
|
||||
socket.onmessage = fn
|
||||
socket.onclose = cl
|
||||
let cl = notification_socket.onclose
|
||||
notification_socket = new WebSocket(socketPath);
|
||||
notification_socket.onmessage = fn
|
||||
notification_socket.onclose = cl
|
||||
socketClosed = false
|
||||
} catch (e) {
|
||||
console.log("Can't connect to socket, reconnecting...")
|
||||
|
|
43
akarpov/templates/gallery/image_form.html
Normal file
43
akarpov/templates/gallery/image_form.html
Normal file
|
@ -0,0 +1,43 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
{% load crispy_forms_tags %}
|
||||
|
||||
{% block javascript %}
|
||||
<script src="{% static 'js/jquery.min.js' %}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form method="post" action="{% url 'gallery:upload' %}" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
{{ form.management_form }}
|
||||
{% for field in form %}
|
||||
{{ field | as_crispy_field }}
|
||||
{% endfor %}
|
||||
<button type="submit">Upload</button>
|
||||
</form>
|
||||
|
||||
<div id="progress-bar" style="width:0; height:20px; background:green;"></div>
|
||||
<script>
|
||||
const form = document.querySelector('form');
|
||||
form.addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(form);
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', form.action, true);
|
||||
xhr.upload.onprogress = function(e) {
|
||||
if (e.lengthComputable) {
|
||||
const percentage = (e.loaded / e.total) * 100;
|
||||
document.getElementById('progress-bar').style.width = percentage + '%';
|
||||
}
|
||||
};
|
||||
xhr.onload = function() {
|
||||
if (this.status === 200) {
|
||||
console.log('Upload complete!');
|
||||
} else {
|
||||
console.error('Upload failed:', this.responseText);
|
||||
}
|
||||
};
|
||||
xhr.send(formData);
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
|
@ -1 +1,36 @@
|
|||
{% extends 'base.html' %}
|
||||
|
||||
{% block css %}
|
||||
<style>
|
||||
.gallery {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: start; /* Align items to the start of the container */
|
||||
align-items: stretch; /* Stretch items to fill the container */
|
||||
}
|
||||
|
||||
.gallery-item {
|
||||
/* Adjust the margin as needed */
|
||||
margin: 5px;
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
flex-basis: auto; /* Automatically adjust the basis based on the content size */
|
||||
}
|
||||
|
||||
.gallery-item img {
|
||||
width: 100%; /* Make image responsive */
|
||||
height: auto; /* Maintain aspect ratio */
|
||||
display: block; /* Remove bottom space under the image */
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="gallery">
|
||||
{% for image in object_list %}
|
||||
<div class="gallery-item">
|
||||
<img src="{{ image.url }}" alt="Image">
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
1
akarpov/templates/gallery/upload.html
Normal file
1
akarpov/templates/gallery/upload.html
Normal file
|
@ -0,0 +1 @@
|
|||
{% extends 'base.html' %}
|
|
@ -1,330 +1,77 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block css %}
|
||||
<style>
|
||||
body {
|
||||
background-color: #EDEDED;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: "Open Sans", sans-serif;
|
||||
font-size: 13pt;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
color: white;
|
||||
cursor: default;
|
||||
#song-info .card {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-family: "Open Sans", sans-serif;
|
||||
font-size: 8pt;
|
||||
font-weight: 400;
|
||||
cursor: default;
|
||||
#history .card {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-family: "Open Sans", sans-serif;
|
||||
font-size: 13pt;
|
||||
font-weight: 300;
|
||||
color: white;
|
||||
cursor: default;
|
||||
#admin-controls {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.player {
|
||||
height: 190px;
|
||||
width: 430px;
|
||||
background-color: #1E2125;
|
||||
position: absolute;
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
-webkit-transform: translate(-50%, -50%);
|
||||
}
|
||||
.player ul {
|
||||
list-style: none;
|
||||
}
|
||||
.player ul li {
|
||||
display: inline-block;
|
||||
.card-img-top {
|
||||
width: 100%;
|
||||
height: 200px; /* You can adjust as needed */
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.cover {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
.cover img {
|
||||
height: 190px;
|
||||
width: 190px;
|
||||
.audio-player {
|
||||
width: 100%;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.info h1 {
|
||||
margin-top: 15px;
|
||||
margin-left: 180px;
|
||||
line-height: 0;
|
||||
}
|
||||
.info h4 {
|
||||
margin-left: 180px;
|
||||
line-height: 20px;
|
||||
color: #636367;
|
||||
}
|
||||
.info h2 {
|
||||
margin-left: 180px;
|
||||
.range-slider {
|
||||
width: 100%;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.button-items {
|
||||
margin-left: 180px;
|
||||
}
|
||||
|
||||
#slider {
|
||||
width: 182px;
|
||||
height: 4px;
|
||||
background: #151518;
|
||||
border-radius: 2px;
|
||||
}
|
||||
#slider div {
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
margin-top: 1px;
|
||||
background: #EF6DBC;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
#timer {
|
||||
color: #494B4E;
|
||||
line-height: 0;
|
||||
font-size: 9pt;
|
||||
float: right;
|
||||
font-family: Arial, Sans-Serif;
|
||||
}
|
||||
|
||||
.controls {
|
||||
margin-top: 20px;
|
||||
}
|
||||
.controls svg:nth-child(2) {
|
||||
margin-left: 5px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
#play {
|
||||
padding: 0 3px;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
x: 0px;
|
||||
y: 0px;
|
||||
enable-background: new 0 0 25 25;
|
||||
}
|
||||
#play g {
|
||||
stroke: #FEFEFE;
|
||||
stroke-width: 1;
|
||||
stroke-miterlimit: 10;
|
||||
}
|
||||
#play g path {
|
||||
fill: #FEFEFE;
|
||||
}
|
||||
|
||||
#play:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
#play:hover g {
|
||||
stroke: #8F4DA9;
|
||||
cursor: pointer;
|
||||
}
|
||||
#play:hover g path {
|
||||
fill: #9b59b6;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.step-backward {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
x: 0px;
|
||||
y: 0px;
|
||||
enable-background: new 0 0 25 25;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.step-backward g polygon {
|
||||
fill: #FEFEFE;
|
||||
}
|
||||
|
||||
.step-foreward {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
x: 0px;
|
||||
y: 0px;
|
||||
enable-background: new 0 0 25 25;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.step-foreward g polygon {
|
||||
fill: #FEFEFE;
|
||||
}
|
||||
|
||||
#pause {
|
||||
x: 0px;
|
||||
y: 0px;
|
||||
enable-background: new 0 0 25 25;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
position: absolute;
|
||||
margin-left: -38px;
|
||||
cursor: pointer;
|
||||
}
|
||||
#pause rect {
|
||||
fill: white;
|
||||
}
|
||||
|
||||
#pause:hover rect {
|
||||
fill: #8F4DA9;
|
||||
}
|
||||
|
||||
.step-backward g polygon:hover, .step-foreward g polygon:hover {
|
||||
fill: #EF6DBC;
|
||||
cursor: pointer;
|
||||
}
|
||||
#skip p {
|
||||
color: #2980b9;
|
||||
}
|
||||
#skip p:hover {
|
||||
color: #e74c3c;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.expend {
|
||||
padding: 0.5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.expend svg:hover g polygon {
|
||||
fill: #EF6DBC;
|
||||
.range-slider input[type="range"] {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="player">
|
||||
<ul>
|
||||
<li class="cover"><img id="cover" src="" alt=""/></li>
|
||||
<li class="info">
|
||||
<h1 id="artist"></h1>
|
||||
<h4 id="album"></h4>
|
||||
<h2 id="name">I Need You Back</h2>
|
||||
|
||||
<div class="button-items">
|
||||
<audio id="music">
|
||||
<source id="music-src" type="audio/mp3">
|
||||
<div class="container mt-5 music-body">
|
||||
<!-- Music Player and Visualization -->
|
||||
<div class="music-player-container">
|
||||
<canvas id="audio-visualization" width="800" height="100"></canvas>
|
||||
<audio id="audio-player" controls>
|
||||
<source src="" type="audio/mpeg">
|
||||
Your browser does not support the audio element.
|
||||
</audio>
|
||||
<div id="slider"><div id="elapsed"></div></div>
|
||||
<p id="timer">0:00</p>
|
||||
<div class="controls">
|
||||
<span class="expend"><svg class="step-backward" viewBox="0 0 25 25" xml:space="preserve">
|
||||
<g><polygon points="4.9,4.3 9,4.3 9,11.6 21.4,4.3 21.4,20.7 9,13.4 9,20.7 4.9,20.7"/></g>
|
||||
</svg></span>
|
||||
</div>
|
||||
|
||||
<svg id="play" viewBox="0 0 25 25" xml:space="preserve">
|
||||
<defs><rect x="-49.5" y="-132.9" width="446.4" height="366.4"/></defs>
|
||||
<g><circle fill="none" cx="12.5" cy="12.5" r="10.8"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.7,6.9V18c0,0,0.2,1.4,1.8,0l8.1-4.8c0,0,1.2-1.1-1-2L9.8,6.5 C9.8,6.5,9.1,6,8.7,6.9z"/>
|
||||
</g>
|
||||
</svg>
|
||||
<!-- Now Playing Information -->
|
||||
<div class="now-playing">
|
||||
<div class="track-image">
|
||||
<img id="track-image" src="" alt="Track Image">
|
||||
</div>
|
||||
<div class="artist-info">
|
||||
<!-- Dynamically populate artist info -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="admin-controls" class="mb-3" style="display: none;">
|
||||
<!-- Admin Controls will be inserted here -->
|
||||
</div>
|
||||
|
||||
<svg id="pause" viewBox="0 0 25 25" xml:space="preserve">
|
||||
<g>
|
||||
<rect x="6" y="4.6" width="3.8" height="15.7"/>
|
||||
<rect x="14" y="4.6" width="3.9" height="15.7"/>
|
||||
</g>
|
||||
</svg>
|
||||
<h2>History</h2>
|
||||
<div id="history" class="mb-3">
|
||||
<!-- Song History will be inserted here -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span class="expend"><svg class="step-foreward" viewBox="0 0 25 25" xml:space="preserve">
|
||||
<g><polygon points="20.7,4.3 16.6,4.3 16.6,11.6 4.3,4.3 4.3,20.7 16.7,13.4 16.6,20.7 20.7,20.7"/></g>
|
||||
</svg></span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block inline_javascript %}
|
||||
|
||||
<script>
|
||||
const music = document.getElementById("music");
|
||||
|
||||
async function playAudio() {
|
||||
try {
|
||||
await music.play();
|
||||
playButton.style.visibility = "hidden";
|
||||
pause.style.visibility = "visible";
|
||||
} catch (err) {
|
||||
playButton.style.visibility = "visible";
|
||||
pause.style.visibility = "hidden";
|
||||
}
|
||||
}
|
||||
|
||||
const music_src = document.getElementById("music-src")
|
||||
const cover_src = document.getElementById("cover")
|
||||
const name = document.getElementById("name")
|
||||
const album = document.getElementById("album")
|
||||
const artist = document.getElementById("artist")
|
||||
|
||||
const socket = new WebSocket(
|
||||
'ws://'
|
||||
+ window.location.host
|
||||
+ '/ws/radio/'
|
||||
);
|
||||
|
||||
socket.onmessage = function(e) {
|
||||
const data = JSON.parse(e.data);
|
||||
console.log(data)
|
||||
music_src.src = data.file
|
||||
if (data.image !== null) {
|
||||
cover_src.src = data.image
|
||||
}
|
||||
name.innerText = data.name
|
||||
playAudio()
|
||||
};
|
||||
|
||||
socket.onclose = function(e) {
|
||||
console.error('Chat socket closed unexpectedly');
|
||||
};
|
||||
const playButton = document.getElementById("play");
|
||||
const pauseButton = document.getElementById("pause");
|
||||
const playhead = document.getElementById("elapsed");
|
||||
const timeline = document.getElementById("slider");
|
||||
const timer = document.getElementById("timer");
|
||||
let duration;
|
||||
pauseButton.style.visibility = "hidden";
|
||||
|
||||
const timelineWidth = timeline.offsetWidth - playhead.offsetWidth;
|
||||
music.addEventListener("timeupdate", timeUpdate, false);
|
||||
|
||||
function timeUpdate() {
|
||||
const playPercent = timelineWidth * (music.currentTime / duration);
|
||||
playhead.style.width = playPercent + "px";
|
||||
|
||||
const secondsIn = Math.floor(((music.currentTime / duration) / 3.5) * 100);
|
||||
if (secondsIn <= 9) {
|
||||
timer.innerHTML = "0:0" + secondsIn;
|
||||
} else {
|
||||
timer.innerHTML = "0:" + secondsIn;
|
||||
}
|
||||
}
|
||||
|
||||
playButton.onclick = function() {
|
||||
playAudio()
|
||||
}
|
||||
|
||||
pauseButton.onclick = function() {
|
||||
music.pause();
|
||||
playButton.style.visibility = "visible";
|
||||
pause.style.visibility = "hidden";
|
||||
}
|
||||
|
||||
music.addEventListener("canplaythrough", function () {
|
||||
duration = music.duration;
|
||||
}, false);
|
||||
</script>
|
||||
<script src="{% static 'js/jquery.js' %}"></script>
|
||||
<script src="{% static 'js/ws_script.js' %}"></script>
|
||||
{% endblock %}
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
from rest_framework import generics, permissions, status, views
|
||||
from rest_framework.response import Response
|
||||
|
||||
from akarpov.common.api import SmallResultsSetPagination
|
||||
from akarpov.common.api.pagination import SmallResultsSetPagination
|
||||
from akarpov.common.jwt import sign_jwt
|
||||
from akarpov.users.api.serializers import (
|
||||
UserEmailVerification,
|
||||
|
|
|
@ -24,6 +24,10 @@
|
|||
"users/",
|
||||
include("akarpov.users.api.urls", namespace="users"),
|
||||
),
|
||||
path(
|
||||
"gallery/",
|
||||
include("akarpov.gallery.api.urls", namespace="gallery"),
|
||||
),
|
||||
path(
|
||||
"notifications/",
|
||||
include("akarpov.notifications.providers.urls", namespace="notifications"),
|
||||
|
|
Loading…
Reference in New Issue
Block a user