Reviewed-on: #81
This commit is contained in:
commit
46da13a0df
|
@ -1,5 +1,3 @@
|
|||
from copy import copy
|
||||
|
||||
from django.contrib import admin
|
||||
from django.forms import ModelForm
|
||||
from django.utils.translation import gettext as _
|
||||
|
@ -60,11 +58,21 @@ class EpisodeAdminForm(ModelForm):
|
|||
class EpisodeAdmin(SortableAdminBase, PageAdmin):
|
||||
form = EpisodeAdminForm
|
||||
list_display = PageAdmin.list_display
|
||||
list_filter = tuple(f for f in PageAdmin.list_filter if f != 'pub_date') + \
|
||||
('diffusion__start', 'pub_date')
|
||||
list_filter = tuple(f for f in PageAdmin.list_filter
|
||||
if f != 'pub_date') + ('diffusion__start', 'pub_date')
|
||||
search_fields = PageAdmin.search_fields + ('parent__title',)
|
||||
# readonly_fields = ('parent',)
|
||||
|
||||
inlines = [TrackInline, SoundInline, DiffusionInline]
|
||||
|
||||
def add_view(self, request, object_id, form_url='', context=None):
|
||||
context = context or {}
|
||||
context['init_app'] = True
|
||||
context['init_el'] = '#inline-tracks'
|
||||
return super().change_view(request, object_id, form_url, context)
|
||||
|
||||
def change_view(self, request, object_id, form_url='', context=None):
|
||||
context = context or {}
|
||||
context['init_app'] = True
|
||||
context['init_el'] = '#inline-tracks'
|
||||
return super().change_view(request, object_id, form_url, context)
|
||||
|
|
|
@ -9,14 +9,15 @@ from adminsortable2.admin import SortableAdminBase, SortableInlineAdminMixin
|
|||
from ..models import Sound, Track
|
||||
|
||||
|
||||
class TrackInline(SortableInlineAdminMixin, admin.TabularInline):
|
||||
class TrackInline(admin.TabularInline):
|
||||
template = 'admin/aircox/playlist_inline.html'
|
||||
model = Track
|
||||
extra = 0
|
||||
fields = ('position', 'artist', 'title', 'info', 'tags')
|
||||
fields = ('position', 'artist', 'title', 'tags', 'album', 'year', 'info')
|
||||
|
||||
list_display = ['artist', 'album', 'title', 'tags', 'related']
|
||||
list_filter = ['artist', 'album', 'title', 'tags']
|
||||
|
||||
list_display = ['artist', 'title', 'tags', 'related']
|
||||
list_filter = ['artist', 'title', 'tags']
|
||||
|
||||
class SoundTrackInline(TrackInline):
|
||||
fields = TrackInline.fields + ('timestamp',)
|
||||
|
@ -24,14 +25,15 @@ class SoundTrackInline(TrackInline):
|
|||
|
||||
class SoundInline(admin.TabularInline):
|
||||
model = Sound
|
||||
fields = ['type', 'name', 'audio', 'duration', 'is_good_quality', 'is_public',
|
||||
'is_downloadable']
|
||||
fields = ['type', 'name', 'audio', 'duration', 'is_good_quality',
|
||||
'is_public', 'is_downloadable']
|
||||
readonly_fields = ['type', 'audio', 'duration', 'is_good_quality']
|
||||
extra = 0
|
||||
max_num = 0
|
||||
|
||||
def audio(self, obj):
|
||||
return mark_safe('<audio src="{}" controls></audio>'.format(obj.file.url))
|
||||
return mark_safe('<audio src="{}" controls></audio>'
|
||||
.format(obj.file.url))
|
||||
audio.short_description = _('Audio')
|
||||
|
||||
def get_queryset(self, request):
|
||||
|
@ -63,23 +65,40 @@ class SoundAdmin(SortableAdminBase, admin.ModelAdmin):
|
|||
related.short_description = _('Program / Episode')
|
||||
|
||||
def audio(self, obj):
|
||||
return mark_safe('<audio src="{}" controls></audio>'.format(obj.file.url)) \
|
||||
return mark_safe('<audio src="{}" controls></audio>'
|
||||
.format(obj.file.url)) \
|
||||
if obj.type != Sound.TYPE_REMOVED else ''
|
||||
audio.short_description = _('Audio')
|
||||
|
||||
def add_view(self, request, object_id, form_url='', context=None):
|
||||
context = context or {}
|
||||
context['init_app'] = True
|
||||
context['init_el'] = '#inline-tracks'
|
||||
context['track_timestamp'] = True
|
||||
return super().change_view(request, object_id, form_url, context)
|
||||
|
||||
def change_view(self, request, object_id, form_url='', context=None):
|
||||
context = context or {}
|
||||
context['init_app'] = True
|
||||
context['init_el'] = '#inline-tracks'
|
||||
context['track_timestamp'] = True
|
||||
return super().change_view(request, object_id, form_url, context)
|
||||
|
||||
|
||||
@admin.register(Track)
|
||||
class TrackAdmin(admin.ModelAdmin):
|
||||
def tag_list(self, obj):
|
||||
return u", ".join(o.name for o in obj.tags.all())
|
||||
|
||||
list_display = ['pk', 'artist', 'title', 'tag_list', 'episode', 'sound', 'ts']
|
||||
list_display = ['pk', 'artist', 'title', 'tag_list', 'episode',
|
||||
'sound', 'ts']
|
||||
list_editable = ['artist', 'title']
|
||||
list_filter = ['artist', 'title', 'tags']
|
||||
|
||||
search_fields = ['artist', 'title']
|
||||
fieldsets = [
|
||||
(_('Playlist'), {'fields': ['episode', 'sound', 'position', 'timestamp']}),
|
||||
(_('Playlist'), {'fields': ['episode', 'sound', 'position',
|
||||
'timestamp']}),
|
||||
(_('Info'), {'fields': ['artist', 'title', 'info', 'tags']}),
|
||||
]
|
||||
|
||||
|
@ -92,8 +111,6 @@ class TrackAdmin(admin.ModelAdmin):
|
|||
h = math.floor(ts / 3600)
|
||||
m = math.floor((ts - h) / 60)
|
||||
s = ts-h*3600-m*60
|
||||
return '{:0>2}:{:0>2}:{:0>2}'.format(h,m,s)
|
||||
return '{:0>2}:{:0>2}:{:0>2}'.format(h, m, s)
|
||||
|
||||
ts.short_description = _('timestamp')
|
||||
|
||||
|
||||
|
|
Binary file not shown.
|
@ -8,7 +8,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: Aircox 0.1\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2022-10-08 11:44+0000\n"
|
||||
"POT-Creation-Date: 2022-12-12 10:15+0000\n"
|
||||
"PO-Revision-Date: 2016-10-10 16:00+02\n"
|
||||
"Last-Translator: Aarys\n"
|
||||
"Language-Team: Aircox's translators team\n"
|
||||
|
@ -18,13 +18,13 @@ msgstr ""
|
|||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
||||
|
||||
#: aircox/admin/episode.py:29 aircox/models/episode.py:183
|
||||
#: aircox/admin/episode.py:27 aircox/models/episode.py:183
|
||||
#: aircox/models/log.py:82
|
||||
msgid "start"
|
||||
msgstr "début"
|
||||
|
||||
#: aircox/admin/episode.py:33 aircox/models/episode.py:184
|
||||
#: aircox/models/program.py:473
|
||||
#: aircox/admin/episode.py:31 aircox/models/episode.py:184
|
||||
#: aircox/models/program.py:469
|
||||
msgid "end"
|
||||
msgstr "fin"
|
||||
|
||||
|
@ -52,7 +52,7 @@ msgstr "Tout"
|
|||
msgid "Publication Settings"
|
||||
msgstr "Paramètre de la publication"
|
||||
|
||||
#: aircox/admin/program.py:40 aircox/models/program.py:286
|
||||
#: aircox/admin/program.py:40 aircox/models/program.py:282
|
||||
msgid "Schedule"
|
||||
msgstr "Horaire"
|
||||
|
||||
|
@ -60,7 +60,7 @@ msgstr "Horaire"
|
|||
msgid "Program Settings"
|
||||
msgstr "Paramètres de l'émission"
|
||||
|
||||
#: aircox/admin/program.py:64 aircox/models/program.py:122
|
||||
#: aircox/admin/program.py:64 aircox/models/program.py:118
|
||||
msgid "Program"
|
||||
msgstr "Émission"
|
||||
|
||||
|
@ -68,23 +68,25 @@ msgstr "Émission"
|
|||
msgid "Day"
|
||||
msgstr "Jour"
|
||||
|
||||
#: aircox/admin/sound.py:35 aircox/admin/sound.py:67
|
||||
#: aircox/admin/sound.py:37 aircox/admin/sound.py:71
|
||||
msgid "Audio"
|
||||
msgstr "Audio"
|
||||
|
||||
#: aircox/admin/sound.py:63
|
||||
#: aircox/admin/sound.py:65
|
||||
msgid "Program / Episode"
|
||||
msgstr "Émission / Épisode"
|
||||
|
||||
#: aircox/admin/sound.py:81 aircox/templates/aircox/episode_detail.html:36
|
||||
#: aircox/admin/sound.py:100
|
||||
#: aircox/templates/admin/aircox/playlist_inline.html:20
|
||||
#: aircox/templates/aircox/episode_detail.html:36
|
||||
msgid "Playlist"
|
||||
msgstr "Playlist"
|
||||
|
||||
#: aircox/admin/sound.py:82
|
||||
#: aircox/admin/sound.py:102
|
||||
msgid "Info"
|
||||
msgstr "Info"
|
||||
|
||||
#: aircox/admin/sound.py:96 aircox/models/sound.py:243
|
||||
#: aircox/admin/sound.py:116 aircox/models/sound.py:240
|
||||
msgid "timestamp"
|
||||
msgstr "temps"
|
||||
|
||||
|
@ -92,8 +94,8 @@ msgstr "temps"
|
|||
msgid "Statistics"
|
||||
msgstr "Statistiques"
|
||||
|
||||
#: aircox/filters.py:8 aircox/templates/admin/base.html:81
|
||||
#: aircox/templates/admin/base.html:95 aircox/templates/admin/base.html:109
|
||||
#: aircox/filters.py:8 aircox/templates/admin/base.html:84
|
||||
#: aircox/templates/admin/base.html:98 aircox/templates/admin/base.html:112
|
||||
#: aircox/templates/aircox/base.html:78
|
||||
#: aircox/templates/aircox/page_list.html:15
|
||||
msgid "Search"
|
||||
|
@ -103,24 +105,24 @@ msgstr "Chercher"
|
|||
msgid "Podcast"
|
||||
msgstr "Podcast"
|
||||
|
||||
#: aircox/management/commands/sounds_monitor.py:204
|
||||
#: aircox/management/commands/sounds_monitor.py:205
|
||||
msgid "unknown"
|
||||
msgstr "inconnu"
|
||||
|
||||
#: aircox/models/article.py:14
|
||||
#: aircox/models/article.py:16
|
||||
msgid "Article"
|
||||
msgstr "Article"
|
||||
|
||||
#: aircox/models/article.py:15 aircox/templates/admin/base.html:92
|
||||
#: aircox/models/article.py:17 aircox/templates/admin/base.html:95
|
||||
#: aircox/templates/aircox/program_detail.html:19
|
||||
msgid "Articles"
|
||||
msgstr "Articles"
|
||||
|
||||
#: aircox/models/episode.py:52
|
||||
#: aircox/models/episode.py:52 aircox/templates/admin/aircox/statistics.html:23
|
||||
msgid "Episode"
|
||||
msgstr "Épisode"
|
||||
|
||||
#: aircox/models/episode.py:53 aircox/templates/admin/base.html:106
|
||||
#: aircox/models/episode.py:53 aircox/templates/admin/base.html:109
|
||||
msgid "Episodes"
|
||||
msgstr "Épisodes"
|
||||
|
||||
|
@ -136,8 +138,8 @@ msgstr "non confirmé"
|
|||
msgid "cancelled"
|
||||
msgstr "annulé"
|
||||
|
||||
#: aircox/models/episode.py:174 aircox/models/sound.py:102
|
||||
#: aircox/models/sound.py:233 aircox/templates/admin/aircox/statistics.html:23
|
||||
#: aircox/models/episode.py:174 aircox/models/sound.py:99
|
||||
#: aircox/models/sound.py:230
|
||||
msgid "episode"
|
||||
msgstr "épisode"
|
||||
|
||||
|
@ -146,7 +148,7 @@ msgid "schedule"
|
|||
msgstr "horaire"
|
||||
|
||||
#: aircox/models/episode.py:181 aircox/models/log.py:91
|
||||
#: aircox/models/sound.py:105 aircox/models/station.py:142
|
||||
#: aircox/models/sound.py:102 aircox/models/station.py:142
|
||||
msgid "type"
|
||||
msgstr "type"
|
||||
|
||||
|
@ -171,12 +173,12 @@ msgstr "rediffusion"
|
|||
msgid "stop"
|
||||
msgstr "stop"
|
||||
|
||||
#: aircox/models/log.py:84 aircox/models/sound.py:89
|
||||
#: aircox/models/log.py:84 aircox/models/sound.py:86
|
||||
msgid "other"
|
||||
msgstr "autre"
|
||||
|
||||
#: aircox/models/log.py:89 aircox/models/page.py:248
|
||||
#: aircox/models/program.py:55 aircox/models/station.py:139
|
||||
#: aircox/models/program.py:54 aircox/models/station.py:139
|
||||
msgid "station"
|
||||
msgstr "station"
|
||||
|
||||
|
@ -184,7 +186,7 @@ msgstr "station"
|
|||
msgid "related station"
|
||||
msgstr "station relative"
|
||||
|
||||
#: aircox/models/log.py:92 aircox/models/program.py:254
|
||||
#: aircox/models/log.py:92 aircox/models/program.py:250
|
||||
msgid "date"
|
||||
msgstr "date"
|
||||
|
||||
|
@ -200,11 +202,12 @@ msgstr "identifiant de la source relative à ce log"
|
|||
msgid "comment"
|
||||
msgstr "commentaire"
|
||||
|
||||
#: aircox/models/log.py:107 aircox/models/sound.py:146
|
||||
#: aircox/models/log.py:107 aircox/models/sound.py:143
|
||||
msgid "Sound"
|
||||
msgstr "Son"
|
||||
|
||||
#: aircox/models/log.py:112 aircox/models/sound.py:259
|
||||
#: aircox/templates/admin/aircox/statistics.html:24
|
||||
msgid "Track"
|
||||
msgstr "Morceau"
|
||||
|
||||
|
@ -217,7 +220,7 @@ msgid "Logs"
|
|||
msgstr "Logs"
|
||||
|
||||
#: aircox/models/page.py:30 aircox/models/page.py:251
|
||||
#: aircox/models/sound.py:247
|
||||
#: aircox/models/sound.py:244
|
||||
msgid "title"
|
||||
msgstr "titre"
|
||||
|
||||
|
@ -338,8 +341,8 @@ msgstr "Commentaires"
|
|||
msgid "menu"
|
||||
msgstr "menu"
|
||||
|
||||
#: aircox/models/page.py:250 aircox/models/sound.py:107
|
||||
#: aircox/models/sound.py:240
|
||||
#: aircox/models/page.py:250 aircox/models/sound.py:104
|
||||
#: aircox/models/sound.py:237
|
||||
msgid "order"
|
||||
msgstr "ordre"
|
||||
|
||||
|
@ -359,234 +362,239 @@ msgstr "Élément du menu"
|
|||
msgid "Menu items"
|
||||
msgstr "Éléments de menu"
|
||||
|
||||
#: aircox/models/program.py:57 aircox/models/station.py:52
|
||||
#: aircox/models/program.py:56 aircox/models/station.py:52
|
||||
#: aircox/models/station.py:144
|
||||
msgid "active"
|
||||
msgstr "actif"
|
||||
|
||||
#: aircox/models/program.py:59
|
||||
#: aircox/models/program.py:58
|
||||
msgid "if not checked this program is no longer active"
|
||||
msgstr "si selectionné, cette émission n'est plus active"
|
||||
|
||||
#: aircox/models/program.py:62
|
||||
#: aircox/models/program.py:61
|
||||
msgid "syncronise"
|
||||
msgstr "synchroniser"
|
||||
|
||||
#: aircox/models/program.py:64
|
||||
#: aircox/models/program.py:63
|
||||
msgid "update later diffusions according to schedule changes"
|
||||
msgstr "met à jour les dates de diffusion à venir lorsque l'horaire change"
|
||||
|
||||
#: aircox/models/program.py:123 aircox/templates/admin/base.html:78
|
||||
#: aircox/models/program.py:119 aircox/templates/admin/base.html:81
|
||||
msgid "Programs"
|
||||
msgstr "Émissions"
|
||||
|
||||
#: aircox/models/program.py:178 aircox/models/program.py:461
|
||||
#: aircox/models/program.py:174 aircox/models/program.py:457
|
||||
msgid "related program"
|
||||
msgstr "émission apparentée"
|
||||
|
||||
#: aircox/models/program.py:182
|
||||
#: aircox/models/program.py:178
|
||||
msgid "rerun of"
|
||||
msgstr "rediffusion de"
|
||||
|
||||
#: aircox/models/program.py:226
|
||||
#: aircox/models/program.py:222
|
||||
msgid "rerun must happen after original"
|
||||
msgstr "la rediffusion doit être après l'original"
|
||||
|
||||
#: aircox/models/program.py:254
|
||||
#: aircox/models/program.py:250
|
||||
msgid "date of the first diffusion"
|
||||
msgstr "date de la première diffusion"
|
||||
|
||||
#: aircox/models/program.py:257
|
||||
#: aircox/templates/admin/aircox/statistics.html:22
|
||||
#: aircox/models/program.py:253
|
||||
msgid "time"
|
||||
msgstr "heure"
|
||||
|
||||
#: aircox/models/program.py:257
|
||||
#: aircox/models/program.py:253
|
||||
msgid "start time"
|
||||
msgstr "heure de début"
|
||||
|
||||
#: aircox/models/program.py:260
|
||||
#: aircox/models/program.py:256
|
||||
msgid "timezone"
|
||||
msgstr "zone horaire"
|
||||
|
||||
#: aircox/models/program.py:263
|
||||
#: aircox/models/program.py:259
|
||||
msgid "timezone used for the date"
|
||||
msgstr "zone horaire utilisée pour la date"
|
||||
|
||||
#: aircox/models/program.py:266 aircox/models/sound.py:120
|
||||
#: aircox/models/program.py:262 aircox/models/sound.py:117
|
||||
msgid "duration"
|
||||
msgstr "durée"
|
||||
|
||||
#: aircox/models/program.py:267
|
||||
#: aircox/models/program.py:263
|
||||
msgid "regular duration"
|
||||
msgstr "durée normale"
|
||||
|
||||
#: aircox/models/program.py:270
|
||||
#: aircox/models/program.py:266
|
||||
msgid "frequency"
|
||||
msgstr "fréquence"
|
||||
|
||||
#: aircox/models/program.py:272
|
||||
#: aircox/models/program.py:268
|
||||
msgid "ponctual"
|
||||
msgstr "ponctuel"
|
||||
|
||||
#: aircox/models/program.py:273
|
||||
#: aircox/models/program.py:269
|
||||
#, python-brace-format
|
||||
msgid "1st {day} of the month"
|
||||
msgstr "1er {day} du mois"
|
||||
|
||||
#: aircox/models/program.py:274
|
||||
#: aircox/models/program.py:270
|
||||
#, python-brace-format
|
||||
msgid "2nd {day} of the month"
|
||||
msgstr "2ème {day} du mois"
|
||||
|
||||
#: aircox/models/program.py:275
|
||||
#: aircox/models/program.py:271
|
||||
#, python-brace-format
|
||||
msgid "3rd {day} of the month"
|
||||
msgstr "3ème {day} du mois"
|
||||
|
||||
#: aircox/models/program.py:276
|
||||
#: aircox/models/program.py:272
|
||||
#, python-brace-format
|
||||
msgid "4th {day} of the month"
|
||||
msgstr "4ème {day} du mois"
|
||||
|
||||
#: aircox/models/program.py:277
|
||||
#: aircox/models/program.py:273
|
||||
#, python-brace-format
|
||||
msgid "last {day} of the month"
|
||||
msgstr "dernier {day} du mois"
|
||||
|
||||
#: aircox/models/program.py:278
|
||||
#: aircox/models/program.py:274
|
||||
#, python-brace-format
|
||||
msgid "1st and 3rd {day} of the month"
|
||||
msgstr "1er et 3ème {day} du mois"
|
||||
|
||||
#: aircox/models/program.py:279
|
||||
#: aircox/models/program.py:275
|
||||
#, python-brace-format
|
||||
msgid "2nd and 4th {day} of the month"
|
||||
msgstr "2ème et 4ème {day} du mois"
|
||||
|
||||
#: aircox/models/program.py:280
|
||||
#, fuzzy, python-brace-format
|
||||
#| msgid "every {day}"
|
||||
#: aircox/models/program.py:276
|
||||
msgid "{day}"
|
||||
msgstr "{day}"
|
||||
|
||||
#: aircox/models/program.py:281
|
||||
#: aircox/models/program.py:277
|
||||
#, python-brace-format
|
||||
msgid "one {day} on two"
|
||||
msgstr "un {day} sur deux"
|
||||
|
||||
#: aircox/models/program.py:287
|
||||
#: aircox/models/program.py:283
|
||||
msgid "Schedules"
|
||||
msgstr "Horaires"
|
||||
|
||||
#: aircox/models/program.py:464
|
||||
#: aircox/models/program.py:460
|
||||
msgid "delay"
|
||||
msgstr "délai"
|
||||
|
||||
#: aircox/models/program.py:465
|
||||
#: aircox/models/program.py:461
|
||||
msgid "minimal delay between two sound plays"
|
||||
msgstr "délai minimum entre deux sons joués"
|
||||
|
||||
#: aircox/models/program.py:468
|
||||
#: aircox/models/program.py:464
|
||||
msgid "begin"
|
||||
msgstr "début"
|
||||
|
||||
#: aircox/models/program.py:469 aircox/models/program.py:475
|
||||
#: aircox/models/program.py:465 aircox/models/program.py:471
|
||||
msgid "used to define a time range this stream is played"
|
||||
msgstr ""
|
||||
"utilisé pour définir un intervalle de temps pendant lequel ce stream est joué"
|
||||
|
||||
#: aircox/models/sound.py:89
|
||||
#: aircox/models/sound.py:86
|
||||
msgid "archive"
|
||||
msgstr "archive"
|
||||
|
||||
#: aircox/models/sound.py:90
|
||||
#: aircox/models/sound.py:87
|
||||
msgid "excerpt"
|
||||
msgstr "extrait"
|
||||
|
||||
#: aircox/models/sound.py:90
|
||||
#: aircox/models/sound.py:87
|
||||
msgid "removed"
|
||||
msgstr "supprimé"
|
||||
|
||||
#: aircox/models/sound.py:93 aircox/models/station.py:37
|
||||
#: aircox/models/sound.py:90 aircox/models/station.py:37
|
||||
msgid "name"
|
||||
msgstr "nom"
|
||||
|
||||
#: aircox/models/sound.py:96
|
||||
#: aircox/models/sound.py:93
|
||||
msgid "program"
|
||||
msgstr "émission"
|
||||
|
||||
#: aircox/models/sound.py:97
|
||||
#: aircox/models/sound.py:94
|
||||
msgid "program related to it"
|
||||
msgstr "émission apparentée à celui-ci"
|
||||
|
||||
#: aircox/models/sound.py:107 aircox/models/sound.py:240
|
||||
#: aircox/models/sound.py:104 aircox/models/sound.py:237
|
||||
msgid "position in the playlist"
|
||||
msgstr "position dans la playlist"
|
||||
|
||||
#: aircox/models/sound.py:116 aircox/models/station.py:135
|
||||
#: aircox/models/sound.py:113 aircox/models/station.py:135
|
||||
msgid "file"
|
||||
msgstr "fichier"
|
||||
|
||||
#: aircox/models/sound.py:122
|
||||
#: aircox/models/sound.py:119
|
||||
msgid "duration of the sound"
|
||||
msgstr "durée du son"
|
||||
|
||||
#: aircox/models/sound.py:125
|
||||
#: aircox/models/sound.py:122
|
||||
msgid "modification time"
|
||||
msgstr "dernière modification"
|
||||
|
||||
#: aircox/models/sound.py:127
|
||||
#: aircox/models/sound.py:124
|
||||
msgid "last modification date and time"
|
||||
msgstr "date et heure de la dernière modification"
|
||||
|
||||
#: aircox/models/sound.py:130
|
||||
#: aircox/models/sound.py:127
|
||||
msgid "good quality"
|
||||
msgstr "bonne qualité"
|
||||
|
||||
#: aircox/models/sound.py:130
|
||||
#: aircox/models/sound.py:127
|
||||
msgid "sound meets quality requirements"
|
||||
msgstr "le son rencontre les exigences de qualité"
|
||||
|
||||
#: aircox/models/sound.py:134
|
||||
#: aircox/models/sound.py:131
|
||||
msgid "public"
|
||||
msgstr "publique"
|
||||
|
||||
#: aircox/models/sound.py:134
|
||||
#: aircox/models/sound.py:131
|
||||
msgid "whether it is publicly available as podcast"
|
||||
msgstr "coché pour rendre le podcast public"
|
||||
|
||||
#: aircox/models/sound.py:138
|
||||
#: aircox/models/sound.py:135
|
||||
msgid "downloadable"
|
||||
msgstr "téléchargeable"
|
||||
|
||||
#: aircox/models/sound.py:139
|
||||
#: aircox/models/sound.py:136
|
||||
msgid ""
|
||||
"whether it can be publicly downloaded by visitors (sound must be public)"
|
||||
msgstr ""
|
||||
"coché pour permettre le téléchargement public (le podcast doit être "
|
||||
"disponible publiquement)"
|
||||
|
||||
#: aircox/models/sound.py:147
|
||||
#: aircox/models/sound.py:144
|
||||
msgid "Sounds"
|
||||
msgstr "Sons"
|
||||
|
||||
#: aircox/models/sound.py:237
|
||||
#: aircox/models/sound.py:234
|
||||
msgid "sound"
|
||||
msgstr "son"
|
||||
|
||||
#: aircox/models/sound.py:245
|
||||
#: aircox/models/sound.py:242
|
||||
msgid "position (in seconds)"
|
||||
msgstr "position (en secondes)"
|
||||
|
||||
#: aircox/models/sound.py:248
|
||||
#: aircox/models/sound.py:245
|
||||
msgid "artist"
|
||||
msgstr "artiste"
|
||||
|
||||
#: aircox/models/sound.py:249 aircox/templates/admin/aircox/statistics.html:25
|
||||
#: aircox/models/sound.py:246
|
||||
msgid "album"
|
||||
msgstr "album"
|
||||
|
||||
#: aircox/models/sound.py:247
|
||||
msgid "tags"
|
||||
msgstr "tags"
|
||||
|
||||
#: aircox/models/sound.py:248
|
||||
msgid "year"
|
||||
msgstr "année"
|
||||
|
||||
#: aircox/models/sound.py:251
|
||||
msgid "information"
|
||||
msgstr "information"
|
||||
|
@ -677,6 +685,18 @@ msgstr ""
|
|||
"liste des paramètres disponibles séparés par des virgules; placé dans le "
|
||||
"fichier de configuration en tant que code brut; relatif au plugin utilisé"
|
||||
|
||||
#: aircox/models/user_settings.py:11
|
||||
msgid "User"
|
||||
msgstr "Utilisateur"
|
||||
|
||||
#: aircox/models/user_settings.py:14
|
||||
msgid "Playlist Editor Columns"
|
||||
msgstr "Colonnes de l'éditeur de playlist"
|
||||
|
||||
#: aircox/models/user_settings.py:16
|
||||
msgid "Playlist Editor Separator"
|
||||
msgstr "Séparateur de l'éditeur de playlist"
|
||||
|
||||
#: aircox/templates/admin/aircox/filters/filter.html:2
|
||||
#, python-format
|
||||
msgid " By %(filter_title)s "
|
||||
|
@ -684,7 +704,7 @@ msgstr "Par %(filter_title)s "
|
|||
|
||||
#: aircox/templates/admin/aircox/page_change_form.html:9
|
||||
#: aircox/templates/admin/aircox/page_change_list.html:7
|
||||
#: aircox/templates/admin/base.html:163
|
||||
#: aircox/templates/admin/base.html:166
|
||||
#: aircox/templates/admin/change_list.html:30
|
||||
#: aircox/templates/aircox/base.html:54
|
||||
msgid "Home"
|
||||
|
@ -715,36 +735,46 @@ msgstr "Sauvegarder et continuer"
|
|||
msgid "Publish"
|
||||
msgstr "Publier"
|
||||
|
||||
#: aircox/templates/admin/aircox/statistics.html:24
|
||||
msgid "track"
|
||||
msgstr "piste"
|
||||
#: aircox/templates/admin/aircox/playlist_inline.html:33
|
||||
#: aircox/templates/admin/aircox/playlist_inline.html:34
|
||||
msgid "Track Position"
|
||||
msgstr "Position dans la playlist"
|
||||
|
||||
#: aircox/templates/admin/aircox/statistics.html:22
|
||||
msgid "Time"
|
||||
msgstr "Heure"
|
||||
|
||||
#: aircox/templates/admin/aircox/statistics.html:25
|
||||
#: aircox/templatetags/aircox_admin.py:52
|
||||
msgid "Tags"
|
||||
msgstr "Étiquettes"
|
||||
|
||||
#: aircox/templates/admin/aircox/statistics.html:67
|
||||
msgid "Total"
|
||||
msgstr "Total"
|
||||
|
||||
#: aircox/templates/admin/base.html:65 aircox/templates/admin/index.html:11
|
||||
#: aircox/templates/admin/base.html:68 aircox/templates/admin/index.html:11
|
||||
#: aircox/templates/aircox/home.html:47
|
||||
msgid "Today"
|
||||
msgstr "Aujourd'hui"
|
||||
|
||||
#: aircox/templates/admin/base.html:121
|
||||
#: aircox/templates/admin/base.html:124
|
||||
msgid "Tools"
|
||||
msgstr "Outils"
|
||||
|
||||
#: aircox/templates/admin/base.html:137
|
||||
#: aircox/templates/admin/base.html:140
|
||||
msgid "View site"
|
||||
msgstr "Voir le site"
|
||||
|
||||
#: aircox/templates/admin/base.html:142
|
||||
#: aircox/templates/admin/base.html:145
|
||||
msgid "Documentation"
|
||||
msgstr "Documentation"
|
||||
|
||||
#: aircox/templates/admin/base.html:146
|
||||
#: aircox/templates/admin/base.html:149
|
||||
msgid "Change password"
|
||||
msgstr "Changer le mot de passe"
|
||||
|
||||
#: aircox/templates/admin/base.html:149
|
||||
#: aircox/templates/admin/base.html:152
|
||||
msgid "Log out"
|
||||
msgstr "Se déconnecter"
|
||||
|
||||
|
@ -958,67 +988,108 @@ msgstr "Épisode en ce moment sur les ondes"
|
|||
msgid "Currently playing"
|
||||
msgstr "En ce moment"
|
||||
|
||||
#: aircox/urls.py:40
|
||||
#: aircox/templatetags/aircox_admin.py:51
|
||||
msgid "Artist"
|
||||
msgstr "Artiste"
|
||||
|
||||
#: aircox/templatetags/aircox_admin.py:51
|
||||
msgid "Album"
|
||||
msgstr "Album"
|
||||
|
||||
#: aircox/templatetags/aircox_admin.py:51
|
||||
msgid "Title"
|
||||
msgstr "Titre"
|
||||
|
||||
#: aircox/templatetags/aircox_admin.py:52
|
||||
msgid "Year"
|
||||
msgstr "Année"
|
||||
|
||||
#: aircox/templatetags/aircox_admin.py:53
|
||||
msgid "Save Settings"
|
||||
msgstr "Enregistrer la configuration"
|
||||
|
||||
#: aircox/templatetags/aircox_admin.py:54
|
||||
msgid "Discard changes"
|
||||
msgstr "Annuler les changements"
|
||||
|
||||
#: aircox/templatetags/aircox_admin.py:55
|
||||
msgid "Columns"
|
||||
msgstr "Colonnes"
|
||||
|
||||
#: aircox/templatetags/aircox_admin.py:56
|
||||
msgid "Add a track"
|
||||
msgstr "Ajouter un morceau"
|
||||
|
||||
#: aircox/templatetags/aircox_admin.py:57
|
||||
msgid "Remove"
|
||||
msgstr "Supprimer"
|
||||
|
||||
#: aircox/templatetags/aircox_admin.py:58
|
||||
#| msgid "timestamp"
|
||||
msgid "Timestamp"
|
||||
msgstr "Temps"
|
||||
|
||||
#: aircox/urls.py:44
|
||||
msgid "articles/"
|
||||
msgstr "articles/"
|
||||
|
||||
#: aircox/urls.py:43
|
||||
#: aircox/urls.py:47
|
||||
msgid "articles/<slug:slug>/"
|
||||
msgstr "articles/<slug:slug>/"
|
||||
|
||||
#: aircox/urls.py:47
|
||||
#: aircox/urls.py:51
|
||||
msgid "episodes/"
|
||||
msgstr "episodes/"
|
||||
|
||||
#: aircox/urls.py:49
|
||||
#: aircox/urls.py:53
|
||||
msgid "episodes/<slug:slug>/"
|
||||
msgstr "episodes/<slug:slug>/"
|
||||
|
||||
#: aircox/urls.py:51
|
||||
#: aircox/urls.py:55
|
||||
msgid "week/"
|
||||
msgstr "semaine/"
|
||||
|
||||
#: aircox/urls.py:53
|
||||
#: aircox/urls.py:57
|
||||
msgid "week/<date:date>/"
|
||||
msgstr "semaine/<date:date>/"
|
||||
|
||||
#: aircox/urls.py:56
|
||||
#: aircox/urls.py:60
|
||||
msgid "logs/"
|
||||
msgstr "logs/"
|
||||
|
||||
#: aircox/urls.py:57
|
||||
#: aircox/urls.py:61
|
||||
msgid "logs/<date:date>/"
|
||||
msgstr "logs/<date:date>/"
|
||||
|
||||
#: aircox/urls.py:60
|
||||
#: aircox/urls.py:64
|
||||
msgid "publications/"
|
||||
msgstr "publications/"
|
||||
|
||||
#: aircox/urls.py:63
|
||||
#: aircox/urls.py:67
|
||||
msgid "pages/"
|
||||
msgstr "pages/"
|
||||
|
||||
#: aircox/urls.py:69
|
||||
#: aircox/urls.py:73
|
||||
msgid "pages/<slug:slug>/"
|
||||
msgstr "pages/<slug:slug>/"
|
||||
|
||||
#: aircox/urls.py:76
|
||||
#: aircox/urls.py:80
|
||||
msgid "programs/"
|
||||
msgstr "emissions/"
|
||||
|
||||
#: aircox/urls.py:78
|
||||
#: aircox/urls.py:82
|
||||
msgid "programs/<slug:slug>/"
|
||||
msgstr "emissions/<slug:slug>/"
|
||||
|
||||
#: aircox/urls.py:80
|
||||
#: aircox/urls.py:84
|
||||
msgid "programs/<slug:parent_slug>/episodes/"
|
||||
msgstr "emissions/<slug:parent_slug>/episodes/"
|
||||
|
||||
#: aircox/urls.py:82
|
||||
#: aircox/urls.py:86
|
||||
msgid "programs/<slug:parent_slug>/articles/"
|
||||
msgstr "emissions/<slug:parent_slug>/articles/"
|
||||
|
||||
#: aircox/urls.py:84
|
||||
#: aircox/urls.py:88
|
||||
msgid "programs/<slug:parent_slug>/publications/"
|
||||
msgstr "emissions/<slug:parent_slug>/publications/"
|
||||
|
||||
|
@ -1026,6 +1097,9 @@ msgstr "emissions/<slug:parent_slug>/publications/"
|
|||
msgid "comments are not allowed"
|
||||
msgstr "les commentaires ne sont pas autorisés"
|
||||
|
||||
#~ msgid "track"
|
||||
#~ msgstr "morceau"
|
||||
|
||||
#~ msgid "if it can be podcasted from the server"
|
||||
#~ msgstr "s'il peut être podcasté depuis le serveur"
|
||||
|
||||
|
|
|
@ -219,7 +219,6 @@ class MonitorHandler(PatternMatchingEventHandler):
|
|||
"""
|
||||
self.subdir = subdir
|
||||
self.pool = pool
|
||||
|
||||
if self.subdir == settings.AIRCOX_SOUND_ARCHIVES_SUBDIR:
|
||||
self.sound_kwargs = {'type': Sound.TYPE_ARCHIVE}
|
||||
else:
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
from .article import Article
|
||||
from .page import Category, Page, StaticPage, Comment, NavItem
|
||||
from .program import Program, Stream, Schedule
|
||||
from .episode import Episode, Diffusion
|
||||
from .log import Log
|
||||
from .sound import Sound, Track
|
||||
from .station import Station, Port
|
||||
from .article import *
|
||||
from .page import *
|
||||
from .program import *
|
||||
from .episode import *
|
||||
from .log import *
|
||||
from .sound import *
|
||||
from .station import *
|
||||
from .user_settings import *
|
||||
|
||||
from . import signals
|
||||
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from .page import Page, PageQuerySet
|
||||
from .program import Program, ProgramChildQuerySet
|
||||
from .page import Page
|
||||
from .program import ProgramChildQuerySet
|
||||
|
||||
|
||||
__all__ = ('Article',)
|
||||
|
||||
|
||||
class Article(Page):
|
||||
|
|
|
@ -9,12 +9,12 @@ from django.utils.functional import cached_property
|
|||
from easy_thumbnails.files import get_thumbnailer
|
||||
|
||||
from aircox import settings, utils
|
||||
from .program import Program, ProgramChildQuerySet, \
|
||||
from .program import ProgramChildQuerySet, \
|
||||
BaseRerun, BaseRerunQuerySet, Schedule
|
||||
from .page import Page, PageQuerySet
|
||||
from .page import Page
|
||||
|
||||
|
||||
__all__ = ['Episode', 'Diffusion', 'DiffusionQuerySet']
|
||||
__all__ = ('Episode', 'Diffusion', 'DiffusionQuerySet')
|
||||
|
||||
|
||||
class Episode(Page):
|
||||
|
@ -31,9 +31,9 @@ class Episode(Page):
|
|||
""" Return serialized data about podcasts. """
|
||||
from ..serializers import PodcastSerializer
|
||||
podcasts = [PodcastSerializer(s).data
|
||||
for s in self.sound_set.public().order_by('type') ]
|
||||
for s in self.sound_set.public().order_by('type')]
|
||||
if self.cover:
|
||||
options = {'size': (128,128), 'crop':'scale'}
|
||||
options = {'size': (128, 128), 'crop': 'scale'}
|
||||
cover = get_thumbnailer(self.cover).get_thumbnail(options).url
|
||||
else:
|
||||
cover = None
|
||||
|
@ -84,7 +84,7 @@ class DiffusionQuerySet(BaseRerunQuerySet):
|
|||
def episode(self, episode=None, id=None):
|
||||
""" Diffusions for this episode """
|
||||
return self.filter(episode=episode) if id is None else \
|
||||
self.filter(episode__id=id)
|
||||
self.filter(episode__id=id)
|
||||
|
||||
def on_air(self):
|
||||
""" On air diffusions """
|
||||
|
@ -104,13 +104,13 @@ class DiffusionQuerySet(BaseRerunQuerySet):
|
|||
end = tz.datetime.combine(date, datetime.time(23, 59, 59, 999))
|
||||
# start = tz.get_current_timezone().localize(start)
|
||||
# end = tz.get_current_timezone().localize(end)
|
||||
qs = self.filter(start__range = (start, end))
|
||||
qs = self.filter(start__range=(start, end))
|
||||
return qs.order_by('start') if order else qs
|
||||
|
||||
def at(self, date, order=True):
|
||||
""" Return diffusions at specified date or datetime """
|
||||
return self.now(date, order) if isinstance(date, tz.datetime) else \
|
||||
self.date(date, order)
|
||||
self.date(date, order)
|
||||
|
||||
def after(self, date=None):
|
||||
"""
|
||||
|
@ -201,7 +201,7 @@ class Diffusion(BaseRerun):
|
|||
|
||||
def __str__(self):
|
||||
str_ = '{episode} - {date}'.format(
|
||||
self=self, episode=self.episode and self.episode.title,
|
||||
episode=self.episode and self.episode.title,
|
||||
date=self.local_start.strftime('%Y/%m/%d %H:%M%z'),
|
||||
)
|
||||
if self.initial:
|
||||
|
@ -324,5 +324,3 @@ class Diffusion(BaseRerun):
|
|||
'end': self.end,
|
||||
'episode': getattr(self, 'episode', None),
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@ from .station import Station
|
|||
logger = logging.getLogger('aircox')
|
||||
|
||||
|
||||
__all__ = ['Log', 'LogQuerySet', 'LogArchiver']
|
||||
__all__ = ('Log', 'LogQuerySet', 'LogArchiver')
|
||||
|
||||
|
||||
class LogQuerySet(models.QuerySet):
|
||||
|
@ -31,7 +31,7 @@ class LogQuerySet(models.QuerySet):
|
|||
def date(self, date):
|
||||
start = tz.datetime.combine(date, datetime.time())
|
||||
end = tz.datetime.combine(date, datetime.time(23, 59, 59, 999))
|
||||
return self.filter(date__range = (start, end))
|
||||
return self.filter(date__range=(start, end))
|
||||
# this filter does not work with mysql
|
||||
# return self.filter(date__date=date)
|
||||
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
from enum import IntEnum
|
||||
import re
|
||||
|
||||
from django.db import models
|
||||
|
@ -18,7 +17,8 @@ from model_utils.managers import InheritanceQuerySet
|
|||
from .station import Station
|
||||
|
||||
|
||||
__all__ = ['Category', 'PageQuerySet', 'Page', 'Comment', 'NavItem']
|
||||
__all__ = ('Category', 'PageQuerySet',
|
||||
'Page', 'StaticPage', 'Comment', 'NavItem')
|
||||
|
||||
|
||||
headline_re = re.compile(r'(<p>)?'
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import calendar
|
||||
from collections import OrderedDict
|
||||
import datetime
|
||||
from enum import IntEnum
|
||||
import logging
|
||||
import os
|
||||
|
@ -10,7 +9,7 @@ import pytz
|
|||
from django.conf import settings as conf
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.db.models import F, Q
|
||||
from django.db.models import F
|
||||
from django.db.models.functions import Concat, Substr
|
||||
from django.utils import timezone as tz
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
@ -24,8 +23,8 @@ from .station import Station
|
|||
logger = logging.getLogger('aircox')
|
||||
|
||||
|
||||
__all__ = ['Program', 'ProgramQuerySet', 'Stream', 'Schedule',
|
||||
'ProgramChildQuerySet', 'BaseRerun', 'BaseRerunQuerySet']
|
||||
__all__ = ('Program', 'ProgramQuerySet', 'Stream', 'Schedule',
|
||||
'ProgramChildQuerySet', 'BaseRerun', 'BaseRerunQuerySet')
|
||||
|
||||
|
||||
class ProgramQuerySet(PageQuerySet):
|
||||
|
|
|
@ -2,7 +2,7 @@ import pytz
|
|||
|
||||
from django.contrib.auth.models import User, Group, Permission
|
||||
from django.db import transaction
|
||||
from django.db.models import F, signals
|
||||
from django.db.models import signals
|
||||
from django.dispatch import receiver
|
||||
from django.utils import timezone as tz
|
||||
|
||||
|
|
|
@ -1,17 +1,14 @@
|
|||
from enum import IntEnum
|
||||
import logging
|
||||
import os
|
||||
|
||||
from django.conf import settings as conf
|
||||
from django.db import models
|
||||
from django.db.models import Q, Value as V
|
||||
from django.db.models.functions import Concat
|
||||
from django.db.models import Q
|
||||
from django.utils import timezone as tz
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from taggit.managers import TaggableManager
|
||||
|
||||
from aircox import settings
|
||||
from .program import Program
|
||||
from .episode import Episode
|
||||
|
||||
|
@ -19,7 +16,7 @@ from .episode import Episode
|
|||
logger = logging.getLogger('aircox')
|
||||
|
||||
|
||||
__all__ = ['Sound', 'SoundQuerySet', 'Track']
|
||||
__all__ = ('Sound', 'SoundQuerySet', 'Track')
|
||||
|
||||
|
||||
class SoundQuerySet(models.QuerySet):
|
||||
|
@ -246,7 +243,10 @@ class Track(models.Model):
|
|||
)
|
||||
title = models.CharField(_('title'), max_length=128)
|
||||
artist = models.CharField(_('artist'), max_length=128)
|
||||
tags = TaggableManager(verbose_name=_('tags'), blank=True,)
|
||||
album = models.CharField(_('album'), max_length=128, null=True, blank=True)
|
||||
tags = TaggableManager(verbose_name=_('tags'), blank=True)
|
||||
year = models.IntegerField(_('year'), blank=True, null=True)
|
||||
# FIXME: remove?
|
||||
info = models.CharField(
|
||||
_('information'),
|
||||
max_length=128,
|
||||
|
|
|
@ -2,13 +2,14 @@ import os
|
|||
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.utils.functional import cached_property
|
||||
|
||||
from filer.fields.image import FilerImageField
|
||||
|
||||
from .. import settings
|
||||
|
||||
|
||||
__all__ = ['Station', 'StationQuerySet', 'Port']
|
||||
__all__ = ('Station', 'StationQuerySet', 'Port')
|
||||
|
||||
|
||||
class StationQuerySet(models.QuerySet):
|
||||
|
@ -74,6 +75,11 @@ class Station(models.Model):
|
|||
|
||||
objects = StationQuerySet.as_manager()
|
||||
|
||||
@cached_property
|
||||
def streams(self):
|
||||
""" Audio streams as list of urls. """
|
||||
return self.audio_streams.split('\n') if self.audio_streams else []
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
|
16
aircox/models/user_settings.py
Normal file
16
aircox/models/user_settings.py
Normal file
|
@ -0,0 +1,16 @@
|
|||
from django.db import models
|
||||
from django.contrib.auth.models import User
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class UserSettings(models.Model):
|
||||
"""
|
||||
Store user's settings.
|
||||
"""
|
||||
user = models.OneToOneField(
|
||||
User, models.CASCADE, verbose_name=_('User'),
|
||||
related_name='aircox_settings')
|
||||
playlist_editor_columns = models.JSONField(
|
||||
_('Playlist Editor Columns'))
|
||||
playlist_editor_sep = models.CharField(
|
||||
_('Playlist Editor Separator'), max_length=16)
|
3
aircox/serializers/__init__.py
Normal file
3
aircox/serializers/__init__.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from .log import *
|
||||
from .sound import *
|
||||
from .admin import *
|
30
aircox/serializers/admin.py
Normal file
30
aircox/serializers/admin.py
Normal file
|
@ -0,0 +1,30 @@
|
|||
from rest_framework import serializers
|
||||
from taggit.serializers import TagListSerializerField, TaggitSerializer
|
||||
|
||||
from ..models import Track, UserSettings
|
||||
|
||||
|
||||
__all__ = ('TrackSerializer', 'UserSettingsSerializer')
|
||||
|
||||
|
||||
class TrackSerializer(TaggitSerializer, serializers.ModelSerializer):
|
||||
tags = TagListSerializerField()
|
||||
|
||||
class Meta:
|
||||
model = Track
|
||||
fields = ('pk', 'artist', 'title', 'album', 'year', 'position',
|
||||
'info', 'tags', 'episode', 'sound', 'timestamp')
|
||||
|
||||
|
||||
class UserSettingsSerializer(serializers.ModelSerializer):
|
||||
# TODO: validate fields values (playlist_editor_columns at least)
|
||||
class Meta:
|
||||
model = UserSettings
|
||||
fields = ('playlist_editor_columns', 'playlist_editor_sep')
|
||||
|
||||
def create(self, validated_data):
|
||||
user = self.context.get('user')
|
||||
if user:
|
||||
validated_data['user_id'] = user.id
|
||||
return super().create(validated_data)
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
from rest_framework import serializers
|
||||
|
||||
from .models import Diffusion, Log, Sound
|
||||
from ..models import Diffusion, Log
|
||||
|
||||
|
||||
__all__ = ['LogInfo', 'LogInfoSerializer']
|
||||
__all__ = ('LogInfo', 'LogInfoSerializer')
|
||||
|
||||
|
||||
class LogInfo:
|
||||
|
@ -51,21 +51,3 @@ class LogInfoSerializer(serializers.Serializer):
|
|||
info = serializers.CharField(max_length=200, required=False)
|
||||
url = serializers.URLField(required=False)
|
||||
cover = serializers.URLField(required=False)
|
||||
|
||||
|
||||
class SoundSerializer(serializers.ModelSerializer):
|
||||
file = serializers.FileField(use_url=False)
|
||||
|
||||
class Meta:
|
||||
model = Sound
|
||||
fields = ['pk', 'name', 'program', 'episode', 'type', 'file',
|
||||
'duration', 'mtime', 'is_good_quality', 'is_public', 'url']
|
||||
|
||||
class PodcastSerializer(serializers.ModelSerializer):
|
||||
# serializers.HyperlinkedIdentityField(view_name='sound', format='html')
|
||||
|
||||
class Meta:
|
||||
model = Sound
|
||||
fields = ['pk', 'name', 'program', 'episode', 'type',
|
||||
'duration', 'mtime', 'url', 'is_downloadable']
|
||||
|
21
aircox/serializers/sound.py
Normal file
21
aircox/serializers/sound.py
Normal file
|
@ -0,0 +1,21 @@
|
|||
from rest_framework import serializers
|
||||
|
||||
from ..models import Sound
|
||||
|
||||
|
||||
class SoundSerializer(serializers.ModelSerializer):
|
||||
file = serializers.FileField(use_url=False)
|
||||
|
||||
class Meta:
|
||||
model = Sound
|
||||
fields = ['pk', 'name', 'program', 'episode', 'type', 'file',
|
||||
'duration', 'mtime', 'is_good_quality', 'is_public', 'url']
|
||||
|
||||
|
||||
class PodcastSerializer(serializers.ModelSerializer):
|
||||
# serializers.HyperlinkedIdentityField(view_name='sound', format='html')
|
||||
|
||||
class Meta:
|
||||
model = Sound
|
||||
fields = ['pk', 'name', 'program', 'episode', 'type',
|
||||
'duration', 'mtime', 'url', 'is_downloadable']
|
12
aircox/static/aircox/admin.html
Normal file
12
aircox/static/aircox/admin.html
Normal file
|
@ -0,0 +1,12 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<title>Vue App</title>
|
||||
<script defer src="js/chunk-vendors.js"></script><script defer src="js/chunk-common.js"></script><script defer src="js/admin.js"></script><link href="css/chunk-vendors.css" rel="stylesheet"><link href="css/chunk-common.css" rel="stylesheet"><link href="css/admin.css" rel="stylesheet"></head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
</html>
|
12
aircox/static/aircox/core.html
Normal file
12
aircox/static/aircox/core.html
Normal file
|
@ -0,0 +1,12 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<title>Vue App</title>
|
||||
<script defer src="js/chunk-vendors.js"></script><script defer src="js/chunk-common.js"></script><script defer src="js/core.js"></script><link href="css/chunk-vendors.css" rel="stylesheet"><link href="css/chunk-common.css" rel="stylesheet"></head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
</html>
|
|
@ -1 +1,24 @@
|
|||
.admin .navbar .navbar-brand{padding-right:1em}.admin .navbar .navbar-brand img{margin:0 .4em;margin-top:.3em;max-height:3em}.admin .breadcrumbs{margin-bottom:1em}.admin .results>#result_list{width:100%;margin:1em 0}.admin ul.menu-list li{list-style-type:none}.admin .submit-row a.deletelink{height:35px}
|
||||
/*!*************************************************************************************************************************************************************************************************************************************!*\
|
||||
!*** css ./node_modules/css-loader/dist/cjs.js??clonedRuleSet-24.use[1]!./node_modules/postcss-loader/dist/cjs.js??clonedRuleSet-24.use[2]!./node_modules/sass-loader/dist/cjs.js??clonedRuleSet-24.use[3]!./src/assets/admin.scss ***!
|
||||
\*************************************************************************************************************************************************************************************************************************************/
|
||||
.admin .navbar .navbar-brand {
|
||||
padding-right: 1em;
|
||||
}
|
||||
.admin .navbar .navbar-brand img {
|
||||
margin: 0em 0.4em;
|
||||
margin-top: 0.3em;
|
||||
max-height: 3em;
|
||||
}
|
||||
.admin .breadcrumbs {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
.admin .results > #result_list {
|
||||
width: 100%;
|
||||
margin: 1em 0em;
|
||||
}
|
||||
.admin ul.menu-list li {
|
||||
list-style-type: none;
|
||||
}
|
||||
.admin .submit-row a.deletelink {
|
||||
height: 35px;
|
||||
}
|
||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -1,2 +1,225 @@
|
|||
(function(){"use strict";var n={5159:function(n,t,e){e(9651),e(8880);var o=e(9643),r=e(1784);const i={...o.Z,components:{...o.Z.components,...r.S}};window.App=i},1784:function(n,t,e){e.d(t,{S:function(){return v}});var o=e(4156),r=e(1847),i=e(6294),u=e(5189),c=e(2530),f=e(6306),a=e(7079),s=e(7467),l=e(8833),p=e(5127);t["Z"]={AAutocomplete:o.Z,AEpisode:r.Z,AList:i.Z,APage:u.Z,APlayer:c.C,APlaylist:f.Z,AProgress:a.Z,ASoundItem:s.Z};const v={AStatistics:l.Z,AStreamer:p.Z}}},t={};function e(o){var r=t[o];if(void 0!==r)return r.exports;var i=t[o]={exports:{}};return n[o](i,i.exports,e),i.exports}e.m=n,function(){var n=[];e.O=function(t,o,r,i){if(!o){var u=1/0;for(s=0;s<n.length;s++){o=n[s][0],r=n[s][1],i=n[s][2];for(var c=!0,f=0;f<o.length;f++)(!1&i||u>=i)&&Object.keys(e.O).every((function(n){return e.O[n](o[f])}))?o.splice(f--,1):(c=!1,i<u&&(u=i));if(c){n.splice(s--,1);var a=r();void 0!==a&&(t=a)}}return t}i=i||0;for(var s=n.length;s>0&&n[s-1][2]>i;s--)n[s]=n[s-1];n[s]=[o,r,i]}}(),function(){e.d=function(n,t){for(var o in t)e.o(t,o)&&!e.o(n,o)&&Object.defineProperty(n,o,{enumerable:!0,get:t[o]})}}(),function(){e.g=function(){if("object"===typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(n){if("object"===typeof window)return window}}()}(),function(){e.o=function(n,t){return Object.prototype.hasOwnProperty.call(n,t)}}(),function(){e.r=function(n){"undefined"!==typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(n,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(n,"__esModule",{value:!0})}}(),function(){var n={328:0};e.O.j=function(t){return 0===n[t]};var t=function(t,o){var r,i,u=o[0],c=o[1],f=o[2],a=0;if(u.some((function(t){return 0!==n[t]}))){for(r in c)e.o(c,r)&&(e.m[r]=c[r]);if(f)var s=f(e)}for(t&&t(o);a<u.length;a++)i=u[a],e.o(n,i)&&n[i]&&n[i][0](),n[i]=0;return e.O(s)},o=self["webpackChunkaircox_assets"]=self["webpackChunkaircox_assets"]||[];o.forEach(t.bind(null,0)),o.push=t.bind(null,o.push.bind(o))}();var o=e.O(void 0,[998,64],(function(){return e(5159)}));o=e.O(o)})();
|
||||
//# sourceMappingURL=admin.js.map
|
||||
/*
|
||||
* ATTENTION: The "eval" devtool has been used (maybe by default in mode: "development").
|
||||
* This devtool is neither made for production nor for readable output files.
|
||||
* It uses "eval()" calls to create a separate source file in the browser devtools.
|
||||
* If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/)
|
||||
* or disable the default devtool with "devtool: false".
|
||||
* If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/).
|
||||
*/
|
||||
/******/ (function() { // webpackBootstrap
|
||||
/******/ "use strict";
|
||||
/******/ var __webpack_modules__ = ({
|
||||
|
||||
/***/ "./src/admin.js":
|
||||
/*!**********************!*\
|
||||
!*** ./src/admin.js ***!
|
||||
\**********************/
|
||||
/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
|
||||
|
||||
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _assets_styles_scss__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./assets/styles.scss */ \"./src/assets/styles.scss\");\n/* harmony import */ var _assets_admin_scss__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./assets/admin.scss */ \"./src/assets/admin.scss\");\n/* harmony import */ var _index_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./index.js */ \"./src/index.js\");\n/* harmony import */ var _app__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./app */ \"./src/app.js\");\n/* harmony import */ var _components__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./components */ \"./src/components/index.js\");\n/* harmony import */ var _track__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ./track */ \"./src/track.js\");\n\n\n\n\n\n\nconst AdminApp = {\n ..._app__WEBPACK_IMPORTED_MODULE_3__[\"default\"],\n components: {\n ..._app__WEBPACK_IMPORTED_MODULE_3__[\"default\"].components,\n ..._components__WEBPACK_IMPORTED_MODULE_4__.admin\n },\n data() {\n return {\n ...super.data,\n Track: _track__WEBPACK_IMPORTED_MODULE_5__[\"default\"]\n };\n }\n};\n/* harmony default export */ __webpack_exports__[\"default\"] = (AdminApp);\nwindow.App = AdminApp;\n\n//# sourceURL=webpack://aircox-assets/./src/admin.js?");
|
||||
|
||||
/***/ }),
|
||||
|
||||
/***/ "./src/assets/admin.scss":
|
||||
/*!*******************************!*\
|
||||
!*** ./src/assets/admin.scss ***!
|
||||
\*******************************/
|
||||
/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
|
||||
|
||||
eval("__webpack_require__.r(__webpack_exports__);\n// extracted by mini-css-extract-plugin\n\n\n//# sourceURL=webpack://aircox-assets/./src/assets/admin.scss?");
|
||||
|
||||
/***/ })
|
||||
|
||||
/******/ });
|
||||
/************************************************************************/
|
||||
/******/ // The module cache
|
||||
/******/ var __webpack_module_cache__ = {};
|
||||
/******/
|
||||
/******/ // The require function
|
||||
/******/ function __webpack_require__(moduleId) {
|
||||
/******/ // Check if module is in cache
|
||||
/******/ var cachedModule = __webpack_module_cache__[moduleId];
|
||||
/******/ if (cachedModule !== undefined) {
|
||||
/******/ return cachedModule.exports;
|
||||
/******/ }
|
||||
/******/ // Create a new module (and put it into the cache)
|
||||
/******/ var module = __webpack_module_cache__[moduleId] = {
|
||||
/******/ id: moduleId,
|
||||
/******/ loaded: false,
|
||||
/******/ exports: {}
|
||||
/******/ };
|
||||
/******/
|
||||
/******/ // Execute the module function
|
||||
/******/ __webpack_modules__[moduleId].call(module.exports, module, module.exports, __webpack_require__);
|
||||
/******/
|
||||
/******/ // Flag the module as loaded
|
||||
/******/ module.loaded = true;
|
||||
/******/
|
||||
/******/ // Return the exports of the module
|
||||
/******/ return module.exports;
|
||||
/******/ }
|
||||
/******/
|
||||
/******/ // expose the modules object (__webpack_modules__)
|
||||
/******/ __webpack_require__.m = __webpack_modules__;
|
||||
/******/
|
||||
/************************************************************************/
|
||||
/******/ /* webpack/runtime/chunk loaded */
|
||||
/******/ !function() {
|
||||
/******/ var deferred = [];
|
||||
/******/ __webpack_require__.O = function(result, chunkIds, fn, priority) {
|
||||
/******/ if(chunkIds) {
|
||||
/******/ priority = priority || 0;
|
||||
/******/ for(var i = deferred.length; i > 0 && deferred[i - 1][2] > priority; i--) deferred[i] = deferred[i - 1];
|
||||
/******/ deferred[i] = [chunkIds, fn, priority];
|
||||
/******/ return;
|
||||
/******/ }
|
||||
/******/ var notFulfilled = Infinity;
|
||||
/******/ for (var i = 0; i < deferred.length; i++) {
|
||||
/******/ var chunkIds = deferred[i][0];
|
||||
/******/ var fn = deferred[i][1];
|
||||
/******/ var priority = deferred[i][2];
|
||||
/******/ var fulfilled = true;
|
||||
/******/ for (var j = 0; j < chunkIds.length; j++) {
|
||||
/******/ if ((priority & 1 === 0 || notFulfilled >= priority) && Object.keys(__webpack_require__.O).every(function(key) { return __webpack_require__.O[key](chunkIds[j]); })) {
|
||||
/******/ chunkIds.splice(j--, 1);
|
||||
/******/ } else {
|
||||
/******/ fulfilled = false;
|
||||
/******/ if(priority < notFulfilled) notFulfilled = priority;
|
||||
/******/ }
|
||||
/******/ }
|
||||
/******/ if(fulfilled) {
|
||||
/******/ deferred.splice(i--, 1)
|
||||
/******/ var r = fn();
|
||||
/******/ if (r !== undefined) result = r;
|
||||
/******/ }
|
||||
/******/ }
|
||||
/******/ return result;
|
||||
/******/ };
|
||||
/******/ }();
|
||||
/******/
|
||||
/******/ /* webpack/runtime/compat get default export */
|
||||
/******/ !function() {
|
||||
/******/ // getDefaultExport function for compatibility with non-harmony modules
|
||||
/******/ __webpack_require__.n = function(module) {
|
||||
/******/ var getter = module && module.__esModule ?
|
||||
/******/ function() { return module['default']; } :
|
||||
/******/ function() { return module; };
|
||||
/******/ __webpack_require__.d(getter, { a: getter });
|
||||
/******/ return getter;
|
||||
/******/ };
|
||||
/******/ }();
|
||||
/******/
|
||||
/******/ /* webpack/runtime/define property getters */
|
||||
/******/ !function() {
|
||||
/******/ // define getter functions for harmony exports
|
||||
/******/ __webpack_require__.d = function(exports, definition) {
|
||||
/******/ for(var key in definition) {
|
||||
/******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
|
||||
/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
|
||||
/******/ }
|
||||
/******/ }
|
||||
/******/ };
|
||||
/******/ }();
|
||||
/******/
|
||||
/******/ /* webpack/runtime/global */
|
||||
/******/ !function() {
|
||||
/******/ __webpack_require__.g = (function() {
|
||||
/******/ if (typeof globalThis === 'object') return globalThis;
|
||||
/******/ try {
|
||||
/******/ return this || new Function('return this')();
|
||||
/******/ } catch (e) {
|
||||
/******/ if (typeof window === 'object') return window;
|
||||
/******/ }
|
||||
/******/ })();
|
||||
/******/ }();
|
||||
/******/
|
||||
/******/ /* webpack/runtime/hasOwnProperty shorthand */
|
||||
/******/ !function() {
|
||||
/******/ __webpack_require__.o = function(obj, prop) { return Object.prototype.hasOwnProperty.call(obj, prop); }
|
||||
/******/ }();
|
||||
/******/
|
||||
/******/ /* webpack/runtime/make namespace object */
|
||||
/******/ !function() {
|
||||
/******/ // define __esModule on exports
|
||||
/******/ __webpack_require__.r = function(exports) {
|
||||
/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
|
||||
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
||||
/******/ }
|
||||
/******/ Object.defineProperty(exports, '__esModule', { value: true });
|
||||
/******/ };
|
||||
/******/ }();
|
||||
/******/
|
||||
/******/ /* webpack/runtime/node module decorator */
|
||||
/******/ !function() {
|
||||
/******/ __webpack_require__.nmd = function(module) {
|
||||
/******/ module.paths = [];
|
||||
/******/ if (!module.children) module.children = [];
|
||||
/******/ return module;
|
||||
/******/ };
|
||||
/******/ }();
|
||||
/******/
|
||||
/******/ /* webpack/runtime/jsonp chunk loading */
|
||||
/******/ !function() {
|
||||
/******/ // no baseURI
|
||||
/******/
|
||||
/******/ // object to store loaded and loading chunks
|
||||
/******/ // undefined = chunk not loaded, null = chunk preloaded/prefetched
|
||||
/******/ // [resolve, reject, Promise] = chunk loading, 0 = chunk loaded
|
||||
/******/ var installedChunks = {
|
||||
/******/ "admin": 0
|
||||
/******/ };
|
||||
/******/
|
||||
/******/ // no chunk on demand loading
|
||||
/******/
|
||||
/******/ // no prefetching
|
||||
/******/
|
||||
/******/ // no preloaded
|
||||
/******/
|
||||
/******/ // no HMR
|
||||
/******/
|
||||
/******/ // no HMR manifest
|
||||
/******/
|
||||
/******/ __webpack_require__.O.j = function(chunkId) { return installedChunks[chunkId] === 0; };
|
||||
/******/
|
||||
/******/ // install a JSONP callback for chunk loading
|
||||
/******/ var webpackJsonpCallback = function(parentChunkLoadingFunction, data) {
|
||||
/******/ var chunkIds = data[0];
|
||||
/******/ var moreModules = data[1];
|
||||
/******/ var runtime = data[2];
|
||||
/******/ // add "moreModules" to the modules object,
|
||||
/******/ // then flag all "chunkIds" as loaded and fire callback
|
||||
/******/ var moduleId, chunkId, i = 0;
|
||||
/******/ if(chunkIds.some(function(id) { return installedChunks[id] !== 0; })) {
|
||||
/******/ for(moduleId in moreModules) {
|
||||
/******/ if(__webpack_require__.o(moreModules, moduleId)) {
|
||||
/******/ __webpack_require__.m[moduleId] = moreModules[moduleId];
|
||||
/******/ }
|
||||
/******/ }
|
||||
/******/ if(runtime) var result = runtime(__webpack_require__);
|
||||
/******/ }
|
||||
/******/ if(parentChunkLoadingFunction) parentChunkLoadingFunction(data);
|
||||
/******/ for(;i < chunkIds.length; i++) {
|
||||
/******/ chunkId = chunkIds[i];
|
||||
/******/ if(__webpack_require__.o(installedChunks, chunkId) && installedChunks[chunkId]) {
|
||||
/******/ installedChunks[chunkId][0]();
|
||||
/******/ }
|
||||
/******/ installedChunks[chunkId] = 0;
|
||||
/******/ }
|
||||
/******/ return __webpack_require__.O(result);
|
||||
/******/ }
|
||||
/******/
|
||||
/******/ var chunkLoadingGlobal = self["webpackChunkaircox_assets"] = self["webpackChunkaircox_assets"] || [];
|
||||
/******/ chunkLoadingGlobal.forEach(webpackJsonpCallback.bind(null, 0));
|
||||
/******/ chunkLoadingGlobal.push = webpackJsonpCallback.bind(null, chunkLoadingGlobal.push.bind(chunkLoadingGlobal));
|
||||
/******/ }();
|
||||
/******/
|
||||
/************************************************************************/
|
||||
/******/
|
||||
/******/ // startup
|
||||
/******/ // Load entry module and return exports
|
||||
/******/ // This entry module depends on other loaded chunks and execution need to be delayed
|
||||
/******/ var __webpack_exports__ = __webpack_require__.O(undefined, ["chunk-vendors","chunk-common"], function() { return __webpack_require__("./src/admin.js"); })
|
||||
/******/ __webpack_exports__ = __webpack_require__.O(__webpack_exports__);
|
||||
/******/
|
||||
/******/ })()
|
||||
;
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -1,2 +1,215 @@
|
|||
(function(){"use strict";var n={1784:function(n,t,e){var r=e(4156),o=e(1847),i=e(6294),u=e(5189),f=e(2530),c=e(6306),a=e(7079),s=e(7467),l=e(8833),p=e(5127);t["Z"]={AAutocomplete:r.Z,AEpisode:o.Z,AList:i.Z,APage:u.Z,APlayer:f.C,APlaylist:c.Z,AProgress:a.Z,ASoundItem:s.Z};l.Z,p.Z},5288:function(n,t,e){e(8880);var r=e(9643);window.App=r.Z}},t={};function e(r){var o=t[r];if(void 0!==o)return o.exports;var i=t[r]={exports:{}};return n[r](i,i.exports,e),i.exports}e.m=n,function(){var n=[];e.O=function(t,r,o,i){if(!r){var u=1/0;for(s=0;s<n.length;s++){r=n[s][0],o=n[s][1],i=n[s][2];for(var f=!0,c=0;c<r.length;c++)(!1&i||u>=i)&&Object.keys(e.O).every((function(n){return e.O[n](r[c])}))?r.splice(c--,1):(f=!1,i<u&&(u=i));if(f){n.splice(s--,1);var a=o();void 0!==a&&(t=a)}}return t}i=i||0;for(var s=n.length;s>0&&n[s-1][2]>i;s--)n[s]=n[s-1];n[s]=[r,o,i]}}(),function(){e.d=function(n,t){for(var r in t)e.o(t,r)&&!e.o(n,r)&&Object.defineProperty(n,r,{enumerable:!0,get:t[r]})}}(),function(){e.g=function(){if("object"===typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(n){if("object"===typeof window)return window}}()}(),function(){e.o=function(n,t){return Object.prototype.hasOwnProperty.call(n,t)}}(),function(){e.r=function(n){"undefined"!==typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(n,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(n,"__esModule",{value:!0})}}(),function(){var n={321:0};e.O.j=function(t){return 0===n[t]};var t=function(t,r){var o,i,u=r[0],f=r[1],c=r[2],a=0;if(u.some((function(t){return 0!==n[t]}))){for(o in f)e.o(f,o)&&(e.m[o]=f[o]);if(c)var s=c(e)}for(t&&t(r);a<u.length;a++)i=u[a],e.o(n,i)&&n[i]&&n[i][0](),n[i]=0;return e.O(s)},r=self["webpackChunkaircox_assets"]=self["webpackChunkaircox_assets"]||[];r.forEach(t.bind(null,0)),r.push=t.bind(null,r.push.bind(r))}();var r=e.O(void 0,[998,64],(function(){return e(5288)}));r=e.O(r)})();
|
||||
//# sourceMappingURL=core.js.map
|
||||
/*
|
||||
* ATTENTION: The "eval" devtool has been used (maybe by default in mode: "development").
|
||||
* This devtool is neither made for production nor for readable output files.
|
||||
* It uses "eval()" calls to create a separate source file in the browser devtools.
|
||||
* If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/)
|
||||
* or disable the default devtool with "devtool: false".
|
||||
* If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/).
|
||||
*/
|
||||
/******/ (function() { // webpackBootstrap
|
||||
/******/ "use strict";
|
||||
/******/ var __webpack_modules__ = ({
|
||||
|
||||
/***/ "./src/core.js":
|
||||
/*!*********************!*\
|
||||
!*** ./src/core.js ***!
|
||||
\*********************/
|
||||
/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
|
||||
|
||||
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _index_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./index.js */ \"./src/index.js\");\n/* harmony import */ var _app_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./app.js */ \"./src/app.js\");\n\n\n/* harmony default export */ __webpack_exports__[\"default\"] = (_app_js__WEBPACK_IMPORTED_MODULE_1__[\"default\"]);\nwindow.App = _app_js__WEBPACK_IMPORTED_MODULE_1__[\"default\"];\n\n//# sourceURL=webpack://aircox-assets/./src/core.js?");
|
||||
|
||||
/***/ })
|
||||
|
||||
/******/ });
|
||||
/************************************************************************/
|
||||
/******/ // The module cache
|
||||
/******/ var __webpack_module_cache__ = {};
|
||||
/******/
|
||||
/******/ // The require function
|
||||
/******/ function __webpack_require__(moduleId) {
|
||||
/******/ // Check if module is in cache
|
||||
/******/ var cachedModule = __webpack_module_cache__[moduleId];
|
||||
/******/ if (cachedModule !== undefined) {
|
||||
/******/ return cachedModule.exports;
|
||||
/******/ }
|
||||
/******/ // Create a new module (and put it into the cache)
|
||||
/******/ var module = __webpack_module_cache__[moduleId] = {
|
||||
/******/ id: moduleId,
|
||||
/******/ loaded: false,
|
||||
/******/ exports: {}
|
||||
/******/ };
|
||||
/******/
|
||||
/******/ // Execute the module function
|
||||
/******/ __webpack_modules__[moduleId].call(module.exports, module, module.exports, __webpack_require__);
|
||||
/******/
|
||||
/******/ // Flag the module as loaded
|
||||
/******/ module.loaded = true;
|
||||
/******/
|
||||
/******/ // Return the exports of the module
|
||||
/******/ return module.exports;
|
||||
/******/ }
|
||||
/******/
|
||||
/******/ // expose the modules object (__webpack_modules__)
|
||||
/******/ __webpack_require__.m = __webpack_modules__;
|
||||
/******/
|
||||
/************************************************************************/
|
||||
/******/ /* webpack/runtime/chunk loaded */
|
||||
/******/ !function() {
|
||||
/******/ var deferred = [];
|
||||
/******/ __webpack_require__.O = function(result, chunkIds, fn, priority) {
|
||||
/******/ if(chunkIds) {
|
||||
/******/ priority = priority || 0;
|
||||
/******/ for(var i = deferred.length; i > 0 && deferred[i - 1][2] > priority; i--) deferred[i] = deferred[i - 1];
|
||||
/******/ deferred[i] = [chunkIds, fn, priority];
|
||||
/******/ return;
|
||||
/******/ }
|
||||
/******/ var notFulfilled = Infinity;
|
||||
/******/ for (var i = 0; i < deferred.length; i++) {
|
||||
/******/ var chunkIds = deferred[i][0];
|
||||
/******/ var fn = deferred[i][1];
|
||||
/******/ var priority = deferred[i][2];
|
||||
/******/ var fulfilled = true;
|
||||
/******/ for (var j = 0; j < chunkIds.length; j++) {
|
||||
/******/ if ((priority & 1 === 0 || notFulfilled >= priority) && Object.keys(__webpack_require__.O).every(function(key) { return __webpack_require__.O[key](chunkIds[j]); })) {
|
||||
/******/ chunkIds.splice(j--, 1);
|
||||
/******/ } else {
|
||||
/******/ fulfilled = false;
|
||||
/******/ if(priority < notFulfilled) notFulfilled = priority;
|
||||
/******/ }
|
||||
/******/ }
|
||||
/******/ if(fulfilled) {
|
||||
/******/ deferred.splice(i--, 1)
|
||||
/******/ var r = fn();
|
||||
/******/ if (r !== undefined) result = r;
|
||||
/******/ }
|
||||
/******/ }
|
||||
/******/ return result;
|
||||
/******/ };
|
||||
/******/ }();
|
||||
/******/
|
||||
/******/ /* webpack/runtime/compat get default export */
|
||||
/******/ !function() {
|
||||
/******/ // getDefaultExport function for compatibility with non-harmony modules
|
||||
/******/ __webpack_require__.n = function(module) {
|
||||
/******/ var getter = module && module.__esModule ?
|
||||
/******/ function() { return module['default']; } :
|
||||
/******/ function() { return module; };
|
||||
/******/ __webpack_require__.d(getter, { a: getter });
|
||||
/******/ return getter;
|
||||
/******/ };
|
||||
/******/ }();
|
||||
/******/
|
||||
/******/ /* webpack/runtime/define property getters */
|
||||
/******/ !function() {
|
||||
/******/ // define getter functions for harmony exports
|
||||
/******/ __webpack_require__.d = function(exports, definition) {
|
||||
/******/ for(var key in definition) {
|
||||
/******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
|
||||
/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
|
||||
/******/ }
|
||||
/******/ }
|
||||
/******/ };
|
||||
/******/ }();
|
||||
/******/
|
||||
/******/ /* webpack/runtime/global */
|
||||
/******/ !function() {
|
||||
/******/ __webpack_require__.g = (function() {
|
||||
/******/ if (typeof globalThis === 'object') return globalThis;
|
||||
/******/ try {
|
||||
/******/ return this || new Function('return this')();
|
||||
/******/ } catch (e) {
|
||||
/******/ if (typeof window === 'object') return window;
|
||||
/******/ }
|
||||
/******/ })();
|
||||
/******/ }();
|
||||
/******/
|
||||
/******/ /* webpack/runtime/hasOwnProperty shorthand */
|
||||
/******/ !function() {
|
||||
/******/ __webpack_require__.o = function(obj, prop) { return Object.prototype.hasOwnProperty.call(obj, prop); }
|
||||
/******/ }();
|
||||
/******/
|
||||
/******/ /* webpack/runtime/make namespace object */
|
||||
/******/ !function() {
|
||||
/******/ // define __esModule on exports
|
||||
/******/ __webpack_require__.r = function(exports) {
|
||||
/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
|
||||
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
||||
/******/ }
|
||||
/******/ Object.defineProperty(exports, '__esModule', { value: true });
|
||||
/******/ };
|
||||
/******/ }();
|
||||
/******/
|
||||
/******/ /* webpack/runtime/node module decorator */
|
||||
/******/ !function() {
|
||||
/******/ __webpack_require__.nmd = function(module) {
|
||||
/******/ module.paths = [];
|
||||
/******/ if (!module.children) module.children = [];
|
||||
/******/ return module;
|
||||
/******/ };
|
||||
/******/ }();
|
||||
/******/
|
||||
/******/ /* webpack/runtime/jsonp chunk loading */
|
||||
/******/ !function() {
|
||||
/******/ // no baseURI
|
||||
/******/
|
||||
/******/ // object to store loaded and loading chunks
|
||||
/******/ // undefined = chunk not loaded, null = chunk preloaded/prefetched
|
||||
/******/ // [resolve, reject, Promise] = chunk loading, 0 = chunk loaded
|
||||
/******/ var installedChunks = {
|
||||
/******/ "core": 0
|
||||
/******/ };
|
||||
/******/
|
||||
/******/ // no chunk on demand loading
|
||||
/******/
|
||||
/******/ // no prefetching
|
||||
/******/
|
||||
/******/ // no preloaded
|
||||
/******/
|
||||
/******/ // no HMR
|
||||
/******/
|
||||
/******/ // no HMR manifest
|
||||
/******/
|
||||
/******/ __webpack_require__.O.j = function(chunkId) { return installedChunks[chunkId] === 0; };
|
||||
/******/
|
||||
/******/ // install a JSONP callback for chunk loading
|
||||
/******/ var webpackJsonpCallback = function(parentChunkLoadingFunction, data) {
|
||||
/******/ var chunkIds = data[0];
|
||||
/******/ var moreModules = data[1];
|
||||
/******/ var runtime = data[2];
|
||||
/******/ // add "moreModules" to the modules object,
|
||||
/******/ // then flag all "chunkIds" as loaded and fire callback
|
||||
/******/ var moduleId, chunkId, i = 0;
|
||||
/******/ if(chunkIds.some(function(id) { return installedChunks[id] !== 0; })) {
|
||||
/******/ for(moduleId in moreModules) {
|
||||
/******/ if(__webpack_require__.o(moreModules, moduleId)) {
|
||||
/******/ __webpack_require__.m[moduleId] = moreModules[moduleId];
|
||||
/******/ }
|
||||
/******/ }
|
||||
/******/ if(runtime) var result = runtime(__webpack_require__);
|
||||
/******/ }
|
||||
/******/ if(parentChunkLoadingFunction) parentChunkLoadingFunction(data);
|
||||
/******/ for(;i < chunkIds.length; i++) {
|
||||
/******/ chunkId = chunkIds[i];
|
||||
/******/ if(__webpack_require__.o(installedChunks, chunkId) && installedChunks[chunkId]) {
|
||||
/******/ installedChunks[chunkId][0]();
|
||||
/******/ }
|
||||
/******/ installedChunks[chunkId] = 0;
|
||||
/******/ }
|
||||
/******/ return __webpack_require__.O(result);
|
||||
/******/ }
|
||||
/******/
|
||||
/******/ var chunkLoadingGlobal = self["webpackChunkaircox_assets"] = self["webpackChunkaircox_assets"] || [];
|
||||
/******/ chunkLoadingGlobal.forEach(webpackJsonpCallback.bind(null, 0));
|
||||
/******/ chunkLoadingGlobal.push = webpackJsonpCallback.bind(null, chunkLoadingGlobal.push.bind(chunkLoadingGlobal));
|
||||
/******/ }();
|
||||
/******/
|
||||
/************************************************************************/
|
||||
/******/
|
||||
/******/ // startup
|
||||
/******/ // Load entry module and return exports
|
||||
/******/ // This entry module depends on other loaded chunks and execution need to be delayed
|
||||
/******/ var __webpack_exports__ = __webpack_require__.O(undefined, ["chunk-vendors","chunk-common"], function() { return __webpack_require__("./src/core.js"); })
|
||||
/******/ __webpack_exports__ = __webpack_require__.O(__webpack_exports__);
|
||||
/******/
|
||||
/******/ })()
|
||||
;
|
|
@ -1,7 +1,85 @@
|
|||
{% comment %}Inline block to edit playlists{% endcomment %}
|
||||
{% load static i18n %}
|
||||
{% load aircox aircox_admin static i18n %}
|
||||
|
||||
{% with inline_admin_formset.formset.instance as playlist %}
|
||||
{% include "adminsortable2/edit_inline/tabular-django-4.1.html" %}
|
||||
{% with inline_admin_formset as admin_formset %}
|
||||
{% with admin_formset.formset as formset %}
|
||||
|
||||
<div id="inline-tracks" class="box mb-5">
|
||||
{{ admin_formset.non_form_errors }}
|
||||
|
||||
<a-playlist-editor
|
||||
:labels="{% track_inline_labels %}"
|
||||
:init-data="{% track_inline_data formset=formset %}"
|
||||
settings-url="{% url "api:user-settings" %}"
|
||||
data-prefix="{{ formset.prefix }}-">
|
||||
<template #title>
|
||||
<h5 class="title is-4">{% trans "Playlist" %}</h5>
|
||||
</template>
|
||||
<template v-slot:top="{items}">
|
||||
<input type="hidden" name="{{ formset.prefix }}-TOTAL_FORMS"
|
||||
:value="items.length || 0"/>
|
||||
<input type="hidden" name="{{ formset.prefix }}-INITIAL_FORMS"
|
||||
value="{{ formset.initial_form_count }}"/>
|
||||
<input type="hidden" name="{{ formset.prefix }}-MIN_NUM_FORMS"
|
||||
value="{{ formset.min_num }}"/>
|
||||
<input type="hidden" name="{{ formset.prefix }}-MAX_NUM_FORMS"
|
||||
value="{{ formset.max_num }}"/>
|
||||
</template>
|
||||
<template #rows-header-head>
|
||||
<th style="max-width:2em" title="{% trans "Track Position" %}"
|
||||
aria-description="{% trans "Track Position" %}">
|
||||
<span class="icon">
|
||||
<i class="fa fa-arrow-down-1-9"></i>
|
||||
</span>
|
||||
</th>
|
||||
</template>
|
||||
<template v-slot:row-head="{item,row}">
|
||||
<td>
|
||||
[[ row+1 ]]
|
||||
<input type="hidden"
|
||||
:name="'{{ formset.prefix }}-' + row + '-position'"
|
||||
:value="row"/>
|
||||
<input t-if="item.data.id" type="hidden"
|
||||
:name="'{{ formset.prefix }}-' + row + '-id'"
|
||||
:value="item.data.id || item.id"/>
|
||||
|
||||
{% for field in admin_formset.fields %}
|
||||
{% if field.name != 'position' and field.widget.is_hidden %}
|
||||
<input type="hidden"
|
||||
:name="'{{ formset.prefix }}-' + row + '-{{ field.name }}'"
|
||||
v-model="item.data[attr]"/>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</td>
|
||||
</template>
|
||||
{% for field in admin_formset.fields %}
|
||||
{% if not field.widget.is_hidden and not field.is_readonly %}
|
||||
<template v-slot:row-{{ field.name }}="{item,cell,value,attr,emit}">
|
||||
<div class="field">
|
||||
{% if field.name in 'artist,title,album' %}
|
||||
<a-autocomplete
|
||||
:input-class="['input', item.error(attr) ? 'is-danger' : 'half-field']"
|
||||
url="{% url 'api:track-autocomplete' %}?{{ field.name }}=${query}&field={{ field.name }}"
|
||||
{% else %}
|
||||
<div class="control">
|
||||
<input type="{{ widget.type }}"
|
||||
:class="['input', item.error(attr) ? 'is-danger' : 'half-field']"
|
||||
{% endif %}
|
||||
:name="'{{ formset.prefix }}-' + cell.row + '-{{ field.name }}'"
|
||||
v-model="item.data[attr]"
|
||||
title="{{ field.help }}"
|
||||
@change="emit('change', col)"/>
|
||||
{% if field.name not in 'artist,title,album' %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<p v-for="error in item.error(attr)" class="help is-danger">
|
||||
[[ error ]] !
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</a-playlist-editor>
|
||||
</div>
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
|
||||
|
|
|
@ -18,10 +18,10 @@
|
|||
<table class="table is-hoverable is-fullwidth">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% translate "time" %}</th>
|
||||
<th>{% translate "episode" %}</th>
|
||||
<th>{% translate "track" %}</th>
|
||||
<th>{% translate "tags" %}</th>
|
||||
<th>{% translate "Time" %}</th>
|
||||
<th>{% translate "Episode" %}</th>
|
||||
<th>{% translate "Track" %}</th>
|
||||
<th>{% translate "Tags" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
|
|
@ -44,6 +44,9 @@
|
|||
{% if not init_app %}
|
||||
initBuilder: false,
|
||||
{% endif %}
|
||||
{% if init_el %}
|
||||
el: "{{ init_el }}",
|
||||
{% endif %}
|
||||
})
|
||||
{% endblock %}
|
||||
})
|
||||
|
|
|
@ -5,12 +5,13 @@ Base template used to display a Page
|
|||
|
||||
Context:
|
||||
- page: page
|
||||
- parent: parent page
|
||||
{% endcomment %}
|
||||
|
||||
{% block header_crumbs %}
|
||||
{{ block.super }}
|
||||
{% if page.category %}
|
||||
/ {{ page.category.title }}
|
||||
{% if parent %} / {% endif %} {{ page.category.title }}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
|
|
|
@ -9,8 +9,9 @@ The audio player
|
|||
role="{% translate "player" %}"
|
||||
aria-description="{% translate "Audio player used to listen to the radio and podcasts" %}">
|
||||
<noscript>
|
||||
<audio src="{{ audio_streams.0 }}" controls>
|
||||
{% for stream in audio_streams %}
|
||||
<audio src="{% if request.station.streams %}{{ request.station.streams.0 }}{% endif %}"
|
||||
controls>
|
||||
{% for stream in request.station.streams %}
|
||||
<source src="{{ stream }}" />
|
||||
{% endfor %}
|
||||
</audio>
|
||||
|
@ -18,7 +19,7 @@ The audio player
|
|||
</noscript>
|
||||
|
||||
<a-player ref="player"
|
||||
:live-args="{url: '{% url "api:live" %}', timeout:10, src: {{ audio_streams|json }} || []}"
|
||||
:live-args="{% player_live_attr %}"
|
||||
button-title="{% translate "Play or pause audio" %}">
|
||||
<template v-slot:content="{ loaded, live, current }">
|
||||
<h4 v-if="loaded" class="title is-4">
|
||||
|
|
|
@ -4,9 +4,8 @@ import json
|
|||
from django import template
|
||||
from django.contrib.admin.templatetags.admin_urls import admin_urlname
|
||||
from django.urls import reverse
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
from aircox.models import Page, Diffusion, Log
|
||||
from aircox.models import Diffusion, Log
|
||||
|
||||
random.seed()
|
||||
register = template.Library()
|
||||
|
@ -17,7 +16,8 @@ def do_admin_url(obj, arg, pass_id=True):
|
|||
""" Reverse admin url for object """
|
||||
name = admin_urlname(obj._meta, arg)
|
||||
return reverse(name, args=(obj.id,)) if pass_id else reverse(name)
|
||||
|
||||
|
||||
|
||||
@register.filter(name='get_tracks')
|
||||
def do_get_tracks(obj):
|
||||
""" Get a list of track for the provided log, diffusion, or episode """
|
||||
|
@ -28,6 +28,7 @@ def do_get_tracks(obj):
|
|||
obj = obj.episode
|
||||
return obj.track_set.all()
|
||||
|
||||
|
||||
@register.simple_tag(name='has_perm', takes_context=True)
|
||||
def do_has_perm(context, obj, perm, user=None):
|
||||
""" Return True if ``user.has_perm('[APP].[perm]_[MODEL]')`` """
|
||||
|
@ -36,18 +37,32 @@ def do_has_perm(context, obj, perm, user=None):
|
|||
return user.has_perm('{}.{}_{}'.format(
|
||||
obj._meta.app_label, perm, obj._meta.model_name))
|
||||
|
||||
|
||||
@register.filter(name='is_diffusion')
|
||||
def do_is_diffusion(obj):
|
||||
""" Return True if object is a Diffusion. """
|
||||
return isinstance(obj, Diffusion)
|
||||
|
||||
|
||||
@register.filter(name='json')
|
||||
def do_json(obj,fields=""):
|
||||
def do_json(obj, fields=""):
|
||||
""" Return object as json """
|
||||
if fields:
|
||||
obj = { k: getattr(obj,k,None) for k in ','.split(fields) }
|
||||
obj = {k: getattr(obj, k, None)
|
||||
for k in ','.split(fields)}
|
||||
return json.dumps(obj)
|
||||
|
||||
|
||||
@register.simple_tag(name='player_live_attr', takes_context=True)
|
||||
def do_player_live_attr(context):
|
||||
""" Player 'live-args' attribute value """
|
||||
station = getattr(context['request'], 'station', None)
|
||||
return json.dumps({
|
||||
'url': reverse('api:live'),
|
||||
'src': station and station.audio_streams.split('\n')
|
||||
})
|
||||
|
||||
|
||||
@register.simple_tag(name='nav_items', takes_context=True)
|
||||
def do_nav_items(context, menu, **kwargs):
|
||||
""" Render navigation items for the provided menu name. """
|
||||
|
@ -55,6 +70,7 @@ def do_nav_items(context, menu, **kwargs):
|
|||
return [(item, item.render(request, **kwargs))
|
||||
for item in station.navitem_set.filter(menu=menu)]
|
||||
|
||||
|
||||
@register.simple_tag(name='update_query')
|
||||
def do_update_query(obj, **kwargs):
|
||||
""" Replace provided querydict's values with **kwargs. """
|
||||
|
@ -65,6 +81,7 @@ def do_update_query(obj, **kwargs):
|
|||
obj.pop(k)
|
||||
return obj
|
||||
|
||||
|
||||
@register.filter(name='verbose_name')
|
||||
def do_verbose_name(obj, plural=False):
|
||||
"""
|
||||
|
@ -72,6 +89,5 @@ def do_verbose_name(obj, plural=False):
|
|||
string (can act for default values).
|
||||
"""
|
||||
return obj if isinstance(obj, str) else \
|
||||
obj._meta.verbose_name_plural if plural else \
|
||||
obj._meta.verbose_name
|
||||
|
||||
obj._meta.verbose_name_plural if plural else \
|
||||
obj._meta.verbose_name
|
||||
|
|
|
@ -1,9 +1,63 @@
|
|||
import json
|
||||
from django import template
|
||||
from django.contrib import admin
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from aircox.serializers.admin import UserSettingsSerializer
|
||||
|
||||
|
||||
__all__ = ('register', 'do_get_admin_tools', 'do_track_inline_data')
|
||||
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@register.simple_tag(name='get_admin_tools')
|
||||
def do_get_admin_tools():
|
||||
return admin.site.get_tools()
|
||||
|
||||
|
||||
@register.simple_tag(name='track_inline_data', takes_context=True)
|
||||
def do_track_inline_data(context, formset):
|
||||
"""
|
||||
Return initial data for playlist editor as dict. Keys are:
|
||||
- ``items``: list of items. Extra keys:
|
||||
- ``__error__``: dict of form fields errors
|
||||
- ``settings``: user's settings
|
||||
"""
|
||||
items = []
|
||||
for form in formset.forms:
|
||||
item = {name: form[name].value()
|
||||
for name in form.fields.keys()}
|
||||
item['__errors__'] = form.errors
|
||||
|
||||
# hack for playlist editor
|
||||
tags = item.get('tags')
|
||||
if tags and not isinstance(tags, str):
|
||||
item['tags'] = ', '.join(tag.name for tag in tags)
|
||||
items.append(item)
|
||||
|
||||
data = {"items": items}
|
||||
user = context['request'].user
|
||||
settings = getattr(user, 'aircox_settings', None)
|
||||
data['settings'] = settings and UserSettingsSerializer(settings).data
|
||||
source = json.dumps(data)
|
||||
return source
|
||||
|
||||
|
||||
track_inline_labels_ = {
|
||||
'artist': _('Artist'), 'album': _('Album'), 'title': _('Title'),
|
||||
'tags': _('Tags'), 'year': _('Year'),
|
||||
'save_settings': _('Save Settings'),
|
||||
'discard_changes': _('Discard changes'),
|
||||
'columns': _('Columns'),
|
||||
'add_track': _('Add a track'),
|
||||
'remove_track': _('Remove'),
|
||||
'timestamp': _('Timestamp'),
|
||||
}
|
||||
|
||||
|
||||
@register.simple_tag(name='track_inline_labels')
|
||||
def do_track_inline_labels():
|
||||
""" Return labels for columns in playlist editor as dict """
|
||||
return json.dumps({k: str(v) for k, v in track_inline_labels_.items()})
|
||||
|
|
|
@ -24,10 +24,14 @@ register_converter(WeekConverter, 'week')
|
|||
|
||||
router = DefaultRouter()
|
||||
router.register('sound', viewsets.SoundViewSet, basename='sound')
|
||||
router.register('track', viewsets.TrackROViewSet, basename='track')
|
||||
|
||||
|
||||
api = [
|
||||
path('logs/', views.LogListAPIView.as_view(), name='live'),
|
||||
path('user/settings/', viewsets.UserSettingsViewSet.as_view(
|
||||
{'get': 'retrieve', 'post': 'update', 'put': 'update'}),
|
||||
name='user-settings'),
|
||||
] + router.urls
|
||||
|
||||
|
||||
|
|
|
@ -1,14 +1,10 @@
|
|||
|
||||
from django.http import Http404
|
||||
from django.views.generic import DetailView, ListView
|
||||
from django.views.generic.base import TemplateResponseMixin, ContextMixin
|
||||
from django.urls import reverse
|
||||
|
||||
from ..models import Page
|
||||
from ..utils import Redirect
|
||||
|
||||
|
||||
__all__ = ['BaseView']
|
||||
__all__ = ('BaseView', 'BaseAPIView')
|
||||
|
||||
|
||||
class BaseView(TemplateResponseMixin, ContextMixin):
|
||||
|
@ -55,13 +51,14 @@ class BaseView(TemplateResponseMixin, ContextMixin):
|
|||
kwargs['audio_streams'] = streams
|
||||
|
||||
if 'model' not in kwargs:
|
||||
model = getattr(self, 'model', None) or hasattr(self, 'object') and \
|
||||
type(self.object)
|
||||
model = getattr(self, 'model', None) or \
|
||||
hasattr(self, 'object') and type(self.object)
|
||||
kwargs['model'] = model
|
||||
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
# FIXME: rename to sth like [Base]?StationAPIView
|
||||
class BaseAPIView:
|
||||
@property
|
||||
def station(self):
|
||||
|
|
|
@ -1,13 +1,18 @@
|
|||
from django.db.models import Q
|
||||
|
||||
from rest_framework import viewsets
|
||||
from rest_framework import status, viewsets
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from django_filters import rest_framework as filters
|
||||
|
||||
from .models import Sound
|
||||
from .serializers import SoundSerializer
|
||||
from .models import Sound, Track
|
||||
from .serializers import SoundSerializer, admin
|
||||
from .views import BaseAPIView
|
||||
|
||||
|
||||
__all__ = ('SoundFilter', 'SoundViewSet', 'TrackFilter', 'TrackROViewSet',
|
||||
'UserSettingsViewSet')
|
||||
|
||||
|
||||
class SoundFilter(filters.FilterSet):
|
||||
station = filters.NumberFilter(field_name='program__station__id')
|
||||
program = filters.NumberFilter(field_name='program_id')
|
||||
|
@ -24,3 +29,63 @@ class SoundViewSet(BaseAPIView, viewsets.ModelViewSet):
|
|||
filter_backends = (filters.DjangoFilterBackend,)
|
||||
filterset_class = SoundFilter
|
||||
|
||||
|
||||
# --- admin
|
||||
class TrackFilter(filters.FilterSet):
|
||||
artist = filters.CharFilter(field_name='artist', lookup_expr='icontains')
|
||||
album = filters.CharFilter(field_name='album', lookup_expr='icontains')
|
||||
title = filters.CharFilter(field_name='title', lookup_expr='icontains')
|
||||
|
||||
|
||||
class TrackROViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
""" Track viewset used for auto completion """
|
||||
serializer_class = admin.TrackSerializer
|
||||
permission_classes = [IsAuthenticated]
|
||||
filter_backends = (filters.DjangoFilterBackend,)
|
||||
filterset_class = TrackFilter
|
||||
queryset = Track.objects.all()
|
||||
|
||||
@action(name='autocomplete', detail=False)
|
||||
def autocomplete(self, request):
|
||||
field = request.GET.get('field', None)
|
||||
if field:
|
||||
queryset = self.filter_queryset(self.get_queryset())
|
||||
values = queryset.values_list(field, flat=True).distinct()
|
||||
return Response(values)
|
||||
return self.list(request)
|
||||
|
||||
|
||||
class UserSettingsViewSet(viewsets.ViewSet):
|
||||
"""
|
||||
User's settings specific to aircox. Allow only to create and edit
|
||||
user's own settings.
|
||||
"""
|
||||
serializer_class = admin.UserSettingsSerializer
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get_serializer(self, instance=None, **kwargs):
|
||||
return self.serializer_class(
|
||||
instance=instance, context={'user': self.request.user},
|
||||
**kwargs)
|
||||
|
||||
@action(detail=False, methods=['GET'])
|
||||
def retrieve(self, request):
|
||||
user = self.request.user
|
||||
settings = getattr(user, 'aircox_settings', None)
|
||||
data = settings and self.get_serializer(settings) or None
|
||||
return Response(data)
|
||||
|
||||
@action(detail=False, methods=['POST', 'PUT'])
|
||||
def update(self, request):
|
||||
user = self.request.user
|
||||
settings = getattr(user, 'aircox_settings', None)
|
||||
data = dict(request.data)
|
||||
data['user_id'] = self.request.user
|
||||
serializer = self.get_serializer(instance=settings, data=request.data)
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
return Response({'status': 'ok'})
|
||||
else:
|
||||
return Response({'errors': serializer.errors},
|
||||
status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
|
|
@ -24,8 +24,8 @@ from django.utils import timezone as tz
|
|||
from aircox.models import Station, Episode, Diffusion, Track, Sound, Log
|
||||
from aircox.utils import date_range
|
||||
|
||||
from aircox_streamer.controllers import Streamer
|
||||
|
||||
from aircox_streamer.controllers import Streamer
|
||||
|
||||
# force using UTC
|
||||
tz.activate(pytz.UTC)
|
||||
|
|
|
@ -53,7 +53,7 @@
|
|||
{# TODO: select station => change the shit #}
|
||||
<a-autocomplete class="control is-expanded"
|
||||
url="{% url "aircox:sound-list" %}?station={{ station.pk }}&search=${query}"
|
||||
name="sound_id" :model="Sound" label-field="name"
|
||||
name="sound_id" :model="Sound" value-field="id" label-field="name"
|
||||
placeholder="{% translate "Select a sound" %}">
|
||||
<template v-slot:item="{item}">
|
||||
[[ item.data.name ]]
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "^6.0.0",
|
||||
"core-js": "^3.8.3",
|
||||
"lodash": "^4.17.21",
|
||||
"vue": "^3.2.13"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
@ -4,10 +4,18 @@ import './index.js'
|
|||
|
||||
import App from './app';
|
||||
import {admin as components} from './components'
|
||||
import Track from './track'
|
||||
|
||||
const AdminApp = {
|
||||
...App,
|
||||
components: {...App.components, ...components},
|
||||
|
||||
data() {
|
||||
return {
|
||||
...super.data,
|
||||
Track,
|
||||
}
|
||||
}
|
||||
}
|
||||
export default AdminApp;
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ $menu-item-active-background-color: #d2d2d2;
|
|||
|
||||
//-- helpers/modifiers
|
||||
.is-fullwidth { width: 100%; }
|
||||
.is-fullheight { height: 100%; }
|
||||
.is-fixed-bottom {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
|
@ -40,6 +41,19 @@ $menu-item-active-background-color: #d2d2d2;
|
|||
.overflow-hidden.is-fullwidth { max-width: 100%; }
|
||||
|
||||
|
||||
*[draggable="true"] {
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
//-- forms
|
||||
input.half-field:not(:active):not(:hover) {
|
||||
border: none;
|
||||
background-color: rgba(0,0,0,0);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
||||
//-- animations
|
||||
@keyframes blink {
|
||||
from { opacity: 1; }
|
||||
to { opacity: 0.4; }
|
||||
|
@ -215,6 +229,10 @@ a.navbar-item.is-active {
|
|||
.player-bar {
|
||||
border-top: 1px $grey-light solid;
|
||||
|
||||
> div {
|
||||
height: 3.75em !important;
|
||||
}
|
||||
|
||||
> .media-left:not(:last-child) {
|
||||
margin-right: 0em;
|
||||
}
|
||||
|
@ -235,7 +253,8 @@ a.navbar-item.is-active {
|
|||
|
||||
.button {
|
||||
font-size: 1.5rem !important;
|
||||
height: 2.5em;
|
||||
height: 100%;
|
||||
padding: auto 0.2em !important;
|
||||
min-width: 2.5em;
|
||||
border-radius: 0px;
|
||||
transition: background-color 1s;
|
||||
|
|
78
assets/src/components/AActionButton.vue
Normal file
78
assets/src/components/AActionButton.vue
Normal file
|
@ -0,0 +1,78 @@
|
|||
<template>
|
||||
<component :is="tag" @click="call" :class="buttonClass">
|
||||
<span v-if="promise && runIcon">
|
||||
<i :class="runIcon"></i>
|
||||
</span>
|
||||
<span v-else-if="icon" class="icon">
|
||||
<i :class="icon"></i>
|
||||
</span>
|
||||
<span v-if="$slots.default"><slot name="default"/></span>
|
||||
</component>
|
||||
</template>
|
||||
<script>
|
||||
import Model from '../model'
|
||||
|
||||
/**
|
||||
* Button that can be used to call API requests on provided url
|
||||
*/
|
||||
export default {
|
||||
emit: ['start', 'done'],
|
||||
|
||||
props: {
|
||||
//! Component tag, by default, `button`
|
||||
tag: { type: String, default: 'a'},
|
||||
//! Button icon
|
||||
icon: String,
|
||||
//! Data or model instance to send
|
||||
data: Object,
|
||||
//! Action method, by default, `POST`
|
||||
method: { type: String, default: 'POST'},
|
||||
//! Action url
|
||||
url: String,
|
||||
//! Extra request options
|
||||
fetchOptions: {type: Object, default: () => {return {}}},
|
||||
//! Component class while action is running
|
||||
runClass: String,
|
||||
//! Icon class while action is running
|
||||
runIcon: String,
|
||||
},
|
||||
|
||||
computed: {
|
||||
//! Input data as model instance
|
||||
item() {
|
||||
return this.data instanceof Model ? this.data
|
||||
: new Model(this.data)
|
||||
},
|
||||
|
||||
//! Computed button class
|
||||
buttonClass() {
|
||||
return this.promise ? this.runClass : ''
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
promise: false
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
call() {
|
||||
if(this.promise || !this.url)
|
||||
return
|
||||
const options = Model.getOptions({
|
||||
...this.fetchOptions,
|
||||
method: this.method,
|
||||
body: JSON.stringify(this.item.data),
|
||||
})
|
||||
this.promise = fetch(this.url, options).then(data => {
|
||||
const response = data.json();
|
||||
this.promise = null;
|
||||
this.$emit('done', response)
|
||||
return response
|
||||
}, data => { this.promise = null; return data })
|
||||
return this.promise
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
|
@ -1,37 +1,44 @@
|
|||
<template>
|
||||
<div :class="dropdownClass">
|
||||
<div class="dropdown-trigger is-fullwidth">
|
||||
<input type="hidden" :name="name"
|
||||
:value="selectedValue" />
|
||||
<div v-show="!selected" class="control is-expanded">
|
||||
<input type="text" :placeholder="placeholder"
|
||||
ref="input" class="input is-fullwidth"
|
||||
@keydown.capture="onKeyPress"
|
||||
@keyup="onKeyUp" @focus="this.cursor < 0 && move(0)"/>
|
||||
</div>
|
||||
<button v-if="selected" class="button is-normal is-fullwidth has-text-left is-inline-block overflow-hidden"
|
||||
@click="select(-1, false, true)">
|
||||
<span class="icon is-small ml-1">
|
||||
<i class="fa fa-pen"></i>
|
||||
</span>
|
||||
<span class="is-inline-block" v-if="selected">
|
||||
<slot name="button" :index="selectedIndex" :item="selected"
|
||||
:value-field="valueField" :labelField="labelField">
|
||||
{{ selected.data[labelField] }}
|
||||
</slot>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="dropdown-menu is-fullwidth">
|
||||
<div class="dropdown-content" style="overflow: hidden">
|
||||
<a v-for="(item, index) in items" :key="item.id"
|
||||
:class="['dropdown-item', (index == this.cursor) ? 'is-active':'']"
|
||||
@click.capture.prevent="select(index, false, false)" :title="item.data[labelField]">
|
||||
<slot name="item" :index="index" :item="item" :value-field="valueField"
|
||||
:labelField="labelField">
|
||||
{{ item.data[labelField] }}
|
||||
</slot>
|
||||
</a>
|
||||
<div class="control">
|
||||
<input type="hidden" :name="name" :value="selectedValue"
|
||||
@change="$emit('change', $event)"/>
|
||||
<input type="text" ref="input" class="input is-fullwidth" :class="inputClass"
|
||||
v-show="!button || !selected"
|
||||
v-model="inputValue"
|
||||
:placeholder="placeholder"
|
||||
@keydown.capture="onKeyDown"
|
||||
@keyup="onKeyUp($event); $emit('keyup', $event)"
|
||||
@keydown="$emit('keydown', $event)"
|
||||
@keypress="$emit('keypress', $event)"
|
||||
@focus="onInputFocus" @blur="onBlur" />
|
||||
<a v-if="selected && button"
|
||||
class="button is-normal is-fullwidth has-text-left is-inline-block overflow-hidden"
|
||||
@click="select(-1, false, true)">
|
||||
<span class="icon is-small ml-1">
|
||||
<i class="fa fa-pen"></i>
|
||||
</span>
|
||||
<span class="is-inline-block" v-if="selected">
|
||||
<slot name="button" :index="selectedIndex" :item="selected"
|
||||
:value-field="valueField" :labelField="labelField">
|
||||
{{ labelField && selected.data[labelField] || selected }}
|
||||
</slot>
|
||||
</span>
|
||||
</a>
|
||||
<div :class="dropdownClass">
|
||||
<div class="dropdown-menu is-fullwidth">
|
||||
<div class="dropdown-content" style="overflow: hidden">
|
||||
<a v-for="(item, index) in items" :key="item.id"
|
||||
href="#" :data-autocomplete-index="index"
|
||||
@click="select(index, false, false)"
|
||||
:class="['dropdown-item', (index == this.cursor) ? 'is-active':'']"
|
||||
:title="labelField && item.data[labelField] || item"
|
||||
tabindex="-1">
|
||||
<slot name="item" :index="index" :item="item" :value-field="valueField"
|
||||
:labelField="labelField">
|
||||
{{ labelField && item.data[labelField] || item }}
|
||||
</slot>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -39,29 +46,63 @@
|
|||
|
||||
<script>
|
||||
// import debounce from 'lodash/debounce'
|
||||
import Model from '../model'
|
||||
|
||||
|
||||
export default {
|
||||
emit: ['change', 'keypress', 'keydown', 'keyup', 'select', 'unselect',
|
||||
'update:modelValue'],
|
||||
|
||||
props: {
|
||||
//! Search URL (where `${query}` is replaced by search term)
|
||||
url: String,
|
||||
//! Items' model
|
||||
model: Function,
|
||||
//! Input tag class
|
||||
inputClass: Array,
|
||||
//! input text placeholder
|
||||
placeholder: String,
|
||||
//! input form field name
|
||||
name: String,
|
||||
//! Field on items to use as label
|
||||
labelField: String,
|
||||
//! Field on selected item to get selectedValue from, if any
|
||||
valueField: {type: String, default: null},
|
||||
count: {type: Number, count: 10},
|
||||
//! If true, show button when value has been selected
|
||||
button: Boolean,
|
||||
//! If true, value must come from a selection
|
||||
mustExist: {type: Boolean, default: false},
|
||||
//! Minimum input size before fetching
|
||||
minFetchLength: {type: Number, default: 3},
|
||||
modelValue: {default: ''},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
value: '',
|
||||
inputValue: this.modelValue || '',
|
||||
query: '',
|
||||
items: [],
|
||||
selectedIndex: -1,
|
||||
cursor: -1,
|
||||
isFetching: false,
|
||||
promise: null,
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
modelValue(value) {
|
||||
this.inputValue = value
|
||||
},
|
||||
|
||||
inputValue(value) {
|
||||
if(value != this.inputValue && value != this.modelValue)
|
||||
this.$emit('update:modelValue', value)
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
isFetching() { return !!this.promise },
|
||||
|
||||
selected() {
|
||||
let index = this.selectedIndex
|
||||
if(index<0)
|
||||
|
@ -71,23 +112,39 @@ export default {
|
|||
},
|
||||
|
||||
selectedValue() {
|
||||
const sel = this.selected
|
||||
return sel && (this.valueField ?
|
||||
sel.data[this.valueField] : sel.id)
|
||||
let value = this.itemValue(this.selected)
|
||||
if(!value && !this.mustExist)
|
||||
value = this.inputValue
|
||||
return value
|
||||
},
|
||||
|
||||
selectedLabel() {
|
||||
const sel = this.selected
|
||||
return sel && sel.data[this.labelField]
|
||||
return this.itemLabel(this.selected)
|
||||
},
|
||||
|
||||
dropdownClass() {
|
||||
const active = this.cursor > -1 && this.items.length;
|
||||
return ['dropdown', active ? 'is-active':'']
|
||||
var active = this.cursor > -1 && this.items.length;
|
||||
if(active && this.items.length == 1 &&
|
||||
this.itemValue(this.items[0]) == this.inputValue)
|
||||
active = false
|
||||
return ['dropdown is-fullwidth', active ? 'is-active':'']
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
itemValue(item) {
|
||||
return this.valueField ? item && item[this.valueField] : item;
|
||||
},
|
||||
|
||||
itemLabel(item) {
|
||||
return this.labelField ? item && item[this.labelField] : item;
|
||||
},
|
||||
|
||||
hide() {
|
||||
this.cursor = -1;
|
||||
this.selectedIndex = -1;
|
||||
},
|
||||
|
||||
move(index=-1, relative=false) {
|
||||
if(relative)
|
||||
index += this.cursor
|
||||
|
@ -100,9 +157,9 @@ export default {
|
|||
else if(index == this.selectedIndex)
|
||||
return
|
||||
|
||||
this.selectedIndex = Math.max(-1, Math.min(index, this.items.length-1))
|
||||
this.selectedIndex = Math.max(-1, Math.min(index, this.items.length-1))
|
||||
if(index >= 0) {
|
||||
this.$refs.input.value = this.selectedLabel
|
||||
this.inputValue = this.selectedLabel
|
||||
this.$refs.input.focus()
|
||||
}
|
||||
if(this.selectedIndex < 0)
|
||||
|
@ -114,11 +171,24 @@ export default {
|
|||
active && this.move(0) || this.move(-1)
|
||||
},
|
||||
|
||||
onKeyPress: function(event) {
|
||||
onInputFocus() {
|
||||
this.cursor < 0 && this.move(0)
|
||||
},
|
||||
|
||||
onBlur(event) {
|
||||
var index = event.relatedTarget && event.relatedTarget.dataset.autocompleteIndex;
|
||||
if(index !== undefined)
|
||||
this.select(index, false, false)
|
||||
this.cursor = -1;
|
||||
},
|
||||
|
||||
onKeyDown(event) {
|
||||
if(event.ctrlKey || event.altKey || event.metaKey)
|
||||
return
|
||||
switch(event.keyCode) {
|
||||
case 13: this.select(this.cursor, false, false)
|
||||
break
|
||||
case 27: this.select()
|
||||
case 27: this.hide(); this.select()
|
||||
break
|
||||
case 38: this.move(-1, true)
|
||||
break
|
||||
|
@ -130,35 +200,47 @@ export default {
|
|||
event.stopPropagation()
|
||||
},
|
||||
|
||||
onKeyUp: function(event) {
|
||||
const value = event.target.value
|
||||
if(value === this.value)
|
||||
onKeyUp(event) {
|
||||
if(event.ctrlKey || event.altKey || event.metaKey)
|
||||
return
|
||||
|
||||
this.value = value;
|
||||
const value = event.target.value
|
||||
if(value === this.query)
|
||||
return
|
||||
|
||||
this.inputValue = value;
|
||||
if(!value)
|
||||
return this.selected && this.select(-1)
|
||||
|
||||
this.fetch(value)
|
||||
if(!this.minFetchLength || value.length >= this.minFetchLength)
|
||||
this.fetch(value)
|
||||
},
|
||||
|
||||
fetch: function(query) {
|
||||
if(!query || this.isFetching)
|
||||
fetch(query) {
|
||||
if(!query || this.promise)
|
||||
return
|
||||
|
||||
this.isFetching = true
|
||||
return this.model.fetch(this.url.replace('${query}', query), {many:true})
|
||||
.then(items => { this.items = items || []
|
||||
this.isFetching = false
|
||||
this.move(0)
|
||||
return items },
|
||||
data => {this.isFetching = false; Promise.reject(data)})
|
||||
this.query = query
|
||||
var url = this.url.replace('${query}', query)
|
||||
var promise = this.model ? this.model.fetch(url, {many:true})
|
||||
: fetch(url, Model.getOptions()).then(d => d.json())
|
||||
|
||||
promise = promise.then(items => {
|
||||
this.items = items || []
|
||||
this.promise = null;
|
||||
this.move(0)
|
||||
return items
|
||||
}, data => {this.promise = null; Promise.reject(data)})
|
||||
this.promise = promise
|
||||
return promise
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
const form = this.$el.closest('form')
|
||||
form.addEventListener('reset', () => { this.value=''; this.select(-1) })
|
||||
form.addEventListener('reset', () => {
|
||||
this.inputValue = this.value;
|
||||
this.select(-1)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,19 +1,22 @@
|
|||
<template>
|
||||
<div>
|
||||
<!-- FIXME: header and footer should be inside list tags -->
|
||||
<slot name="header"></slot>
|
||||
<ul :class="listClass">
|
||||
<component :is="listTag" :class="listClass">
|
||||
<template v-for="(item,index) in items" :key="index">
|
||||
<li :class="itemClass" @click="select(index)">
|
||||
<component :is="itemTag" :class="itemClass" @click="select(index)"
|
||||
:draggable="orderable" :data-index="index"
|
||||
@dragstart="onDragStart" @dragover="onDragOver" @drop="onDrop">
|
||||
<slot name="item" :selected="index == selectedIndex" :set="set" :index="index" :item="item"></slot>
|
||||
</li>
|
||||
</component>
|
||||
</template>
|
||||
</ul>
|
||||
</component>
|
||||
<slot name="footer"></slot>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
emits: ['select', 'unselect'],
|
||||
emits: ['select', 'unselect', 'move'],
|
||||
data() {
|
||||
return {
|
||||
selectedIndex: this.defaultIndex,
|
||||
|
@ -25,6 +28,9 @@ export default {
|
|||
itemClass: String,
|
||||
defaultIndex: { type: Number, default: -1},
|
||||
set: Object,
|
||||
orderable: { type: Boolean, default: false },
|
||||
itemTag: { default: 'li' },
|
||||
listTag: { default: 'ul' },
|
||||
},
|
||||
|
||||
computed: {
|
||||
|
@ -61,6 +67,34 @@ export default {
|
|||
this.$emit('unselect', { item: this.selected, index: this.selectedIndex});
|
||||
this.selectedIndex = -1;
|
||||
},
|
||||
|
||||
onDragStart(ev) {
|
||||
const dataset = ev.target.dataset;
|
||||
const data = `row:${dataset.index}`
|
||||
ev.dataTransfer.setData("text/cell", data)
|
||||
ev.dataTransfer.dropEffect = 'move'
|
||||
},
|
||||
|
||||
onDragOver(ev) {
|
||||
ev.preventDefault()
|
||||
ev.dataTransfer.dropEffect = 'move'
|
||||
},
|
||||
|
||||
onDrop(ev) {
|
||||
const data = ev.dataTransfer.getData("text/cell")
|
||||
if(!data || !data.startsWith('row:'))
|
||||
return
|
||||
|
||||
ev.preventDefault()
|
||||
const from = Number(data.slice(4))
|
||||
const target = ev.target.tagName == this.itemTag ? ev.target
|
||||
: ev.target.closest(this.itemTag)
|
||||
this.$emit('move', {
|
||||
from, target,
|
||||
to: Number(target.dataset.index),
|
||||
set: this.set,
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div class="player">
|
||||
<div :class="['player-panels', panel ? 'is-open' : '']">
|
||||
<APlaylist ref="pin" class="player-panel menu" v-show="panel == 'pin'"
|
||||
<APlaylist ref="pin" class="player-panel menu" v-show="panel == 'pin' && sets.pin.length"
|
||||
name="Pinned"
|
||||
:actions="['page']"
|
||||
:editable="true" :player="self" :set="sets.pin" @select="togglePlay('pin', $event.index)"
|
||||
|
@ -13,7 +13,7 @@
|
|||
</p>
|
||||
</template>
|
||||
</APlaylist>
|
||||
<APlaylist ref="queue" class="player-panel menu" v-show="panel == 'queue'"
|
||||
<APlaylist ref="queue" class="player-panel menu" v-show="panel == 'queue' && sets.queue.length"
|
||||
:actions="['page']"
|
||||
:editable="true" :player="self" :set="sets.queue" @select="togglePlay('queue', $event.index)"
|
||||
listClass="menu-list" itemClass="menu-item">
|
||||
|
@ -51,14 +51,14 @@
|
|||
<span>Live</span>
|
||||
</button>
|
||||
<button ref="pinPlaylistButton" :class="playlistButtonClass('pin')"
|
||||
@click="togglePanel('pin')">
|
||||
<span class="mr-2 is-size-6" v-if="sets.pin.length">
|
||||
@click="togglePanel('pin')" v-show="sets.pin.length">
|
||||
<span class="is-size-6" v-if="sets.pin.length">
|
||||
{{ sets.pin.length }}</span>
|
||||
<span class="icon"><span class="fa fa-thumbtack"></span></span>
|
||||
</button>
|
||||
<button :class="playlistButtonClass('queue')"
|
||||
@click="togglePanel('queue')">
|
||||
<span class="mr-2 is-size-6" v-if="sets.queue.length">
|
||||
@click="togglePanel('queue')" v-show="sets.queue.length">
|
||||
<span class="is-size-6" v-if="sets.queue.length">
|
||||
{{ sets.queue.length }}</span>
|
||||
<span class="icon"><span class="fa fa-list"></span></span>
|
||||
</button>
|
||||
|
@ -186,13 +186,11 @@ export default {
|
|||
if(!item)
|
||||
throw `No sound at index ${index} for playlist ${playlist}`;
|
||||
this.loaded = item
|
||||
this.current = item
|
||||
src = item.src;
|
||||
}
|
||||
// from live
|
||||
else {
|
||||
this.loaded = null;
|
||||
this.current = this.live.current
|
||||
src = this.live.src;
|
||||
}
|
||||
|
||||
|
|
329
assets/src/components/APlaylistEditor.vue
Normal file
329
assets/src/components/APlaylistEditor.vue
Normal file
|
@ -0,0 +1,329 @@
|
|||
<template>
|
||||
<div class="playlist-editor">
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<slot name="title" />
|
||||
</div>
|
||||
<div class="column has-text-right">
|
||||
<div class="float-right field has-addons">
|
||||
<p class="control">
|
||||
<a :class="['button','p-2', page == Page.Text ? 'is-primary' : 'is-light']"
|
||||
@click="page = Page.Text">
|
||||
<span class="icon is-small">
|
||||
<i class="fa fa-pencil"></i>
|
||||
</span>
|
||||
<span>Texte</span>
|
||||
</a>
|
||||
</p>
|
||||
<p class="control">
|
||||
<a :class="['button','p-2', page == Page.List ? 'is-primary' : 'is-light']"
|
||||
@click="page = Page.List">
|
||||
<span class="icon is-small">
|
||||
<i class="fa fa-list"></i>
|
||||
</span>
|
||||
<span>Liste</span>
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<slot name="top" :set="set" :columns="columns" :items="items"/>
|
||||
<section class="page" v-show="page == Page.Text">
|
||||
<textarea ref="textarea" class="is-fullwidth is-size-6" rows="20"
|
||||
@change="updateList"
|
||||
/>
|
||||
|
||||
</section>
|
||||
<section class="page" v-show="page == Page.List">
|
||||
<a-rows :set="set" :columns="columns" :labels="labels"
|
||||
:allow-create="true"
|
||||
:orderable="true" @move="listItemMove" @colmove="columnMove"
|
||||
@cell="onCellEvent">
|
||||
<template v-for="[name,slot] of rowsSlots" :key="slot"
|
||||
v-slot:[slot]="data">
|
||||
<slot v-if="name != 'row-tail'" :name="name" v-bind="data"/>
|
||||
</template>
|
||||
|
||||
<template v-slot:row-tail="data">
|
||||
<slot v-if="$slots['row-tail']" :name="row-tail" v-bind="data"/>
|
||||
<td>
|
||||
<a class="button is-danger is-outlined p-3 is-size-6"
|
||||
@click="items.splice(data.row,1)"
|
||||
:title="labels.remove_track"
|
||||
:aria-label="labels.remove_track">
|
||||
<span class="icon"><i class="fa fa-trash" /></span>
|
||||
</a>
|
||||
</td>
|
||||
</template>
|
||||
</a-rows>
|
||||
</section>
|
||||
|
||||
<div class="mt-2">
|
||||
<div class="float-right">
|
||||
<a class="button is-warning p-2 ml-2"
|
||||
@click="loadData({items: this.initData.items},true)">
|
||||
<span class="icon"><i class="fa fa-rotate" /></span>
|
||||
<span>{{ labels.discard_changes }}</span>
|
||||
</a>
|
||||
<a class="button is-primary p-2 ml-2" t-if="page == page.List"
|
||||
@click="this.set.push(new this.set.model())">
|
||||
<span class="icon"><i class="fa fa-plus"/></span>
|
||||
<span>{{ labels.add_track }}</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="field is-inline-block is-vcentered mr-3">
|
||||
<label class="label is-inline mr-2"
|
||||
style="vertical-align: middle">
|
||||
Séparateur</label>
|
||||
<div class="control is-inline-block"
|
||||
style="vertical-align: middle;">
|
||||
<input type="text" ref="sep" class="input is-inline is-text-centered is-small"
|
||||
style="max-width: 5em;"
|
||||
v-model="separator" @change="updateList()"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field is-inline-block is-vcentered mr-3">
|
||||
<label class="label is-inline mr-2"
|
||||
style="vertical-align: middle">
|
||||
{{ labels.columns }}</label>
|
||||
<table class="table is-bordered is-inline-block"
|
||||
style="vertical-align: middle">
|
||||
<tr>
|
||||
<a-row :columns="columns" :item="labels"
|
||||
@move="formatMove" :orderable="true">
|
||||
<template v-slot:cell-after="{cell}">
|
||||
<td style="cursor:pointer;" v-if="cell.col < columns.length-1">
|
||||
<span class="icon" @click="formatMove({from: cell.col, to: cell.col+1})"
|
||||
><i class="fa fa-left-right"/>
|
||||
</span>
|
||||
</td>
|
||||
</template>
|
||||
</a-row>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="field is-vcentered is-inline-block"
|
||||
v-if="settingsChanged">
|
||||
<a-action-button icon="fa fa-floppy-disk"
|
||||
class="button control p-3 is-info" run-class="blink"
|
||||
:url="settingsUrl" method="POST"
|
||||
:data="settings"
|
||||
:aria-label="labels.save_settings"
|
||||
@done="settingsSaved()">
|
||||
{{ labels.save_settings }}
|
||||
</a-action-button>
|
||||
</div>
|
||||
</div>
|
||||
<slot name="bottom" :set="set" :columns="columns" :items="items"/>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import {dropRightWhile, cloneDeep, isEqual} from 'lodash'
|
||||
import {Set} from '../model'
|
||||
import Track from '../track'
|
||||
|
||||
import AActionButton from './AActionButton'
|
||||
import ARow from './ARow.vue'
|
||||
import ARows from './ARows.vue'
|
||||
|
||||
/// Page display
|
||||
export const Page = {
|
||||
Text: 0, List: 1, Settings: 2,
|
||||
}
|
||||
|
||||
export default {
|
||||
components: { AActionButton, ARow, ARows },
|
||||
props: {
|
||||
initData: Object,
|
||||
dataPrefix: String,
|
||||
labels: Object,
|
||||
settingsUrl: String,
|
||||
defaultColumns: {
|
||||
type: Array,
|
||||
default: () => ['artist', 'title', 'tags', 'album', 'year', 'timestamp']},
|
||||
},
|
||||
|
||||
data() {
|
||||
const settings = {
|
||||
playlist_editor_columns: this.defaultColumns,
|
||||
playlist_editor_sep: ' -- ',
|
||||
}
|
||||
return {
|
||||
Page: Page,
|
||||
page: Page.Text,
|
||||
set: new Set(Track),
|
||||
extraData: {},
|
||||
settings,
|
||||
savedSettings: cloneDeep(settings),
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
settingsChanged() {
|
||||
var k = Object.keys(this.savedSettings)
|
||||
.findIndex(k => !isEqual(this.settings[k], this.savedSettings[k]))
|
||||
return k != -1
|
||||
},
|
||||
|
||||
separator: {
|
||||
set(value) {
|
||||
this.settings.playlist_editor_sep = value
|
||||
if(this.page == Page.List)
|
||||
this.updateInput()
|
||||
},
|
||||
get() { return this.settings.playlist_editor_sep }
|
||||
},
|
||||
|
||||
columns: {
|
||||
set(value) {
|
||||
var cols = value.filter(x => x in this.defaultColumns)
|
||||
var left = this.defaultColumns.filter(x => !(x in cols))
|
||||
value = cols.concat(left)
|
||||
this.settings.playlist_editor_columns = value
|
||||
},
|
||||
get() {
|
||||
return this.settings.playlist_editor_columns
|
||||
}
|
||||
},
|
||||
|
||||
items() {
|
||||
return this.set.items
|
||||
},
|
||||
|
||||
rowsSlots() {
|
||||
return Object.keys(this.$slots)
|
||||
.filter(x => x.startsWith('row-') || x.startsWith('rows-'))
|
||||
.map(x => [x, x.startsWith('rows-') ? x.slice(5) : x])
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
onCellEvent(event) {
|
||||
switch(event.name) {
|
||||
case 'change': this.updateInput();
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
formatMove({from, to}) {
|
||||
const value = this.columns[from]
|
||||
this.settings.playlist_editor_columns.splice(from, 1)
|
||||
this.settings.playlist_editor_columns.splice(to, 0, value)
|
||||
if(this.page == Page.Text)
|
||||
this.updateList()
|
||||
else
|
||||
this.updateInput()
|
||||
},
|
||||
|
||||
columnMove({from, to}) {
|
||||
const value = this.columns[from]
|
||||
this.columns.splice(from, 1)
|
||||
this.columns.splice(to, 0, value)
|
||||
this.updateInput()
|
||||
},
|
||||
|
||||
listItemMove({from, to, set}) {
|
||||
set.move(from, to);
|
||||
this.updateInput()
|
||||
},
|
||||
|
||||
updateList() {
|
||||
const items = this.toList(this.$refs.textarea.value)
|
||||
this.set.reset(items)
|
||||
},
|
||||
|
||||
updateInput() {
|
||||
const input = this.toText(this.items)
|
||||
this.$refs.textarea.value = input
|
||||
},
|
||||
|
||||
/**
|
||||
* From input and separator, return list of items.
|
||||
*/
|
||||
toList(input) {
|
||||
var lines = input.split('\n')
|
||||
var items = []
|
||||
|
||||
for(let line of lines) {
|
||||
line = line.trimLeft()
|
||||
if(!line)
|
||||
continue
|
||||
|
||||
var lineBits = line.split(this.separator)
|
||||
var item = {}
|
||||
for(var col in this.columns) {
|
||||
if(col >= lineBits.length)
|
||||
break
|
||||
const attr = this.columns[col]
|
||||
item[attr] = lineBits[col].trim()
|
||||
}
|
||||
item && items.push(item)
|
||||
}
|
||||
return items
|
||||
},
|
||||
|
||||
/**
|
||||
* From items and separator return a string
|
||||
*/
|
||||
toText(items) {
|
||||
const sep = ` ${this.separator.trim()} `
|
||||
const lines = []
|
||||
for(let item of items) {
|
||||
if(!item)
|
||||
continue
|
||||
var line = []
|
||||
for(var col of this.columns)
|
||||
line.push(item.data[col] || '')
|
||||
line = dropRightWhile(line, x => !x || !('' + x).trim())
|
||||
line = line.join(sep).trimRight()
|
||||
lines.push(line)
|
||||
}
|
||||
return lines.join('\n')
|
||||
},
|
||||
|
||||
|
||||
_data_key(key) {
|
||||
key = key.slice(this.dataPrefix.length)
|
||||
try {
|
||||
var [index, attr] = key.split('-', 1)
|
||||
return [Number(index), attr]
|
||||
}
|
||||
catch(err) {
|
||||
return [null, key]
|
||||
}
|
||||
},
|
||||
|
||||
//! Update saved settings from this.settings
|
||||
settingsSaved(settings=null) {
|
||||
if(settings !== null)
|
||||
this.settings = settings
|
||||
this.savedSettings = cloneDeep(this.settings)
|
||||
},
|
||||
|
||||
/**
|
||||
* Load initial data
|
||||
*/
|
||||
loadData({items=[], settings=null}, reset=false) {
|
||||
if(reset) {
|
||||
this.set.items = []
|
||||
}
|
||||
for(var index in items)
|
||||
this.set.push(cloneDeep(items[index]))
|
||||
if(settings)
|
||||
this.settingsSaved(settings)
|
||||
this.updateInput()
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
initData(val) {
|
||||
this.loadData(val)
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.initData && this.loadData(this.initData)
|
||||
this.page = this.items.length ? Page.List : Page.Text
|
||||
},
|
||||
}
|
||||
</script>
|
146
assets/src/components/ARow.vue
Normal file
146
assets/src/components/ARow.vue
Normal file
|
@ -0,0 +1,146 @@
|
|||
<template>
|
||||
<tr>
|
||||
<slot name="head" :item="item" :row="row"/>
|
||||
<template v-for="(attr,col) in columns" :key="col">
|
||||
<slot name="cell-before" :item="item" :cell="cells[col]"
|
||||
:attr="attr"/>
|
||||
<component :is="cellTag" :class="['cell', 'cell-' + attr]" :data-col="col"
|
||||
:draggable="orderable"
|
||||
@dragstart="onDragStart" @dragover="onDragOver" @drop="onDrop">
|
||||
<slot :name="attr" :item="item" :cell="cells[col]"
|
||||
:data="itemData" :attr="attr" :emit="cellEmit"
|
||||
:value="itemData && itemData[attr]">
|
||||
{{ itemData && itemData[attr] }}
|
||||
</slot>
|
||||
<slot name="cell" :item="item" :cell="cells[col]"
|
||||
:data="itemData" :attr="attr" :emit="cellEmit"
|
||||
:value="itemData && itemData[attr]"/>
|
||||
</component>
|
||||
<slot name="cell-after" :item="item" :col="col" :cell="cells[col]"
|
||||
:attr="attr"/>
|
||||
</template>
|
||||
<slot name="tail" :item="item" :row="row"/>
|
||||
</tr>
|
||||
</template>
|
||||
<script>
|
||||
import {isReactive, toRefs} from 'vue'
|
||||
import Model from '../model'
|
||||
|
||||
export default {
|
||||
emit: ['move', 'cell'],
|
||||
|
||||
props: {
|
||||
//! Item to display in row
|
||||
item: Object,
|
||||
//! Columns to display, as items' attributes
|
||||
columns: Array,
|
||||
//! Default cell's info
|
||||
cell: {type: Object, default() { return {row: 0}}},
|
||||
//! Cell component tag
|
||||
cellTag: {type: String, default: 'td'},
|
||||
//! If true, can reorder cell by drag & drop
|
||||
orderable: {type: Boolean, default: false},
|
||||
},
|
||||
|
||||
computed: {
|
||||
/**
|
||||
* Row index
|
||||
*/
|
||||
row() { return this.cell && this.cell.row || 0 },
|
||||
|
||||
/**
|
||||
* Item's data if model instance, otherwise item
|
||||
*/
|
||||
itemData() {
|
||||
return this.item instanceof Model ? this.item.data : this.item;
|
||||
},
|
||||
|
||||
/**
|
||||
* Computed cell infos
|
||||
*/
|
||||
cells() {
|
||||
const cell = isReactive(this.cell) && toRefs(this.cell) || this.cell || {}
|
||||
const cells = []
|
||||
for(var col in this.columns)
|
||||
cells.push({...cell, col: Number(col)})
|
||||
return cells
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Emit a 'cell' event.
|
||||
* Event data: `{name, cell, data, item}`
|
||||
* @param {Number} col: cell column's index
|
||||
* @param {String} name: cell's event name
|
||||
* @param {} data: cell's event data
|
||||
*/
|
||||
cellEmit(name, cell, data) {
|
||||
this.$emit('cell', {
|
||||
name, cell, data,
|
||||
item: this.item,
|
||||
})
|
||||
},
|
||||
|
||||
onDragStart(ev) {
|
||||
const dataset = ev.target.dataset;
|
||||
const data = `cell:${dataset.col}`
|
||||
ev.dataTransfer.setData("text/cell", data)
|
||||
ev.dataTransfer.dropEffect = 'move'
|
||||
},
|
||||
|
||||
onDragOver(ev) {
|
||||
ev.preventDefault()
|
||||
ev.dataTransfer.dropEffect = 'move'
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle drop event, emit `'move': { from, to }`.
|
||||
*/
|
||||
onDrop(ev) {
|
||||
const data = ev.dataTransfer.getData("text/cell")
|
||||
if(!data || !data.startsWith('cell:'))
|
||||
return
|
||||
|
||||
ev.preventDefault()
|
||||
this.$emit('move', {
|
||||
from: Number(data.slice(5)),
|
||||
to: Number(ev.target.dataset.col),
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Return DOM node for cells at provided position `col`
|
||||
*/
|
||||
getCellEl(col) {
|
||||
const els = this.$el.querySelectorAll(this.cellTag)
|
||||
for(var el of els)
|
||||
if(col == Number(el.dataset.col))
|
||||
return el;
|
||||
return null
|
||||
},
|
||||
|
||||
/**
|
||||
* Focus cell's form input. If from is provided, related focus
|
||||
*/
|
||||
focus(col, from) {
|
||||
if(from)
|
||||
col += from.col
|
||||
|
||||
const target = this.getCellEl(col)
|
||||
if(!target)
|
||||
return
|
||||
const control = target.querySelector('input:not([type="hidden"])') ||
|
||||
target.querySelector('button') ||
|
||||
target.querySelector('select') ||
|
||||
target.querySelector('a');
|
||||
control && control.focus()
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.$el.__row = this
|
||||
},
|
||||
}
|
||||
|
||||
</script>
|
118
assets/src/components/ARows.vue
Normal file
118
assets/src/components/ARows.vue
Normal file
|
@ -0,0 +1,118 @@
|
|||
<template>
|
||||
<table class="table is-stripped is-fullwidth">
|
||||
<thead>
|
||||
<a-row :item="labels" :columns="columns" :orderable="orderable"
|
||||
@move="$emit('colmove', $event)">
|
||||
<template v-if="$slots['header-head']" v-slot:head="data">
|
||||
<slot name="header-head" v-bind="data"/>
|
||||
</template>
|
||||
<template v-if="$slots['header-tail']" v-slot:tail="data">
|
||||
<slot name="header-tail" v-bind="data"/>
|
||||
</template>
|
||||
</a-row>
|
||||
</thead>
|
||||
<tbody>
|
||||
<slot name="head"/>
|
||||
<template v-for="(item,row) in items" :key="row">
|
||||
<!-- data-index comes from AList component drag & drop -->
|
||||
<a-row :item="item" :cell="{row}" :columns="columns" :data-index="row"
|
||||
:data-row="row"
|
||||
:draggable="orderable"
|
||||
@dragstart="onDragStart" @dragover="onDragOver" @drop="onDrop"
|
||||
@cell="onCellEvent(row, $event)">
|
||||
<template v-for="[name,slot] of rowSlots" :key="slot" v-slot:[slot]="data">
|
||||
<template v-if="slot == 'head' || slot == 'tail'">
|
||||
<slot :name="name" v-bind="data"/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div>
|
||||
<slot :name="name" v-bind="data"/>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</a-row>
|
||||
</template>
|
||||
<slot name="tail"/>
|
||||
</tbody>
|
||||
</table>
|
||||
</template>
|
||||
<script>
|
||||
import AList from './AList.vue'
|
||||
import ARow from './ARow.vue'
|
||||
|
||||
const Component = {
|
||||
extends: AList,
|
||||
components: { ARow },
|
||||
emit: ['cell', 'colmove'],
|
||||
|
||||
props: {
|
||||
...AList.props,
|
||||
columns: Array,
|
||||
labels: Object,
|
||||
allowCreate: Boolean,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
...super.data,
|
||||
extraItem: new this.set.model(),
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
rowCells() {
|
||||
const cells = []
|
||||
for(var row in this.items)
|
||||
cells.push({row})
|
||||
},
|
||||
|
||||
rowSlots() {
|
||||
return Object.keys(this.$slots).filter(x => x.startsWith('row-'))
|
||||
.map(x => [x, x.slice(4)])
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* React on 'cell' event, re-emitting it with additional values:
|
||||
* - `set`: data set
|
||||
* - `row`: row index
|
||||
*
|
||||
* @param {Number} row: row index
|
||||
* @param {} data: cell's event data
|
||||
*/
|
||||
onCellEvent(row, event) {
|
||||
if(event.name == 'focus')
|
||||
this.focus(event.data, event.cell)
|
||||
this.$emit('cell', {
|
||||
...event, row,
|
||||
set: this.set
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Return row component at provided index
|
||||
*/
|
||||
getRow(row) {
|
||||
const els = this.$el.querySelectorAll('tr')
|
||||
for(var el of els)
|
||||
if(el.__row && row == Number(el.dataset.row))
|
||||
return el.__row
|
||||
},
|
||||
|
||||
/**
|
||||
* Focus on a cell
|
||||
*/
|
||||
focus(row, col, from=null) {
|
||||
if(from)
|
||||
row += from.row
|
||||
row = this.getRow(row)
|
||||
row && row.focus(col, from)
|
||||
},
|
||||
},
|
||||
}
|
||||
Component.props.itemTag.default = 'tr'
|
||||
Component.props.listTag.default = 'tbody'
|
||||
|
||||
export default Component
|
||||
</script>
|
|
@ -4,6 +4,7 @@ import AList from './AList.vue'
|
|||
import APage from './APage.vue'
|
||||
import APlayer from './APlayer.vue'
|
||||
import APlaylist from './APlaylist.vue'
|
||||
import APlaylistEditor from './APlaylistEditor.vue'
|
||||
import AProgress from './AProgress.vue'
|
||||
import ASoundItem from './ASoundItem.vue'
|
||||
import AStatistics from './AStatistics.vue'
|
||||
|
@ -12,12 +13,15 @@ import AStreamer from './AStreamer.vue'
|
|||
/**
|
||||
* Core components
|
||||
*/
|
||||
export default {
|
||||
export const base = {
|
||||
AAutocomplete, AEpisode, AList, APage, APlayer, APlaylist,
|
||||
AProgress, ASoundItem,
|
||||
}
|
||||
|
||||
export default base
|
||||
|
||||
export const admin = {
|
||||
AStatistics, AStreamer,
|
||||
...base,
|
||||
AStatistics, AStreamer, APlaylistEditor
|
||||
}
|
||||
|
||||
|
|
|
@ -32,7 +32,7 @@ window.aircox = {
|
|||
* Initialize main application and player.
|
||||
*/
|
||||
init(props=null, {config=null, builder=null, initBuilder=true,
|
||||
initPlayer=true, hotReload=false}={})
|
||||
initPlayer=true, hotReload=false, el=null}={})
|
||||
{
|
||||
if(initPlayer) {
|
||||
let playerBuilder = this.playerBuilder
|
||||
|
@ -44,6 +44,9 @@ window.aircox = {
|
|||
this.builder = builder
|
||||
if(config || window.App)
|
||||
builder.config = config || window.App
|
||||
if(el)
|
||||
builder.config.el = el
|
||||
|
||||
builder.title = document.title
|
||||
builder.mount({props})
|
||||
|
||||
|
|
|
@ -35,17 +35,21 @@ export default class Model {
|
|||
* Instanciate model with provided data and options.
|
||||
* By default `url` is taken from `data.url_`.
|
||||
*/
|
||||
constructor(data, {url=null, ...options}={}) {
|
||||
constructor(data={}, {url=null, ...options}={}) {
|
||||
this.url = url || data.url_;
|
||||
this.options = options;
|
||||
this.commit(data);
|
||||
}
|
||||
|
||||
get errors() {
|
||||
return this.data && this.data.__errors__
|
||||
}
|
||||
|
||||
/**
|
||||
* Get instance id from its data
|
||||
*/
|
||||
static getId(data) {
|
||||
return data.id;
|
||||
return 'id' in data ? data.id : data.pk;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -112,8 +116,16 @@ export default class Model {
|
|||
* Update instance's data with provided data. Return None
|
||||
*/
|
||||
commit(data) {
|
||||
this.id = this.constructor.getId(data);
|
||||
this.data = data;
|
||||
this.id = this.constructor.getId(this.data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update model data, without reset previous value
|
||||
*/
|
||||
update(data) {
|
||||
this.data = {...this.data, ...data}
|
||||
this.id = this.constructor.getId(this.data)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -130,9 +142,25 @@ export default class Model {
|
|||
let item = window.localStorage.getItem(key);
|
||||
return item === null ? item : new this(JSON.parse(item));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if model instance has no data
|
||||
*/
|
||||
get isEmpty() {
|
||||
return !this.data || Object.keys(this.data).findIndex(k => !!this.data[k] && this.data[k] !== 0) == -1
|
||||
}
|
||||
|
||||
/**
|
||||
* Return error for a specific attribute name if any
|
||||
*/
|
||||
error(attr=null) {
|
||||
return attr === null ? this.errors : this.errors && this.errors[attr]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* List of models
|
||||
*/
|
||||
|
@ -231,6 +259,25 @@ export class Set {
|
|||
this.items.splice(index,1);
|
||||
save && this.save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear items, assign new ones
|
||||
*/
|
||||
reset(items=[]) {
|
||||
// TODO: check reactivity
|
||||
this.items = []
|
||||
for(var item of items)
|
||||
this.push(item)
|
||||
}
|
||||
|
||||
move(from, to) {
|
||||
if(from >= this.length || to > this.length)
|
||||
throw "source or target index is not in range"
|
||||
|
||||
const value = this.items[from]
|
||||
this.items.splice(from, 1)
|
||||
this.items.splice(to, 0, value)
|
||||
}
|
||||
}
|
||||
|
||||
Set[Symbol.iterator] = function () {
|
||||
|
|
7
assets/src/track.js
Normal file
7
assets/src/track.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
import Model from './model'
|
||||
|
||||
export default class Track extends Model {
|
||||
static getId(data) { return data.pk }
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user