first backend frontend split
with docker compose file nextjs something something also includes migrations
@@ -0,0 +1,36 @@
|
||||
from django.contrib import admin
|
||||
from django.utils.http import urlencode
|
||||
from django.shortcuts import redirect
|
||||
from .models import Track, Artist, Album, Background, Vote, Profile
|
||||
|
||||
class TrackAdmin(admin.ModelAdmin):
|
||||
pass
|
||||
class ArtistAdmin(admin.ModelAdmin):
|
||||
pass
|
||||
class AlbumAdmin(admin.ModelAdmin):
|
||||
pass
|
||||
class BackgroundAdmin(admin.ModelAdmin):
|
||||
pass
|
||||
class VoteAdmin(admin.ModelAdmin):
|
||||
pass
|
||||
class ProfileAdmin(admin.ModelAdmin):
|
||||
pass
|
||||
class AllTrackAdmin(admin.ModelAdmin):
|
||||
list_display = ["name", "artist", "old", "from_playlist"]
|
||||
list_filter = ["old", "from_playlist"]
|
||||
def get_queryset(self, request):
|
||||
return (Track.all_tracks.all())
|
||||
def changelist_view(self, request, extra_context=None):
|
||||
if 'old__exact' not in request.GET:
|
||||
default_filter = urlencode({'old__exact': '0'}) # 'True' or 'False'
|
||||
return redirect(f"{request.path}?{default_filter}")
|
||||
return super().changelist_view(request, extra_context=extra_context)
|
||||
|
||||
|
||||
#admin.site.register(Track, TrackAdmin)
|
||||
admin.site.register(Track, AllTrackAdmin)
|
||||
admin.site.register(Artist, ArtistAdmin)
|
||||
admin.site.register(Album, AlbumAdmin)
|
||||
admin.site.register(Background, BackgroundAdmin)
|
||||
admin.site.register(Vote, VoteAdmin)
|
||||
admin.site.register(Profile, ProfileAdmin)
|
||||
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class PlaylistConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'playlist'
|
||||
@@ -0,0 +1,48 @@
|
||||
# Generated by Django 5.0.1 on 2024-02-04 22:50
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Artist',
|
||||
fields=[
|
||||
('id', models.CharField(max_length=128, primary_key=True, serialize=False)),
|
||||
('name', models.CharField(max_length=255)),
|
||||
('popularity', models.SmallIntegerField(blank=True, null=True)),
|
||||
('genres', models.JSONField()),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Album',
|
||||
fields=[
|
||||
('id', models.CharField(max_length=128, primary_key=True, serialize=False)),
|
||||
('name', models.CharField(max_length=255)),
|
||||
('album_type', models.CharField(max_length=255)),
|
||||
('total_tracks', models.PositiveSmallIntegerField(null=True)),
|
||||
('image', models.ImageField(upload_to='albums/')),
|
||||
('artists', models.ManyToManyField(to='playlist.artist')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Track',
|
||||
fields=[
|
||||
('id', models.CharField(max_length=128, primary_key=True, serialize=False)),
|
||||
('title', models.CharField(max_length=255)),
|
||||
('explicit', models.BooleanField(default=False)),
|
||||
('duration_ms', models.PositiveIntegerField(null=True)),
|
||||
('popularity', models.PositiveIntegerField(null=True)),
|
||||
('banter', models.TextField()),
|
||||
('album', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='playlist.album')),
|
||||
('artists', models.ManyToManyField(to='playlist.artist')),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.0.1 on 2024-02-05 21:06
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('playlist', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='track',
|
||||
name='old',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.0.1 on 2024-02-05 21:11
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('playlist', '0002_track_old'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='track',
|
||||
old_name='title',
|
||||
new_name='name',
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 5.0.1 on 2024-02-05 22:11
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('playlist', '0003_rename_title_track_name'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='artist',
|
||||
name='genres',
|
||||
field=models.JSONField(null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='track',
|
||||
name='banter',
|
||||
field=models.TextField(blank=True),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,28 @@
|
||||
# Generated by Django 5.0.1 on 2024-02-07 12:26
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('playlist', '0004_alter_artist_genres_alter_track_banter'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Vote',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('points', models.IntegerField()),
|
||||
('track', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='playlist.track')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'unique_together': {('user', 'track')},
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.0.1 on 2024-02-07 19:48
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('playlist', '0005_vote'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='track',
|
||||
name='banter_done',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.0.1 on 2024-02-12 13:42
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('playlist', '0006_track_banter_done'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='album',
|
||||
name='image_url',
|
||||
field=models.URLField(null=True),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,32 @@
|
||||
# Generated by Django 5.0.1 on 2024-02-21 13:26
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('playlist', '0007_album_image_url'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Background',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('image', models.ImageField(upload_to='wallpapers/')),
|
||||
('repeat', models.BooleanField(default=False)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Profile',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('background', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='playlist.background')),
|
||||
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,19 @@
|
||||
# Generated by Django 5.0.1 on 2024-02-21 13:31
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('playlist', '0008_background_profile'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='background',
|
||||
name='name',
|
||||
field=models.CharField(default='Wallpaper', max_length=128),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.0.1 on 2024-02-21 14:38
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('playlist', '0009_background_name'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='background',
|
||||
name='color',
|
||||
field=models.CharField(blank=True, max_length=32),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.0.1 on 2024-02-21 14:38
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('playlist', '0010_background_color'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='background',
|
||||
name='image',
|
||||
field=models.ImageField(null=True, upload_to='wallpapers/'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.0.1 on 2024-02-21 14:39
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('playlist', '0011_alter_background_image'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='background',
|
||||
name='image',
|
||||
field=models.ImageField(blank=True, null=True, upload_to='wallpapers/'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,19 @@
|
||||
# Generated by Django 5.0.1 on 2024-02-26 18:38
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('playlist', '0012_alter_background_image'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='profile',
|
||||
name='last_voted',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='playlist.track'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 5.0.1 on 2024-02-26 20:46
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('playlist', '0013_profile_last_voted'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='profile',
|
||||
name='quota',
|
||||
field=models.FloatField(default=10.0),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='profile',
|
||||
name='quota_last_updated',
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,19 @@
|
||||
# Generated by Django 5.0.1 on 2024-02-26 22:17
|
||||
|
||||
import django.utils.timezone
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('playlist', '0014_profile_quota_profile_quota_last_updated'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='profile',
|
||||
name='quota_last_updated',
|
||||
field=models.DateTimeField(default=django.utils.timezone.now),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,31 @@
|
||||
# Generated by Django 5.0.1 on 2024-03-08 23:53
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('playlist', '0015_alter_profile_quota_last_updated'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='profile',
|
||||
name='spt_access_token',
|
||||
field=models.CharField(default='', max_length=128),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='profile',
|
||||
name='spt_refresh_token',
|
||||
field=models.CharField(default='', max_length=128),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Playlist',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255)),
|
||||
('tracks', models.ManyToManyField(to='playlist.track')),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.0.1 on 2024-03-08 23:56
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('playlist', '0016_profile_spt_access_token_profile_spt_refresh_token_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='profile',
|
||||
name='spt_expires',
|
||||
field=models.DateTimeField(default=None, null=True),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 5.0.1 on 2024-03-13 08:38
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('playlist', '0017_profile_spt_expires'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='profile',
|
||||
name='spt_access_token',
|
||||
field=models.CharField(default='', max_length=128, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='profile',
|
||||
name='spt_refresh_token',
|
||||
field=models.CharField(default='', max_length=128, null=True),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.0.4 on 2024-07-20 06:58
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('playlist', '0018_alter_profile_spt_access_token_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='vote',
|
||||
name='skipped',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,31 @@
|
||||
# Generated by Django 5.0.4 on 2024-07-21 07:59
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('playlist', '0019_vote_skipped'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='track',
|
||||
name='nominated_by',
|
||||
field=models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='profile',
|
||||
name='spt_access_token',
|
||||
field=models.CharField(blank=True, default='', max_length=256, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='profile',
|
||||
name='spt_refresh_token',
|
||||
field=models.CharField(blank=True, default='', max_length=256, null=True),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,46 @@
|
||||
# Generated by Django 5.0.4 on 2024-07-22 20:32
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('playlist', '0020_track_nominated_by_alter_profile_spt_access_token_and_more'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='track',
|
||||
name='from_playlist',
|
||||
field=models.CharField(blank=True, default=None, max_length=255, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='track',
|
||||
name='album',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='playlist.album'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='track',
|
||||
name='banter',
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='track',
|
||||
name='duration_ms',
|
||||
field=models.PositiveIntegerField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='track',
|
||||
name='nominated_by',
|
||||
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='track',
|
||||
name='popularity',
|
||||
field=models.PositiveIntegerField(blank=True, null=True),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.0.4 on 2024-07-23 11:29
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('playlist', '0021_track_from_playlist_alter_track_album_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='background',
|
||||
name='cover',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,6 @@
|
||||
from .track import Track
|
||||
from .artist import Artist
|
||||
from .album import Album
|
||||
from .vote import Vote
|
||||
from .profile import Profile, Background
|
||||
from .playlist import Playlist
|
||||
@@ -0,0 +1,13 @@
|
||||
from django.db import models
|
||||
from .artist import Artist
|
||||
|
||||
class Album(models.Model):
|
||||
def __str__(self):
|
||||
return self.name
|
||||
id = models.CharField(primary_key=True, max_length=128)
|
||||
name = models.CharField(max_length=255)
|
||||
artists = models.ManyToManyField(Artist)
|
||||
album_type = models.CharField(max_length=255)
|
||||
total_tracks = models.PositiveSmallIntegerField(null=True)
|
||||
image = models.ImageField(upload_to="albums/")
|
||||
image_url = models.URLField(null=True)
|
||||
@@ -0,0 +1,9 @@
|
||||
from django.db import models
|
||||
|
||||
class Artist(models.Model):
|
||||
def __str__(self):
|
||||
return self.name
|
||||
id = models.CharField(primary_key=True, max_length=128)
|
||||
name = models.CharField(max_length=255)
|
||||
popularity = models.SmallIntegerField(null=True, blank=True)
|
||||
genres = models.JSONField(null=True)
|
||||
@@ -0,0 +1,8 @@
|
||||
from django.db import models
|
||||
from .track import Track
|
||||
|
||||
class Playlist(models.Model):
|
||||
def __str__(self):
|
||||
return self.name
|
||||
name = models.CharField(max_length=255)
|
||||
tracks = models.ManyToManyField(Track)
|
||||
@@ -0,0 +1,145 @@
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import User
|
||||
from .track import Track
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
from django.utils import timezone
|
||||
from datetime import datetime, timedelta, UTC
|
||||
from ..spotify import spt
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
NOMINATIONS_PER_DAY = 10
|
||||
MAX_QUOTA = 22
|
||||
|
||||
class Background(models.Model):
|
||||
def __str__(self):
|
||||
return self.name
|
||||
name = models.CharField(max_length=128)
|
||||
image = models.ImageField(upload_to="wallpapers/", null=True, blank=True)
|
||||
repeat = models.BooleanField(default=False)
|
||||
cover = models.BooleanField(default=False)
|
||||
color = models.CharField(max_length=32, blank=True)
|
||||
|
||||
class Profile(models.Model):
|
||||
def __str__(self):
|
||||
return str(self.user)
|
||||
user = models.OneToOneField(User, on_delete=models.CASCADE)
|
||||
background = models.ForeignKey(Background, null=True, on_delete=models.SET_NULL)
|
||||
last_voted = models.ForeignKey(Track, null=True, on_delete=models.SET_NULL)
|
||||
|
||||
quota = models.FloatField(default=10.0)
|
||||
quota_last_updated = models.DateTimeField(default=timezone.now, null=False)
|
||||
|
||||
spt_access_token = models.CharField(max_length=256, default="", blank=True, null=True)
|
||||
spt_refresh_token = models.CharField(max_length=256, default="", blank=True, null=True)
|
||||
spt_expires = models.DateTimeField(default=None, null=True)
|
||||
|
||||
@property
|
||||
def quota_display(self):
|
||||
return int(self.quota)
|
||||
|
||||
def update_quota(self):
|
||||
elapsed = timezone.now() - self.quota_last_updated
|
||||
if elapsed.total_seconds() > 300:
|
||||
noms_per_second = NOMINATIONS_PER_DAY/(60*60*24)
|
||||
self.quota += noms_per_second * elapsed.total_seconds()
|
||||
self.quota = min(self.quota, MAX_QUOTA)
|
||||
print(f"{self.user.username}: Er zijn {elapsed.total_seconds()}s voorbij, je nieuwe quota is {self.quota}")
|
||||
self.quota_last_updated = timezone.now()
|
||||
|
||||
@property
|
||||
def can_nominate(self):
|
||||
"""Is this user allowed to nominate at this point in time?"""
|
||||
end_date = os.getenv('DATE_NOM_END')
|
||||
if self.user.is_superuser:
|
||||
return (True, None)
|
||||
if end_date:
|
||||
end_date = datetime.fromisoformat(end_date)
|
||||
if end_date <= datetime.now(tz=UTC):
|
||||
return (False, "over")
|
||||
if self.quota < 1:
|
||||
return (False, "quota")
|
||||
return (True, None)
|
||||
|
||||
@property
|
||||
def can_vote(self):
|
||||
"""Is this user allowed to vote at this point in time?"""
|
||||
end_date = os.getenv('DATE_VOTE_END')
|
||||
if self.user.is_superuser or not end_date:
|
||||
return True
|
||||
end_date = datetime.fromisoformat(end_date)
|
||||
if end_date <= datetime.now(tz=UTC):
|
||||
return False
|
||||
return True
|
||||
|
||||
@property
|
||||
def vote_end(self):
|
||||
return os.getenv('DATE_VOTE_END')
|
||||
|
||||
@property
|
||||
def nominate_end(self):
|
||||
return os.getenv('DATE_NOM_END')
|
||||
|
||||
def init_spt(self, code):
|
||||
"""Initial setting of spotify tokens from user accept code"""
|
||||
data = spt.get_token_json(
|
||||
grant_type='authorization_code',
|
||||
code=code
|
||||
)
|
||||
self.save_spt(data)
|
||||
|
||||
def refresh_spt(self):
|
||||
"""Refreshes expired access token using stored refresh token"""
|
||||
if self.spt_refresh_token:
|
||||
data = spt.get_token_json(
|
||||
grant_type='refresh_token',
|
||||
refresh_token=self.spt_refresh_token
|
||||
)
|
||||
self.save_spt(data)
|
||||
|
||||
def save_spt(self, data):
|
||||
"""Saves tokens from json response"""
|
||||
access = data.get('access_token', None)
|
||||
refresh = data.get('refresh_token', self.spt_refresh_token)
|
||||
expires = data.get('expires_in', 3600)
|
||||
self.spt_access_token = access
|
||||
self.spt_refresh_token = refresh
|
||||
self.spt_expires = timezone.now() + timedelta(seconds=expires - 300)
|
||||
self.save()
|
||||
|
||||
def unlink_spt(self):
|
||||
self.spt_access_token = None
|
||||
self.spt_refresh_token = None
|
||||
self.spt_expires = None
|
||||
|
||||
@property
|
||||
def can_spt(self):
|
||||
return (
|
||||
self.spt_access_token and
|
||||
self.spt_expires and
|
||||
(timezone.now() < self.spt_expires)
|
||||
)
|
||||
|
||||
def get_or_update_spt(self):
|
||||
if self.spt_refresh_token:
|
||||
if not self.can_spt:
|
||||
self.refresh_spt()
|
||||
return self.spt_access_token
|
||||
return None
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.update_quota()
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
@receiver(post_save, sender=User)
|
||||
def create_user_profile(sender, instance, created, **kwargs):
|
||||
if created:
|
||||
Profile.objects.create(user=instance)
|
||||
|
||||
@receiver(post_save, sender=User)
|
||||
def save_user_profile(sender, instance, **kwargs):
|
||||
Profile.objects.get_or_create(user=instance)
|
||||
instance.profile.save()
|
||||
@@ -0,0 +1,144 @@
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import User
|
||||
from random import choice
|
||||
from .album import Album
|
||||
from .artist import Artist
|
||||
from .vote import Vote
|
||||
from playlist.spotify import spt
|
||||
#from playlist.utils import track_from_json
|
||||
import re
|
||||
|
||||
class TrackManager(models.Manager):
|
||||
def get_queryset(self):
|
||||
return super().get_queryset().filter(old=False)
|
||||
|
||||
def from_json(self, cls, json):
|
||||
try:
|
||||
return cls.objects.get(pk=json["id"])
|
||||
except cls.DoesNotExist:
|
||||
keys = [f.name for f in cls._meta.get_fields()]
|
||||
subset = {key:json[key] for key in set(keys) & set(json.keys())}
|
||||
return cls.objects.create(**subset)
|
||||
|
||||
def track_from_json(self, json):
|
||||
"""Creates the track, album and artist from spotify response."""
|
||||
# Album artists
|
||||
album_artists = []
|
||||
for artist in json["album"]["artists"]:
|
||||
album_artists.append(self.from_json(Artist, artist))
|
||||
del json["album"]["artists"]
|
||||
|
||||
# Album
|
||||
album = self.from_json(Album, json["album"])
|
||||
album.image_url = json["album"]["images"][0]["url"]
|
||||
album.save()
|
||||
del json["album"]
|
||||
for artist in album_artists:
|
||||
album.artists.add(artist)
|
||||
|
||||
# Track artists
|
||||
track_artists = []
|
||||
for artist in json["artists"]:
|
||||
track_artists.append(self.from_json(Artist, artist))
|
||||
del json["artists"]
|
||||
|
||||
# Track
|
||||
track = self.from_json(Track, json)
|
||||
for artist in track_artists:
|
||||
track.artists.add(artist)
|
||||
track.album = album
|
||||
track.save()
|
||||
return track
|
||||
|
||||
def create_from_spotify(self, spotify_link, profile):
|
||||
if re.match(r"^https:\/\/spotify.link.*", spotify_link):
|
||||
spotify_link = spt.unshort(spotify_link)
|
||||
|
||||
m = re.match(r"^https:\/\/open.spotify.com\/track\/([a-zA-Z0-9]+)\??", spotify_link)
|
||||
if m is None:
|
||||
return None, Exception("Invalid Spotify track URL")
|
||||
|
||||
spotify_id = m.group(1)
|
||||
|
||||
try:
|
||||
existing_track = self.model.all_tracks.get(pk=spotify_id)
|
||||
if existing_track.old:
|
||||
existing_track.old = False
|
||||
existing_track.user = profile.user
|
||||
existing_track.save()
|
||||
return (existing_track, "Updated old track")
|
||||
else:
|
||||
return (None, "Track already exists")
|
||||
except:
|
||||
pass
|
||||
try:
|
||||
json = spt.get_song_info(spotify_id)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
return (None, "Error fetching track")
|
||||
#return TemplateResponse(request, "Nominate.html", {"message_type": "error", "message": "Fout bij het ophalen van het liedje."})
|
||||
|
||||
track = self.track_from_json(json)
|
||||
track.nominated_by = profile.user
|
||||
track.save()
|
||||
dup = track.is_duplicate_of()
|
||||
if dup:
|
||||
track.delete()
|
||||
return (None, "Track already exists")
|
||||
#get_banter.delay(track.id)
|
||||
profile.quota -= 1
|
||||
profile.save()
|
||||
return (track, None)
|
||||
|
||||
class Track(models.Model):
|
||||
def __str__(self):
|
||||
s = f"'{self.name}'"
|
||||
if self.artists.count() > 0:
|
||||
artists = list(map(lambda a:a.name, self.artists.all()))
|
||||
s += " door "
|
||||
s += " & ".join(artists)
|
||||
return s
|
||||
@property
|
||||
def artist(self):
|
||||
if self.artists.count() > 0:
|
||||
artists = list(map(lambda a:a.name, self.artists.all()))
|
||||
return " & ".join(artists)
|
||||
|
||||
@property
|
||||
def name_sanitized(self):
|
||||
return re.match("[^\-\(]*\w", self.name)[0]
|
||||
|
||||
@property
|
||||
def link(self):
|
||||
return f'https://open.spotify.com/track/{self.id}'
|
||||
|
||||
@property
|
||||
def score(self):
|
||||
return Vote.objects.filter(track=self.id).aggregate(rating=models.Avg("points"))["rating"]
|
||||
|
||||
def is_duplicate_of(self):
|
||||
tracks = Track.objects.exclude(id=self.id).filter(name__startswith=self.name_sanitized)
|
||||
artists = [artist.name for artist in self.artists.all()]
|
||||
for artist in artists:
|
||||
tracks = tracks.filter(artists__name__exact=artist)
|
||||
return tracks.first()
|
||||
|
||||
|
||||
id = models.CharField(primary_key=True, max_length=128)
|
||||
nominated_by = models.ForeignKey(User, blank=True, null=True, default=None, on_delete=models.SET_NULL)
|
||||
name = models.CharField(max_length=255)
|
||||
artists = models.ManyToManyField(Artist)
|
||||
album = models.ForeignKey(Album, blank=True, null=True, on_delete=models.SET_NULL)
|
||||
|
||||
explicit = models.BooleanField(default=False)
|
||||
duration_ms = models.PositiveIntegerField(blank=True, null=True)
|
||||
popularity = models.PositiveIntegerField(blank=True, null=True)
|
||||
|
||||
banter = models.TextField(null=True, blank=True)
|
||||
banter_done = models.BooleanField(default=False)
|
||||
|
||||
old = models.BooleanField(default=False)
|
||||
from_playlist = models.CharField(max_length=255, null=True, blank=True, default=None)
|
||||
|
||||
objects = TrackManager()
|
||||
all_tracks = models.Manager()
|
||||
@@ -0,0 +1,11 @@
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.validators import MinValueValidator, MaxValueValidator
|
||||
|
||||
class Vote(models.Model):
|
||||
class Meta:
|
||||
unique_together = ["user", "track"]
|
||||
track = models.ForeignKey('Track', on_delete=models.CASCADE)
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
points = models.IntegerField(validators=[MinValueValidator(1), MaxValueValidator(5)])
|
||||
skipped = models.BooleanField(default=False)
|
||||
@@ -0,0 +1,6 @@
|
||||
from .track import *
|
||||
from .artist import *
|
||||
from .user import *
|
||||
from .album import *
|
||||
from .profile import *
|
||||
from .vote import *
|
||||
@@ -0,0 +1,12 @@
|
||||
from django.db.models.functions import Extract
|
||||
from playlist.models import Album
|
||||
from rest_framework import serializers
|
||||
|
||||
class AlbumSerializer(serializers.HyperlinkedModelSerializer):
|
||||
class Meta:
|
||||
model = Album
|
||||
fields = '__all__'
|
||||
extra_kwargs = {
|
||||
'url': {'view_name': 'api-album', 'lookup_field': 'id'},
|
||||
'artists': {'view_name': 'api-artist', 'lookup_field': 'id'},
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
from playlist.models import Artist
|
||||
from rest_framework import serializers
|
||||
|
||||
class ArtistSerializer(serializers.HyperlinkedModelSerializer):
|
||||
class Meta:
|
||||
model = Artist
|
||||
fields = ['id', 'name', 'url']
|
||||
extra_kwargs = {
|
||||
'url': {'view_name': 'api-artist', 'lookup_field': 'id'},
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
from playlist.models import Profile, Background
|
||||
from rest_framework import serializers
|
||||
|
||||
class BackgroundSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Background
|
||||
fields = '__all__'
|
||||
|
||||
class ProfileSerializer(serializers.ModelSerializer):
|
||||
background = BackgroundSerializer()
|
||||
|
||||
class Meta:
|
||||
model = Profile
|
||||
fields = '__all__'
|
||||
|
||||
class ProfileUpdateSerializer(serializers.ModelSerializer):
|
||||
background = serializers.PrimaryKeyRelatedField(many=False, queryset=Background.objects.all())
|
||||
class Meta:
|
||||
model = Profile
|
||||
fields = ['background']
|
||||
@@ -0,0 +1,13 @@
|
||||
from playlist.models import Track
|
||||
from rest_framework import serializers
|
||||
|
||||
class TrackSerializer(serializers.HyperlinkedModelSerializer):
|
||||
class Meta:
|
||||
model = Track
|
||||
fields = '__all__'
|
||||
extra_kwargs = {
|
||||
'url': {'view_name': 'api-track', 'lookup_field': 'id'},
|
||||
'artists': {'view_name': 'api-artist', 'lookup_field': 'id'},
|
||||
'album': {'view_name': 'api-album', 'lookup_field': 'id'},
|
||||
'nominated_by': {'view_name': 'api-user', 'lookup_field': 'id'}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
from django.contrib.auth.models import User
|
||||
from rest_framework import serializers
|
||||
|
||||
class UserSerializer(serializers.HyperlinkedModelSerializer):
|
||||
# Add explicit reverse lookup to profile
|
||||
profile = serializers.HyperlinkedRelatedField(
|
||||
view_name='api-profile',
|
||||
lookup_field='id',
|
||||
read_only=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ['url', 'id', 'username', 'email', 'profile']
|
||||
extra_kwargs = {
|
||||
'url': {'view_name': 'api-user', 'lookup_field': 'id'}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
from rest_framework import serializers
|
||||
from playlist.models import Vote, Track
|
||||
|
||||
class VoteSerializer(serializers.HyperlinkedModelSerializer):
|
||||
class Meta:
|
||||
model = Vote
|
||||
fields = '__all__'
|
||||
extra_kwargs = {
|
||||
'url': {'view_name': 'api-vote', 'lookup_field': 'id'},
|
||||
'track': {'view_name': 'api-track', 'lookup_field': 'id'},
|
||||
'user': {'view_name': 'api-user', 'lookup_field': 'id'}
|
||||
}
|
||||
|
||||
class VoteSaveSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Vote
|
||||
fields = ['track', 'points', 'skipped']
|
||||
|
||||
def create(self, validated_data):
|
||||
"""
|
||||
Create a new vote. Deletes a previous skipped vote if it exists.
|
||||
"""
|
||||
user = self.context['request'].user
|
||||
track = validated_data['track']
|
||||
previous_vote = Vote.objects.filter(user=user, track=track).first()
|
||||
if previous_vote and previous_vote.skipped:
|
||||
previous_vote.delete()
|
||||
return Vote.objects.create(**validated_data)
|
||||
@@ -0,0 +1,105 @@
|
||||
import requests
|
||||
import os
|
||||
import re
|
||||
from datetime import datetime, timedelta
|
||||
from dotenv import load_dotenv
|
||||
from django.urls import reverse
|
||||
from django.conf import settings
|
||||
|
||||
load_dotenv()
|
||||
|
||||
|
||||
class Spotify():
|
||||
def __init__(self):
|
||||
self.token = None
|
||||
self.valid_until = None
|
||||
|
||||
def redirect_uri(self):
|
||||
return settings.CURRENT_HOST + reverse('spotify-callback')
|
||||
|
||||
def token_valid(self):
|
||||
if self.token and self.valid_until and (datetime.now() < self.valid_until):
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_token_json(self, grant_type, code=None, refresh_token=None):
|
||||
url = 'https://accounts.spotify.com/api/token'
|
||||
headers = {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Authorization': 'Basic ' + os.getenv('SPOTIFY_ENCODED_ID')
|
||||
}
|
||||
data = {
|
||||
'grant_type': grant_type
|
||||
}
|
||||
if code:
|
||||
data['code'] = code
|
||||
data['redirect_uri'] = self.redirect_uri()
|
||||
if refresh_token:
|
||||
data['refresh_token'] = refresh_token
|
||||
r = requests.post(url, headers=headers, data=data)
|
||||
return r.json()
|
||||
|
||||
def get_system_token(self):
|
||||
if self.token_valid():
|
||||
return self.token
|
||||
data = self.get_token_json('client_credentials')
|
||||
self.token = data["access_token"]
|
||||
self.valid_until = datetime.now() + timedelta(seconds=data["expires_in"]-300)
|
||||
return self.token
|
||||
|
||||
def get_user_token(self, code):
|
||||
data = self.get_token_json('authorization_code', code, redirect=True)
|
||||
print(data)
|
||||
return data
|
||||
return (data.get('access_token', None), data.get('refresh_token', None))
|
||||
|
||||
def get_oauth_redirect(self):
|
||||
scopes = ['user-read-private', 'user-read-email']
|
||||
CLIENT_ID = 'bcc523219d1d4248a7e8892e809a5767' #TODO: delet.
|
||||
url = f'https://accounts.spotify.com/authorize?'
|
||||
url += f'client_id={CLIENT_ID}&'
|
||||
url += f'response_type=code&'
|
||||
url += f'scope={"%20".join(scopes)}&'
|
||||
url += f'redirect_uri={self.redirect_uri()}'
|
||||
print(url)
|
||||
return url
|
||||
|
||||
def unshort(self, url):
|
||||
"""Converts a spotify.link url into a normal spotify URL with ID"""
|
||||
r = requests.get(url, allow_redirects=False)
|
||||
r.raise_for_status()
|
||||
return r.headers["Location"]
|
||||
|
||||
def get_song_info(self, spotify_id):
|
||||
token = self.get_system_token()
|
||||
url = 'https://api.spotify.com/v1/tracks/' + spotify_id
|
||||
headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer ' + token
|
||||
}
|
||||
r = requests.get(url, headers=headers)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
def get_profile(self, access_token):
|
||||
url = 'https://api.spotify.com/v1/me'
|
||||
headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer ' + access_token
|
||||
}
|
||||
r = requests.get(url, headers=headers)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
def get_playlist_info(self, playlist_id):
|
||||
token = self.get_system_token()
|
||||
url = 'https://api.spotify.com/v1/playlists/' + playlist_id
|
||||
headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer ' + token
|
||||
}
|
||||
r = requests.get(url, headers=headers)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
spt = Spotify()
|
||||
@@ -0,0 +1,7 @@
|
||||
Copyright 2020 Jordan Scales
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 7.2 KiB |
@@ -0,0 +1,51 @@
|
||||
/* Based on https://codepen.io/agalliat/pen/mdedZYK */
|
||||
.window {
|
||||
display: none;
|
||||
}
|
||||
|
||||
body {
|
||||
display: block;
|
||||
background-color: #000084;
|
||||
font-family: "Pixelated MS Sans Serif", Arial;
|
||||
color: #bbb;
|
||||
font-size: 1rem;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#bsod {
|
||||
display: block !important;
|
||||
width: max(70vw, 400px);
|
||||
max-width: 800px;
|
||||
flex-shrink: 1;
|
||||
gap: 1rem;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
|
||||
#bsod h1 {
|
||||
font-size: 1rem;
|
||||
background-color: #bbb;
|
||||
color: #000084;
|
||||
padding: 1rem;
|
||||
box-shadow: 1rem 1rem black;
|
||||
margin: 2rem;
|
||||
text-align: center;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
#bsod div {
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
#bsod .continue {
|
||||
text-align: center;
|
||||
font-size: 1.5rem;
|
||||
margin: 2rem;
|
||||
}
|
||||
|
After Width: | Height: | Size: 556 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1,68 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="64mm"
|
||||
height="64mm"
|
||||
viewBox="0 0 64 64"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
inkscape:export-filename="prev.svg"
|
||||
inkscape:export-xdpi="25.4"
|
||||
inkscape:export-ydpi="25.4"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="mm" />
|
||||
<defs
|
||||
id="defs1" />
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<path
|
||||
sodipodi:type="star"
|
||||
style="fill:#808080;stroke-width:0.264583"
|
||||
id="path1"
|
||||
inkscape:flatsided="true"
|
||||
sodipodi:sides="3"
|
||||
sodipodi:cx="2.6995511"
|
||||
sodipodi:cy="31.154797"
|
||||
sodipodi:r1="21.559158"
|
||||
sodipodi:r2="10.779579"
|
||||
sodipodi:arg1="0"
|
||||
sodipodi:arg2="1.0471976"
|
||||
inkscape:rounded="0"
|
||||
inkscape:randomized="0"
|
||||
d="m 24.258709,31.154797 -32.3387371,18.670778 0,-37.341557 z"
|
||||
inkscape:transform-center-x="3.9999988"
|
||||
transform="matrix(-0.742144,0,0,1.2854311,26.003456,-8.0473449)" />
|
||||
<path
|
||||
sodipodi:type="star"
|
||||
style="fill:#808080;stroke-width:0.264583"
|
||||
id="path1-5"
|
||||
inkscape:flatsided="true"
|
||||
sodipodi:sides="3"
|
||||
sodipodi:cx="2.6995511"
|
||||
sodipodi:cy="31.154797"
|
||||
sodipodi:r1="21.559158"
|
||||
sodipodi:r2="10.779579"
|
||||
sodipodi:arg1="0"
|
||||
sodipodi:arg2="1.0471976"
|
||||
inkscape:rounded="0"
|
||||
inkscape:randomized="0"
|
||||
d="m 24.258709,31.154797 -32.3387371,18.670778 0,-37.341557 z"
|
||||
inkscape:transform-center-x="3.9999988"
|
||||
transform="matrix(-0.742144,0,0,1.2854311,50.003456,-8.0473447)" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 462 B |
|
After Width: | Height: | Size: 419 B |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 12 KiB |
@@ -0,0 +1,58 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="64mm"
|
||||
height="64mm"
|
||||
viewBox="0 0 64 64"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
inkscape:export-filename="back.svg"
|
||||
inkscape:export-xdpi="25.4"
|
||||
inkscape:export-ydpi="25.4"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="mm" />
|
||||
<defs
|
||||
id="defs1" />
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<path
|
||||
sodipodi:type="star"
|
||||
style="fill:#808080;stroke-width:0.264583"
|
||||
id="path1"
|
||||
inkscape:flatsided="true"
|
||||
sodipodi:sides="3"
|
||||
sodipodi:cx="9.8473034"
|
||||
sodipodi:cy="6.337101"
|
||||
sodipodi:r1="31.498077"
|
||||
sodipodi:r2="15.749038"
|
||||
sodipodi:arg1="0.52359878"
|
||||
sodipodi:arg2="1.5707963"
|
||||
inkscape:rounded="0"
|
||||
inkscape:randomized="0"
|
||||
d="m 37.125439,22.08614 -54.556271,0 27.2781355,-47.247116 z"
|
||||
inkscape:transform-center-y="-5.3333328"
|
||||
transform="matrix(0.87982554,0,0,0.67729001,23.336091,21.041278)" />
|
||||
<rect
|
||||
style="fill:#808080;stroke-width:0.295996"
|
||||
id="rect1"
|
||||
width="48"
|
||||
height="16"
|
||||
x="8"
|
||||
y="42" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 451 B |
@@ -0,0 +1,68 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="64mm"
|
||||
height="64mm"
|
||||
viewBox="0 0 64 64"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
inkscape:export-filename="next.svg"
|
||||
inkscape:export-xdpi="25.4"
|
||||
inkscape:export-ydpi="25.4"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="mm" />
|
||||
<defs
|
||||
id="defs1" />
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<path
|
||||
sodipodi:type="star"
|
||||
style="fill:#808080;stroke-width:0.264583"
|
||||
id="path1"
|
||||
inkscape:flatsided="true"
|
||||
sodipodi:sides="3"
|
||||
sodipodi:cx="2.6995511"
|
||||
sodipodi:cy="31.154797"
|
||||
sodipodi:r1="21.559158"
|
||||
sodipodi:r2="10.779579"
|
||||
sodipodi:arg1="0"
|
||||
sodipodi:arg2="1.0471976"
|
||||
inkscape:rounded="0"
|
||||
inkscape:randomized="0"
|
||||
d="m 24.258709,31.154797 -32.3387371,18.670778 0,-37.341557 z"
|
||||
inkscape:transform-center-x="-3.9999985"
|
||||
transform="matrix(0.742144,0,0,1.2854311,37.996544,-8.0473449)" />
|
||||
<path
|
||||
sodipodi:type="star"
|
||||
style="fill:#808080;stroke-width:0.264583"
|
||||
id="path1-5"
|
||||
inkscape:flatsided="true"
|
||||
sodipodi:sides="3"
|
||||
sodipodi:cx="2.6995511"
|
||||
sodipodi:cy="31.154797"
|
||||
sodipodi:r1="21.559158"
|
||||
sodipodi:r2="10.779579"
|
||||
sodipodi:arg1="0"
|
||||
sodipodi:arg2="1.0471976"
|
||||
inkscape:rounded="0"
|
||||
inkscape:randomized="0"
|
||||
d="m 24.258709,31.154797 -32.3387371,18.670778 0,-37.341557 z"
|
||||
inkscape:transform-center-x="-3.9999988"
|
||||
transform="matrix(0.742144,0,0,1.2854311,13.996544,-8.0473447)" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 498 B |
|
After Width: | Height: | Size: 749 B |
|
After Width: | Height: | Size: 411 B |
@@ -0,0 +1,76 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="64mm"
|
||||
height="64mm"
|
||||
viewBox="0 0 64 64"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
inkscape:export-filename="prev.svg"
|
||||
inkscape:export-xdpi="25.4"
|
||||
inkscape:export-ydpi="25.4"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="mm" />
|
||||
<defs
|
||||
id="defs1" />
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<path
|
||||
sodipodi:type="star"
|
||||
style="fill:#808080;stroke-width:0.264583"
|
||||
id="path1"
|
||||
inkscape:flatsided="true"
|
||||
sodipodi:sides="3"
|
||||
sodipodi:cx="2.6995511"
|
||||
sodipodi:cy="31.154797"
|
||||
sodipodi:r1="21.559158"
|
||||
sodipodi:r2="10.779579"
|
||||
sodipodi:arg1="0"
|
||||
sodipodi:arg2="1.0471976"
|
||||
inkscape:rounded="0"
|
||||
inkscape:randomized="0"
|
||||
d="m 24.258709,31.154797 -32.3387371,18.670778 0,-37.341557 z"
|
||||
inkscape:transform-center-x="-3.9999985"
|
||||
transform="matrix(0.742144,0,0,1.2854311,37.996544,-8.0473449)" />
|
||||
<path
|
||||
sodipodi:type="star"
|
||||
style="fill:#808080;stroke-width:0.264583"
|
||||
id="path1-5"
|
||||
inkscape:flatsided="true"
|
||||
sodipodi:sides="3"
|
||||
sodipodi:cx="2.6995511"
|
||||
sodipodi:cy="31.154797"
|
||||
sodipodi:r1="21.559158"
|
||||
sodipodi:r2="10.779579"
|
||||
sodipodi:arg1="0"
|
||||
sodipodi:arg2="1.0471976"
|
||||
inkscape:rounded="0"
|
||||
inkscape:randomized="0"
|
||||
d="m 24.258709,31.154797 -32.3387371,18.670778 0,-37.341557 z"
|
||||
inkscape:transform-center-x="-3.9999988"
|
||||
transform="matrix(0.742144,0,0,1.2854311,13.996544,-8.0473447)" />
|
||||
<rect
|
||||
style="fill:#808080;stroke-width:0.223385"
|
||||
id="rect1"
|
||||
width="8"
|
||||
height="48"
|
||||
x="-59.999992"
|
||||
y="8"
|
||||
transform="scale(-1,1)" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
@@ -0,0 +1,48 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="64mm"
|
||||
height="64mm"
|
||||
viewBox="0 0 64 64"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
inkscape:export-filename="pause.svg"
|
||||
inkscape:export-xdpi="12.7"
|
||||
inkscape:export-ydpi="12.7"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="mm" />
|
||||
<defs
|
||||
id="defs1" />
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<rect
|
||||
id="rect2"
|
||||
width="16"
|
||||
height="48"
|
||||
x="8"
|
||||
y="8"
|
||||
style="fill:#808080;stroke-width:0.304549" />
|
||||
<rect
|
||||
id="rect2-5"
|
||||
width="16"
|
||||
height="48"
|
||||
x="40"
|
||||
y="8"
|
||||
style="fill:#808080;stroke-width:0.304549" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,48 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="64mm"
|
||||
height="64mm"
|
||||
viewBox="0 0 64 64"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
inkscape:export-filename="play.svg"
|
||||
inkscape:export-xdpi="12.7"
|
||||
inkscape:export-ydpi="12.7"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="mm" />
|
||||
<defs
|
||||
id="defs1" />
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<rect
|
||||
id="rect2"
|
||||
width="16"
|
||||
height="48"
|
||||
x="8"
|
||||
y="8"
|
||||
style="stroke-width:0.304549" />
|
||||
<rect
|
||||
id="rect2-5"
|
||||
width="16"
|
||||
height="48"
|
||||
x="40"
|
||||
y="8"
|
||||
style="stroke-width:0.304549" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,51 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="64mm"
|
||||
height="64mm"
|
||||
viewBox="0 0 64 64"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
inkscape:export-filename="play.svg"
|
||||
inkscape:export-xdpi="12.7"
|
||||
inkscape:export-ydpi="12.7"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="mm" />
|
||||
<defs
|
||||
id="defs1" />
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<path
|
||||
sodipodi:type="star"
|
||||
id="path2"
|
||||
inkscape:flatsided="true"
|
||||
sodipodi:sides="3"
|
||||
sodipodi:cx="3.9214177"
|
||||
sodipodi:cy="28.775017"
|
||||
sodipodi:r1="30.64756"
|
||||
sodipodi:r2="15.32378"
|
||||
sodipodi:arg1="0"
|
||||
sodipodi:arg2="1.0471976"
|
||||
inkscape:rounded="0"
|
||||
inkscape:randomized="0"
|
||||
d="m 34.568978,28.775017 -45.97134,26.541565 0,-53.0831308 z"
|
||||
inkscape:transform-center-x="-8.0000004"
|
||||
style="fill:#808080;stroke-width:0.264583"
|
||||
transform="matrix(1.0441288,0,0,0.90424206,19.905535,5.9804195)" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,48 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="64mm"
|
||||
height="64mm"
|
||||
viewBox="0 0 64 64"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="mm" />
|
||||
<defs
|
||||
id="defs1" />
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<path
|
||||
sodipodi:type="star"
|
||||
id="path2"
|
||||
inkscape:flatsided="true"
|
||||
sodipodi:sides="3"
|
||||
sodipodi:cx="3.9214177"
|
||||
sodipodi:cy="28.775017"
|
||||
sodipodi:r1="30.64756"
|
||||
sodipodi:r2="15.32378"
|
||||
sodipodi:arg1="0"
|
||||
sodipodi:arg2="1.0471976"
|
||||
inkscape:rounded="0"
|
||||
inkscape:randomized="0"
|
||||
d="m 34.568978,28.775017 -45.97134,26.541565 0,-53.0831308 z"
|
||||
inkscape:transform-center-x="-8.0000004"
|
||||
style="stroke-width:0.264583"
|
||||
transform="matrix(1.0441288,0,0,0.90424206,19.905535,5.9804195)" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1,75 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="64mm"
|
||||
height="64mm"
|
||||
viewBox="0 0 64 64"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
inkscape:export-filename="back.svg"
|
||||
inkscape:export-xdpi="25.4"
|
||||
inkscape:export-ydpi="25.4"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="mm" />
|
||||
<defs
|
||||
id="defs1" />
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<path
|
||||
sodipodi:type="star"
|
||||
style="fill:#808080;stroke-width:0.264583"
|
||||
id="path1"
|
||||
inkscape:flatsided="true"
|
||||
sodipodi:sides="3"
|
||||
sodipodi:cx="2.6995511"
|
||||
sodipodi:cy="31.154797"
|
||||
sodipodi:r1="21.559158"
|
||||
sodipodi:r2="10.779579"
|
||||
sodipodi:arg1="0"
|
||||
sodipodi:arg2="1.0471976"
|
||||
inkscape:rounded="0"
|
||||
inkscape:randomized="0"
|
||||
d="m 24.258709,31.154797 -32.3387371,18.670778 0,-37.341557 z"
|
||||
inkscape:transform-center-x="3.9999988"
|
||||
transform="matrix(-0.742144,0,0,1.2854311,26.003456,-8.0473449)" />
|
||||
<path
|
||||
sodipodi:type="star"
|
||||
style="fill:#808080;stroke-width:0.264583"
|
||||
id="path1-5"
|
||||
inkscape:flatsided="true"
|
||||
sodipodi:sides="3"
|
||||
sodipodi:cx="2.6995511"
|
||||
sodipodi:cy="31.154797"
|
||||
sodipodi:r1="21.559158"
|
||||
sodipodi:r2="10.779579"
|
||||
sodipodi:arg1="0"
|
||||
sodipodi:arg2="1.0471976"
|
||||
inkscape:rounded="0"
|
||||
inkscape:randomized="0"
|
||||
d="m 24.258709,31.154797 -32.3387371,18.670778 0,-37.341557 z"
|
||||
inkscape:transform-center-x="3.9999988"
|
||||
transform="matrix(-0.742144,0,0,1.2854311,50.003456,-8.0473447)" />
|
||||
<rect
|
||||
style="fill:#808080;stroke-width:0.223385"
|
||||
id="rect1"
|
||||
width="8"
|
||||
height="48"
|
||||
x="4"
|
||||
y="8" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
@@ -0,0 +1,47 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="21"
|
||||
height="21"
|
||||
fill="none"
|
||||
version="1.1"
|
||||
id="svg4"
|
||||
sodipodi:docname="svg.svg"
|
||||
inkscape:export-filename="seek.svg"
|
||||
inkscape:export-xdpi="96"
|
||||
inkscape:export-ydpi="96"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs4" />
|
||||
<sodipodi:namedview
|
||||
id="namedview4"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="true"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
showgrid="false" />
|
||||
<path
|
||||
d="M 0,0 V 1 H 20 V 20 H 0 v 1 H 21 V 0 Z"
|
||||
style="fill:#000000;stroke-width:1.48889"
|
||||
id="path10" />
|
||||
<path
|
||||
d="M 0,1 V 20 H 20 V 1 Z M 5,5 H 17 V 17 H 5 Z"
|
||||
style="fill:#87888f;stroke-width:4.57988"
|
||||
id="path11"
|
||||
sodipodi:nodetypes="cccccccccc" />
|
||||
<path
|
||||
d="M 0,0 V 19 H 1 V 1 H 20 V 0 Z"
|
||||
style="fill:#ffffff;stroke-width:1.3985"
|
||||
id="path9"
|
||||
sodipodi:nodetypes="ccccccc" />
|
||||
<path
|
||||
d="M 1,1 V 19 H 19 V 1 Z M 4,4 H 16 V 16 H 4 Z"
|
||||
style="fill:#c0c7c8;stroke-width:1.49631"
|
||||
id="path8"
|
||||
sodipodi:nodetypes="cccccccccc" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1,39 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="120mm"
|
||||
height="18.999998mm"
|
||||
viewBox="0 0 120 18.999998"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
inkscape:export-filename="slider.svg"
|
||||
inkscape:export-xdpi="25.4"
|
||||
inkscape:export-ydpi="25.4"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="true"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="mm" />
|
||||
<defs
|
||||
id="defs1" />
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 0,19 120,0"
|
||||
id="path18"
|
||||
sodipodi:nodetypes="cc" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 394 B |
|
After Width: | Height: | Size: 378 B |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 524 B |
|
After Width: | Height: | Size: 683 B |
@@ -0,0 +1,41 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="64mm"
|
||||
height="64mm"
|
||||
viewBox="0 0 64 64"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
inkscape:export-filename="stop.svg"
|
||||
inkscape:export-xdpi="12.7"
|
||||
inkscape:export-ydpi="12.7"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="mm" />
|
||||
<defs
|
||||
id="defs1" />
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<rect
|
||||
id="rect2"
|
||||
width="48"
|
||||
height="49.048836"
|
||||
x="8"
|
||||
y="8"
|
||||
style="fill:#808080;stroke-width:0.533226" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,48 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="64mm"
|
||||
height="64mm"
|
||||
viewBox="0 0 64 64"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
inkscape:export-filename="pause.svg"
|
||||
inkscape:export-xdpi="12.7"
|
||||
inkscape:export-ydpi="12.7"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="mm" />
|
||||
<defs
|
||||
id="defs1" />
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<rect
|
||||
id="rect2"
|
||||
width="48"
|
||||
height="49.048836"
|
||||
x="8"
|
||||
y="8"
|
||||
style="stroke-width:0.533226" />
|
||||
<rect
|
||||
id="rect2-5"
|
||||
width="16"
|
||||
height="48"
|
||||
x="40"
|
||||
y="8"
|
||||
style="stroke-width:0.304549" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 239 B |
@@ -0,0 +1,63 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="11"
|
||||
height="21"
|
||||
fill="none"
|
||||
version="1.1"
|
||||
id="svg4"
|
||||
sodipodi:docname="svg.svg"
|
||||
inkscape:export-filename="vol-select.svg"
|
||||
inkscape:export-xdpi="96"
|
||||
inkscape:export-ydpi="96"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs4" />
|
||||
<sodipodi:namedview
|
||||
id="namedview4"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="true"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
showgrid="false" />
|
||||
<rect
|
||||
style="fill:#000000;fill-opacity:1;stroke-width:1.07758"
|
||||
id="rect7"
|
||||
width="11"
|
||||
height="21"
|
||||
x="0"
|
||||
y="0" />
|
||||
<rect
|
||||
style="fill:#87888f;fill-opacity:1;stroke-width:3.23846"
|
||||
id="rect4"
|
||||
width="10"
|
||||
height="19"
|
||||
x="0"
|
||||
y="1" />
|
||||
<rect
|
||||
style="fill:#ffffff;fill-opacity:1;stroke-width:0.962516"
|
||||
id="rect6"
|
||||
width="9"
|
||||
height="19"
|
||||
x="0"
|
||||
y="0" />
|
||||
<rect
|
||||
style="fill:#c0c7c8;fill-opacity:1;stroke-width:0.997539"
|
||||
id="rect5"
|
||||
width="8"
|
||||
height="18"
|
||||
x="1"
|
||||
y="1" />
|
||||
<rect
|
||||
style="fill:#ffffff;fill-opacity:1;stroke-width:1.24518"
|
||||
id="rect8"
|
||||
width="1.3213186"
|
||||
height="1"
|
||||
x="8.6786814"
|
||||
y="0" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 393 B |
|
After Width: | Height: | Size: 458 B |
@@ -0,0 +1,38 @@
|
||||
// Thanks to https://codepen.io/rebelchris/pen/abZRpqM
|
||||
let timer = null;
|
||||
function startCountdown(dateString) {
|
||||
if (timer) clearInterval(timer);
|
||||
let cd = document.getElementById("countdown")
|
||||
if (cd) {
|
||||
cd.classList.remove("hide");
|
||||
if (dateString == "None") cd.classList.add("hide");
|
||||
}
|
||||
const end = new Date(dateString).getTime();
|
||||
const daysEl = document.getElementById('days');
|
||||
const hoursEl = document.getElementById('hours');
|
||||
const minutesEl = document.getElementById('minutes');
|
||||
const secondsEl = document.getElementById('seconds');
|
||||
const seconds = 1000;
|
||||
const minutes = seconds * 60;
|
||||
const hours = minutes * 60;
|
||||
const days = hours * 24;
|
||||
timer = setInterval(updateTime, seconds);
|
||||
function updateTime() {
|
||||
if (daysEl) {
|
||||
let now = new Date().getTime();
|
||||
const difference = end - now;
|
||||
|
||||
if (difference < 0) {
|
||||
if (timer) clearInterval(timer);
|
||||
cd.classList.add("hide");
|
||||
return;
|
||||
}
|
||||
|
||||
daysEl.innerText = Math.floor(difference / days);
|
||||
hoursEl.innerText = Math.floor( (difference % days) / hours );
|
||||
minutesEl.innerText = Math.floor( (difference % hours) / minutes );
|
||||
secondsEl.innerText = Math.floor( (difference % minutes) / seconds );
|
||||
}
|
||||
}
|
||||
updateTime();
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
|
||||
@@ -0,0 +1,549 @@
|
||||
body {
|
||||
background-color: #01817F;
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background-position: 50% 50%;
|
||||
}
|
||||
|
||||
#main-window {
|
||||
position: fixed;
|
||||
z-index:99;
|
||||
min-height: 520px;
|
||||
width:560px;
|
||||
overflow:hidden;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
justify-content: space-between;
|
||||
margin: 10px;
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
#main-window {
|
||||
margin: 0;
|
||||
transform: none !important;
|
||||
left: 0;
|
||||
width: calc(100% - 6px);
|
||||
}
|
||||
|
||||
#main-window > .window-body {
|
||||
min-height: calc(100vh - 42px);
|
||||
}
|
||||
}
|
||||
|
||||
input[type=submit] {
|
||||
filter: contrast(1);
|
||||
}
|
||||
|
||||
input#spotify-input {
|
||||
width: 100%;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
input#spotify-input + input[type=submit]{
|
||||
background-image: url(img/sound.png);
|
||||
background-repeat:no-repeat;
|
||||
padding: 4px 8px 4px 24px;
|
||||
height: 16px;
|
||||
min-width: 0;
|
||||
background-position: 4px 50%;
|
||||
font-weight: bold;
|
||||
|
||||
}
|
||||
|
||||
.tree-view.overview {
|
||||
height: 384px;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
#bsod, .hide {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.offscreen {
|
||||
clip: rect(0 0 0 0);
|
||||
clip-path: inset(50%);
|
||||
height: 1px;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
white-space: nowrap;
|
||||
width: 1px;
|
||||
}
|
||||
|
||||
.window.popup {
|
||||
position: fixed;
|
||||
z-index: 100;
|
||||
top: calc(50% - 75px);
|
||||
left: calc(50% - 150px);
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.window.popup .title-bar-text {
|
||||
height: 13px;
|
||||
overflow:hidden;
|
||||
text-overflow:ellipsis;
|
||||
white-space:nowrap;
|
||||
}
|
||||
|
||||
.window.popup.login {
|
||||
width: 380px;
|
||||
left: calc(50% - 190px);
|
||||
}
|
||||
|
||||
.window.popup .window-body {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
}
|
||||
.window.popup .window-body .text {
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.window.popup .window-body .icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
padding: 16px;
|
||||
background-position: center center;
|
||||
background-repeat: no-repeat;
|
||||
background-image: url('img/check.png');
|
||||
}
|
||||
|
||||
.window.popup .window-body .icon.login {
|
||||
background-image: url('img/login.png');
|
||||
background-attachment:unset;
|
||||
}
|
||||
|
||||
.window.popup .window-body .icon.error {
|
||||
background-image: url('img/error.png');
|
||||
}
|
||||
|
||||
.window.popup .window-body .icon.warning {
|
||||
background-image: url('img/warning.png');
|
||||
}
|
||||
|
||||
|
||||
.window {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 0;
|
||||
max-width:calc(100% - 5px);
|
||||
}
|
||||
|
||||
.window.hide {
|
||||
display:none;
|
||||
}
|
||||
|
||||
.window form {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.window-body {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
}
|
||||
|
||||
/* tab menu fix */
|
||||
menu[role="tablist"] > li > a {
|
||||
padding: 6px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
iframe {
|
||||
opacity: 0;
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
iframe.hide {
|
||||
left: 100%;
|
||||
|
||||
}
|
||||
|
||||
/* Media player */
|
||||
|
||||
#album {
|
||||
background: black;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#album h1, #album h2 {
|
||||
color: white;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
max-width: 100%;
|
||||
white-space: nowrap;
|
||||
text-overflow:ellipsis;
|
||||
}
|
||||
|
||||
#album h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
#album h2 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: normal;
|
||||
}
|
||||
#album img {
|
||||
width: 256px;
|
||||
}
|
||||
|
||||
#seek {
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
#seek::-moz-range-thumb {
|
||||
background:transparent;
|
||||
background-image: url(img/seek.svg);
|
||||
width: 21px;
|
||||
transform: none;
|
||||
}
|
||||
input[type="range"]#seek::-webkit-slider-thumb {
|
||||
background:transparent;
|
||||
background-image: url(img/seek.svg);
|
||||
width: 21px;
|
||||
transform: translateX(-5px) translateY(-4px);
|
||||
}
|
||||
|
||||
input[type="range"]#seek::-webkit-slider-runnable-track{
|
||||
height: 11px;
|
||||
background: transparent;
|
||||
box-shadow: 1px 0 0 white, 1px 1px 0 white, 0 1px 0 white, -1px 0 0 #461c1c, -1px -1px 0 #333, -1px 1px black, 1px -1px #a460a4
|
||||
}
|
||||
|
||||
#seek::-moz-range-track {
|
||||
height: 11px;
|
||||
background: transparent;
|
||||
box-shadow: 1px 0 0 white, 1px 1px 0 white, 0 1px 0 white, -1px 0 0 #461c1c, -1px -1px 0 #333, -1px 1px black, 1px -1px #a460a4
|
||||
}
|
||||
|
||||
|
||||
#media-control {
|
||||
display:flex;
|
||||
}
|
||||
|
||||
|
||||
#media-control button {
|
||||
min-width: 0;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background-size: 50%;
|
||||
background-repeat: no-repeat;
|
||||
background-position: 50% 50%;
|
||||
box-shadow:none;
|
||||
}
|
||||
|
||||
#media-control:not(.waiting) button:not(.dummy):hover {
|
||||
box-shadow: inset -1px -1px #0a0a0a,inset 1px 1px #fff,inset -2px -2px grey,inset 2px 2px #dfdfdf
|
||||
}
|
||||
|
||||
#media-control .spacer {
|
||||
display: inline-block;
|
||||
height: 24px;
|
||||
width: 1px;
|
||||
margin: 4px 5px;
|
||||
background: gray;
|
||||
}
|
||||
|
||||
#media-control button#play {
|
||||
background-image:url(img/play.svg);
|
||||
}
|
||||
|
||||
#media-control button#pause {
|
||||
background-image:url(img/pause.svg);
|
||||
}
|
||||
|
||||
#media-control button#stop {
|
||||
background-image:url(img/stop.svg);
|
||||
}
|
||||
|
||||
#media-control.waiting button#play {
|
||||
background-image:url(img/play-inactive.svg);
|
||||
}
|
||||
#media-control.waiting button#pause {
|
||||
background-image:url(img/pause-inactive.svg);
|
||||
}
|
||||
#media-control.waiting button#stop {
|
||||
background-image:url(img/stop-inactive.svg);
|
||||
}
|
||||
|
||||
#media-control button#prev {
|
||||
background-image:url(img/prev.svg);
|
||||
}
|
||||
#media-control button#back {
|
||||
background-image:url(img/back.svg);
|
||||
}
|
||||
#media-control button#forward {
|
||||
background-image:url(img/forward.svg);
|
||||
}
|
||||
#media-control button#next {
|
||||
background-image:url(img/next.svg);
|
||||
}
|
||||
#media-control button#eject {
|
||||
background-image:url(img/eject.svg);
|
||||
}
|
||||
|
||||
#media-control #volume {
|
||||
background-image:url(img/volume.png);
|
||||
background-repeat:no-repeat;
|
||||
padding-left: 24px;
|
||||
background-position: 0 50%;
|
||||
max-width: 120px;
|
||||
}
|
||||
|
||||
#media-control #volume.playing {
|
||||
background-image:url(img/pawel.png);
|
||||
}
|
||||
#media-control #volume.muted {
|
||||
background-image:url(img/muted.png);
|
||||
}
|
||||
|
||||
#volume::-moz-range-thumb {
|
||||
background: transparent;
|
||||
background-image:url(img/vol-select.svg);
|
||||
}
|
||||
|
||||
|
||||
input[type="range"]#volume::-webkit-slider-thumb {
|
||||
background: transparent;
|
||||
-webkit-appearance: none;
|
||||
background-image:url(img/vol-select.svg);
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
#volume::-moz-range-track {
|
||||
background: none;
|
||||
background-image: url(img/slider.svg);
|
||||
background-position: 0 50%;
|
||||
background-size: contain;
|
||||
background-repeat:no-repeat;
|
||||
box-shadow: none;
|
||||
border: none;
|
||||
border-bottom: 1px solid white;
|
||||
border-right: 1px solid white;
|
||||
height: 19px;
|
||||
}
|
||||
|
||||
#volume::-webkit-slider-runnable-track {
|
||||
background: none;
|
||||
background-image: url(img/slider.svg);
|
||||
background-position: 0 50%;
|
||||
background-size: contain;
|
||||
background-repeat:no-repeat;
|
||||
box-shadow: none;
|
||||
border: none;
|
||||
border-bottom: 1px solid white;
|
||||
border-right: 1px solid white;
|
||||
height: 19px;
|
||||
}
|
||||
|
||||
/* Clippy */
|
||||
#clippy {
|
||||
position: fixed;
|
||||
z-index:99;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
align-items: flex-end;
|
||||
width: 256px;
|
||||
left: 580px;
|
||||
top: 0;
|
||||
height:580px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
#clippy .body {
|
||||
background: #ffffe0;
|
||||
color: black;
|
||||
padding: 10px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid black;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#clippy .body .content {
|
||||
max-height: 375px;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
@media (max-width: 500px) {
|
||||
#clippy {
|
||||
width: calc(100vw - 10px);
|
||||
top: unset;
|
||||
left: unset;
|
||||
transform: none !important;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
height: unset;
|
||||
}
|
||||
#clippy .body.hide {
|
||||
display: none;
|
||||
}
|
||||
.body.hide ~ img {
|
||||
display:none;
|
||||
}
|
||||
}
|
||||
|
||||
#clippy .body.hide {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#clippy .body::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: -13px;
|
||||
right: 128px;
|
||||
border-width: 13px 17px 0;
|
||||
border-style: solid;
|
||||
border-color: lightyellow transparent;
|
||||
display: block;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
#clippy .body::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: -14px;
|
||||
right: 127px;
|
||||
border-width: 14px 19px 0;
|
||||
border-style: solid;
|
||||
border-color: black transparent;
|
||||
display: block;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
#clippy .body button {
|
||||
background: lightyellow;
|
||||
border: 1px #999 solid;
|
||||
border-radius: 2px;
|
||||
box-shadow: none;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
#clippy img {
|
||||
width: 128px;
|
||||
}
|
||||
|
||||
#vote-stars {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#vote-stars .star-wrapper {
|
||||
height: 31px;
|
||||
width:160px;
|
||||
}
|
||||
|
||||
#vote-stars a {
|
||||
cursor:pointer;
|
||||
display: inline-block;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
margin-right: -2px;
|
||||
background-image: url(img/star-inactive.png);
|
||||
}
|
||||
#vote-stars .star-wrapper:hover a {
|
||||
background-image: url(img/star.png);
|
||||
}
|
||||
|
||||
#vote-stars a:hover {
|
||||
background-image: url(img/star.png);
|
||||
}
|
||||
|
||||
#vote-stars a:hover ~ a {
|
||||
background-image: url(img/star-inactive.png) !important;
|
||||
}
|
||||
|
||||
#average-stars {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
#average-stars .stars {
|
||||
background-image: url(img/star-inactive.png);
|
||||
background-repeat: repeat-x;
|
||||
height: 32px;
|
||||
width: 160px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#average-stars .stars-overlay {
|
||||
background-image: url(img/star.png);
|
||||
background-repeat: repeat-x;
|
||||
height: 32px;
|
||||
width: 160px;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.undo-wrapper {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
justify-content: center;
|
||||
flex-grow: 1;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.undo-wrapper p {
|
||||
flex-grow: 1;
|
||||
padding: 0 5px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.undo {
|
||||
background-image: url(img/undo.png);
|
||||
background-repeat: no-repeat;
|
||||
background-position: 50% 0;
|
||||
padding: 32px 0 4px 0;
|
||||
height: 50px;
|
||||
min-width: 40px;
|
||||
}
|
||||
|
||||
.skip {
|
||||
background-image: url(img/download.png);
|
||||
background-repeat: no-repeat;
|
||||
background-position: 50% 20%;
|
||||
padding: 32px 0 4px 0;
|
||||
height: 50px;
|
||||
min-width: 40px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.progress-indicator {
|
||||
height: 26px;
|
||||
padding: 4px 3px;
|
||||
width: 340px;
|
||||
}
|
||||
|
||||
.field-row.settings label {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.field-row.settings {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.settings .icon {
|
||||
height: 40px;
|
||||
background-repeat: no-repeat;
|
||||
background-position: 50% 50%;
|
||||
width: 40px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.settings .admin-panel {
|
||||
background-image: url(img/gears.png);
|
||||
}
|
||||
|
||||
.settings .spotify {
|
||||
background-image: url(img/spotify.png);
|
||||
background-size: 32px;
|
||||
}
|
||||
|
||||
.settings #spotify-cancel {
|
||||
background-image: url(img/spotify-cancel.png);
|
||||
background-size: 16px;
|
||||
background-position: 50% 50%;
|
||||
background-repeat: no-repeat;
|
||||
min-width: 0;
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
from celery import shared_task
|
||||
from .models import Track
|
||||
from django.core.files import File
|
||||
from django.core.files.uploadedfile import InMemoryUploadedFile
|
||||
from django.core.files.base import ContentFile
|
||||
|
||||
import os
|
||||
import dotenv
|
||||
|
||||
import json
|
||||
from requests import get, post
|
||||
from requests.exceptions import ConnectionError
|
||||
from PIL import Image
|
||||
from io import BytesIO
|
||||
|
||||
dotenv.load_dotenv()
|
||||
|
||||
@shared_task
|
||||
def get_banter(id):
|
||||
"""
|
||||
Requests an AI model for some banter.
|
||||
Currently expects an ollama instance to be running,
|
||||
but might be possible to use OpenAI API in future.
|
||||
"""
|
||||
track = Track.objects.get(pk=id)
|
||||
track.banter = ""
|
||||
|
||||
try:
|
||||
get(os.getenv('AI_ENDPOINT'))
|
||||
except ConnectionError:
|
||||
track.banter = "Error"
|
||||
track.banter_done = True
|
||||
track.save()
|
||||
return f"{track}: AI endpoint not available."
|
||||
|
||||
try:
|
||||
r = post(
|
||||
os.getenv('AI_ENDPOINT'),
|
||||
json={
|
||||
'model': os.getenv('AI_MODEL'),
|
||||
'prompt': str(track)
|
||||
},
|
||||
stream=True
|
||||
)
|
||||
r.raise_for_status()
|
||||
|
||||
for line in r.iter_lines():
|
||||
body = json.loads(line)
|
||||
if 'error' in body:
|
||||
raise Exception(body['error'])
|
||||
track.banter += body.get('response', '')
|
||||
|
||||
if body.get('done', False):
|
||||
track.banter_done = True
|
||||
track.save()
|
||||
return f"{track}: {body['context']}"
|
||||
except Exception as e:
|
||||
track.banter_done = True
|
||||
track.save()
|
||||
return f"{track}: {e}"
|
||||
|
||||
@shared_task
|
||||
def get_and_dither_image(url, instance, attribute):
|
||||
"""Expects a url, and a Django model cls with an ImageField attribute"""
|
||||
print(f"Parsing {url} for {instance} with attribute {attribute}")
|
||||
r = get(url)
|
||||
i = Image.open(BytesIO(r.content))
|
||||
i = i.resize(size=(256, 256)).convert("P").quantize(colors=32)
|
||||
buffer = BytesIO()
|
||||
i.save(fp=buffer, format='PNG')
|
||||
file = InMemoryUploadedFile(ContentFile(buffer.getvalue()), None, instance.name + ".png", "image/png", buffer.tell, "utf-8")
|
||||
instance.image = file
|
||||
instance.save()
|
||||
@@ -0,0 +1,103 @@
|
||||
{% load static %}
|
||||
<html>
|
||||
<head>
|
||||
{% include "head.html" %}
|
||||
</head>
|
||||
<body hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'>
|
||||
<div class="window" id="main-window">
|
||||
<div class="title-bar main-title-bar">
|
||||
<div class="title-bar-text">
|
||||
{% block window-title %}{% endblock %}
|
||||
</div>
|
||||
<div class="title-bar-controls">
|
||||
<button aria-label="Minimize" onclick="resetWindow()"></button>
|
||||
<button aria-label="Restore" onclick="resetWindow()"></button>
|
||||
<form action="{% url 'oidc_logout' %}" method="post">
|
||||
{% csrf_token %}
|
||||
<button aria-label="Close" onclick="hideWindow()"></button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="window-body">
|
||||
<menu role="tablist" hx-boost="true" class="multirows">
|
||||
<li role="tab" {% if request.path == '/vote/' %}aria-selected="true" {% endif %}>
|
||||
<a href="{% url 'vote' %}">Stemmen</a>
|
||||
</li>
|
||||
<li role="tab" {% if request.path == '/track/' %}aria-selected="true" {% endif %}>
|
||||
<a href="{% url 'nominate' %}">Nomineren</a>
|
||||
</li>
|
||||
<li role="tab" {% if request.path == '/overview/' %}aria-selected="true" {% endif %}>
|
||||
<a href="{% url 'overview' %}">Overzicht</a>
|
||||
</li>
|
||||
<li role="tab" {% if request.path == '/settings/' %}aria-selected="true" {% endif %}>
|
||||
<a href="{% url 'settings' %}">Instellingen</a>
|
||||
</li>
|
||||
</menu>
|
||||
{% block multi-row %}{% endblock %}
|
||||
<div class="window" role="tabpanel">
|
||||
<div class="window-body">
|
||||
{% if message_type %}
|
||||
<script>
|
||||
function hidePopup() {
|
||||
document.getElementById("info-window").classList.add("hide");
|
||||
}
|
||||
</script>
|
||||
<div class="window popup" id="info-window">
|
||||
<div class="title-bar">
|
||||
<div class="title-bar-text">
|
||||
{% if message_title %}{{message_title}}{% else %}{{message_type | title}}{% endif %}
|
||||
</div>
|
||||
<div class="title-bar-controls">
|
||||
<button aria-label="Close" onclick="hidePopup()"></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="window-body">
|
||||
<div class="icon {{message_type}}"></div>
|
||||
<div class="text">
|
||||
<div>
|
||||
{{message}}
|
||||
</div>
|
||||
<section class="field-row" style="justify-content: flex-end">
|
||||
<button onclick="hidePopup()">Ok</button>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% block main-content %}{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% block status-bar %}{% endblock%}
|
||||
</div>
|
||||
{% block clippy %}{% endblock %}
|
||||
<script>
|
||||
position.get_pos(); // Gets position from localStorage
|
||||
clippy = document.getElementById("clippy")
|
||||
window.addEventListener('load', function () {
|
||||
interact('.main-title-bar').draggable({
|
||||
listeners: {
|
||||
move (event) {
|
||||
position.x += event.dx
|
||||
position.y += event.dy
|
||||
event.target.parentElement.style.transform = `translate(${position.x}px, ${position.y}px)`
|
||||
if (clippy) document.getElementById("clippy").style.transform = `translate(${position.x}px, ${position.y}px)`
|
||||
},
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
function hideWindow() {
|
||||
document.getElementById("main-window").classList.add("hide");
|
||||
}
|
||||
|
||||
function resetWindow() {
|
||||
position.reset()
|
||||
document.getElementById("main-window").style.transform = '';
|
||||
if (clippy) document.getElementById("clippy").style.transform = '';
|
||||
}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,18 @@
|
||||
{% load static %}
|
||||
{% if body %}
|
||||
<script>
|
||||
function closeclippy(e) {
|
||||
console.log(e)
|
||||
e.parentElement.classList.add("hide")
|
||||
}
|
||||
</script>
|
||||
<div id="clippy" class="hide">
|
||||
<div class="body">
|
||||
<div class="content">{{ body }}</div>
|
||||
<br /><button onclick="closeclippy(this)">Ok</button>
|
||||
</div>
|
||||
{% if img %}
|
||||
<img src="{% static img %}" />
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -0,0 +1,66 @@
|
||||
{% load static %}
|
||||
<html>
|
||||
<head>
|
||||
{% include "head.html" %}
|
||||
</head>
|
||||
<body hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'>
|
||||
<script>
|
||||
a = new Audio("{% static 'startup.wav' %}");
|
||||
a.volume = 0.5;
|
||||
a.play();
|
||||
</script>
|
||||
<div class="window popup login" id="info-window">
|
||||
<div class="title-bar">
|
||||
<div class="title-bar-text">Welkom bij Muzak</div>
|
||||
<div class="title-bar-controls">
|
||||
<button aria-label="Close"></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="window-body">
|
||||
<div class="icon login"></div>
|
||||
<div class="text">
|
||||
<div>
|
||||
<p>Gebruik onderstaande knop om in te loggen.</p>
|
||||
<p>Als je geen inloggegevens hebt, meld je dan bij de beheerder van deze site.</p>
|
||||
<div class="field-row">
|
||||
<form action="{% url 'oidc_authentication_init' %}" method="get">
|
||||
<input type="submit" value="Inloggen" class="default">
|
||||
</form>
|
||||
<button onclick="bsod()">Annuleren</button>
|
||||
<script>
|
||||
function bsod() {
|
||||
var css = document.createElement("link")
|
||||
css.setAttribute("rel", "stylesheet")
|
||||
css.setAttribute("type", "text/css")
|
||||
css.setAttribute("href", "{% static 'bsod.css' %}")
|
||||
document.getElementsByTagName("head")[0].appendChild(css)
|
||||
}
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="bsod">
|
||||
<h1>Lekkerste recept voor suikerbrood</h1>
|
||||
<div>* 500 gram bloem</div>
|
||||
<div>* 1 zakje gist</div>
|
||||
<div>* 1 theelepel kaneeel</div>
|
||||
<div>* 3 eetlepels gembersiroop</div>
|
||||
<div>* 200 ml melk</div>
|
||||
<div>* 250 gram kandijsuiker</div>
|
||||
<div>
|
||||
Neem je bloem en zakje gist en meng dit samen met je kaneel in een kom.
|
||||
Maak een kuiltje in het midden en schenk er gembersiroop in en lauwwarme, niet te hete, melk.
|
||||
Meng dit tot deeg, voeg een snufje zout toe en laat dat 15 minuten staan.
|
||||
Kneed het deeg tot een bal en laat het een uur rijzen onder een vochtige theedoek.
|
||||
Maak van het gerezen deeg een dikke lap en verdeel er je suiker over.
|
||||
Rol het geheel weer op en doe het over de lengte in een ingeboterde en ingesuikerde cakevorm.
|
||||
Laat het nog een uurtje rijzen.
|
||||
Bak het in ongeveer 30 minuten in een voorverwarmde oven op 200 graden.
|
||||
Voilà. Suikerbrood. Eet smakelijk.
|
||||
</div>
|
||||
<div class="continue">Druk op F5 om verder te gaan..</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,112 @@
|
||||
{% load static %}
|
||||
<div id="embed"></div>
|
||||
<div id="album">
|
||||
{% if track.album.image %}
|
||||
<img src="{{track.album.image.url}}" />
|
||||
{% elif track.album.image_url %}
|
||||
<img src="{{track.album.image_url}}" />
|
||||
{% else %}
|
||||
<img src="https://kagi.com/proxy/windows-95-logo-png-1.png?c=xjdP8sWN58YWflc6NcGN5ZvzREJyOwtrou-TSsaXxcx37J0VVJbSeycOcAptMJEUpzzvqmbKQ3JRuSmPN8HCK43OxjkRe_cQvncxVzlCblc%3D" />
|
||||
{% endif %}
|
||||
<h1 title="{{track.name}}">{{track.name}}</h1>
|
||||
<h2 title="{{track.artist}}">{{track.artist}}</h2>
|
||||
</div>
|
||||
<input id="seek" type="range" min="0" step="0.1" max="100" value="0" />
|
||||
<div id="media-control" class="waiting">
|
||||
<button id="play"></button>
|
||||
<button id="pause"></button>
|
||||
<button id="stop"></button>
|
||||
<div class="spacer"></div>
|
||||
<button id="prev" class="dummy"></button>
|
||||
<button id="back" class="dummy"></button>
|
||||
<button id="forward" class="dummy"></button>
|
||||
<button id="next" class="dummy"></button>
|
||||
<div class="spacer"></div>
|
||||
<button id="eject" class="dummy"></button>
|
||||
<div class="spacer"></div>
|
||||
<input id="volume" type="range" min="1" max="11" value="5" />
|
||||
</div>
|
||||
<audio id="kankerherrie" loop src="{% static 'kankerherrie.mp3' %}"></audio>
|
||||
<script src="https://open.spotify.com/embed/iframe-api/v1"></script>
|
||||
<script>
|
||||
window.onSpotifyIframeApiReady = doSpotify;
|
||||
doSpotify();
|
||||
function doSpotify (IFrameAPI) {
|
||||
if (IFrameAPI) {
|
||||
ifa = IFrameAPI;
|
||||
} else if (!ifa) return;
|
||||
let element = document.getElementById('embed');
|
||||
let playing = false;
|
||||
let playingPosition = 0;
|
||||
let duration = {{track.duration_ms}};
|
||||
let options = {
|
||||
uri: 'spotify:track:{{track.id}}',
|
||||
height: 152
|
||||
};
|
||||
const callback = (EmbedController) => {
|
||||
document.getElementById("play").addEventListener('click', () => {
|
||||
EmbedController.play();
|
||||
playing = true;
|
||||
if (playingPosition) {
|
||||
setTimeout(() => {
|
||||
EmbedController.seek(parseInt(playingPosition/1000));
|
||||
playingPosition = 0
|
||||
}, 100)
|
||||
}
|
||||
});
|
||||
function pause() {
|
||||
if (playing) {
|
||||
EmbedController.togglePlay();
|
||||
playing = false;
|
||||
}
|
||||
}
|
||||
document.getElementById("pause").addEventListener('click', pause);
|
||||
document.getElementById("stop").addEventListener('click', () => {
|
||||
setTimeout(() => {
|
||||
playingPosition = 0;
|
||||
EmbedController.seek(0);
|
||||
document.getElementById("seek").value = 0;
|
||||
}, 500);
|
||||
pause();
|
||||
});
|
||||
EmbedController.addListener('ready', e => {
|
||||
document.getElementById('media-control').classList.remove('waiting')
|
||||
document.getElementById("seek").addEventListener('change', (e) => {
|
||||
let percentile = parseFloat(e.target.value)/100;
|
||||
EmbedController.seek(duration/1000*percentile);
|
||||
});
|
||||
});
|
||||
EmbedController.addListener('playback_update', e => {
|
||||
duration = e.data.duration;
|
||||
if (playingPosition == 0) document.getElementById('seek').value = `${parseInt(e.data.position) / parseInt(e.data.duration) * 100}`;
|
||||
if (e.data.isPaused) {
|
||||
playingPosition = e.data.position;
|
||||
}
|
||||
else if (playingPosition != 0 && e.data.position != 0) {
|
||||
EmbedController.seek(parseInt(playingPosition/1000));
|
||||
playingPosition = 0;
|
||||
}
|
||||
})
|
||||
};
|
||||
ifa.createController(element, options, callback);
|
||||
iframe = document.getElementsByTagName("iframe")[0]
|
||||
iframe.addEventListener('load', (e) => {
|
||||
console.log(e);
|
||||
iframe.classList.add('hide');
|
||||
});
|
||||
};
|
||||
document.getElementById("volume").addEventListener('input', (event) => {
|
||||
let kankerherrie = document.getElementById("kankerherrie")
|
||||
console.log(event);
|
||||
let volume = event.target.value / 11;
|
||||
event.target.classList.add("playing");
|
||||
event.target.classList.remove("muted");
|
||||
kankerherrie.play();
|
||||
if (volume <= 1/11) {
|
||||
kankerherrie.pause();
|
||||
event.target.classList.remove("playing");
|
||||
event.target.classList.add("muted");
|
||||
}
|
||||
kankerherrie.volume = volume;
|
||||
})
|
||||
</script>
|
||||