diff --git a/aircox/admin/sound.py b/aircox/admin/sound.py index b2082cd..7dff945 100644 --- a/aircox/admin/sound.py +++ b/aircox/admin/sound.py @@ -28,7 +28,7 @@ class SoundInline(admin.TabularInline): max_num = 0 def audio(self, obj): - return mark_safe(''.format(obj.url())) + return mark_safe(''.format(obj.file.url)) audio.short_descripton = _('Audio') def get_queryset(self, request): @@ -46,10 +46,10 @@ class SoundAdmin(admin.ModelAdmin): search_fields = ['name', 'program__title'] fieldsets = [ - (None, {'fields': ['name', 'path', 'type', 'program', 'episode']}), + (None, {'fields': ['name', 'file', 'type', 'program', 'episode']}), (None, {'fields': ['duration', 'is_public', 'is_good_quality', 'mtime']}), ] - readonly_fields = ('path', 'duration',) + readonly_fields = ('file', 'duration',) inlines = [SoundTrackInline] def related(self, obj): @@ -59,7 +59,7 @@ class SoundAdmin(admin.ModelAdmin): related.short_description = _('Program / Episode') def audio(self, obj): - return mark_safe(''.format(obj.url())) + return mark_safe(''.format(obj.file.url)) audio.short_descripton = _('Audio') diff --git a/aircox/admin_site.py b/aircox/admin_site.py index ad189d8..2ee378d 100644 --- a/aircox/admin_site.py +++ b/aircox/admin_site.py @@ -40,7 +40,7 @@ class AdminSite(admin.AdminSite): return context def get_urls(self): - urls = super().get_urls() + [ + urls = [ path('api/', include((self.router.urls, 'api'))), path('tools/statistics/', self.admin_view(StatisticsView.as_view()), @@ -48,7 +48,7 @@ class AdminSite(admin.AdminSite): path('tools/statistics//', self.admin_view(StatisticsView.as_view()), name='tools-stats'), - ] + self.extra_urls + ] + self.extra_urls + super().get_urls() return urls def get_tools(self): diff --git a/aircox/management/commands/import_playlist.py b/aircox/management/commands/import_playlist.py index 26716a5..656fdea 100755 --- a/aircox/management/commands/import_playlist.py +++ b/aircox/management/commands/import_playlist.py @@ -128,7 +128,7 @@ class Command(BaseCommand): def handle(self, path, *args, **options): # FIXME: absolute/relative path of sounds vs given path if options.get('sound'): - sound = Sound.objects.filter(path__icontains=options.get('sound'))\ + sound = Sound.objects.filter(file__icontains=options.get('sound'))\ .first() else: path_, ext = os.path.splitext(path) diff --git a/aircox/management/commands/sounds_monitor.py b/aircox/management/commands/sounds_monitor.py index 00f8367..896fab3 100755 --- a/aircox/management/commands/sounds_monitor.py +++ b/aircox/management/commands/sounds_monitor.py @@ -36,6 +36,7 @@ import mutagen from watchdog.observers import Observer from watchdog.events import PatternMatchingEventHandler, FileModifiedEvent +from django.conf import settings as conf from django.core.management.base import BaseCommand, CommandError from django.utils import timezone as tz from django.utils.translation import gettext as _ @@ -64,12 +65,16 @@ class SoundFile: def __init__(self, path): self.path = path + @property + def sound_path(self): + return self.path.replace(conf.MEDIA_ROOT + '/', '') + def sync(self, sound=None, program=None, deleted=False, **kwargs): """ Update related sound model and save it. """ if deleted: - sound = Sound.objects.filter(path=self.path).first() + sound = Sound.objects.filter(file=self.path).first() if sound: sound.type = sound.TYPE_REMOVED sound.check_on_file() @@ -78,13 +83,13 @@ class SoundFile: # FIXME: sound.program as not null program = kwargs['program'] = Program.get_from_path(self.path) - sound, created = Sound.objects.get_or_create(path=self.path, defaults=kwargs) \ + sound, created = Sound.objects.get_or_create(file=self.sound_path, defaults=kwargs) \ if not sound else (sound, False) self.sound = sound sound.program = program if created or sound.check_on_file(): - logger.info('sound is new or have been modified -> %s', self.path) + logger.info('sound is new or have been modified -> %s', self.sound_path) self.read_path() sound.name = self.path_info.get('name') @@ -153,7 +158,7 @@ class SoundFile: if not diffusion: return None - logger.info('%s <--> %s', self.sound.path, str(diffusion.episode)) + logger.info('%s <--> %s', self.sound.file.name, str(diffusion.episode)) self.sound.episode = diffusion.episode return diffusion @@ -172,7 +177,7 @@ class SoundFile: return # import playlist - path = os.path.splitext(self.sound.path)[0] + '.csv' + path = os.path.splitext(self.sound.file.path)[0] + '.csv' if os.path.exists(path): PlaylistImport(path, sound=sound).run() # use metadata @@ -227,7 +232,7 @@ class MonitorHandler(PatternMatchingEventHandler): def on_moved(self, event): logger.info('sound moved: %s -> %s', event.src_path, event.dest_path) def moved(event, sound_kwargs): - sound = Sound.objects.filter(path=event.src_path) + sound = Sound.objects.filter(file=event.src_path) sound_file = SoundFile(event.dest_path) if not sound else sound sound_file.sync(**sound_kwargs) self.pool.submit(moved, event, self.sound_kwargs) @@ -268,7 +273,8 @@ class Command(BaseCommand): program, settings.AIRCOX_SOUND_EXCERPTS_SUBDIR, type=Sound.TYPE_EXCERPT, ) - dirs.append(os.path.join(program.path)) + dirs.append(os.path.join(program.abspath)) + return dirs def scan_for_program(self, program, subdir, **sound_kwargs): """ @@ -279,7 +285,7 @@ class Command(BaseCommand): if not program.ensure_dir(subdir): return - subdir = os.path.join(program.path, subdir) + subdir = os.path.join(program.abspath, subdir) sounds = [] # sounds in directory @@ -293,7 +299,7 @@ class Command(BaseCommand): sounds.append(sound_file.sound.pk) # sounds in db & unchecked - sounds = Sound.objects.filter(path__startswith=subdir). \ + sounds = Sound.objects.filter(file__startswith=subdir). \ exclude(pk__in=sounds) self.check_sounds(sounds, program=program) @@ -302,7 +308,7 @@ class Command(BaseCommand): # check files for sound in qs: if sound.check_on_file(): - SoundFile(sound.path).sync(sound=sound, **sync_kwargs) + SoundFile(sound.file.path).sync(sound=sound, **sync_kwargs) def monitor(self): """ Run in monitor mode """ diff --git a/aircox/management/commands/sounds_quality_check.py b/aircox/management/commands/sounds_quality_check.py index 529f35a..88d0df9 100755 --- a/aircox/management/commands/sounds_quality_check.py +++ b/aircox/management/commands/sounds_quality_check.py @@ -160,7 +160,7 @@ class Command (BaseCommand): self.bad = [] self.good = [] for sound in self.sounds: - logger.info('analyse ' + sound.path) + logger.info('analyse ' + sound.file.name) sound.analyse() sound.check(attr, minmax[0], minmax[1]) if sound.bad: @@ -171,6 +171,6 @@ class Command (BaseCommand): # resume if options.get('resume'): for sound in self.good: - logger.info('\033[92m+ %s\033[0m', sound.path) + logger.info('\033[92m+ %s\033[0m', sound.file.name) for sound in self.bad: - logger.info('\033[91m+ %s\033[0m', sound.path) + logger.info('\033[91m+ %s\033[0m', sound.file.name) diff --git a/aircox/models/program.py b/aircox/models/program.py index 0931c3a..892c0ab 100644 --- a/aircox/models/program.py +++ b/aircox/models/program.py @@ -7,6 +7,7 @@ import os import shutil import pytz +from django.conf import settings as conf from django.core.exceptions import ValidationError from django.db import models from django.db.models import F, Q @@ -73,14 +74,17 @@ class Program(Page): self.slug.replace('-', '_')) @property - def archives_path(self): - return os.path.join(self.path, settings.AIRCOX_SOUND_ARCHIVES_SUBDIR) + def abspath(self): + """ Return absolute path to program's dir """ + return os.path.join(conf.MEDIA_ROOT, self.path) - @property - def excerpts_path(self): - return os.path.join( - self.path, settings.AIRCOX_SOUND_ARCHIVES_SUBDIR - ) + def archives_path(self, abs=False): + return os.path.join(abs and self.abspath or self.path, + settings.AIRCOX_SOUND_ARCHIVES_SUBDIR) + + def excerpts_path(self, abs=False): + return os.path.join(abs and self.abspath or self.path, + settings.AIRCOX_SOUND_ARCHIVES_SUBDIR) def __init__(self, *kargs, **kwargs): super().__init__(*kargs, **kwargs) @@ -94,6 +98,8 @@ class Program(Page): Return a Program from the given path. We assume the path has been given in a previous time by this model (Program.path getter). """ + if path.startswith(conf.MEDIA_ROOT): + path = path.replace(conf.MEDIA_ROOT + '/', '') path = path.replace(settings.AIRCOX_PROGRAMS_DIR, '') while path[0] == '/': @@ -107,10 +113,9 @@ class Program(Page): Make sur the program's dir exists (and optionally subdir). Return True if the dir (or subdir) exists. """ - path = os.path.join(self.path, subdir) if subdir else \ - self.path + path = os.path.join(self.abspath, subdir) if subdir else \ + self.abspath os.makedirs(path, exist_ok=True) - return os.path.exists(path) class Meta: @@ -127,14 +132,15 @@ class Program(Page): # TODO: move in signals path_ = getattr(self, '__initial_path', None) + abspath = os.path.join(conf.MEDIA_ROOT, path_) if path_ is not None and path_ != self.path and \ - os.path.exists(path_) and not os.path.exists(self.path): + os.path.exists(abspath) and not os.path.exists(self.abspath): logger.info('program #%s\'s dir changed to %s - update it.', self.id, self.title) - shutil.move(path_, self.path) + shutil.move(abspath, self.abspath) Sound.objects.filter(path__startswith=path_) \ - .update(path=Concat('path', Substr(F('path'), len(path_)))) + .update(file=Concat('file', Substr(F('file'), len(path_)))) class ProgramChildQuerySet(PageQuerySet): diff --git a/aircox/models/sound.py b/aircox/models/sound.py index 9246b93..e805d11 100644 --- a/aircox/models/sound.py +++ b/aircox/models/sound.py @@ -4,7 +4,8 @@ import os from django.conf import settings as main_settings from django.db import models -from django.db.models import Q +from django.db.models import Q, Value as V +from django.db.models.functions import Concat from django.utils import timezone as tz from django.utils.translation import gettext_lazy as _ @@ -54,7 +55,9 @@ class SoundQuerySet(models.QuerySet): self = self.archive() if order_by: self = self.order_by('path') - return self.filter(path__isnull=False).values_list('path', flat=True) + return self.filter(file__isnull=False) \ + .annotate(file_path=Concat(V(conf.MEDIA_ROOT), 'file')) \ + .values_list('file_path', flat=True) def search(self, query): return self.filter( @@ -94,21 +97,15 @@ class Sound(models.Model): position = models.PositiveSmallIntegerField( _('order'), default=0, help_text=_('position in the playlist'), ) - # FIXME: url() does not use the same directory than here - # should we use FileField for more reliability? - path = models.FilePathField( - _('file'), - path=settings.AIRCOX_PROGRAMS_DIR, - match=r'(' + '|'.join(settings.AIRCOX_SOUND_FILE_EXT) - .replace('.', r'\.') + ')$', - recursive=True, max_length=255, - blank=True, null=True, unique=True, + + def _upload_to(self, filename): + subdir = AIRCOX_SOUND_ARCHIVES_SUBDIR if self.type == self.TYPE_ARCHIVE else \ + AIRCOX_SOUND_EXCERPTS_SUBDIR + return os.path.join(o.program.path, subdir, filename) + + file = models.FileField( + _('file'), upload_to=_upload_to ) - #embed = models.TextField( - # _('embed'), - # blank=True, null=True, - # help_text=_('HTML code to embed a sound from an external plateform'), - #) duration = models.TimeField( _('duration'), blank=True, null=True, @@ -134,8 +131,12 @@ class Sound(models.Model): verbose_name = _('Sound') verbose_name_plural = _('Sounds') + @property + def url(self): + return self.file and self.file.url + def __str__(self): - return '/'.join(self.path.split('/')[-3:]) + return '/'.join(self.file.path.split('/')[-3:]) def save(self, check=True, *args, **kwargs): if self.episode is not None and self.program is None: @@ -145,17 +146,12 @@ class Sound(models.Model): self.__check_name() super().save(*args, **kwargs) - def url(self): - """ Return an url to the file. """ - path = self.path.replace(main_settings.MEDIA_ROOT, '', 1) - return (main_settings.MEDIA_URL + path).replace('//','/') - # TODO: rename get_file_mtime(self) def get_mtime(self): """ Get the last modification date from file """ - mtime = os.stat(self.path).st_mtime + mtime = os.stat(self.file.path).st_mtime mtime = tz.datetime.fromtimestamp(mtime) mtime = mtime.replace(microsecond=0) return tz.make_aware(mtime, tz.get_current_timezone()) @@ -163,7 +159,7 @@ class Sound(models.Model): def file_exists(self): """ Return true if the file still exists. """ - return os.path.exists(self.path) + return os.path.exists(self.file.path) def check_on_file(self): """ @@ -173,7 +169,7 @@ class Sound(models.Model): if not self.file_exists(): if self.type == self.TYPE_REMOVED: return - logger.info('sound %s: has been removed', self.path) + logger.info('sound %s: has been removed', self.file.name) self.type = self.TYPE_REMOVED return True @@ -183,7 +179,7 @@ class Sound(models.Model): if self.type == self.TYPE_REMOVED and self.program: changed = True self.type = self.TYPE_ARCHIVE \ - if self.path.startswith(self.program.archives_path) else \ + if self.file.path.startswith(self.program.archives_path) else \ self.TYPE_EXCERPT # check mtime -> reset quality if changed (assume file changed) @@ -193,15 +189,15 @@ class Sound(models.Model): self.mtime = mtime self.is_good_quality = None logger.info('sound %s: m_time has changed. Reset quality info', - self.path) + self.file.name) return True return changed def __check_name(self): - if not self.name and self.path: + if not self.name and self.file and self.file.path: # FIXME: later, remove date? - self.name = os.path.basename(self.path) + self.name = os.path.basename(self.file.name) self.name = os.path.splitext(self.name)[0] self.name = self.name.replace('_', ' ') diff --git a/aircox/serializers.py b/aircox/serializers.py index 344efe4..746ca55 100644 --- a/aircox/serializers.py +++ b/aircox/serializers.py @@ -55,7 +55,6 @@ class LogInfoSerializer(serializers.Serializer): class SoundSerializer(serializers.ModelSerializer): # serializers.HyperlinkedIdentityField(view_name='sound', format='html') - class Meta: model = Sound fields = ['pk', 'name', 'program', 'episode', 'type', @@ -64,7 +63,7 @@ class SoundSerializer(serializers.ModelSerializer): def get_field_names(self, *args): names = super().get_field_names(*args) if 'request' in self.context and self.context['request'].user.is_staff: - names.append('path') + names.append('file') return names class PodcastSerializer(serializers.ModelSerializer): diff --git a/aircox/settings.py b/aircox/settings.py index c30d549..fb54742 100755 --- a/aircox/settings.py +++ b/aircox/settings.py @@ -87,8 +87,7 @@ ensure('AIRCOX_DEFAULT_USER_GROUPS', { # Directory for the programs data # TODO: rename to PROGRAMS_ROOT -ensure('AIRCOX_PROGRAMS_DIR', - os.path.join(settings.MEDIA_ROOT, 'programs')) +ensure('AIRCOX_PROGRAMS_DIR', 'programs') ######################################################################## @@ -152,8 +151,3 @@ ensure('AIRCOX_IMPORT_PLAYLIST_CSV_DELIMITER', ';') ensure('AIRCOX_IMPORT_PLAYLIST_CSV_TEXT_QUOTE', '"') -if settings.MEDIA_ROOT not in AIRCOX_PROGRAMS_DIR: - # PROGRAMS_DIR must be in MEDIA_ROOT for easy files url resolution - # later should this restriction disappear. - raise ValueError("settings: AIRCOX_PROGRAMS_DIR must be in MEDIA_ROOT") - diff --git a/aircox/static/aircox/js/chunk-common.js b/aircox/static/aircox/js/chunk-common.js index e05a0bd..fe48887 100644 --- a/aircox/static/aircox/js/chunk-common.js +++ b/aircox/static/aircox/js/chunk-common.js @@ -55,7 +55,7 @@ eval("__webpack_require__.r(__webpack_exports__);\n/* harmony default export */ \*****************************************************************************************************************************************************************************************/ /***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) { -eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"State\": function() { return /* binding */ State; }\n/* harmony export */ });\n/* harmony import */ var _live__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../live */ \"./src/live.js\");\n/* harmony import */ var _sound__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../sound */ \"./src/sound.js\");\n/* harmony import */ var _model__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ../model */ \"./src/model.js\");\n/* harmony import */ var _APlaylist__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./APlaylist */ \"./src/components/APlaylist.vue\");\n/* harmony import */ var _AProgress__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./AProgress */ \"./src/components/AProgress.vue\");\n\n\n\n\n\nconst State = {\n paused: 0,\n playing: 1,\n loading: 2\n};\n/* harmony default export */ __webpack_exports__[\"default\"] = ({\n components: {\n APlaylist: _APlaylist__WEBPACK_IMPORTED_MODULE_3__[\"default\"],\n AProgress: _AProgress__WEBPACK_IMPORTED_MODULE_4__[\"default\"]\n },\n\n data() {\n let audio = new Audio();\n audio.addEventListener('ended', e => this.onState(e));\n audio.addEventListener('pause', e => this.onState(e));\n audio.addEventListener('playing', e => this.onState(e));\n audio.addEventListener('timeupdate', () => {\n this.currentTime = this.audio.currentTime;\n });\n audio.addEventListener('durationchange', () => {\n this.duration = Number.isFinite(this.audio.duration) ? this.audio.duration : null;\n });\n let live = this.liveArgs ? new _live__WEBPACK_IMPORTED_MODULE_0__[\"default\"](this.liveArgs) : null;\n live && live.refresh();\n return {\n audio,\n duration: 0,\n currentTime: 0,\n state: State.paused,\n live,\n /// Loaded item\n loaded: null,\n //! Active panel name\n panel: null,\n //! current playing playlist name\n playlistName: null,\n //! players' playlists' sets\n sets: {\n queue: _model__WEBPACK_IMPORTED_MODULE_2__.Set.storeLoad(_sound__WEBPACK_IMPORTED_MODULE_1__[\"default\"], \"playlist.queue\", {\n max: 30,\n unique: true\n }),\n pin: _model__WEBPACK_IMPORTED_MODULE_2__.Set.storeLoad(_sound__WEBPACK_IMPORTED_MODULE_1__[\"default\"], \"player.pin\", {\n max: 30,\n unique: true\n })\n }\n };\n },\n\n props: {\n buttonTitle: String,\n liveArgs: Object\n },\n computed: {\n self() {\n return this;\n },\n\n paused() {\n return this.state == State.paused;\n },\n\n playing() {\n return this.state == State.playing;\n },\n\n loading() {\n return this.state == State.loading;\n },\n\n playlist() {\n return this.playlistName ? this.$refs[this.playlistName] : null;\n },\n\n current() {\n return this.loaded ? this.loaded : this.live && this.live.current;\n }\n\n },\n methods: {\n displayTime(seconds) {\n seconds = parseInt(seconds);\n let s = seconds % 60;\n seconds = (seconds - s) / 60;\n let m = seconds % 60;\n let h = (seconds - m) / 60;\n let [ss, mm, hh] = [s.toString().padStart(2, '0'), m.toString().padStart(2, '0'), h.toString().padStart(2, '0')];\n return h ? `${hh}:${mm}:${ss}` : `${mm}:${ss}`;\n },\n\n playlistButtonClass(name) {\n let set = this.sets[name];\n return (set ? (set.length ? \"\" : \"has-text-grey-light \") + (this.panel == name ? \"is-info \" : this.playlistName == name ? 'is-primary ' : '') : '') + \"button has-text-weight-bold\";\n },\n\n /// Show/hide panel\n togglePanel(panel) {\n this.panel = this.panel == panel ? null : panel;\n },\n\n /// Return True if item is loaded\n isLoaded(item) {\n return this.loaded && this.loaded.id == item.id;\n },\n\n /// Return True if item is loaded\n isPlaying(item) {\n return this.isLoaded(item) && !this.paused;\n },\n\n _setPlaylist(playlist) {\n this.playlistName = playlist;\n\n for (var p in this.sets) if (p != playlist) this.$refs[p].unselect();\n },\n\n /// Load a sound from playlist or live\n load(playlist = null, index = 0) {\n let src = null; // from playlist\n\n if (playlist !== null) {\n let item = this.$refs[playlist].get(index);\n if (!item) throw `No sound at index ${index} for playlist ${playlist}`;\n this.loaded = item;\n src = item.src;\n } // from live\n else {\n this.loaded = null;\n src = this.live.src;\n }\n\n this._setPlaylist(playlist); // load sources\n\n\n const audio = this.audio;\n\n if (src instanceof Array) {\n audio.innerHTML = '';\n audio.removeAttribute('src');\n\n for (var s of src) {\n let source = document.createElement('source');\n source.setAttribute('src', s);\n audio.appendChild(source);\n }\n } else {\n audio.src = src;\n }\n\n audio.load();\n },\n\n play(playlist = null, index = 0) {\n this.load(playlist, index);\n this.audio.play().catch(e => console.error(e));\n },\n\n /// Push items to playlist (by name)\n push(playlist, ...items) {\n return this.$refs[playlist].push(...items);\n },\n\n /// Push and play items\n playItems(playlist, ...items) {\n let index = this.push(playlist, ...items);\n this.$refs[playlist].selectedIndex = index;\n this.play(playlist, index);\n },\n\n /// Handle click event that plays multiple items (from `data-sounds` attribute)\n playButtonClick(event) {\n var items = JSON.parse(event.currentTarget.dataset.sounds);\n this.playItems('queue', ...items);\n },\n\n /// Pause\n pause() {\n this.audio.pause();\n },\n\n //! Play/pause\n togglePlay(playlist = null, index = 0) {\n if (playlist !== null) {\n let item = this.sets[playlist].get(index);\n\n if (!this.playlist || this.playlistName !== playlist || this.loaded != item) {\n this.play(playlist, index);\n return;\n }\n }\n\n if (this.paused) this.audio.play().catch(e => console.error(e));else this.audio.pause();\n },\n\n //! Pin/Unpin an item\n togglePin(item) {\n let index = this.sets.pin.findIndex(item);\n if (index > -1) this.sets.pin.remove(index);else {\n this.sets.pin.push(item);\n this.$refs.pinPlaylistButton.focus();\n }\n },\n\n /// Audio player state change event\n onState(event) {\n const audio = this.audio;\n this.state = audio.paused ? State.paused : State.playing;\n if (event.type == 'ended' && (!this.playlist || this.playlist.selectNext() == -1)) this.play();\n }\n\n },\n\n mounted() {\n this.load();\n }\n\n});\n\n//# sourceURL=webpack://aircox-assets/./src/components/APlayer.vue?./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use%5B0%5D!./node_modules/vue-loader/dist/index.js??ruleSet%5B0%5D.use%5B0%5D"); +eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"State\": function() { return /* binding */ State; }\n/* harmony export */ });\n/* harmony import */ var _live__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../live */ \"./src/live.js\");\n/* harmony import */ var _sound__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../sound */ \"./src/sound.js\");\n/* harmony import */ var _model__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ../model */ \"./src/model.js\");\n/* harmony import */ var _APlaylist__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./APlaylist */ \"./src/components/APlaylist.vue\");\n/* harmony import */ var _AProgress__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./AProgress */ \"./src/components/AProgress.vue\");\n\n\n\n\n\nconst State = {\n paused: 0,\n playing: 1,\n loading: 2\n};\n/* harmony default export */ __webpack_exports__[\"default\"] = ({\n components: {\n APlaylist: _APlaylist__WEBPACK_IMPORTED_MODULE_3__[\"default\"],\n AProgress: _AProgress__WEBPACK_IMPORTED_MODULE_4__[\"default\"]\n },\n\n data() {\n let audio = new Audio();\n audio.addEventListener('ended', e => this.onState(e));\n audio.addEventListener('pause', e => this.onState(e));\n audio.addEventListener('playing', e => this.onState(e));\n audio.addEventListener('timeupdate', () => {\n this.currentTime = this.audio.currentTime;\n });\n audio.addEventListener('durationchange', () => {\n this.duration = Number.isFinite(this.audio.duration) ? this.audio.duration : null;\n });\n let live = this.liveArgs ? new _live__WEBPACK_IMPORTED_MODULE_0__[\"default\"](this.liveArgs) : null;\n live && live.refresh();\n return {\n audio,\n duration: 0,\n currentTime: 0,\n state: State.paused,\n live,\n /// Loaded item\n loaded: null,\n //! Active panel name\n panel: null,\n //! current playing playlist name\n playlistName: null,\n //! players' playlists' sets\n sets: {\n queue: _model__WEBPACK_IMPORTED_MODULE_2__.Set.storeLoad(_sound__WEBPACK_IMPORTED_MODULE_1__[\"default\"], \"playlist.queue\", {\n max: 30,\n unique: true\n }),\n pin: _model__WEBPACK_IMPORTED_MODULE_2__.Set.storeLoad(_sound__WEBPACK_IMPORTED_MODULE_1__[\"default\"], \"player.pin\", {\n max: 30,\n unique: true\n })\n }\n };\n },\n\n props: {\n buttonTitle: String,\n liveArgs: Object\n },\n computed: {\n self() {\n return this;\n },\n\n paused() {\n return this.state == State.paused;\n },\n\n playing() {\n return this.state == State.playing;\n },\n\n loading() {\n return this.state == State.loading;\n },\n\n playlist() {\n return this.playlistName ? this.$refs[this.playlistName] : null;\n },\n\n current() {\n return this.loaded ? this.loaded : this.live && this.live.current;\n }\n\n },\n methods: {\n displayTime(seconds) {\n seconds = parseInt(seconds);\n let s = seconds % 60;\n seconds = (seconds - s) / 60;\n let m = seconds % 60;\n let h = (seconds - m) / 60;\n let [ss, mm, hh] = [s.toString().padStart(2, '0'), m.toString().padStart(2, '0'), h.toString().padStart(2, '0')];\n return h ? `${hh}:${mm}:${ss}` : `${mm}:${ss}`;\n },\n\n playlistButtonClass(name) {\n let set = this.sets[name];\n return (set ? (set.length ? \"\" : \"has-text-grey-light \") + (this.panel == name ? \"is-info \" : this.playlistName == name ? 'is-primary ' : '') : '') + \"button has-text-weight-bold\";\n },\n\n /// Show/hide panel\n togglePanel(panel) {\n this.panel = this.panel == panel ? null : panel;\n },\n\n /// Return True if item is loaded\n isLoaded(item) {\n return this.loaded && this.loaded.id == item.id;\n },\n\n /// Return True if item is loaded\n isPlaying(item) {\n return this.isLoaded(item) && !this.paused;\n },\n\n _setPlaylist(playlist) {\n this.playlistName = playlist;\n\n for (var p in this.sets) if (p != playlist) this.$refs[p].unselect();\n },\n\n /// Load a sound from playlist or live\n load(playlist = null, index = 0) {\n let src = null; // from playlist\n\n if (playlist !== null) {\n let item = this.$refs[playlist].get(index);\n if (!item) throw `No sound at index ${index} for playlist ${playlist}`;\n this.loaded = item;\n src = item.src;\n } // from live\n else {\n this.loaded = null;\n src = this.live.src;\n }\n\n this._setPlaylist(playlist); // load sources\n\n\n const audio = this.audio;\n\n if (src instanceof Array) {\n audio.innerHTML = '';\n audio.removeAttribute('src');\n\n for (var s of src) {\n let source = document.createElement('source');\n source.setAttribute('src', s);\n audio.appendChild(source);\n }\n } else {\n audio.src = src;\n }\n\n audio.load();\n },\n\n play(playlist = null, index = 0) {\n this.load(playlist, index);\n this.audio.play().catch(e => console.error(e));\n },\n\n /// Push items to playlist (by name)\n push(playlist, ...items) {\n return this.sets[playlist].push(...items);\n },\n\n /// Push and play items\n playItems(playlist, ...items) {\n let index = this.push(playlist, ...items);\n this.$refs[playlist].selectedIndex = index;\n this.play(playlist, index);\n },\n\n /// Handle click event that plays multiple items (from `data-sounds` attribute)\n playButtonClick(event) {\n var items = JSON.parse(event.currentTarget.dataset.sounds);\n this.playItems('queue', ...items);\n },\n\n /// Pause\n pause() {\n this.audio.pause();\n },\n\n //! Play/pause\n togglePlay(playlist = null, index = 0) {\n if (playlist !== null) {\n let item = this.sets[playlist].get(index);\n\n if (!this.playlist || this.playlistName !== playlist || this.loaded != item) {\n this.play(playlist, index);\n return;\n }\n }\n\n if (this.paused) this.audio.play().catch(e => console.error(e));else this.audio.pause();\n },\n\n //! Pin/Unpin an item\n togglePin(item) {\n let index = this.sets.pin.findIndex(item);\n if (index > -1) this.sets.pin.remove(index);else {\n this.sets.pin.push(item);\n this.$refs.pinPlaylistButton.focus();\n }\n },\n\n /// Audio player state change event\n onState(event) {\n const audio = this.audio;\n this.state = audio.paused ? State.paused : State.playing;\n if (event.type == 'ended' && (!this.playlist || this.playlist.selectNext() == -1)) this.play();\n }\n\n },\n\n mounted() {\n this.load();\n }\n\n});\n\n//# sourceURL=webpack://aircox-assets/./src/components/APlayer.vue?./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use%5B0%5D!./node_modules/vue-loader/dist/index.js??ruleSet%5B0%5D.use%5B0%5D"); /***/ }), @@ -65,7 +65,7 @@ eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpac \*******************************************************************************************************************************************************************************************/ /***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) { -eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _AList__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./AList */ \"./src/components/AList.vue\");\n/* harmony import */ var _ASoundItem__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./ASoundItem */ \"./src/components/ASoundItem.vue\");\n\n\n/* harmony default export */ __webpack_exports__[\"default\"] = ({\n extends: _AList__WEBPACK_IMPORTED_MODULE_0__[\"default\"],\n emits: [..._AList__WEBPACK_IMPORTED_MODULE_0__[\"default\"].emits, 'remove'],\n components: {\n ASoundItem: _ASoundItem__WEBPACK_IMPORTED_MODULE_1__[\"default\"]\n },\n props: {\n actions: Array,\n name: String,\n player: Object,\n editable: Boolean\n },\n computed: {\n self() {\n return this;\n }\n\n },\n methods: {\n hasAction(action) {\n return this.actions && this.actions.indexOf(action) != -1;\n },\n\n selectNext() {\n let index = this.selectedIndex + 1;\n return this.select(index >= this.items.length ? -1 : index);\n },\n\n togglePlay(index) {\n if (this.player.isPlaying(this.set.get(index))) this.player.pause();else this.select(index);\n }\n\n }\n});\n\n//# sourceURL=webpack://aircox-assets/./src/components/APlaylist.vue?./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use%5B0%5D!./node_modules/vue-loader/dist/index.js??ruleSet%5B0%5D.use%5B0%5D"); +eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _AList__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./AList */ \"./src/components/AList.vue\");\n/* harmony import */ var _ASoundItem__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./ASoundItem */ \"./src/components/ASoundItem.vue\");\n\n\n/* harmony default export */ __webpack_exports__[\"default\"] = ({\n extends: _AList__WEBPACK_IMPORTED_MODULE_0__[\"default\"],\n emits: [..._AList__WEBPACK_IMPORTED_MODULE_0__[\"default\"].emits, 'remove'],\n components: {\n ASoundItem: _ASoundItem__WEBPACK_IMPORTED_MODULE_1__[\"default\"]\n },\n props: {\n actions: Array,\n name: String,\n player: Object,\n editable: Boolean\n },\n computed: {\n self() {\n return this;\n },\n\n player_() {\n return this.player || window.aircox.player;\n }\n\n },\n methods: {\n hasAction(action) {\n return this.actions && this.actions.indexOf(action) != -1;\n },\n\n selectNext() {\n let index = this.selectedIndex + 1;\n return this.select(index >= this.items.length ? -1 : index);\n },\n\n togglePlay(index) {\n if (this.player_.isPlaying(this.set.get(index))) this.player_.pause();else this.select(index);\n }\n\n }\n});\n\n//# sourceURL=webpack://aircox-assets/./src/components/APlaylist.vue?./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use%5B0%5D!./node_modules/vue-loader/dist/index.js??ruleSet%5B0%5D.use%5B0%5D"); /***/ }), @@ -165,7 +165,7 @@ eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpac \***********************************************************************************************************************************************************************************************************************************************************************/ /***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) { -eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"render\": function() { return /* binding */ render; }\n/* harmony export */ });\n/* harmony import */ var vue__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! vue */ \"./node_modules/vue/dist/vue.esm-bundler.js\");\n\nconst _hoisted_1 = [\"onClick\"];\nconst _hoisted_2 = [\"onClick\"];\n\nconst _hoisted_3 = /*#__PURE__*/(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"span\", {\n class: \"icon is-small\"\n}, [/*#__PURE__*/(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"span\", {\n class: \"fa fa-minus\"\n})], -1\n/* HOISTED */\n);\n\nconst _hoisted_4 = [_hoisted_3];\nfunction render(_ctx, _cache, $props, $setup, $data, $options) {\n const _component_ASoundItem = (0,vue__WEBPACK_IMPORTED_MODULE_0__.resolveComponent)(\"ASoundItem\");\n\n return (0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"div\", null, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.renderSlot)(_ctx.$slots, \"header\"), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"ul\", {\n class: (0,vue__WEBPACK_IMPORTED_MODULE_0__.normalizeClass)(_ctx.listClass)\n }, [((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(true), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(vue__WEBPACK_IMPORTED_MODULE_0__.Fragment, null, (0,vue__WEBPACK_IMPORTED_MODULE_0__.renderList)(_ctx.items, (item, index) => {\n return (0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"li\", {\n class: (0,vue__WEBPACK_IMPORTED_MODULE_0__.normalizeClass)(_ctx.itemClass),\n onClick: $event => !$options.hasAction('play') && _ctx.select(index),\n key: index\n }, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"a\", {\n class: (0,vue__WEBPACK_IMPORTED_MODULE_0__.normalizeClass)(index == _ctx.selectedIndex ? 'is-active' : '')\n }, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createVNode)(_component_ASoundItem, {\n data: item,\n index: index,\n player: $props.player,\n set: _ctx.set,\n onTogglePlay: $event => $options.togglePlay(index),\n actions: $props.actions\n }, {\n actions: (0,vue__WEBPACK_IMPORTED_MODULE_0__.withCtx)(({}) => [$props.editable ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"button\", {\n key: 0,\n class: \"button\",\n onClick: (0,vue__WEBPACK_IMPORTED_MODULE_0__.withModifiers)($event => _ctx.remove(index, true), [\"stop\"])\n }, _hoisted_4, 8\n /* PROPS */\n , _hoisted_2)) : (0,vue__WEBPACK_IMPORTED_MODULE_0__.createCommentVNode)(\"v-if\", true)]),\n _: 2\n /* DYNAMIC */\n\n }, 1032\n /* PROPS, DYNAMIC_SLOTS */\n , [\"data\", \"index\", \"player\", \"set\", \"onTogglePlay\", \"actions\"])], 2\n /* CLASS */\n )], 10\n /* CLASS, PROPS */\n , _hoisted_1);\n }), 128\n /* KEYED_FRAGMENT */\n ))], 2\n /* CLASS */\n ), (0,vue__WEBPACK_IMPORTED_MODULE_0__.renderSlot)(_ctx.$slots, \"footer\")]);\n}\n\n//# sourceURL=webpack://aircox-assets/./src/components/APlaylist.vue?./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use%5B0%5D!./node_modules/vue-loader/dist/templateLoader.js??ruleSet%5B1%5D.rules%5B3%5D!./node_modules/vue-loader/dist/index.js??ruleSet%5B0%5D.use%5B0%5D"); +eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"render\": function() { return /* binding */ render; }\n/* harmony export */ });\n/* harmony import */ var vue__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! vue */ \"./node_modules/vue/dist/vue.esm-bundler.js\");\n\nconst _hoisted_1 = [\"onClick\"];\nconst _hoisted_2 = [\"onClick\"];\n\nconst _hoisted_3 = /*#__PURE__*/(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"span\", {\n class: \"icon is-small\"\n}, [/*#__PURE__*/(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"span\", {\n class: \"fa fa-minus\"\n})], -1\n/* HOISTED */\n);\n\nconst _hoisted_4 = [_hoisted_3];\nfunction render(_ctx, _cache, $props, $setup, $data, $options) {\n const _component_ASoundItem = (0,vue__WEBPACK_IMPORTED_MODULE_0__.resolveComponent)(\"ASoundItem\");\n\n return (0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"div\", null, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.renderSlot)(_ctx.$slots, \"header\"), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"ul\", {\n class: (0,vue__WEBPACK_IMPORTED_MODULE_0__.normalizeClass)(_ctx.listClass)\n }, [((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(true), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(vue__WEBPACK_IMPORTED_MODULE_0__.Fragment, null, (0,vue__WEBPACK_IMPORTED_MODULE_0__.renderList)(_ctx.items, (item, index) => {\n return (0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"li\", {\n class: (0,vue__WEBPACK_IMPORTED_MODULE_0__.normalizeClass)(_ctx.itemClass),\n onClick: $event => !$options.hasAction('play') && _ctx.select(index),\n key: index\n }, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"a\", {\n class: (0,vue__WEBPACK_IMPORTED_MODULE_0__.normalizeClass)(index == _ctx.selectedIndex ? 'is-active' : '')\n }, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createVNode)(_component_ASoundItem, {\n data: item,\n index: index,\n set: _ctx.set,\n player: $options.player_,\n onTogglePlay: $event => $options.togglePlay(index),\n actions: $props.actions\n }, {\n actions: (0,vue__WEBPACK_IMPORTED_MODULE_0__.withCtx)(({}) => [$props.editable ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"button\", {\n key: 0,\n class: \"button\",\n onClick: (0,vue__WEBPACK_IMPORTED_MODULE_0__.withModifiers)($event => _ctx.remove(index, true), [\"stop\"])\n }, _hoisted_4, 8\n /* PROPS */\n , _hoisted_2)) : (0,vue__WEBPACK_IMPORTED_MODULE_0__.createCommentVNode)(\"v-if\", true)]),\n _: 2\n /* DYNAMIC */\n\n }, 1032\n /* PROPS, DYNAMIC_SLOTS */\n , [\"data\", \"index\", \"set\", \"player\", \"onTogglePlay\", \"actions\"])], 2\n /* CLASS */\n )], 10\n /* CLASS, PROPS */\n , _hoisted_1);\n }), 128\n /* KEYED_FRAGMENT */\n ))], 2\n /* CLASS */\n ), (0,vue__WEBPACK_IMPORTED_MODULE_0__.renderSlot)(_ctx.$slots, \"footer\")]);\n}\n\n//# sourceURL=webpack://aircox-assets/./src/components/APlaylist.vue?./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use%5B0%5D!./node_modules/vue-loader/dist/templateLoader.js??ruleSet%5B1%5D.rules%5B3%5D!./node_modules/vue-loader/dist/index.js??ruleSet%5B0%5D.use%5B0%5D"); /***/ }), @@ -215,7 +215,7 @@ eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpac \********************/ /***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) { -eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"PlayerApp\": function() { return /* binding */ PlayerApp; }\n/* harmony export */ });\n/* harmony import */ var _components__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./components */ \"./src/components/index.js\");\n\nconst App = {\n el: '#app',\n delimiters: ['[[', ']]'],\n components: { ..._components__WEBPACK_IMPORTED_MODULE_0__[\"default\"]\n },\n computed: {\n player() {\n return window.aircox.player;\n }\n\n }\n};\nconst PlayerApp = {\n el: '#player',\n components: { ..._components__WEBPACK_IMPORTED_MODULE_0__[\"default\"]\n }\n};\n/* harmony default export */ __webpack_exports__[\"default\"] = (App);\n\n//# sourceURL=webpack://aircox-assets/./src/app.js?"); +eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"PlayerApp\": function() { return /* binding */ PlayerApp; }\n/* harmony export */ });\n/* harmony import */ var _components__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./components */ \"./src/components/index.js\");\n\nconst App = {\n el: '#app',\n delimiters: ['[[', ']]'],\n components: { ..._components__WEBPACK_IMPORTED_MODULE_0__[\"default\"]\n },\n computed: {\n player() {\n return window.aircox.player;\n }\n\n }\n};\nconst PlayerApp = {\n el: '#player',\n delimiters: ['[[', ']]'],\n components: { ..._components__WEBPACK_IMPORTED_MODULE_0__[\"default\"]\n }\n};\n/* harmony default export */ __webpack_exports__[\"default\"] = (App);\n\n//# sourceURL=webpack://aircox-assets/./src/app.js?"); /***/ }), @@ -245,7 +245,7 @@ eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpac \**********************/ /***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) { -eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _fortawesome_fontawesome_free_css_all_min_css__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @fortawesome/fontawesome-free/css/all.min.css */ \"./node_modules/@fortawesome/fontawesome-free/css/all.min.css\");\n/* harmony import */ var _fortawesome_fontawesome_free_css_fontawesome_min_css__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! @fortawesome/fontawesome-free/css/fontawesome.min.css */ \"./node_modules/@fortawesome/fontawesome-free/css/fontawesome.min.css\");\n/* harmony import */ var _app__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./app */ \"./src/app.js\");\n/* harmony import */ var _appBuilder__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./appBuilder */ \"./src/appBuilder.js\");\n/* harmony import */ var _sound__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./sound */ \"./src/sound.js\");\n/* harmony import */ var _model__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ./model */ \"./src/model.js\");\n/* harmony import */ var _assets_styles_scss__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ./assets/styles.scss */ \"./src/assets/styles.scss\");\n/**\n * This module includes code available for both the public website and\n * administration interface)\n */\n//-- vendor\n\n //-- aircox\n\n\n\n\n\n\nwindow.aircox = {\n // main application\n builder: new _appBuilder__WEBPACK_IMPORTED_MODULE_3__[\"default\"](_app__WEBPACK_IMPORTED_MODULE_2__[\"default\"]),\n\n get app() {\n return this.builder.app;\n },\n\n // player application\n playerBuilder: new _appBuilder__WEBPACK_IMPORTED_MODULE_3__[\"default\"](_app__WEBPACK_IMPORTED_MODULE_2__.PlayerApp),\n\n get playerApp() {\n return this.playerBuilder && this.playerBuilder.app;\n },\n\n get player() {\n return this.playerBuilder.vm && this.playerBuilder.vm.$refs.player;\n },\n\n Set: _model__WEBPACK_IMPORTED_MODULE_5__.Set,\n Sound: _sound__WEBPACK_IMPORTED_MODULE_4__[\"default\"],\n\n /**\n * Initialize main application and player.\n */\n init(props = null, {\n config = null,\n builder = null,\n initBuilder = true,\n initPlayer = true,\n hotReload = false\n } = {}) {\n if (initBuilder) {\n builder = builder || this.builder;\n this.builder = builder;\n if (config || window.App) builder.config = config || window.App;\n builder.title = document.title;\n builder.mount({\n props\n });\n if (hotReload) builder.enableHotReload(hotReload);\n }\n\n if (initPlayer) {\n let playerBuilder = this.playerBuilder;\n playerBuilder.mount();\n }\n }\n\n};\n\n//# sourceURL=webpack://aircox-assets/./src/index.js?"); +eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _fortawesome_fontawesome_free_css_all_min_css__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @fortawesome/fontawesome-free/css/all.min.css */ \"./node_modules/@fortawesome/fontawesome-free/css/all.min.css\");\n/* harmony import */ var _fortawesome_fontawesome_free_css_fontawesome_min_css__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! @fortawesome/fontawesome-free/css/fontawesome.min.css */ \"./node_modules/@fortawesome/fontawesome-free/css/fontawesome.min.css\");\n/* harmony import */ var _app__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./app */ \"./src/app.js\");\n/* harmony import */ var _appBuilder__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./appBuilder */ \"./src/appBuilder.js\");\n/* harmony import */ var _sound__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./sound */ \"./src/sound.js\");\n/* harmony import */ var _model__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ./model */ \"./src/model.js\");\n/* harmony import */ var _assets_styles_scss__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ./assets/styles.scss */ \"./src/assets/styles.scss\");\n/**\n * This module includes code available for both the public website and\n * administration interface)\n */\n//-- vendor\n\n //-- aircox\n\n\n\n\n\n\nwindow.aircox = {\n // main application\n builder: new _appBuilder__WEBPACK_IMPORTED_MODULE_3__[\"default\"](_app__WEBPACK_IMPORTED_MODULE_2__[\"default\"]),\n\n get app() {\n return this.builder.app;\n },\n\n // player application\n playerBuilder: new _appBuilder__WEBPACK_IMPORTED_MODULE_3__[\"default\"](_app__WEBPACK_IMPORTED_MODULE_2__.PlayerApp),\n\n get playerApp() {\n return this.playerBuilder && this.playerBuilder.app;\n },\n\n get player() {\n return this.playerBuilder.vm && this.playerBuilder.vm.$refs.player;\n },\n\n Set: _model__WEBPACK_IMPORTED_MODULE_5__.Set,\n Sound: _sound__WEBPACK_IMPORTED_MODULE_4__[\"default\"],\n\n /**\n * Initialize main application and player.\n */\n init(props = null, {\n config = null,\n builder = null,\n initBuilder = true,\n initPlayer = true,\n hotReload = false\n } = {}) {\n if (initPlayer) {\n let playerBuilder = this.playerBuilder;\n playerBuilder.mount();\n }\n\n if (initBuilder) {\n builder = builder || this.builder;\n this.builder = builder;\n if (config || window.App) builder.config = config || window.App;\n builder.title = document.title;\n builder.mount({\n props\n });\n if (hotReload) builder.enableHotReload(hotReload);\n }\n }\n\n};\n\n//# sourceURL=webpack://aircox-assets/./src/index.js?"); /***/ }), diff --git a/aircox/templates/aircox/widgets/podcast_item.html b/aircox/templates/aircox/widgets/podcast_item.html index 9f7cc6b..74b8eae 100644 --- a/aircox/templates/aircox/widgets/podcast_item.html +++ b/aircox/templates/aircox/widgets/podcast_item.html @@ -8,7 +8,7 @@ List item for a podcast. {% if object.embed %} {{ object.embed|safe }} {% else %} -