diff --git a/app/client/src/locales/ru.json b/app/client/src/locales/ru.json index 69795ca..b40c8eb 100644 --- a/app/client/src/locales/ru.json +++ b/app/client/src/locales/ru.json @@ -5,7 +5,7 @@ "Tracks": "Треки", "Playlists": "Плейлисты", "Albums": "Альбомы", - "Artists": "Артисты", + "Artists": "Исполнители", "More": "Ещё", "Settings": "Настройки", "Downloads": "Загрузки", @@ -20,7 +20,7 @@ "Start downloading": "Начать загрузку", "Cancel": "Отмена", "Stream logging is disabled!": "Отправка статистики отключена!", - "Enable it in settings for history to work properly.": "Включите её в настройках для работы рекомендаций.", + "Enable it in settings for history to work properly.": "Включите её в настройках для работы истории.", "History": "История", "Create new playlist": "Новый плейлист", "TRACKS": "Треки", @@ -48,7 +48,7 @@ "albums": "альбомы", "Play top": "Играть популярные", "Radio": "Радио", - "Show all albums": "Показать все", + "Show all albums": "Показать все альбомы", "Show all singles": "Показать все синглы", "Show more": "Ещё", "Downloaded": "Загрузки", @@ -108,7 +108,7 @@ "Don't minimize to tray": "Не сворачивать в трей", "Close on exit": "Закрывать при выходе", "Settings saved!": "Настройки сохранены!", - "Available only in Electron version!": "Доступно только в версии на Electron!", + "Available only in Electron version!": "Доступно только в версии Electron!", "Crossfade (ms)": "Кроссфейд (мс)", "Select primary color": "Выберите основной цвет", "Light theme": "Светлая тема", @@ -143,7 +143,7 @@ "Art Resolution": "Разрешение обложки", "Public": "Публичный", "Private": "Приватный", - "Collaborative": "Совместное", + "Collaborative": "Совместный", "Edit playlist": "Изменить плейлист", "Save": "Сохранить", "Edit": "Редактировать", @@ -161,11 +161,11 @@ "Artists:": "Исполнители:", "Yes": "Да", "No": "Нет", - "Download Filename": "Скачать шаблон для названия", + "Download Filename": "Шаблон названия файла для скачивания", "Language": "Язык", "Background Image": "Фоновое изображение", "Enter URL or absolute path. WARNING: Requires reload!": "Введите URL или полный путь. ВНИМАНИЕ: Требуется перезагрузка!", "LGBT Mode": "Режим ЛГБТ", - "Native top bar": "Верхнюю панель", + "Native top bar": "Системная верхняя панель", "Requires restart of Freezer!": "Требуется перезагрузка!" } \ No newline at end of file diff --git a/app/client/src/main.js b/app/client/src/main.js index ea4d49b..d6a75f6 100644 --- a/app/client/src/main.js +++ b/app/client/src/main.js @@ -73,7 +73,7 @@ new Vue({ track: null, audio: null, volume: 0.00, - //0 = Stopped, 1 = Paused, 2 = Playing, 3 = Loading + //0 = Stopped, 1 = Paused, 2 = Playing, 3 = Loading, 4 = Starting playing state: 0, loaders: 0, playbackInfo: {}, @@ -88,7 +88,7 @@ new Vue({ }, //0 - normal, 1 - repeat list, 2 - repeat track - repeat: 0, + repeatMode: 0, shuffled: false, //Library cache @@ -122,26 +122,42 @@ new Vue({ methods: { // PLAYBACK METHODS isPlaying() { - return this.state == 2; + return this.state == 2 || this.state == 4; }, - play() { - if (!this.audio || this.state != 1) return; - this.audio.play(); - this.state = 2; + async load() { + let audio = this.audio; + if (!audio || this.state != 1) return; + this.state = 1; + await audio.play(); + audio.pause(); + }, + + async play() { + let audio = this.audio; + if (!audio || this.state != 1) return; + this.state = 4; + await audio.play(); + if (this.state == 1 || audio != this.audio) { + audio.pause(); + } else { + this.state = 2 + } }, pause() { - if (!this.audio || this.state != 2) return; - this.audio.pause(); + if (!this.audio || !this.isPlaying()) return; + if (this.state == 2) this.audio.pause(); this.state = 1; }, - toggle() { + async toggle() { if (this.isPlaying()) return this.pause(); - this.play(); + await this.play(); }, seek(t) { - if (!this.audio || isNaN(t) || !t) return; + if (!this.audio || isNaN(t) || (!t && t !== 0)) return; //ms -> s + if(t < 0) t = 0; + if(t > this.duration()) t = this.duration(); this.audio.currentTime = (t / 1000); this.position = t; @@ -169,7 +185,7 @@ new Vue({ if (index >= this.queue.data.length || index < 0) return; this.queue.index = index; await this.playTrack(this.queue.data[this.queue.index]); - this.play(); + await this.play(); this.savePlaybackInfo(); }, //Skip n tracks, can be negative @@ -209,6 +225,15 @@ new Vue({ this.skip(1); this.savePlaybackInfo(); }, + + skipPrev() { + if (this.queue.index != 0 && (!this.position || this.position <= 10000)) + this.skip(-1); + else // Rewind to start if more than 10 seconds in, which is the default behavior for most players + this.seek(0); + this.savePlaybackInfo(); + }, + toggleMute() { if (this.audio) this.audio.muted = !this.audio.muted; this.muted = !this.muted; @@ -220,11 +245,13 @@ new Vue({ this.track = track; this.loaders++; - this.state = 3; //Stop audio - let autoplay = (this.state == 2); - if (this.audio) this.audio.pause(); - if (this.audio) this.audio.currentTime = 0; + let autoplay = (this.state == 2 || this.state == 4); + if (this.audio) { + this.pause(); + this.audio.currentTime = 0; + } + this.state = 3; //Load track meta let playbackInfo = await this.loadPlaybackInfo(track.streamUrl, track.duration); @@ -251,7 +278,7 @@ new Vue({ this.audio = new Audio(url); this.configureAudio(); this.state = 1; - if (autoplay) this.play(); + if (autoplay) await this.play(); //MediaSession this.updateMediaSession(); @@ -266,7 +293,7 @@ new Vue({ //Gapless playback if (this.position >= (this.duration() - (this.settings.crossfadeDuration + 7500)) && this.state == 2) { - if (this.repeat != 2) + if (this.repeatMode != 2) this.loadGapless(); } @@ -276,7 +303,8 @@ new Vue({ let currentVolume = this.audio.volume; let oldAudio = this.audio; this.audio = this.gapless.audio; - this.audio.play(); + this.state = 1; + await this.play(); //Update meta this.playbackInfo = this.gapless.info; @@ -297,7 +325,7 @@ new Vue({ await new Promise((res) => setTimeout(() => res(), 50)); } //Restore original volume - this.audio.voume = currentVolume; + this.audio.volume = currentVolume; oldAudio.pause(); @@ -321,15 +349,15 @@ new Vue({ if (this.gapless.crossfade) return; //Repeat track - if (this.repeat == 2) { + if (this.repeatMode == 2) { this.seek(0); - this.audio.play(); + await this.audio.play(); this.updateState(); return; } //Repeat list - if (this.repeat == 1 && this.queue.index == this.queue.data.length - 1) { + if (this.repeatMode == 1 && this.queue.index == this.queue.data.length - 1) { this.skip(-(this.queue.data.length - 1)); return; } @@ -347,7 +375,24 @@ new Vue({ }, //Update media session with current track metadata updateMediaSession() { - if (!this.track || !('mediaSession' in navigator)) return; + if (!('mediaSession' in navigator)) return; + + //Controls + navigator.mediaSession.setActionHandler('play', this.play); + navigator.mediaSession.setActionHandler('pause', this.pause); + navigator.mediaSession.setActionHandler('nexttrack', this.skipNext); + navigator.mediaSession.setActionHandler('previoustrack', this.skipPrev); + navigator.mediaSession.setActionHandler('seekbackward', details => { + this.seek(this.position - details.seekOffset * 1000); + }); + navigator.mediaSession.setActionHandler('seekforward', details => { + this.seek(this.position + details.seekOffset * 1000); + }); + navigator.mediaSession.setActionHandler('seekto', details => { + this.seek(details.seekTime * 1000); + }); + + if (!this.track) return; // eslint-disable-next-line no-undef navigator.mediaSession.metadata = new MediaMetadata({ @@ -359,11 +404,6 @@ new Vue({ {src: this.getImageUrl(this.track.albumArt, 512), sizes: '512x512', type: 'image/jpeg'} ] }); - //Controls - navigator.mediaSession.setActionHandler('play', this.play); - navigator.mediaSession.setActionHandler('pause', this.pause); - navigator.mediaSession.setActionHandler('nexttrack', this.skipNext); - navigator.mediaSession.setActionHandler('previoustrack', () => this.skip(-1)); }, //Get Deezer CDN image url getImageUrl(img, size = 256) { @@ -406,7 +446,7 @@ new Vue({ if (this.loaders != 0 || this.gapless.promise || this.gapless.audio || this.gapless.crossfade) return; //Repeat list - if (this.repeat == 1 && this.queue.index == this.queue.data.length - 1) { + if (this.repeatMode == 1 && this.queue.index == this.queue.data.length - 1) { this.gapless.track = this.queue.data[0]; this.gapless.index = 0; } else { @@ -466,7 +506,7 @@ new Vue({ queue: this.queue, position: this.position, track: this.track, - repeat: this.repeat, + repeatMode: this.repeatMode, shuffled: this.shuffled } await this.$axios.post('/playback', data); @@ -503,12 +543,13 @@ new Vue({ //Send state update to integrations async updateState() { //Wait for duration - if (this.state == 2 && (this.duration() == null || isNaN(this.duration()))) { + if (this.isPlaying() && (this.duration() == null || isNaN(this.duration()))) { setTimeout(() => { this.updateState(); }, 500); return; } + this.$io.emit('stateChange', { position: this.position, duration: this.duration(), @@ -516,9 +557,17 @@ new Vue({ track: this.track }); + if (('mediaSession' in navigator) && navigator.mediaSession.setPositionState && this.duration() != null && !isNaN(this.duration())) { + navigator.mediaSession.setPositionState({ + position: this.position / 1000, + duration: this.duration() / 1000, + playbackRate: 1, + }); + } + //Update in electron if (this.settings.electron) { - ipcRenderer.send('playing', this.state == 2); + ipcRenderer.send('playing', this.isPlaying()); } }, updateLanguage(l) { @@ -574,10 +623,11 @@ new Vue({ if (pd.data != {}) { if (pd.data.queue) this.queue = pd.data.queue; if (pd.data.track) this.track = pd.data.track; - if (pd.data.repeat) this.repeat = pd.data.repeat; + if (pd.data.repeatMode) this.repeatMode = pd.data.repeatMode; if (pd.data.shuffled) this.shuffled = pd.data.shuffled; this.playTrack(this.track).then(() => { this.seek(pd.data.position); + this.load(); }); } @@ -603,14 +653,14 @@ new Vue({ }); //Control from electron - ipcRenderer.on('togglePlayback', () => { - this.toggle(); + ipcRenderer.on('togglePlayback', async () => { + await this.toggle(); }); ipcRenderer.on('skipNext', () => { - this.skip(1); + this.skipNext(); }); ipcRenderer.on('skipPrev', () => { - this.skip(-1); + this.skipPrev(); }) } @@ -685,14 +735,14 @@ new Vue({ }); //Keystrokes - document.addEventListener('keyup', (e) => { + document.addEventListener('keyup', async (e) => { //Don't handle keystrokes in text fields if (e.target.tagName == "INPUT") return; //Don't handle if specials if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) return; //K toggle playback - if (e.code == "KeyK" || e.code == "Space") this.$root.toggle(); + if (e.code == "KeyK" || e.code == "Space") await this.$root.toggle(); //L +10s (from YT) if (e.code == "KeyL") this.$root.seek((this.position + 10000)); //J -10s (from YT) diff --git a/app/client/src/views/FullscreenPlayer.vue b/app/client/src/views/FullscreenPlayer.vue index 29d9d31..622d767 100644 --- a/app/client/src/views/FullscreenPlayer.vue +++ b/app/client/src/views/FullscreenPlayer.vue @@ -51,7 +51,7 @@ - + mdi-skip-previous @@ -74,9 +74,9 @@
- mdi-repeat - mdi-repeat - mdi-repeat-once + mdi-repeat + mdi-repeat + mdi-repeat-once mdi-shuffle @@ -315,11 +315,11 @@ export default { }, //Repeat button click repeatClick() { - if (this.$root.repeat == 2) { - this.$root.repeat = 0; + if (this.$root.repeatMode == 2) { + this.$root.repeatMode = 0; return; } - this.$root.repeat += 1; + this.$root.repeatMode += 1; }, //Copy link share() {