forked from rc/aircox
Reviewed-on: rc/aircox#81
This commit is contained in:
commit
46da13a0df
|
@ -1,5 +1,3 @@
|
||||||
from copy import copy
|
|
||||||
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.forms import ModelForm
|
from django.forms import ModelForm
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
@ -60,11 +58,21 @@ class EpisodeAdminForm(ModelForm):
|
||||||
class EpisodeAdmin(SortableAdminBase, PageAdmin):
|
class EpisodeAdmin(SortableAdminBase, PageAdmin):
|
||||||
form = EpisodeAdminForm
|
form = EpisodeAdminForm
|
||||||
list_display = PageAdmin.list_display
|
list_display = PageAdmin.list_display
|
||||||
list_filter = tuple(f for f in PageAdmin.list_filter if f != 'pub_date') + \
|
list_filter = tuple(f for f in PageAdmin.list_filter
|
||||||
('diffusion__start', 'pub_date')
|
if f != 'pub_date') + ('diffusion__start', 'pub_date')
|
||||||
search_fields = PageAdmin.search_fields + ('parent__title',)
|
search_fields = PageAdmin.search_fields + ('parent__title',)
|
||||||
# readonly_fields = ('parent',)
|
# readonly_fields = ('parent',)
|
||||||
|
|
||||||
inlines = [TrackInline, SoundInline, DiffusionInline]
|
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
|
from ..models import Sound, Track
|
||||||
|
|
||||||
|
|
||||||
class TrackInline(SortableInlineAdminMixin, admin.TabularInline):
|
class TrackInline(admin.TabularInline):
|
||||||
template = 'admin/aircox/playlist_inline.html'
|
template = 'admin/aircox/playlist_inline.html'
|
||||||
model = Track
|
model = Track
|
||||||
extra = 0
|
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):
|
class SoundTrackInline(TrackInline):
|
||||||
fields = TrackInline.fields + ('timestamp',)
|
fields = TrackInline.fields + ('timestamp',)
|
||||||
|
@ -24,14 +25,15 @@ class SoundTrackInline(TrackInline):
|
||||||
|
|
||||||
class SoundInline(admin.TabularInline):
|
class SoundInline(admin.TabularInline):
|
||||||
model = Sound
|
model = Sound
|
||||||
fields = ['type', 'name', 'audio', 'duration', 'is_good_quality', 'is_public',
|
fields = ['type', 'name', 'audio', 'duration', 'is_good_quality',
|
||||||
'is_downloadable']
|
'is_public', 'is_downloadable']
|
||||||
readonly_fields = ['type', 'audio', 'duration', 'is_good_quality']
|
readonly_fields = ['type', 'audio', 'duration', 'is_good_quality']
|
||||||
extra = 0
|
extra = 0
|
||||||
max_num = 0
|
max_num = 0
|
||||||
|
|
||||||
def audio(self, obj):
|
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')
|
audio.short_description = _('Audio')
|
||||||
|
|
||||||
def get_queryset(self, request):
|
def get_queryset(self, request):
|
||||||
|
@ -63,23 +65,40 @@ class SoundAdmin(SortableAdminBase, admin.ModelAdmin):
|
||||||
related.short_description = _('Program / Episode')
|
related.short_description = _('Program / Episode')
|
||||||
|
|
||||||
def audio(self, obj):
|
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 ''
|
if obj.type != Sound.TYPE_REMOVED else ''
|
||||||
audio.short_description = _('Audio')
|
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)
|
@admin.register(Track)
|
||||||
class TrackAdmin(admin.ModelAdmin):
|
class TrackAdmin(admin.ModelAdmin):
|
||||||
def tag_list(self, obj):
|
def tag_list(self, obj):
|
||||||
return u", ".join(o.name for o in obj.tags.all())
|
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_editable = ['artist', 'title']
|
||||||
list_filter = ['artist', 'title', 'tags']
|
list_filter = ['artist', 'title', 'tags']
|
||||||
|
|
||||||
search_fields = ['artist', 'title']
|
search_fields = ['artist', 'title']
|
||||||
fieldsets = [
|
fieldsets = [
|
||||||
(_('Playlist'), {'fields': ['episode', 'sound', 'position', 'timestamp']}),
|
(_('Playlist'), {'fields': ['episode', 'sound', 'position',
|
||||||
|
'timestamp']}),
|
||||||
(_('Info'), {'fields': ['artist', 'title', 'info', 'tags']}),
|
(_('Info'), {'fields': ['artist', 'title', 'info', 'tags']}),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -95,5 +114,3 @@ class TrackAdmin(admin.ModelAdmin):
|
||||||
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')
|
ts.short_description = _('timestamp')
|
||||||
|
|
||||||
|
|
||||||
|
|
Binary file not shown.
|
@ -8,7 +8,7 @@ msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: Aircox 0.1\n"
|
"Project-Id-Version: Aircox 0.1\n"
|
||||||
"Report-Msgid-Bugs-To: \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"
|
"PO-Revision-Date: 2016-10-10 16:00+02\n"
|
||||||
"Last-Translator: Aarys\n"
|
"Last-Translator: Aarys\n"
|
||||||
"Language-Team: Aircox's translators team\n"
|
"Language-Team: Aircox's translators team\n"
|
||||||
|
@ -18,13 +18,13 @@ msgstr ""
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"Plural-Forms: nplurals=2; plural=(n > 1);\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
|
#: aircox/models/log.py:82
|
||||||
msgid "start"
|
msgid "start"
|
||||||
msgstr "début"
|
msgstr "début"
|
||||||
|
|
||||||
#: aircox/admin/episode.py:33 aircox/models/episode.py:184
|
#: aircox/admin/episode.py:31 aircox/models/episode.py:184
|
||||||
#: aircox/models/program.py:473
|
#: aircox/models/program.py:469
|
||||||
msgid "end"
|
msgid "end"
|
||||||
msgstr "fin"
|
msgstr "fin"
|
||||||
|
|
||||||
|
@ -52,7 +52,7 @@ msgstr "Tout"
|
||||||
msgid "Publication Settings"
|
msgid "Publication Settings"
|
||||||
msgstr "Paramètre de la publication"
|
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"
|
msgid "Schedule"
|
||||||
msgstr "Horaire"
|
msgstr "Horaire"
|
||||||
|
|
||||||
|
@ -60,7 +60,7 @@ msgstr "Horaire"
|
||||||
msgid "Program Settings"
|
msgid "Program Settings"
|
||||||
msgstr "Paramètres de l'émission"
|
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"
|
msgid "Program"
|
||||||
msgstr "Émission"
|
msgstr "Émission"
|
||||||
|
|
||||||
|
@ -68,23 +68,25 @@ msgstr "Émission"
|
||||||
msgid "Day"
|
msgid "Day"
|
||||||
msgstr "Jour"
|
msgstr "Jour"
|
||||||
|
|
||||||
#: aircox/admin/sound.py:35 aircox/admin/sound.py:67
|
#: aircox/admin/sound.py:37 aircox/admin/sound.py:71
|
||||||
msgid "Audio"
|
msgid "Audio"
|
||||||
msgstr "Audio"
|
msgstr "Audio"
|
||||||
|
|
||||||
#: aircox/admin/sound.py:63
|
#: aircox/admin/sound.py:65
|
||||||
msgid "Program / Episode"
|
msgid "Program / Episode"
|
||||||
msgstr "Émission / Épisode"
|
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"
|
msgid "Playlist"
|
||||||
msgstr "Playlist"
|
msgstr "Playlist"
|
||||||
|
|
||||||
#: aircox/admin/sound.py:82
|
#: aircox/admin/sound.py:102
|
||||||
msgid "Info"
|
msgid "Info"
|
||||||
msgstr "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"
|
msgid "timestamp"
|
||||||
msgstr "temps"
|
msgstr "temps"
|
||||||
|
|
||||||
|
@ -92,8 +94,8 @@ msgstr "temps"
|
||||||
msgid "Statistics"
|
msgid "Statistics"
|
||||||
msgstr "Statistiques"
|
msgstr "Statistiques"
|
||||||
|
|
||||||
#: aircox/filters.py:8 aircox/templates/admin/base.html:81
|
#: aircox/filters.py:8 aircox/templates/admin/base.html:84
|
||||||
#: aircox/templates/admin/base.html:95 aircox/templates/admin/base.html:109
|
#: aircox/templates/admin/base.html:98 aircox/templates/admin/base.html:112
|
||||||
#: aircox/templates/aircox/base.html:78
|
#: aircox/templates/aircox/base.html:78
|
||||||
#: aircox/templates/aircox/page_list.html:15
|
#: aircox/templates/aircox/page_list.html:15
|
||||||
msgid "Search"
|
msgid "Search"
|
||||||
|
@ -103,24 +105,24 @@ msgstr "Chercher"
|
||||||
msgid "Podcast"
|
msgid "Podcast"
|
||||||
msgstr "Podcast"
|
msgstr "Podcast"
|
||||||
|
|
||||||
#: aircox/management/commands/sounds_monitor.py:204
|
#: aircox/management/commands/sounds_monitor.py:205
|
||||||
msgid "unknown"
|
msgid "unknown"
|
||||||
msgstr "inconnu"
|
msgstr "inconnu"
|
||||||
|
|
||||||
#: aircox/models/article.py:14
|
#: aircox/models/article.py:16
|
||||||
msgid "Article"
|
msgid "Article"
|
||||||
msgstr "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
|
#: aircox/templates/aircox/program_detail.html:19
|
||||||
msgid "Articles"
|
msgid "Articles"
|
||||||
msgstr "Articles"
|
msgstr "Articles"
|
||||||
|
|
||||||
#: aircox/models/episode.py:52
|
#: aircox/models/episode.py:52 aircox/templates/admin/aircox/statistics.html:23
|
||||||
msgid "Episode"
|
msgid "Episode"
|
||||||
msgstr "Épisode"
|
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"
|
msgid "Episodes"
|
||||||
msgstr "Épisodes"
|
msgstr "Épisodes"
|
||||||
|
|
||||||
|
@ -136,8 +138,8 @@ msgstr "non confirmé"
|
||||||
msgid "cancelled"
|
msgid "cancelled"
|
||||||
msgstr "annulé"
|
msgstr "annulé"
|
||||||
|
|
||||||
#: aircox/models/episode.py:174 aircox/models/sound.py:102
|
#: aircox/models/episode.py:174 aircox/models/sound.py:99
|
||||||
#: aircox/models/sound.py:233 aircox/templates/admin/aircox/statistics.html:23
|
#: aircox/models/sound.py:230
|
||||||
msgid "episode"
|
msgid "episode"
|
||||||
msgstr "épisode"
|
msgstr "épisode"
|
||||||
|
|
||||||
|
@ -146,7 +148,7 @@ msgid "schedule"
|
||||||
msgstr "horaire"
|
msgstr "horaire"
|
||||||
|
|
||||||
#: aircox/models/episode.py:181 aircox/models/log.py:91
|
#: 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"
|
msgid "type"
|
||||||
msgstr "type"
|
msgstr "type"
|
||||||
|
|
||||||
|
@ -171,12 +173,12 @@ msgstr "rediffusion"
|
||||||
msgid "stop"
|
msgid "stop"
|
||||||
msgstr "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"
|
msgid "other"
|
||||||
msgstr "autre"
|
msgstr "autre"
|
||||||
|
|
||||||
#: aircox/models/log.py:89 aircox/models/page.py:248
|
#: 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"
|
msgid "station"
|
||||||
msgstr "station"
|
msgstr "station"
|
||||||
|
|
||||||
|
@ -184,7 +186,7 @@ msgstr "station"
|
||||||
msgid "related station"
|
msgid "related station"
|
||||||
msgstr "station relative"
|
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"
|
msgid "date"
|
||||||
msgstr "date"
|
msgstr "date"
|
||||||
|
|
||||||
|
@ -200,11 +202,12 @@ msgstr "identifiant de la source relative à ce log"
|
||||||
msgid "comment"
|
msgid "comment"
|
||||||
msgstr "commentaire"
|
msgstr "commentaire"
|
||||||
|
|
||||||
#: aircox/models/log.py:107 aircox/models/sound.py:146
|
#: aircox/models/log.py:107 aircox/models/sound.py:143
|
||||||
msgid "Sound"
|
msgid "Sound"
|
||||||
msgstr "Son"
|
msgstr "Son"
|
||||||
|
|
||||||
#: aircox/models/log.py:112 aircox/models/sound.py:259
|
#: aircox/models/log.py:112 aircox/models/sound.py:259
|
||||||
|
#: aircox/templates/admin/aircox/statistics.html:24
|
||||||
msgid "Track"
|
msgid "Track"
|
||||||
msgstr "Morceau"
|
msgstr "Morceau"
|
||||||
|
|
||||||
|
@ -217,7 +220,7 @@ msgid "Logs"
|
||||||
msgstr "Logs"
|
msgstr "Logs"
|
||||||
|
|
||||||
#: aircox/models/page.py:30 aircox/models/page.py:251
|
#: aircox/models/page.py:30 aircox/models/page.py:251
|
||||||
#: aircox/models/sound.py:247
|
#: aircox/models/sound.py:244
|
||||||
msgid "title"
|
msgid "title"
|
||||||
msgstr "titre"
|
msgstr "titre"
|
||||||
|
|
||||||
|
@ -338,8 +341,8 @@ msgstr "Commentaires"
|
||||||
msgid "menu"
|
msgid "menu"
|
||||||
msgstr "menu"
|
msgstr "menu"
|
||||||
|
|
||||||
#: aircox/models/page.py:250 aircox/models/sound.py:107
|
#: aircox/models/page.py:250 aircox/models/sound.py:104
|
||||||
#: aircox/models/sound.py:240
|
#: aircox/models/sound.py:237
|
||||||
msgid "order"
|
msgid "order"
|
||||||
msgstr "ordre"
|
msgstr "ordre"
|
||||||
|
|
||||||
|
@ -359,234 +362,239 @@ msgstr "Élément du menu"
|
||||||
msgid "Menu items"
|
msgid "Menu items"
|
||||||
msgstr "Éléments de menu"
|
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
|
#: aircox/models/station.py:144
|
||||||
msgid "active"
|
msgid "active"
|
||||||
msgstr "actif"
|
msgstr "actif"
|
||||||
|
|
||||||
#: aircox/models/program.py:59
|
#: aircox/models/program.py:58
|
||||||
msgid "if not checked this program is no longer active"
|
msgid "if not checked this program is no longer active"
|
||||||
msgstr "si selectionné, cette émission n'est plus active"
|
msgstr "si selectionné, cette émission n'est plus active"
|
||||||
|
|
||||||
#: aircox/models/program.py:62
|
#: aircox/models/program.py:61
|
||||||
msgid "syncronise"
|
msgid "syncronise"
|
||||||
msgstr "synchroniser"
|
msgstr "synchroniser"
|
||||||
|
|
||||||
#: aircox/models/program.py:64
|
#: aircox/models/program.py:63
|
||||||
msgid "update later diffusions according to schedule changes"
|
msgid "update later diffusions according to schedule changes"
|
||||||
msgstr "met à jour les dates de diffusion à venir lorsque l'horaire change"
|
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"
|
msgid "Programs"
|
||||||
msgstr "Émissions"
|
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"
|
msgid "related program"
|
||||||
msgstr "émission apparentée"
|
msgstr "émission apparentée"
|
||||||
|
|
||||||
#: aircox/models/program.py:182
|
#: aircox/models/program.py:178
|
||||||
msgid "rerun of"
|
msgid "rerun of"
|
||||||
msgstr "rediffusion de"
|
msgstr "rediffusion de"
|
||||||
|
|
||||||
#: aircox/models/program.py:226
|
#: aircox/models/program.py:222
|
||||||
msgid "rerun must happen after original"
|
msgid "rerun must happen after original"
|
||||||
msgstr "la rediffusion doit être après l'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"
|
msgid "date of the first diffusion"
|
||||||
msgstr "date de la première diffusion"
|
msgstr "date de la première diffusion"
|
||||||
|
|
||||||
#: aircox/models/program.py:257
|
#: aircox/models/program.py:253
|
||||||
#: aircox/templates/admin/aircox/statistics.html:22
|
|
||||||
msgid "time"
|
msgid "time"
|
||||||
msgstr "heure"
|
msgstr "heure"
|
||||||
|
|
||||||
#: aircox/models/program.py:257
|
#: aircox/models/program.py:253
|
||||||
msgid "start time"
|
msgid "start time"
|
||||||
msgstr "heure de début"
|
msgstr "heure de début"
|
||||||
|
|
||||||
#: aircox/models/program.py:260
|
#: aircox/models/program.py:256
|
||||||
msgid "timezone"
|
msgid "timezone"
|
||||||
msgstr "zone horaire"
|
msgstr "zone horaire"
|
||||||
|
|
||||||
#: aircox/models/program.py:263
|
#: aircox/models/program.py:259
|
||||||
msgid "timezone used for the date"
|
msgid "timezone used for the date"
|
||||||
msgstr "zone horaire utilisée pour la 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"
|
msgid "duration"
|
||||||
msgstr "durée"
|
msgstr "durée"
|
||||||
|
|
||||||
#: aircox/models/program.py:267
|
#: aircox/models/program.py:263
|
||||||
msgid "regular duration"
|
msgid "regular duration"
|
||||||
msgstr "durée normale"
|
msgstr "durée normale"
|
||||||
|
|
||||||
#: aircox/models/program.py:270
|
#: aircox/models/program.py:266
|
||||||
msgid "frequency"
|
msgid "frequency"
|
||||||
msgstr "fréquence"
|
msgstr "fréquence"
|
||||||
|
|
||||||
#: aircox/models/program.py:272
|
#: aircox/models/program.py:268
|
||||||
msgid "ponctual"
|
msgid "ponctual"
|
||||||
msgstr "ponctuel"
|
msgstr "ponctuel"
|
||||||
|
|
||||||
#: aircox/models/program.py:273
|
#: aircox/models/program.py:269
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "1st {day} of the month"
|
msgid "1st {day} of the month"
|
||||||
msgstr "1er {day} du mois"
|
msgstr "1er {day} du mois"
|
||||||
|
|
||||||
#: aircox/models/program.py:274
|
#: aircox/models/program.py:270
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "2nd {day} of the month"
|
msgid "2nd {day} of the month"
|
||||||
msgstr "2ème {day} du mois"
|
msgstr "2ème {day} du mois"
|
||||||
|
|
||||||
#: aircox/models/program.py:275
|
#: aircox/models/program.py:271
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "3rd {day} of the month"
|
msgid "3rd {day} of the month"
|
||||||
msgstr "3ème {day} du mois"
|
msgstr "3ème {day} du mois"
|
||||||
|
|
||||||
#: aircox/models/program.py:276
|
#: aircox/models/program.py:272
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "4th {day} of the month"
|
msgid "4th {day} of the month"
|
||||||
msgstr "4ème {day} du mois"
|
msgstr "4ème {day} du mois"
|
||||||
|
|
||||||
#: aircox/models/program.py:277
|
#: aircox/models/program.py:273
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "last {day} of the month"
|
msgid "last {day} of the month"
|
||||||
msgstr "dernier {day} du mois"
|
msgstr "dernier {day} du mois"
|
||||||
|
|
||||||
#: aircox/models/program.py:278
|
#: aircox/models/program.py:274
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "1st and 3rd {day} of the month"
|
msgid "1st and 3rd {day} of the month"
|
||||||
msgstr "1er et 3ème {day} du mois"
|
msgstr "1er et 3ème {day} du mois"
|
||||||
|
|
||||||
#: aircox/models/program.py:279
|
#: aircox/models/program.py:275
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "2nd and 4th {day} of the month"
|
msgid "2nd and 4th {day} of the month"
|
||||||
msgstr "2ème et 4ème {day} du mois"
|
msgstr "2ème et 4ème {day} du mois"
|
||||||
|
|
||||||
#: aircox/models/program.py:280
|
#: aircox/models/program.py:276
|
||||||
#, fuzzy, python-brace-format
|
|
||||||
#| msgid "every {day}"
|
|
||||||
msgid "{day}"
|
msgid "{day}"
|
||||||
msgstr "{day}"
|
msgstr "{day}"
|
||||||
|
|
||||||
#: aircox/models/program.py:281
|
#: aircox/models/program.py:277
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "one {day} on two"
|
msgid "one {day} on two"
|
||||||
msgstr "un {day} sur deux"
|
msgstr "un {day} sur deux"
|
||||||
|
|
||||||
#: aircox/models/program.py:287
|
#: aircox/models/program.py:283
|
||||||
msgid "Schedules"
|
msgid "Schedules"
|
||||||
msgstr "Horaires"
|
msgstr "Horaires"
|
||||||
|
|
||||||
#: aircox/models/program.py:464
|
#: aircox/models/program.py:460
|
||||||
msgid "delay"
|
msgid "delay"
|
||||||
msgstr "délai"
|
msgstr "délai"
|
||||||
|
|
||||||
#: aircox/models/program.py:465
|
#: aircox/models/program.py:461
|
||||||
msgid "minimal delay between two sound plays"
|
msgid "minimal delay between two sound plays"
|
||||||
msgstr "délai minimum entre deux sons joués"
|
msgstr "délai minimum entre deux sons joués"
|
||||||
|
|
||||||
#: aircox/models/program.py:468
|
#: aircox/models/program.py:464
|
||||||
msgid "begin"
|
msgid "begin"
|
||||||
msgstr "début"
|
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"
|
msgid "used to define a time range this stream is played"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"utilisé pour définir un intervalle de temps pendant lequel ce stream est joué"
|
"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"
|
msgid "archive"
|
||||||
msgstr "archive"
|
msgstr "archive"
|
||||||
|
|
||||||
#: aircox/models/sound.py:90
|
#: aircox/models/sound.py:87
|
||||||
msgid "excerpt"
|
msgid "excerpt"
|
||||||
msgstr "extrait"
|
msgstr "extrait"
|
||||||
|
|
||||||
#: aircox/models/sound.py:90
|
#: aircox/models/sound.py:87
|
||||||
msgid "removed"
|
msgid "removed"
|
||||||
msgstr "supprimé"
|
msgstr "supprimé"
|
||||||
|
|
||||||
#: aircox/models/sound.py:93 aircox/models/station.py:37
|
#: aircox/models/sound.py:90 aircox/models/station.py:37
|
||||||
msgid "name"
|
msgid "name"
|
||||||
msgstr "nom"
|
msgstr "nom"
|
||||||
|
|
||||||
#: aircox/models/sound.py:96
|
#: aircox/models/sound.py:93
|
||||||
msgid "program"
|
msgid "program"
|
||||||
msgstr "émission"
|
msgstr "émission"
|
||||||
|
|
||||||
#: aircox/models/sound.py:97
|
#: aircox/models/sound.py:94
|
||||||
msgid "program related to it"
|
msgid "program related to it"
|
||||||
msgstr "émission apparentée à celui-ci"
|
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"
|
msgid "position in the playlist"
|
||||||
msgstr "position dans la 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"
|
msgid "file"
|
||||||
msgstr "fichier"
|
msgstr "fichier"
|
||||||
|
|
||||||
#: aircox/models/sound.py:122
|
#: aircox/models/sound.py:119
|
||||||
msgid "duration of the sound"
|
msgid "duration of the sound"
|
||||||
msgstr "durée du son"
|
msgstr "durée du son"
|
||||||
|
|
||||||
#: aircox/models/sound.py:125
|
#: aircox/models/sound.py:122
|
||||||
msgid "modification time"
|
msgid "modification time"
|
||||||
msgstr "dernière modification"
|
msgstr "dernière modification"
|
||||||
|
|
||||||
#: aircox/models/sound.py:127
|
#: aircox/models/sound.py:124
|
||||||
msgid "last modification date and time"
|
msgid "last modification date and time"
|
||||||
msgstr "date et heure de la dernière modification"
|
msgstr "date et heure de la dernière modification"
|
||||||
|
|
||||||
#: aircox/models/sound.py:130
|
#: aircox/models/sound.py:127
|
||||||
msgid "good quality"
|
msgid "good quality"
|
||||||
msgstr "bonne qualité"
|
msgstr "bonne qualité"
|
||||||
|
|
||||||
#: aircox/models/sound.py:130
|
#: aircox/models/sound.py:127
|
||||||
msgid "sound meets quality requirements"
|
msgid "sound meets quality requirements"
|
||||||
msgstr "le son rencontre les exigences de qualité"
|
msgstr "le son rencontre les exigences de qualité"
|
||||||
|
|
||||||
#: aircox/models/sound.py:134
|
#: aircox/models/sound.py:131
|
||||||
msgid "public"
|
msgid "public"
|
||||||
msgstr "publique"
|
msgstr "publique"
|
||||||
|
|
||||||
#: aircox/models/sound.py:134
|
#: aircox/models/sound.py:131
|
||||||
msgid "whether it is publicly available as podcast"
|
msgid "whether it is publicly available as podcast"
|
||||||
msgstr "coché pour rendre le podcast public"
|
msgstr "coché pour rendre le podcast public"
|
||||||
|
|
||||||
#: aircox/models/sound.py:138
|
#: aircox/models/sound.py:135
|
||||||
msgid "downloadable"
|
msgid "downloadable"
|
||||||
msgstr "téléchargeable"
|
msgstr "téléchargeable"
|
||||||
|
|
||||||
#: aircox/models/sound.py:139
|
#: aircox/models/sound.py:136
|
||||||
msgid ""
|
msgid ""
|
||||||
"whether it can be publicly downloaded by visitors (sound must be public)"
|
"whether it can be publicly downloaded by visitors (sound must be public)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"coché pour permettre le téléchargement public (le podcast doit être "
|
"coché pour permettre le téléchargement public (le podcast doit être "
|
||||||
"disponible publiquement)"
|
"disponible publiquement)"
|
||||||
|
|
||||||
#: aircox/models/sound.py:147
|
#: aircox/models/sound.py:144
|
||||||
msgid "Sounds"
|
msgid "Sounds"
|
||||||
msgstr "Sons"
|
msgstr "Sons"
|
||||||
|
|
||||||
#: aircox/models/sound.py:237
|
#: aircox/models/sound.py:234
|
||||||
msgid "sound"
|
msgid "sound"
|
||||||
msgstr "son"
|
msgstr "son"
|
||||||
|
|
||||||
#: aircox/models/sound.py:245
|
#: aircox/models/sound.py:242
|
||||||
msgid "position (in seconds)"
|
msgid "position (in seconds)"
|
||||||
msgstr "position (en secondes)"
|
msgstr "position (en secondes)"
|
||||||
|
|
||||||
#: aircox/models/sound.py:248
|
#: aircox/models/sound.py:245
|
||||||
msgid "artist"
|
msgid "artist"
|
||||||
msgstr "artiste"
|
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"
|
msgid "tags"
|
||||||
msgstr "tags"
|
msgstr "tags"
|
||||||
|
|
||||||
|
#: aircox/models/sound.py:248
|
||||||
|
msgid "year"
|
||||||
|
msgstr "année"
|
||||||
|
|
||||||
#: aircox/models/sound.py:251
|
#: aircox/models/sound.py:251
|
||||||
msgid "information"
|
msgid "information"
|
||||||
msgstr "information"
|
msgstr "information"
|
||||||
|
@ -677,6 +685,18 @@ msgstr ""
|
||||||
"liste des paramètres disponibles séparés par des virgules; placé dans le "
|
"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é"
|
"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
|
#: aircox/templates/admin/aircox/filters/filter.html:2
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid " By %(filter_title)s "
|
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_form.html:9
|
||||||
#: aircox/templates/admin/aircox/page_change_list.html:7
|
#: 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/admin/change_list.html:30
|
||||||
#: aircox/templates/aircox/base.html:54
|
#: aircox/templates/aircox/base.html:54
|
||||||
msgid "Home"
|
msgid "Home"
|
||||||
|
@ -715,36 +735,46 @@ msgstr "Sauvegarder et continuer"
|
||||||
msgid "Publish"
|
msgid "Publish"
|
||||||
msgstr "Publier"
|
msgstr "Publier"
|
||||||
|
|
||||||
#: aircox/templates/admin/aircox/statistics.html:24
|
#: aircox/templates/admin/aircox/playlist_inline.html:33
|
||||||
msgid "track"
|
#: aircox/templates/admin/aircox/playlist_inline.html:34
|
||||||
msgstr "piste"
|
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
|
#: aircox/templates/admin/aircox/statistics.html:67
|
||||||
msgid "Total"
|
msgid "Total"
|
||||||
msgstr "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
|
#: aircox/templates/aircox/home.html:47
|
||||||
msgid "Today"
|
msgid "Today"
|
||||||
msgstr "Aujourd'hui"
|
msgstr "Aujourd'hui"
|
||||||
|
|
||||||
#: aircox/templates/admin/base.html:121
|
#: aircox/templates/admin/base.html:124
|
||||||
msgid "Tools"
|
msgid "Tools"
|
||||||
msgstr "Outils"
|
msgstr "Outils"
|
||||||
|
|
||||||
#: aircox/templates/admin/base.html:137
|
#: aircox/templates/admin/base.html:140
|
||||||
msgid "View site"
|
msgid "View site"
|
||||||
msgstr "Voir le site"
|
msgstr "Voir le site"
|
||||||
|
|
||||||
#: aircox/templates/admin/base.html:142
|
#: aircox/templates/admin/base.html:145
|
||||||
msgid "Documentation"
|
msgid "Documentation"
|
||||||
msgstr "Documentation"
|
msgstr "Documentation"
|
||||||
|
|
||||||
#: aircox/templates/admin/base.html:146
|
#: aircox/templates/admin/base.html:149
|
||||||
msgid "Change password"
|
msgid "Change password"
|
||||||
msgstr "Changer le mot de passe"
|
msgstr "Changer le mot de passe"
|
||||||
|
|
||||||
#: aircox/templates/admin/base.html:149
|
#: aircox/templates/admin/base.html:152
|
||||||
msgid "Log out"
|
msgid "Log out"
|
||||||
msgstr "Se déconnecter"
|
msgstr "Se déconnecter"
|
||||||
|
|
||||||
|
@ -958,67 +988,108 @@ msgstr "Épisode en ce moment sur les ondes"
|
||||||
msgid "Currently playing"
|
msgid "Currently playing"
|
||||||
msgstr "En ce moment"
|
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/"
|
msgid "articles/"
|
||||||
msgstr "articles/"
|
msgstr "articles/"
|
||||||
|
|
||||||
#: aircox/urls.py:43
|
#: aircox/urls.py:47
|
||||||
msgid "articles/<slug:slug>/"
|
msgid "articles/<slug:slug>/"
|
||||||
msgstr "articles/<slug:slug>/"
|
msgstr "articles/<slug:slug>/"
|
||||||
|
|
||||||
#: aircox/urls.py:47
|
#: aircox/urls.py:51
|
||||||
msgid "episodes/"
|
msgid "episodes/"
|
||||||
msgstr "episodes/"
|
msgstr "episodes/"
|
||||||
|
|
||||||
#: aircox/urls.py:49
|
#: aircox/urls.py:53
|
||||||
msgid "episodes/<slug:slug>/"
|
msgid "episodes/<slug:slug>/"
|
||||||
msgstr "episodes/<slug:slug>/"
|
msgstr "episodes/<slug:slug>/"
|
||||||
|
|
||||||
#: aircox/urls.py:51
|
#: aircox/urls.py:55
|
||||||
msgid "week/"
|
msgid "week/"
|
||||||
msgstr "semaine/"
|
msgstr "semaine/"
|
||||||
|
|
||||||
#: aircox/urls.py:53
|
#: aircox/urls.py:57
|
||||||
msgid "week/<date:date>/"
|
msgid "week/<date:date>/"
|
||||||
msgstr "semaine/<date:date>/"
|
msgstr "semaine/<date:date>/"
|
||||||
|
|
||||||
#: aircox/urls.py:56
|
#: aircox/urls.py:60
|
||||||
msgid "logs/"
|
msgid "logs/"
|
||||||
msgstr "logs/"
|
msgstr "logs/"
|
||||||
|
|
||||||
#: aircox/urls.py:57
|
#: aircox/urls.py:61
|
||||||
msgid "logs/<date:date>/"
|
msgid "logs/<date:date>/"
|
||||||
msgstr "logs/<date:date>/"
|
msgstr "logs/<date:date>/"
|
||||||
|
|
||||||
#: aircox/urls.py:60
|
#: aircox/urls.py:64
|
||||||
msgid "publications/"
|
msgid "publications/"
|
||||||
msgstr "publications/"
|
msgstr "publications/"
|
||||||
|
|
||||||
#: aircox/urls.py:63
|
#: aircox/urls.py:67
|
||||||
msgid "pages/"
|
msgid "pages/"
|
||||||
msgstr "pages/"
|
msgstr "pages/"
|
||||||
|
|
||||||
#: aircox/urls.py:69
|
#: aircox/urls.py:73
|
||||||
msgid "pages/<slug:slug>/"
|
msgid "pages/<slug:slug>/"
|
||||||
msgstr "pages/<slug:slug>/"
|
msgstr "pages/<slug:slug>/"
|
||||||
|
|
||||||
#: aircox/urls.py:76
|
#: aircox/urls.py:80
|
||||||
msgid "programs/"
|
msgid "programs/"
|
||||||
msgstr "emissions/"
|
msgstr "emissions/"
|
||||||
|
|
||||||
#: aircox/urls.py:78
|
#: aircox/urls.py:82
|
||||||
msgid "programs/<slug:slug>/"
|
msgid "programs/<slug:slug>/"
|
||||||
msgstr "emissions/<slug:slug>/"
|
msgstr "emissions/<slug:slug>/"
|
||||||
|
|
||||||
#: aircox/urls.py:80
|
#: aircox/urls.py:84
|
||||||
msgid "programs/<slug:parent_slug>/episodes/"
|
msgid "programs/<slug:parent_slug>/episodes/"
|
||||||
msgstr "emissions/<slug:parent_slug>/episodes/"
|
msgstr "emissions/<slug:parent_slug>/episodes/"
|
||||||
|
|
||||||
#: aircox/urls.py:82
|
#: aircox/urls.py:86
|
||||||
msgid "programs/<slug:parent_slug>/articles/"
|
msgid "programs/<slug:parent_slug>/articles/"
|
||||||
msgstr "emissions/<slug:parent_slug>/articles/"
|
msgstr "emissions/<slug:parent_slug>/articles/"
|
||||||
|
|
||||||
#: aircox/urls.py:84
|
#: aircox/urls.py:88
|
||||||
msgid "programs/<slug:parent_slug>/publications/"
|
msgid "programs/<slug:parent_slug>/publications/"
|
||||||
msgstr "emissions/<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"
|
msgid "comments are not allowed"
|
||||||
msgstr "les commentaires ne sont pas autorisés"
|
msgstr "les commentaires ne sont pas autorisés"
|
||||||
|
|
||||||
|
#~ msgid "track"
|
||||||
|
#~ msgstr "morceau"
|
||||||
|
|
||||||
#~ msgid "if it can be podcasted from the server"
|
#~ msgid "if it can be podcasted from the server"
|
||||||
#~ msgstr "s'il peut être podcasté depuis le serveur"
|
#~ msgstr "s'il peut être podcasté depuis le serveur"
|
||||||
|
|
||||||
|
|
|
@ -219,7 +219,6 @@ class MonitorHandler(PatternMatchingEventHandler):
|
||||||
"""
|
"""
|
||||||
self.subdir = subdir
|
self.subdir = subdir
|
||||||
self.pool = pool
|
self.pool = pool
|
||||||
|
|
||||||
if self.subdir == settings.AIRCOX_SOUND_ARCHIVES_SUBDIR:
|
if self.subdir == settings.AIRCOX_SOUND_ARCHIVES_SUBDIR:
|
||||||
self.sound_kwargs = {'type': Sound.TYPE_ARCHIVE}
|
self.sound_kwargs = {'type': Sound.TYPE_ARCHIVE}
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
from .article import Article
|
from .article import *
|
||||||
from .page import Category, Page, StaticPage, Comment, NavItem
|
from .page import *
|
||||||
from .program import Program, Stream, Schedule
|
from .program import *
|
||||||
from .episode import Episode, Diffusion
|
from .episode import *
|
||||||
from .log import Log
|
from .log import *
|
||||||
from .sound import Sound, Track
|
from .sound import *
|
||||||
from .station import Station, Port
|
from .station import *
|
||||||
|
from .user_settings import *
|
||||||
|
|
||||||
from . import signals
|
from . import signals
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
from django.db import models
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from .page import Page, PageQuerySet
|
from .page import Page
|
||||||
from .program import Program, ProgramChildQuerySet
|
from .program import ProgramChildQuerySet
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ('Article',)
|
||||||
|
|
||||||
|
|
||||||
class Article(Page):
|
class Article(Page):
|
||||||
|
|
|
@ -9,12 +9,12 @@ from django.utils.functional import cached_property
|
||||||
from easy_thumbnails.files import get_thumbnailer
|
from easy_thumbnails.files import get_thumbnailer
|
||||||
|
|
||||||
from aircox import settings, utils
|
from aircox import settings, utils
|
||||||
from .program import Program, ProgramChildQuerySet, \
|
from .program import ProgramChildQuerySet, \
|
||||||
BaseRerun, BaseRerunQuerySet, Schedule
|
BaseRerun, BaseRerunQuerySet, Schedule
|
||||||
from .page import Page, PageQuerySet
|
from .page import Page
|
||||||
|
|
||||||
|
|
||||||
__all__ = ['Episode', 'Diffusion', 'DiffusionQuerySet']
|
__all__ = ('Episode', 'Diffusion', 'DiffusionQuerySet')
|
||||||
|
|
||||||
|
|
||||||
class Episode(Page):
|
class Episode(Page):
|
||||||
|
@ -201,7 +201,7 @@ class Diffusion(BaseRerun):
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
str_ = '{episode} - {date}'.format(
|
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'),
|
date=self.local_start.strftime('%Y/%m/%d %H:%M%z'),
|
||||||
)
|
)
|
||||||
if self.initial:
|
if self.initial:
|
||||||
|
@ -324,5 +324,3 @@ class Diffusion(BaseRerun):
|
||||||
'end': self.end,
|
'end': self.end,
|
||||||
'episode': getattr(self, 'episode', None),
|
'episode': getattr(self, 'episode', None),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,7 @@ from .station import Station
|
||||||
logger = logging.getLogger('aircox')
|
logger = logging.getLogger('aircox')
|
||||||
|
|
||||||
|
|
||||||
__all__ = ['Log', 'LogQuerySet', 'LogArchiver']
|
__all__ = ('Log', 'LogQuerySet', 'LogArchiver')
|
||||||
|
|
||||||
|
|
||||||
class LogQuerySet(models.QuerySet):
|
class LogQuerySet(models.QuerySet):
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
from enum import IntEnum
|
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
@ -18,7 +17,8 @@ from model_utils.managers import InheritanceQuerySet
|
||||||
from .station import Station
|
from .station import Station
|
||||||
|
|
||||||
|
|
||||||
__all__ = ['Category', 'PageQuerySet', 'Page', 'Comment', 'NavItem']
|
__all__ = ('Category', 'PageQuerySet',
|
||||||
|
'Page', 'StaticPage', 'Comment', 'NavItem')
|
||||||
|
|
||||||
|
|
||||||
headline_re = re.compile(r'(<p>)?'
|
headline_re = re.compile(r'(<p>)?'
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import calendar
|
import calendar
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
import datetime
|
|
||||||
from enum import IntEnum
|
from enum import IntEnum
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
@ -10,7 +9,7 @@ import pytz
|
||||||
from django.conf import settings as conf
|
from django.conf import settings as conf
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import models
|
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.db.models.functions import Concat, Substr
|
||||||
from django.utils import timezone as tz
|
from django.utils import timezone as tz
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
@ -24,8 +23,8 @@ from .station import Station
|
||||||
logger = logging.getLogger('aircox')
|
logger = logging.getLogger('aircox')
|
||||||
|
|
||||||
|
|
||||||
__all__ = ['Program', 'ProgramQuerySet', 'Stream', 'Schedule',
|
__all__ = ('Program', 'ProgramQuerySet', 'Stream', 'Schedule',
|
||||||
'ProgramChildQuerySet', 'BaseRerun', 'BaseRerunQuerySet']
|
'ProgramChildQuerySet', 'BaseRerun', 'BaseRerunQuerySet')
|
||||||
|
|
||||||
|
|
||||||
class ProgramQuerySet(PageQuerySet):
|
class ProgramQuerySet(PageQuerySet):
|
||||||
|
|
|
@ -2,7 +2,7 @@ import pytz
|
||||||
|
|
||||||
from django.contrib.auth.models import User, Group, Permission
|
from django.contrib.auth.models import User, Group, Permission
|
||||||
from django.db import transaction
|
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.dispatch import receiver
|
||||||
from django.utils import timezone as tz
|
from django.utils import timezone as tz
|
||||||
|
|
||||||
|
|
|
@ -1,17 +1,14 @@
|
||||||
from enum import IntEnum
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from django.conf import settings as conf
|
from django.conf import settings as conf
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import Q, Value as V
|
from django.db.models import Q
|
||||||
from django.db.models.functions import Concat
|
|
||||||
from django.utils import timezone as tz
|
from django.utils import timezone as tz
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from taggit.managers import TaggableManager
|
from taggit.managers import TaggableManager
|
||||||
|
|
||||||
from aircox import settings
|
|
||||||
from .program import Program
|
from .program import Program
|
||||||
from .episode import Episode
|
from .episode import Episode
|
||||||
|
|
||||||
|
@ -19,7 +16,7 @@ from .episode import Episode
|
||||||
logger = logging.getLogger('aircox')
|
logger = logging.getLogger('aircox')
|
||||||
|
|
||||||
|
|
||||||
__all__ = ['Sound', 'SoundQuerySet', 'Track']
|
__all__ = ('Sound', 'SoundQuerySet', 'Track')
|
||||||
|
|
||||||
|
|
||||||
class SoundQuerySet(models.QuerySet):
|
class SoundQuerySet(models.QuerySet):
|
||||||
|
@ -246,7 +243,10 @@ class Track(models.Model):
|
||||||
)
|
)
|
||||||
title = models.CharField(_('title'), max_length=128)
|
title = models.CharField(_('title'), max_length=128)
|
||||||
artist = models.CharField(_('artist'), 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(
|
info = models.CharField(
|
||||||
_('information'),
|
_('information'),
|
||||||
max_length=128,
|
max_length=128,
|
||||||
|
|
|
@ -2,13 +2,14 @@ import os
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from django.utils.functional import cached_property
|
||||||
|
|
||||||
from filer.fields.image import FilerImageField
|
from filer.fields.image import FilerImageField
|
||||||
|
|
||||||
from .. import settings
|
from .. import settings
|
||||||
|
|
||||||
|
|
||||||
__all__ = ['Station', 'StationQuerySet', 'Port']
|
__all__ = ('Station', 'StationQuerySet', 'Port')
|
||||||
|
|
||||||
|
|
||||||
class StationQuerySet(models.QuerySet):
|
class StationQuerySet(models.QuerySet):
|
||||||
|
@ -74,6 +75,11 @@ class Station(models.Model):
|
||||||
|
|
||||||
objects = StationQuerySet.as_manager()
|
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):
|
def __str__(self):
|
||||||
return self.name
|
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 rest_framework import serializers
|
||||||
|
|
||||||
from .models import Diffusion, Log, Sound
|
from ..models import Diffusion, Log
|
||||||
|
|
||||||
|
|
||||||
__all__ = ['LogInfo', 'LogInfoSerializer']
|
__all__ = ('LogInfo', 'LogInfoSerializer')
|
||||||
|
|
||||||
|
|
||||||
class LogInfo:
|
class LogInfo:
|
||||||
|
@ -51,21 +51,3 @@ class LogInfoSerializer(serializers.Serializer):
|
||||||
info = serializers.CharField(max_length=200, required=False)
|
info = serializers.CharField(max_length=200, required=False)
|
||||||
url = serializers.URLField(required=False)
|
url = serializers.URLField(required=False)
|
||||||
cover = 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 %}
|
{% comment %}Inline block to edit playlists{% endcomment %}
|
||||||
{% load static i18n %}
|
{% load aircox aircox_admin static i18n %}
|
||||||
|
|
||||||
{% with inline_admin_formset.formset.instance as playlist %}
|
{% with inline_admin_formset as admin_formset %}
|
||||||
{% include "adminsortable2/edit_inline/tabular-django-4.1.html" %}
|
{% 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 %}
|
{% endwith %}
|
||||||
|
|
||||||
|
|
|
@ -18,10 +18,10 @@
|
||||||
<table class="table is-hoverable is-fullwidth">
|
<table class="table is-hoverable is-fullwidth">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>{% translate "time" %}</th>
|
<th>{% translate "Time" %}</th>
|
||||||
<th>{% translate "episode" %}</th>
|
<th>{% translate "Episode" %}</th>
|
||||||
<th>{% translate "track" %}</th>
|
<th>{% translate "Track" %}</th>
|
||||||
<th>{% translate "tags" %}</th>
|
<th>{% translate "Tags" %}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|
|
@ -44,6 +44,9 @@
|
||||||
{% if not init_app %}
|
{% if not init_app %}
|
||||||
initBuilder: false,
|
initBuilder: false,
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if init_el %}
|
||||||
|
el: "{{ init_el }}",
|
||||||
|
{% endif %}
|
||||||
})
|
})
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
})
|
})
|
||||||
|
|
|
@ -5,12 +5,13 @@ Base template used to display a Page
|
||||||
|
|
||||||
Context:
|
Context:
|
||||||
- page: page
|
- page: page
|
||||||
|
- parent: parent page
|
||||||
{% endcomment %}
|
{% endcomment %}
|
||||||
|
|
||||||
{% block header_crumbs %}
|
{% block header_crumbs %}
|
||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
{% if page.category %}
|
{% if page.category %}
|
||||||
/ {{ page.category.title }}
|
{% if parent %} / {% endif %} {{ page.category.title }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
|
@ -9,8 +9,9 @@ The audio player
|
||||||
role="{% translate "player" %}"
|
role="{% translate "player" %}"
|
||||||
aria-description="{% translate "Audio player used to listen to the radio and podcasts" %}">
|
aria-description="{% translate "Audio player used to listen to the radio and podcasts" %}">
|
||||||
<noscript>
|
<noscript>
|
||||||
<audio src="{{ audio_streams.0 }}" controls>
|
<audio src="{% if request.station.streams %}{{ request.station.streams.0 }}{% endif %}"
|
||||||
{% for stream in audio_streams %}
|
controls>
|
||||||
|
{% for stream in request.station.streams %}
|
||||||
<source src="{{ stream }}" />
|
<source src="{{ stream }}" />
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</audio>
|
</audio>
|
||||||
|
@ -18,7 +19,7 @@ The audio player
|
||||||
</noscript>
|
</noscript>
|
||||||
|
|
||||||
<a-player ref="player"
|
<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" %}">
|
button-title="{% translate "Play or pause audio" %}">
|
||||||
<template v-slot:content="{ loaded, live, current }">
|
<template v-slot:content="{ loaded, live, current }">
|
||||||
<h4 v-if="loaded" class="title is-4">
|
<h4 v-if="loaded" class="title is-4">
|
||||||
|
|
|
@ -4,9 +4,8 @@ import json
|
||||||
from django import template
|
from django import template
|
||||||
from django.contrib.admin.templatetags.admin_urls import admin_urlname
|
from django.contrib.admin.templatetags.admin_urls import admin_urlname
|
||||||
from django.urls import reverse
|
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()
|
random.seed()
|
||||||
register = template.Library()
|
register = template.Library()
|
||||||
|
@ -18,6 +17,7 @@ def do_admin_url(obj, arg, pass_id=True):
|
||||||
name = admin_urlname(obj._meta, arg)
|
name = admin_urlname(obj._meta, arg)
|
||||||
return reverse(name, args=(obj.id,)) if pass_id else reverse(name)
|
return reverse(name, args=(obj.id,)) if pass_id else reverse(name)
|
||||||
|
|
||||||
|
|
||||||
@register.filter(name='get_tracks')
|
@register.filter(name='get_tracks')
|
||||||
def do_get_tracks(obj):
|
def do_get_tracks(obj):
|
||||||
""" Get a list of track for the provided log, diffusion, or episode """
|
""" Get a list of track for the provided log, diffusion, or episode """
|
||||||
|
@ -28,6 +28,7 @@ def do_get_tracks(obj):
|
||||||
obj = obj.episode
|
obj = obj.episode
|
||||||
return obj.track_set.all()
|
return obj.track_set.all()
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag(name='has_perm', takes_context=True)
|
@register.simple_tag(name='has_perm', takes_context=True)
|
||||||
def do_has_perm(context, obj, perm, user=None):
|
def do_has_perm(context, obj, perm, user=None):
|
||||||
""" Return True if ``user.has_perm('[APP].[perm]_[MODEL]')`` """
|
""" 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(
|
return user.has_perm('{}.{}_{}'.format(
|
||||||
obj._meta.app_label, perm, obj._meta.model_name))
|
obj._meta.app_label, perm, obj._meta.model_name))
|
||||||
|
|
||||||
|
|
||||||
@register.filter(name='is_diffusion')
|
@register.filter(name='is_diffusion')
|
||||||
def do_is_diffusion(obj):
|
def do_is_diffusion(obj):
|
||||||
""" Return True if object is a Diffusion. """
|
""" Return True if object is a Diffusion. """
|
||||||
return isinstance(obj, Diffusion)
|
return isinstance(obj, Diffusion)
|
||||||
|
|
||||||
|
|
||||||
@register.filter(name='json')
|
@register.filter(name='json')
|
||||||
def do_json(obj, fields=""):
|
def do_json(obj, fields=""):
|
||||||
""" Return object as json """
|
""" Return object as json """
|
||||||
if fields:
|
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)
|
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)
|
@register.simple_tag(name='nav_items', takes_context=True)
|
||||||
def do_nav_items(context, menu, **kwargs):
|
def do_nav_items(context, menu, **kwargs):
|
||||||
""" Render navigation items for the provided menu name. """
|
""" 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))
|
return [(item, item.render(request, **kwargs))
|
||||||
for item in station.navitem_set.filter(menu=menu)]
|
for item in station.navitem_set.filter(menu=menu)]
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag(name='update_query')
|
@register.simple_tag(name='update_query')
|
||||||
def do_update_query(obj, **kwargs):
|
def do_update_query(obj, **kwargs):
|
||||||
""" Replace provided querydict's values with **kwargs. """
|
""" Replace provided querydict's values with **kwargs. """
|
||||||
|
@ -65,6 +81,7 @@ def do_update_query(obj, **kwargs):
|
||||||
obj.pop(k)
|
obj.pop(k)
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
|
|
||||||
@register.filter(name='verbose_name')
|
@register.filter(name='verbose_name')
|
||||||
def do_verbose_name(obj, plural=False):
|
def do_verbose_name(obj, plural=False):
|
||||||
"""
|
"""
|
||||||
|
@ -74,4 +91,3 @@ def do_verbose_name(obj, plural=False):
|
||||||
return obj if isinstance(obj, str) else \
|
return obj if isinstance(obj, str) else \
|
||||||
obj._meta.verbose_name_plural if plural else \
|
obj._meta.verbose_name_plural if plural else \
|
||||||
obj._meta.verbose_name
|
obj._meta.verbose_name
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,63 @@
|
||||||
|
import json
|
||||||
from django import template
|
from django import template
|
||||||
from django.contrib import admin
|
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 = template.Library()
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag(name='get_admin_tools')
|
@register.simple_tag(name='get_admin_tools')
|
||||||
def do_get_admin_tools():
|
def do_get_admin_tools():
|
||||||
return admin.site.get_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 = DefaultRouter()
|
||||||
router.register('sound', viewsets.SoundViewSet, basename='sound')
|
router.register('sound', viewsets.SoundViewSet, basename='sound')
|
||||||
|
router.register('track', viewsets.TrackROViewSet, basename='track')
|
||||||
|
|
||||||
|
|
||||||
api = [
|
api = [
|
||||||
path('logs/', views.LogListAPIView.as_view(), name='live'),
|
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
|
] + 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.views.generic.base import TemplateResponseMixin, ContextMixin
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from ..models import Page
|
from ..models import Page
|
||||||
from ..utils import Redirect
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = ['BaseView']
|
__all__ = ('BaseView', 'BaseAPIView')
|
||||||
|
|
||||||
|
|
||||||
class BaseView(TemplateResponseMixin, ContextMixin):
|
class BaseView(TemplateResponseMixin, ContextMixin):
|
||||||
|
@ -55,13 +51,14 @@ class BaseView(TemplateResponseMixin, ContextMixin):
|
||||||
kwargs['audio_streams'] = streams
|
kwargs['audio_streams'] = streams
|
||||||
|
|
||||||
if 'model' not in kwargs:
|
if 'model' not in kwargs:
|
||||||
model = getattr(self, 'model', None) or hasattr(self, 'object') and \
|
model = getattr(self, 'model', None) or \
|
||||||
type(self.object)
|
hasattr(self, 'object') and type(self.object)
|
||||||
kwargs['model'] = model
|
kwargs['model'] = model
|
||||||
|
|
||||||
return super().get_context_data(**kwargs)
|
return super().get_context_data(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
# FIXME: rename to sth like [Base]?StationAPIView
|
||||||
class BaseAPIView:
|
class BaseAPIView:
|
||||||
@property
|
@property
|
||||||
def station(self):
|
def station(self):
|
||||||
|
|
|
@ -1,13 +1,18 @@
|
||||||
from django.db.models import Q
|
from rest_framework import status, viewsets
|
||||||
|
from rest_framework.decorators import action
|
||||||
from rest_framework import viewsets
|
from rest_framework.permissions import IsAuthenticated
|
||||||
|
from rest_framework.response import Response
|
||||||
from django_filters import rest_framework as filters
|
from django_filters import rest_framework as filters
|
||||||
|
|
||||||
from .models import Sound
|
from .models import Sound, Track
|
||||||
from .serializers import SoundSerializer
|
from .serializers import SoundSerializer, admin
|
||||||
from .views import BaseAPIView
|
from .views import BaseAPIView
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ('SoundFilter', 'SoundViewSet', 'TrackFilter', 'TrackROViewSet',
|
||||||
|
'UserSettingsViewSet')
|
||||||
|
|
||||||
|
|
||||||
class SoundFilter(filters.FilterSet):
|
class SoundFilter(filters.FilterSet):
|
||||||
station = filters.NumberFilter(field_name='program__station__id')
|
station = filters.NumberFilter(field_name='program__station__id')
|
||||||
program = filters.NumberFilter(field_name='program_id')
|
program = filters.NumberFilter(field_name='program_id')
|
||||||
|
@ -24,3 +29,63 @@ class SoundViewSet(BaseAPIView, viewsets.ModelViewSet):
|
||||||
filter_backends = (filters.DjangoFilterBackend,)
|
filter_backends = (filters.DjangoFilterBackend,)
|
||||||
filterset_class = SoundFilter
|
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.models import Station, Episode, Diffusion, Track, Sound, Log
|
||||||
from aircox.utils import date_range
|
from aircox.utils import date_range
|
||||||
|
|
||||||
from aircox_streamer.controllers import Streamer
|
|
||||||
|
|
||||||
|
from aircox_streamer.controllers import Streamer
|
||||||
|
|
||||||
# force using UTC
|
# force using UTC
|
||||||
tz.activate(pytz.UTC)
|
tz.activate(pytz.UTC)
|
||||||
|
|
|
@ -53,7 +53,7 @@
|
||||||
{# TODO: select station => change the shit #}
|
{# TODO: select station => change the shit #}
|
||||||
<a-autocomplete class="control is-expanded"
|
<a-autocomplete class="control is-expanded"
|
||||||
url="{% url "aircox:sound-list" %}?station={{ station.pk }}&search=${query}"
|
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" %}">
|
placeholder="{% translate "Select a sound" %}">
|
||||||
<template v-slot:item="{item}">
|
<template v-slot:item="{item}">
|
||||||
[[ item.data.name ]]
|
[[ item.data.name ]]
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-free": "^6.0.0",
|
"@fortawesome/fontawesome-free": "^6.0.0",
|
||||||
"core-js": "^3.8.3",
|
"core-js": "^3.8.3",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
"vue": "^3.2.13"
|
"vue": "^3.2.13"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
@ -4,10 +4,18 @@ import './index.js'
|
||||||
|
|
||||||
import App from './app';
|
import App from './app';
|
||||||
import {admin as components} from './components'
|
import {admin as components} from './components'
|
||||||
|
import Track from './track'
|
||||||
|
|
||||||
const AdminApp = {
|
const AdminApp = {
|
||||||
...App,
|
...App,
|
||||||
components: {...App.components, ...components},
|
components: {...App.components, ...components},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
...super.data,
|
||||||
|
Track,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
export default AdminApp;
|
export default AdminApp;
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,7 @@ $menu-item-active-background-color: #d2d2d2;
|
||||||
|
|
||||||
//-- helpers/modifiers
|
//-- helpers/modifiers
|
||||||
.is-fullwidth { width: 100%; }
|
.is-fullwidth { width: 100%; }
|
||||||
|
.is-fullheight { height: 100%; }
|
||||||
.is-fixed-bottom {
|
.is-fixed-bottom {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
|
@ -40,6 +41,19 @@ $menu-item-active-background-color: #d2d2d2;
|
||||||
.overflow-hidden.is-fullwidth { max-width: 100%; }
|
.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 {
|
@keyframes blink {
|
||||||
from { opacity: 1; }
|
from { opacity: 1; }
|
||||||
to { opacity: 0.4; }
|
to { opacity: 0.4; }
|
||||||
|
@ -215,6 +229,10 @@ a.navbar-item.is-active {
|
||||||
.player-bar {
|
.player-bar {
|
||||||
border-top: 1px $grey-light solid;
|
border-top: 1px $grey-light solid;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
height: 3.75em !important;
|
||||||
|
}
|
||||||
|
|
||||||
> .media-left:not(:last-child) {
|
> .media-left:not(:last-child) {
|
||||||
margin-right: 0em;
|
margin-right: 0em;
|
||||||
}
|
}
|
||||||
|
@ -235,7 +253,8 @@ a.navbar-item.is-active {
|
||||||
|
|
||||||
.button {
|
.button {
|
||||||
font-size: 1.5rem !important;
|
font-size: 1.5rem !important;
|
||||||
height: 2.5em;
|
height: 100%;
|
||||||
|
padding: auto 0.2em !important;
|
||||||
min-width: 2.5em;
|
min-width: 2.5em;
|
||||||
border-radius: 0px;
|
border-radius: 0px;
|
||||||
transition: background-color 1s;
|
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,15 +1,18 @@
|
||||||
<template>
|
<template>
|
||||||
<div :class="dropdownClass">
|
<div class="control">
|
||||||
<div class="dropdown-trigger is-fullwidth">
|
<input type="hidden" :name="name" :value="selectedValue"
|
||||||
<input type="hidden" :name="name"
|
@change="$emit('change', $event)"/>
|
||||||
:value="selectedValue" />
|
<input type="text" ref="input" class="input is-fullwidth" :class="inputClass"
|
||||||
<div v-show="!selected" class="control is-expanded">
|
v-show="!button || !selected"
|
||||||
<input type="text" :placeholder="placeholder"
|
v-model="inputValue"
|
||||||
ref="input" class="input is-fullwidth"
|
:placeholder="placeholder"
|
||||||
@keydown.capture="onKeyPress"
|
@keydown.capture="onKeyDown"
|
||||||
@keyup="onKeyUp" @focus="this.cursor < 0 && move(0)"/>
|
@keyup="onKeyUp($event); $emit('keyup', $event)"
|
||||||
</div>
|
@keydown="$emit('keydown', $event)"
|
||||||
<button v-if="selected" class="button is-normal is-fullwidth has-text-left is-inline-block overflow-hidden"
|
@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)">
|
@click="select(-1, false, true)">
|
||||||
<span class="icon is-small ml-1">
|
<span class="icon is-small ml-1">
|
||||||
<i class="fa fa-pen"></i>
|
<i class="fa fa-pen"></i>
|
||||||
|
@ -17,51 +20,89 @@
|
||||||
<span class="is-inline-block" v-if="selected">
|
<span class="is-inline-block" v-if="selected">
|
||||||
<slot name="button" :index="selectedIndex" :item="selected"
|
<slot name="button" :index="selectedIndex" :item="selected"
|
||||||
:value-field="valueField" :labelField="labelField">
|
:value-field="valueField" :labelField="labelField">
|
||||||
{{ selected.data[labelField] }}
|
{{ labelField && selected.data[labelField] || selected }}
|
||||||
</slot>
|
</slot>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</a>
|
||||||
</div>
|
<div :class="dropdownClass">
|
||||||
<div class="dropdown-menu is-fullwidth">
|
<div class="dropdown-menu is-fullwidth">
|
||||||
<div class="dropdown-content" style="overflow: hidden">
|
<div class="dropdown-content" style="overflow: hidden">
|
||||||
<a v-for="(item, index) in items" :key="item.id"
|
<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':'']"
|
:class="['dropdown-item', (index == this.cursor) ? 'is-active':'']"
|
||||||
@click.capture.prevent="select(index, false, false)" :title="item.data[labelField]">
|
:title="labelField && item.data[labelField] || item"
|
||||||
|
tabindex="-1">
|
||||||
<slot name="item" :index="index" :item="item" :value-field="valueField"
|
<slot name="item" :index="index" :item="item" :value-field="valueField"
|
||||||
:labelField="labelField">
|
:labelField="labelField">
|
||||||
{{ item.data[labelField] }}
|
{{ labelField && item.data[labelField] || item }}
|
||||||
</slot>
|
</slot>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// import debounce from 'lodash/debounce'
|
// import debounce from 'lodash/debounce'
|
||||||
|
import Model from '../model'
|
||||||
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
emit: ['change', 'keypress', 'keydown', 'keyup', 'select', 'unselect',
|
||||||
|
'update:modelValue'],
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
|
//! Search URL (where `${query}` is replaced by search term)
|
||||||
url: String,
|
url: String,
|
||||||
|
//! Items' model
|
||||||
model: Function,
|
model: Function,
|
||||||
|
//! Input tag class
|
||||||
|
inputClass: Array,
|
||||||
|
//! input text placeholder
|
||||||
placeholder: String,
|
placeholder: String,
|
||||||
|
//! input form field name
|
||||||
name: String,
|
name: String,
|
||||||
|
//! Field on items to use as label
|
||||||
labelField: String,
|
labelField: String,
|
||||||
|
//! Field on selected item to get selectedValue from, if any
|
||||||
valueField: {type: String, default: null},
|
valueField: {type: String, default: null},
|
||||||
count: {type: Number, count: 10},
|
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() {
|
data() {
|
||||||
return {
|
return {
|
||||||
value: '',
|
inputValue: this.modelValue || '',
|
||||||
|
query: '',
|
||||||
items: [],
|
items: [],
|
||||||
selectedIndex: -1,
|
selectedIndex: -1,
|
||||||
cursor: -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: {
|
computed: {
|
||||||
|
isFetching() { return !!this.promise },
|
||||||
|
|
||||||
selected() {
|
selected() {
|
||||||
let index = this.selectedIndex
|
let index = this.selectedIndex
|
||||||
if(index<0)
|
if(index<0)
|
||||||
|
@ -71,23 +112,39 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
selectedValue() {
|
selectedValue() {
|
||||||
const sel = this.selected
|
let value = this.itemValue(this.selected)
|
||||||
return sel && (this.valueField ?
|
if(!value && !this.mustExist)
|
||||||
sel.data[this.valueField] : sel.id)
|
value = this.inputValue
|
||||||
|
return value
|
||||||
},
|
},
|
||||||
|
|
||||||
selectedLabel() {
|
selectedLabel() {
|
||||||
const sel = this.selected
|
return this.itemLabel(this.selected)
|
||||||
return sel && sel.data[this.labelField]
|
|
||||||
},
|
},
|
||||||
|
|
||||||
dropdownClass() {
|
dropdownClass() {
|
||||||
const active = this.cursor > -1 && this.items.length;
|
var active = this.cursor > -1 && this.items.length;
|
||||||
return ['dropdown', active ? 'is-active':'']
|
if(active && this.items.length == 1 &&
|
||||||
|
this.itemValue(this.items[0]) == this.inputValue)
|
||||||
|
active = false
|
||||||
|
return ['dropdown is-fullwidth', active ? 'is-active':'']
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
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) {
|
move(index=-1, relative=false) {
|
||||||
if(relative)
|
if(relative)
|
||||||
index += this.cursor
|
index += this.cursor
|
||||||
|
@ -102,7 +159,7 @@ export default {
|
||||||
|
|
||||||
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) {
|
if(index >= 0) {
|
||||||
this.$refs.input.value = this.selectedLabel
|
this.inputValue = this.selectedLabel
|
||||||
this.$refs.input.focus()
|
this.$refs.input.focus()
|
||||||
}
|
}
|
||||||
if(this.selectedIndex < 0)
|
if(this.selectedIndex < 0)
|
||||||
|
@ -114,11 +171,24 @@ export default {
|
||||||
active && this.move(0) || this.move(-1)
|
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) {
|
switch(event.keyCode) {
|
||||||
case 13: this.select(this.cursor, false, false)
|
case 13: this.select(this.cursor, false, false)
|
||||||
break
|
break
|
||||||
case 27: this.select()
|
case 27: this.hide(); this.select()
|
||||||
break
|
break
|
||||||
case 38: this.move(-1, true)
|
case 38: this.move(-1, true)
|
||||||
break
|
break
|
||||||
|
@ -130,35 +200,47 @@ export default {
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
},
|
},
|
||||||
|
|
||||||
onKeyUp: function(event) {
|
onKeyUp(event) {
|
||||||
const value = event.target.value
|
if(event.ctrlKey || event.altKey || event.metaKey)
|
||||||
if(value === this.value)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
this.value = value;
|
const value = event.target.value
|
||||||
|
if(value === this.query)
|
||||||
|
return
|
||||||
|
|
||||||
|
this.inputValue = value;
|
||||||
if(!value)
|
if(!value)
|
||||||
return this.selected && this.select(-1)
|
return this.selected && this.select(-1)
|
||||||
|
if(!this.minFetchLength || value.length >= this.minFetchLength)
|
||||||
this.fetch(value)
|
this.fetch(value)
|
||||||
},
|
},
|
||||||
|
|
||||||
fetch: function(query) {
|
fetch(query) {
|
||||||
if(!query || this.isFetching)
|
if(!query || this.promise)
|
||||||
return
|
return
|
||||||
|
|
||||||
this.isFetching = true
|
this.query = query
|
||||||
return this.model.fetch(this.url.replace('${query}', query), {many:true})
|
var url = this.url.replace('${query}', query)
|
||||||
.then(items => { this.items = items || []
|
var promise = this.model ? this.model.fetch(url, {many:true})
|
||||||
this.isFetching = false
|
: fetch(url, Model.getOptions()).then(d => d.json())
|
||||||
|
|
||||||
|
promise = promise.then(items => {
|
||||||
|
this.items = items || []
|
||||||
|
this.promise = null;
|
||||||
this.move(0)
|
this.move(0)
|
||||||
return items },
|
return items
|
||||||
data => {this.isFetching = false; Promise.reject(data)})
|
}, data => {this.promise = null; Promise.reject(data)})
|
||||||
|
this.promise = promise
|
||||||
|
return promise
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
const form = this.$el.closest('form')
|
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>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
|
<!-- FIXME: header and footer should be inside list tags -->
|
||||||
<slot name="header"></slot>
|
<slot name="header"></slot>
|
||||||
<ul :class="listClass">
|
<component :is="listTag" :class="listClass">
|
||||||
<template v-for="(item,index) in items" :key="index">
|
<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>
|
<slot name="item" :selected="index == selectedIndex" :set="set" :index="index" :item="item"></slot>
|
||||||
</li>
|
</component>
|
||||||
</template>
|
</template>
|
||||||
</ul>
|
</component>
|
||||||
<slot name="footer"></slot>
|
<slot name="footer"></slot>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
emits: ['select', 'unselect'],
|
emits: ['select', 'unselect', 'move'],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
selectedIndex: this.defaultIndex,
|
selectedIndex: this.defaultIndex,
|
||||||
|
@ -25,6 +28,9 @@ export default {
|
||||||
itemClass: String,
|
itemClass: String,
|
||||||
defaultIndex: { type: Number, default: -1},
|
defaultIndex: { type: Number, default: -1},
|
||||||
set: Object,
|
set: Object,
|
||||||
|
orderable: { type: Boolean, default: false },
|
||||||
|
itemTag: { default: 'li' },
|
||||||
|
listTag: { default: 'ul' },
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
|
@ -61,6 +67,34 @@ export default {
|
||||||
this.$emit('unselect', { item: this.selected, index: this.selectedIndex});
|
this.$emit('unselect', { item: this.selected, index: this.selectedIndex});
|
||||||
this.selectedIndex = -1;
|
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>
|
</script>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="player">
|
<div class="player">
|
||||||
<div :class="['player-panels', panel ? 'is-open' : '']">
|
<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"
|
name="Pinned"
|
||||||
:actions="['page']"
|
:actions="['page']"
|
||||||
:editable="true" :player="self" :set="sets.pin" @select="togglePlay('pin', $event.index)"
|
:editable="true" :player="self" :set="sets.pin" @select="togglePlay('pin', $event.index)"
|
||||||
|
@ -13,7 +13,7 @@
|
||||||
</p>
|
</p>
|
||||||
</template>
|
</template>
|
||||||
</APlaylist>
|
</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']"
|
:actions="['page']"
|
||||||
:editable="true" :player="self" :set="sets.queue" @select="togglePlay('queue', $event.index)"
|
:editable="true" :player="self" :set="sets.queue" @select="togglePlay('queue', $event.index)"
|
||||||
listClass="menu-list" itemClass="menu-item">
|
listClass="menu-list" itemClass="menu-item">
|
||||||
|
@ -51,14 +51,14 @@
|
||||||
<span>Live</span>
|
<span>Live</span>
|
||||||
</button>
|
</button>
|
||||||
<button ref="pinPlaylistButton" :class="playlistButtonClass('pin')"
|
<button ref="pinPlaylistButton" :class="playlistButtonClass('pin')"
|
||||||
@click="togglePanel('pin')">
|
@click="togglePanel('pin')" v-show="sets.pin.length">
|
||||||
<span class="mr-2 is-size-6" v-if="sets.pin.length">
|
<span class="is-size-6" v-if="sets.pin.length">
|
||||||
{{ sets.pin.length }}</span>
|
{{ sets.pin.length }}</span>
|
||||||
<span class="icon"><span class="fa fa-thumbtack"></span></span>
|
<span class="icon"><span class="fa fa-thumbtack"></span></span>
|
||||||
</button>
|
</button>
|
||||||
<button :class="playlistButtonClass('queue')"
|
<button :class="playlistButtonClass('queue')"
|
||||||
@click="togglePanel('queue')">
|
@click="togglePanel('queue')" v-show="sets.queue.length">
|
||||||
<span class="mr-2 is-size-6" v-if="sets.queue.length">
|
<span class="is-size-6" v-if="sets.queue.length">
|
||||||
{{ sets.queue.length }}</span>
|
{{ sets.queue.length }}</span>
|
||||||
<span class="icon"><span class="fa fa-list"></span></span>
|
<span class="icon"><span class="fa fa-list"></span></span>
|
||||||
</button>
|
</button>
|
||||||
|
@ -186,13 +186,11 @@ export default {
|
||||||
if(!item)
|
if(!item)
|
||||||
throw `No sound at index ${index} for playlist ${playlist}`;
|
throw `No sound at index ${index} for playlist ${playlist}`;
|
||||||
this.loaded = item
|
this.loaded = item
|
||||||
this.current = item
|
|
||||||
src = item.src;
|
src = item.src;
|
||||||
}
|
}
|
||||||
// from live
|
// from live
|
||||||
else {
|
else {
|
||||||
this.loaded = null;
|
this.loaded = null;
|
||||||
this.current = this.live.current
|
|
||||||
src = this.live.src;
|
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 APage from './APage.vue'
|
||||||
import APlayer from './APlayer.vue'
|
import APlayer from './APlayer.vue'
|
||||||
import APlaylist from './APlaylist.vue'
|
import APlaylist from './APlaylist.vue'
|
||||||
|
import APlaylistEditor from './APlaylistEditor.vue'
|
||||||
import AProgress from './AProgress.vue'
|
import AProgress from './AProgress.vue'
|
||||||
import ASoundItem from './ASoundItem.vue'
|
import ASoundItem from './ASoundItem.vue'
|
||||||
import AStatistics from './AStatistics.vue'
|
import AStatistics from './AStatistics.vue'
|
||||||
|
@ -12,12 +13,15 @@ import AStreamer from './AStreamer.vue'
|
||||||
/**
|
/**
|
||||||
* Core components
|
* Core components
|
||||||
*/
|
*/
|
||||||
export default {
|
export const base = {
|
||||||
AAutocomplete, AEpisode, AList, APage, APlayer, APlaylist,
|
AAutocomplete, AEpisode, AList, APage, APlayer, APlaylist,
|
||||||
AProgress, ASoundItem,
|
AProgress, ASoundItem,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default base
|
||||||
|
|
||||||
export const admin = {
|
export const admin = {
|
||||||
AStatistics, AStreamer,
|
...base,
|
||||||
|
AStatistics, AStreamer, APlaylistEditor
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -32,7 +32,7 @@ window.aircox = {
|
||||||
* Initialize main application and player.
|
* Initialize main application and player.
|
||||||
*/
|
*/
|
||||||
init(props=null, {config=null, builder=null, initBuilder=true,
|
init(props=null, {config=null, builder=null, initBuilder=true,
|
||||||
initPlayer=true, hotReload=false}={})
|
initPlayer=true, hotReload=false, el=null}={})
|
||||||
{
|
{
|
||||||
if(initPlayer) {
|
if(initPlayer) {
|
||||||
let playerBuilder = this.playerBuilder
|
let playerBuilder = this.playerBuilder
|
||||||
|
@ -44,6 +44,9 @@ window.aircox = {
|
||||||
this.builder = builder
|
this.builder = builder
|
||||||
if(config || window.App)
|
if(config || window.App)
|
||||||
builder.config = config || window.App
|
builder.config = config || window.App
|
||||||
|
if(el)
|
||||||
|
builder.config.el = el
|
||||||
|
|
||||||
builder.title = document.title
|
builder.title = document.title
|
||||||
builder.mount({props})
|
builder.mount({props})
|
||||||
|
|
||||||
|
|
|
@ -35,17 +35,21 @@ export default class Model {
|
||||||
* Instanciate model with provided data and options.
|
* Instanciate model with provided data and options.
|
||||||
* By default `url` is taken from `data.url_`.
|
* By default `url` is taken from `data.url_`.
|
||||||
*/
|
*/
|
||||||
constructor(data, {url=null, ...options}={}) {
|
constructor(data={}, {url=null, ...options}={}) {
|
||||||
this.url = url || data.url_;
|
this.url = url || data.url_;
|
||||||
this.options = options;
|
this.options = options;
|
||||||
this.commit(data);
|
this.commit(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get errors() {
|
||||||
|
return this.data && this.data.__errors__
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get instance id from its data
|
* Get instance id from its data
|
||||||
*/
|
*/
|
||||||
static getId(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
|
* Update instance's data with provided data. Return None
|
||||||
*/
|
*/
|
||||||
commit(data) {
|
commit(data) {
|
||||||
this.id = this.constructor.getId(data);
|
|
||||||
this.data = 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,8 +142,24 @@ export default class Model {
|
||||||
let item = window.localStorage.getItem(key);
|
let item = window.localStorage.getItem(key);
|
||||||
return item === null ? item : new this(JSON.parse(item));
|
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
|
* List of models
|
||||||
|
@ -231,6 +259,25 @@ export class Set {
|
||||||
this.items.splice(index,1);
|
this.items.splice(index,1);
|
||||||
save && this.save();
|
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 () {
|
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