This commit is contained in:
lyswhut
2022-10-29 11:36:35 +08:00
parent 66b965d9f7
commit e45cee6596
715 changed files with 57096 additions and 31771 deletions
@@ -0,0 +1,79 @@
<template>
<div v-show="!isFullscreen" :class="$style.controlBtn">
<button type="button" :class="[$style.btn, $style.close]" :aria-label="$t('close')" @click="closeWindow">
<svg :class="$style.controlBtniIcon" version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" width="100%" viewBox="0 0 24 24" space="preserve">
<use xlink:href="#icon-window-close" />
</svg>
</button>
<button type="button" :class="[$style.btn, $style.min]" :aria-label="$t('min')" @click="minWindow">
<svg :class="$style.controlBtniIcon" version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" width="100%" viewBox="0 0 24 24" space="preserve">
<use xlink:href="#icon-window-minimize" />
</svg>
</button>
</div>
</template>
<script setup>
import { minWindow, closeWindow } from '@renderer/utils/ipc'
// import { getRandom } from '../../utils'
import { isFullscreen } from '@renderer/store'
</script>
<style lang="less" module>
@import '@renderer/assets/styles/layout.less';
@control-btn-width: @height-toolbar * .26;
@control-btn-height: 6%;
.controlBtn {
box-sizing: border-box;
padding: 0 7px;
display: flex;
align-items: center;
justify-content: space-evenly;
width: 100%;
height: @control-btn-height;
-webkit-app-region: no-drag;
opacity: .5;
transition: opacity @transition-normal;
&:hover {
opacity: .8;
.controlBtniIcon {
opacity: 1;
}
}
}
.btn {
position: relative;
width: @control-btn-width;
height: @control-btn-width;
background: none;
border: none;
display: flex;
// justify-content: center;
// align-items: center;
outline: none;
padding: 1px;
cursor: pointer;
border-radius: 50%;
color: var(--color-font);
&.min {
background-color: var(--color-btn-min);
}
// &.max {
// background-color: var(--color-btn-max);
// }
&.close {
background-color: var(--color-btn-close);
}
}
.controlBtniIcon {
opacity: 0;
transition: opacity 0.2s ease-in-out;
}
</style>
@@ -0,0 +1,187 @@
<template>
<div :class="$style.menu">
<ul :class="$style.list" role="toolbar">
<li v-for="item in menus" :key="item.to" :class="$style.navItem" role="presentation">
<router-link :class="[$style.link, {[$style.active]: $route.meta.name == item.name}]" role="tab" :aria-selected="$route.meta.name == item.name" :to="item.to" :aria-label="item.tips">
<div :class="$style.icon">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" :viewBox="item.iconSize" space="preserve">
<use :xlink:href="item.icon" />
</svg>
</div>
</router-link>
</li>
</ul>
</div>
</template>
<script>
import { appSetting } from '@renderer/store/setting'
export default {
name: 'NavBar',
setup() {
return {
appSetting,
}
},
computed: {
menus() {
return [
{
to: '/search',
tips: this.$t('search'),
icon: '#icon-search-2',
iconSize: '0 0 425.2 425.2',
name: 'Search',
enable: true,
},
{
to: '/songList/list',
tips: this.$t('song_list'),
icon: '#icon-album',
iconSize: '0 0 425.2 425.2',
name: 'SongList',
enable: true,
},
{
to: '/leaderboard',
tips: this.$t('leaderboard'),
icon: '#icon-leaderboard',
iconSize: '0 0 425.22 425.2',
name: 'Leaderboard',
enable: true,
},
{
to: '/list',
tips: this.$t('my_list'),
icon: '#icon-love',
iconSize: '0 0 444.87 391.18',
name: 'List',
enable: true,
},
{
to: '/download',
tips: this.$t('download'),
icon: '#icon-download-2',
iconSize: '0 0 425.2 425.2',
enable: this.appSetting['download.enable'],
name: 'Download',
},
{
to: '/setting',
tips: this.$t('setting'),
icon: '#icon-setting',
iconSize: '0 0 493.23 436.47',
enable: true,
name: 'Setting',
},
].filter(m => m.enable)
},
},
}
</script>
<style lang="less" module>
@import '@renderer/assets/styles/layout.less';
.menu {
flex: auto;
// &.controlBtnLeft {
// display: flex;
// flex-flow: column nowrap;
// justify-content: center;
// padding-bottom: @control-btn-height;
// }
// padding: 5px;
}
.list {
-webkit-app-region: no-drag;
// margin-bottom: 15px;
&:last-child {
margin-bottom: 0;
}
// background-color: pink;
// dt {
// padding-left: 5px;
// font-size: 11px;
// transition: @transition-normal;
// transition-property: color;
// color: @color-theme-font-label;
// .mixin-ellipsis-1;
// }
}
.navItem {
position: relative;
&:before {
content: '';
display: block;
width: 100%;
padding-bottom: 84%;
}
}
.link {
position: absolute;
left: 0%;
top: 0%;
width: 100%;
height: 100%;
// left: 15%;
// top: 15%;
// width: 70%;
// height: 70%;
// display: block;
box-sizing: border-box;
// text-decoration: none;
// border-radius: 20%;
// padding: 18px 3px;
// margin: 5px 0;
// border-left: 5px solid transparent;
transition: @transition-normal;
transition-property: color;
color: var(--color-nav-font);
cursor: pointer;
font-size: 11.5px;
text-align: center;
outline: none;
display: flex;
align-items: center;
justify-content: center;
transition: 0.3s ease;
transition-property: background-color, opacity;
// border-radius: @radius-border;
.mixin-ellipsis-1;
&.active {
// border-left-color: @color-theme-active;
background-color: var(--color-primary-light-400-alpha-600);
&:hover {
background-color: var(--color-primary-light-300-alpha-600);
}
}
&:hover {
color: var(--color-nav-font);
&:not(.active) {
opacity: .8;
background-color: var(--color-primary-light-500-alpha-600);
}
}
&:active:not(.active) {
opacity: .6;
background-color: var(--color-primary-light-200-alpha-600);
}
}
.icon {
// margin-bottom: 5px;
&> svg {
width: 32%;
}
}
</style>
@@ -0,0 +1,55 @@
<template>
<div :class="[$style.aside, { [$style.fullscreen]: isFullscreen }]">
<ControlBtns v-if="appSetting['common.controlBtnPosition'] == 'left'" />
<div v-else :class="$style.logo">L X</div>
<NavBar />
</div>
</template>
<script setup>
import { isFullscreen } from '@renderer/store'
import { appSetting } from '@renderer/store/setting'
import ControlBtns from './ControlBtns'
import NavBar from './NavBar'
</script>
<style lang="less" module>
@import '@renderer/assets/styles/layout.less';
.aside {
// box-shadow: 0 0 5px rgba(0, 0, 0, .3);
transition: @transition-normal;
transition-property: background-color;
// background-color: @color-theme-sidebar;
// background-color: @color-aside-background;
// border-right: 2px solid var(--color-primary);
-webkit-app-region: drag;
-webkit-user-select: none;
display: flex;
flex-flow: column nowrap;
&.fullscreen {
-webkit-app-region: no-drag;
.logo {
display: none;
}
}
}
.logo {
box-sizing: border-box;
padding: 0 13%;
height: 50px;
color: var(--color-nav-font);
opacity: .8;
flex: none;
text-align: center;
line-height: 50px;
font-weight: bold;
// -webkit-app-region: no-drag;
}
</style>
File diff suppressed because one or more lines are too long
@@ -0,0 +1,145 @@
<template>
<material-modal :show="!isAgreePact || isShowPact" max-width="70%" :bg-close="isAgreePact" :close-btn="isAgreePact" @close="handleClose(false)">
<main :class="$style.main">
<h2>许可协议</h2>
<div class="select scroll" :class="$style.content">
<p>本项目(软件)基于<strong class="hover underline" @click="openUrl('http://www.apache.org/licenses/LICENSE-2.0')">Apache License 2.0</strong>&nbsp;许可证发行,在使用本软件前,你(使用者)需签署本协议才可继续使用,以下协议是对于 Apache License 2.0 的补充,如有冲突,以以下协议为准。</p><br>
<p>词语约定:本协议中的“本软件”指洛雪音乐桌面版项目;“使用者”指签署本协议的使用者;“官方音乐平台”指对本软件内置的包括酷我、酷狗、咪咕等音乐源的官方平台统称;“版权数据”指包括但不限于图像、音频、名字等在内的他人拥有所属版权的数据。</p><br>
<p><strong>1、</strong>本软件的数据来源原理是从各官方音乐平台的公开服务器中拉取数据,经过对数据简单地筛选与合并后进行展示,因此本软件不对数据的准确性负责。</p>
<p><strong>2、</strong>使用本软件的过程中可能会产生版权数据,对于这些版权数据,本软件不拥有它们的所有权,为了避免造成侵权,使用者务必在<strong>24小时内</strong>清除使用本软件的过程中所产生的版权数据。</p>
<p><strong>3、</strong>本软件内的官方音乐平台别名为本软件内对官方音乐平台的一个称呼,不包含恶意,如果官方音乐平台觉得不妥,可联系本软件更改或移除。</p>
<p><strong>4、</strong>本软件内使用的部分包括但不限于字体、图片等资源来源于互联网,如果出现侵权可联系本软件移除。</p>
<p><strong>5、</strong>由于使用本软件产生的包括由于本协议或由于使用或无法使用本软件而引起的任何性质的任何直接、间接、特殊、偶然或结果性损害(包括但不限于因商誉损失、停工、计算机故障或故障引起的损害赔偿,或任何及所有其他商业损害或损失)由使用者负责。</p>
<p><strong>6、</strong>本项目完全免费,且开源发布于&nbsp;<span class="hover underline" @click="openUrl('https://github.com/lyswhut/lx-music-desktop#readme')">GitHub</span>&nbsp;面向全世界人用作对技术的学习交流,本软件不对项目内的技术可能存在违反当地法律法规的行为作保证,<strong>禁止在违反当地法律法规的情况下使用本软件</strong>,对于使用者在明知或不知当地法律法规不允许的情况下使用本软件所造成的任何违法违规行为由使用者承担,本软件不承担由此造成的任何直接、间接、特殊、偶然或结果性责任。</p><br>
<p><strong>*</strong>&nbsp;若协议更新,恕不另行通知,可到开源地址查看。</p>
<p><strong>*</strong>&nbsp;本软件的初衷是帮助官方音乐平台简化数据后代为展示,帮助使用者根据歌曲名、艺术家等关键字快速地定位所需内容所在的音乐平台。</p>
<p><strong>*</strong>&nbsp;音乐平台不易,建议到对应音乐平台支持正版资源。</p><br>
<p v-if="!isAgreePact"><strong>若你(使用者)接受以上协议,请点击下面的“接受”按钮签署本协议,若不接受,请点击“不接受”后退出软件并清除本软件的所有数据。</strong></p>
</div>
<div v-if="!isAgreePact" :class="$style.btns">
<base-btn :class="$style.btn" @click="handleClose(true)">{{ $t('not_agree') }}</base-btn>
<base-btn :class="$style.btn" :disabled="!btnEnable" @click="handleClick()">{{ $t('agree') }} {{ timeStr }}</base-btn>
</div>
</main>
</material-modal>
</template>
<script>
import { quitApp } from '@renderer/utils/ipc'
import { openUrl } from '@common/utils/electron'
import { isShowPact } from '@renderer/store'
import { appSetting, saveAgreePact } from '@renderer/store/setting'
import { computed } from '@common/utils/vueTools'
export default {
setup() {
const isAgreePact = computed(() => appSetting['common.isAgreePact'])
return {
isShowPact,
isAgreePact,
appSetting,
}
},
data() {
return {
time: 20,
}
},
computed: {
btnEnable() {
return this.time == 0
},
timeStr() {
return this.btnEnable ? '' : `(${this.time})`
},
},
watch: {
isAgreePact(n) {
if (n) return
this.time = 5
this.startTimeout()
},
},
mounted() {
this.$nextTick(() => {
if (!this.isAgreePact) {
this.startTimeout()
}
})
},
methods: {
handleClick() {
saveAgreePact(true)
window.setTimeout(() => {
this.$dialog({
message: Buffer.from('e69cace8bdafe4bbb6e5ae8ce585a8e5858de8b4b9e4b894e5bc80e6ba90efbc8ce5a682e69e9ce4bda0e698afe88ab1e992b1e8b4ade4b9b0e79a84efbc8ce8afb7e79bb4e68ea5e7bb99e5b7aee8af84efbc810a0a5468697320736f667477617265206973206672656520616e64206f70656e20736f757263652e', 'hex').toString(),
confirmButtonText: Buffer.from('e5a5bde79a8420284f4b29', 'hex').toString(),
})
}, 2e3)
},
handleClose(isExit) {
if (isExit) return quitApp(true)
isShowPact.value = false
},
openUrl(url) {
openUrl(url)
},
startTimeout() {
window.setTimeout(() => {
if (--this.time > 0) this.startTimeout()
}, 1e3)
},
},
}
</script>
<style lang="less" module>
@import '@renderer/assets/styles/layout.less';
.main {
padding: 15px 8px 12px;
min-width: 200px;
min-height: 0;
display: flex;
flex-flow: column nowrap;
justify-content: center;
h2 {
font-size: 16px;
color: var(--color-font);
line-height: 1.3;
text-align: center;
}
}
.content {
flex: auto;
margin: 15px 0;
padding: 0 7px;
h3 {
font-weight: bold;
line-height: 2;
}
p {
line-height: 1.5;
font-size: 14px;
text-align: justify;
}
}
.btns {
display: flex;
align-items: center;
justify-content: space-between;
}
.btn {
display: block;
width: 48%;
&:last-child {
margin-bottom: 0;
}
}
</style>
@@ -0,0 +1,118 @@
<template>
<div :class="$style.controlBtn">
<common-volume-bar />
<div :class="$style.titleBtn" :aria-label="toggleDesktopLyricBtnTitle" @click="toggleDesktopLyric" @contextmenu="toggleLockDesktopLyric">
<svg v-show="appSetting['desktopLyric.enable']" version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" height="100%" viewBox="0 0 512 512" space="preserve">
<use xlink:href="#icon-desktop-lyric-on" />
</svg>
<svg v-show="!appSetting['desktopLyric.enable']" version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" height="100%" viewBox="0 0 512 512" space="preserve">
<use xlink:href="#icon-desktop-lyric-off" />
</svg>
</div>
<div :class="$style.titleBtn" :aria-label="nextTogglePlayName" @click="toggleNextPlayMode">
<svg v-if="appSetting['player.togglePlayMethod'] == 'listLoop'" version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" height="80%" viewBox="0 0 24 24" space="preserve">
<use xlink:href="#icon-list-loop" />
</svg>
<svg v-else-if="appSetting['player.togglePlayMethod'] == 'random'" version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" width="100%" viewBox="0 0 24 24" space="preserve">
<use xlink:href="#icon-list-random" />
</svg>
<svg v-else-if="appSetting['player.togglePlayMethod'] == 'list'" version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" width="120%" viewBox="0 0 24 24" space="preserve">
<use xlink:href="#icon-list-order" />
</svg>
<svg v-else-if="appSetting['player.togglePlayMethod'] == 'singleLoop'" version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" width="100%" viewBox="0 0 24 24" space="preserve">
<use xlink:href="#icon-single-loop" />
</svg>
<svg v-else version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" width="120%" viewBox="0 0 24 24" space="preserve">
<use xlink:href="#icon-single" />
</svg>
</div>
<div :class="$style.titleBtn" :aria-label="$t('player__add_music_to')" @click="addMusicTo">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" height="80%" viewBox="0 0 512 512" space="preserve">
<use xlink:href="#icon-add-2" />
</svg>
</div>
<common-list-add-modal v-model:show="isShowAddMusicTo" :music-info="playMusicInfo.musicInfo" />
</div>
</template>
<script>
import { ref } from '@common/utils/vueTools'
import useNextTogglePlay from '@renderer/utils/compositions/useNextTogglePlay'
import useToggleDesktopLyric from '@renderer/utils/compositions/useToggleDesktopLyric'
import { musicInfo, playMusicInfo } from '@renderer/store/player/state'
import { appSetting } from '@renderer/store/setting'
export default {
setup() {
const isShowAddMusicTo = ref(false)
const {
nextTogglePlayName,
toggleNextPlayMode,
} = useNextTogglePlay()
const {
toggleDesktopLyricBtnTitle,
toggleDesktopLyric,
toggleLockDesktopLyric,
} = useToggleDesktopLyric()
const addMusicTo = () => {
if (!musicInfo.id) return
isShowAddMusicTo.value = true
}
return {
appSetting,
isShowAddMusicTo,
nextTogglePlayName,
toggleNextPlayMode,
toggleDesktopLyricBtnTitle,
toggleDesktopLyric,
toggleLockDesktopLyric,
addMusicTo,
playMusicInfo,
}
},
}
</script>
<style lang="less" module>
@import '@renderer/assets/styles/layout.less';
.controlBtn {
flex: none;
display: flex;
flex-flow: row nowrap;
}
.titleBtn {
flex: none;
margin-left: 5px;
height: 100%;
width: 20px;
color: var(--color-button-font);
display: flex;
flex-flow: column nowrap;
justify-content: center;
align-items: center;
transition: opacity 0.2s ease;
opacity: .6;
cursor: pointer;
svg {
filter: drop-shadow(0 0 1px rgba(0, 0, 0, 0.2));
}
&:hover {
opacity: 1;
}
&:active {
opacity: 1;
}
}
</style>
@@ -0,0 +1,323 @@
<template>
<div :class="$style.player">
<div :class="$style.left" :aria-label="$t('player__pic_tip')" @contextmenu="handleToMusicLocation" @click="showPlayerDetail">
<img v-if="musicInfo.pic" :src="musicInfo.pic" loading="lazy" decoding="async" @error="imgError">
<svg v-else version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" height="102%" width="100%" viewBox="0 0 60 60" space="preserve">
<use :xlink:href="`#${$style.iconPic}`" />
</svg>
</div>
<div :class="$style.middle">
<div :class="$style.column1">
<div :class="$style.container">
<div :class="$style.title" :aria-label="title + $t('copy_tip')" @click="handleCopy(title)">{{ title }}</div>
<control-btns />
</div>
</div>
<div :class="$style.column2">
<common-progress-bar v-if="!isShowPlayerDetail" :progress="progress" :handle-transition-end="handleTransitionEnd" :is-active-transition="isActiveTransition" />
</div>
<div :class="$style.column3">
<template v-if="!isShowPlayerDetail"><span :class="$style.statusText">{{ statusText }}</span><span>{{ nowPlayTimeStr }}</span><span style="margin: 0 5px;">/</span><span>{{ maxPlayTimeStr }}</span></template>
</div>
</div>
<div :class="$style.right">
<div :class="$style.playBtn" :aria-label="$t('player__prev')" style="transform: rotate(180deg);" @click="playPrev()">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" height="100%" viewBox="0 0 220.847 220.847" space="preserve">
<use xlink:href="#icon-nextMusic" />
</svg>
</div>
<div :class="$style.playBtn" :aria-label="isPlay ? $t('player__pause') : $t('player__play')" @click="togglePlay">
<svg v-if="isPlay" version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" height="100%" viewBox="0 0 277.338 277.338" space="preserve">
<use xlink:href="#icon-pause" />
</svg>
<svg v-else version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" height="100%" viewBox="0 0 170 170" space="preserve">
<use xlink:href="#icon-play" />
</svg>
</div>
<div :class="$style.playBtn" :aria-label="$t('player__next')" @click="playNext()">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" height="100%" viewBox="0 0 220.847 220.847" space="preserve">
<use xlink:href="#icon-nextMusic" />
</svg>
</div>
</div>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" style="display: none;">
<g :id="$style.iconPic">
<path d="M29,0C12.984,0,0,12.984,0,29c0,16.016,12.984,29,29,29s29-12.984,29-29C58,12.984,45.016,0,29,0zM29,36.08c-3.91,0-7.08-3.17-7.08-7.08c0-3.91,3.17-7.08,7.08-7.08s7.08,3.17,7.08,7.08C36.08,32.91,32.91,36.08,29,36.08z" />
<path :class="$style.c1" d="M6.487,22.932c-0.077,0-0.156-0.009-0.234-0.027c-0.537-0.13-0.868-0.67-0.739-1.206c0.946-3.935,2.955-7.522,5.809-10.376s6.441-4.862,10.376-5.809c0.536-0.127,1.077,0.202,1.206,0.739c0.129,0.536-0.202,1.076-0.739,1.206c-3.575,0.859-6.836,2.685-9.429,5.277s-4.418,5.854-5.277,9.429C7.349,22.624,6.938,22.932,6.487,22.932z" />
<path :class="$style.c1" d="M36.066,52.514c-0.451,0-0.861-0.308-0.972-0.767c-0.129-0.536,0.202-1.076,0.739-1.206c3.576-0.859,6.837-2.685,9.43-5.277s4.418-5.854,5.277-9.429c0.129-0.538,0.668-0.868,1.206-0.739c0.537,0.13,0.868,0.67,0.739,1.206c-0.946,3.935-2.955,7.522-5.809,10.376s-6.441,4.862-10.377,5.809C36.223,52.505,36.144,52.514,36.066,52.514z" />
<path :class="$style.c1" d="M11.313,24.226c-0.075,0-0.151-0.008-0.228-0.026c-0.538-0.125-0.873-0.663-0.747-1.2c0.72-3.09,2.282-5.904,4.52-8.141c2.236-2.237,5.051-3.8,8.141-4.52c0.535-0.131,1.075,0.209,1.2,0.747c0.126,0.537-0.209,1.075-0.747,1.2c-2.725,0.635-5.207,2.014-7.18,3.986s-3.352,4.455-3.986,7.18C12.179,23.914,11.768,24.226,11.313,24.226z" />
<path :class="$style.c1" d="M34.773,47.688c-0.454,0-0.865-0.312-0.973-0.773c-0.126-0.537,0.209-1.075,0.747-1.2c2.725-0.635,5.207-2.014,7.18-3.986s3.352-4.455,3.986-7.18c0.125-0.538,0.662-0.88,1.2-0.747c0.538,0.125,0.873,0.663,0.747,1.2c-0.72,3.09-2.282,5.904-4.52,8.141c-2.236,2.237-5.051,3.8-8.141,4.52C34.925,47.68,34.849,47.688,34.773,47.688z" />
<path :class="$style.c1" d="M16.14,25.519c-0.071,0-0.143-0.008-0.215-0.023c-0.539-0.118-0.881-0.651-0.763-1.19c0.997-4.557,4.586-8.146,9.143-9.143c0.537-0.116,1.071,0.222,1.19,0.763c0.118,0.539-0.224,1.072-0.763,1.19c-3.796,0.831-6.786,3.821-7.617,7.617C17.013,25.2,16.6,25.519,16.14,25.519z" />
<path :class="$style.c1" d="M33.48,42.861c-0.46,0-0.873-0.318-0.976-0.786c-0.118-0.539,0.224-1.072,0.763-1.19c3.796-0.831,6.786-3.821,7.617-7.617c0.118-0.541,0.65-0.881,1.19-0.763c0.539,0.118,0.881,0.651,0.763,1.19c-0.997,4.557-4.586,8.146-9.143,9.143C33.623,42.854,33.552,42.861,33.48,42.861z" />
<path :class="$style.c2" d="M29,38.08c-5.007,0-9.08-4.073-9.08-9.08s4.073-9.08,9.08-9.08s9.08,4.073,9.08,9.08S34.007,38.08,29,38.08z M29,23.92c-2.801,0-5.08,2.279-5.08,5.08s2.279,5.08,5.08,5.08s5.08-2.279,5.08-5.08S31.801,23.92,29,23.92z" />
</g>
</svg>
</div>
</template>
<script>
import { computed } from '@common/utils/vueTools'
import { useRouter } from '@common/utils/vueRouter'
import { clipboardWriteText } from '@common/utils/electron'
import ControlBtns from './ControlBtns'
import usePlayProgress from '@renderer/utils/compositions/usePlayProgress'
// import { lyric } from '@renderer/core/share/lyric'
import {
statusText,
musicInfo,
isShowPlayerDetail,
isPlay,
playInfo,
playMusicInfo,
} from '@renderer/store/player/state'
import {
setMusicInfo,
setShowPlayerDetail,
} from '@renderer/store/player/action'
import { appSetting } from '@renderer/store/setting'
import { togglePlay, playNext, playPrev } from '@renderer/core/player'
export default {
name: 'CorePlayBar',
components: {
ControlBtns,
},
setup() {
const router = useRouter()
const {
nowPlayTimeStr,
maxPlayTimeStr,
progress,
isActiveTransition,
handleTransitionEnd,
} = usePlayProgress()
const showPlayerDetail = () => {
if (!playMusicInfo.musicInfo) return
setShowPlayerDetail(true)
}
const handleCopy = (text) => {
clipboardWriteText(text)
}
const imgError = () => {
// console.log(e)
setMusicInfo({ pic: null })
}
const handleToMusicLocation = () => {
const listId = playMusicInfo.listId
if (!listId || listId == '__temp__' || listId == 'download' || !playMusicInfo.musicInfo) return
if (playInfo.playIndex == -1) return
router.push({
path: '/list',
query: {
id: listId,
scrollIndex: playInfo.playIndex,
},
})
}
const title = computed(() => {
return musicInfo.name
? appSetting['download.fileName'].replace('歌名', musicInfo.name).replace('歌手', musicInfo.singer)
: '^-^'
})
// onBeforeUnmount(() => {
// window.eventHub.emit(eventPlayerNames.setTogglePlay)
// })
return {
musicInfo,
nowPlayTimeStr,
maxPlayTimeStr,
progress,
isActiveTransition,
handleTransitionEnd,
handleCopy,
imgError,
statusText,
title,
showPlayerDetail,
isPlay,
togglePlay,
playNext,
playPrev,
handleToMusicLocation,
isShowPlayerDetail,
}
},
}
</script>
<style lang="less" module>
@import '@renderer/assets/styles/layout.less';
.player {
height: @height-player;
background-color: var(--color-main-background);
border-top: 1px solid var(--color-primary-alpha-900);
box-sizing: border-box;
display: flex;
contain: strict;
padding: 5px 0;
z-index: 2;
// box-shadow: 0px 0px 4px rgba(0, 0, 0, 0.1);
* {
box-sizing: border-box;
}
}
.left {
width: @height-player - 2;
height: 100%;
// color: var(--color-primary);
// transition: @transition-normal;
// transition-property: color;
flex: none;
padding: 2PX;
opacity: 1;
// transition: @transition-normal;
// transition-property: opacity;
display: flex;
justify-content: center;
// align-items: center;
cursor: pointer;
&:hover {
opacity: .8;
}
svg {
fill: currentColor;
}
img {
box-shadow: 0 0 2px rgba(0, 0, 0, 0.3);
max-width: 100%;
max-height: 100%;
transition: @transition-normal;
transition-property: border-color;
// border-radius: 50%;
border-radius: @radius-border;
// border: 2px solid @color-theme_2-background_1;
}
}
.middle {
flex: auto;
height: 100%;
padding: 2px 10px 2px 8px;
display: flex;
flex-flow: column nowrap;
}
.right {
height: 100%;
flex: none;
display: flex;
flex-flow: row nowrap;
align-items: center;
padding-left: 15px;
padding-right: 20px;
}
.column1 {
flex: auto;
position: relative;
font-size: 16px;
.container {
position: absolute;
left: 0;
width: 100%;
height: 100%;
display: flex;
flex-flow: row nowrap;
justify-content: space-between;
}
}
.title {
flex: 0 1 auto;
min-width: 0;
padding-right: 5px;
font-size: 14px;
line-height: 18px;
.mixin-ellipsis-1;
}
.play-btn {
+ .play-btn {
margin-left: 15px;
}
flex: none;
height: 46%;
// margin-top: -2px;
// transition: @transition-normal;
// transition-property: color;
color: var(--color-button-font);
transition: opacity 0.2s ease;
opacity: 1;
cursor: pointer;
svg {
fill: currentColor;
filter: drop-shadow(0 0 1px rgba(0, 0, 0, 0.2));
}
&:hover {
opacity: 0.8;
}
&:active {
opacity: 0.6;
}
}
.column2 {
flex: none;
padding: 3px 0;
height: 10px;
position: relative;
}
.column3 {
transition: @transition-normal;
transition-property: color;
color: var(--color-font);
flex: none;
font-size: 12px;
display: flex;
padding-top: 2px;
// justify-content: space-between;
height: 22px;
align-items: center;
}
.status-text {
font-size: 0.98em;
transition: @transition-normal;
transition-property: color;
color: var(--color-font-label);
.mixin-ellipsis-1;
// padding: 0 5px;
padding-right: 5px;
flex: 1 1 0;
// text-align: center;
line-height: 1.2;
width: 0;
}
#icon-pic {
// color: var(--color-primary);
.c1 {
transition: @transition-normal;
transition-property: fill;
// fill: @color-player-pic-c1;
}
.c2 {
transition: @transition-normal;
transition-property: fill;
// fill: @color-player-pic-c2;
}
}
</style>
@@ -0,0 +1,353 @@
<template>
<div :class="['right', $style.right]" :style="lrcFontSize">
<transition enter-active-class="animated fadeIn" leave-active-class="animated fadeOut">
<div
v-show="!isShowLrcSelectContent"
ref="dom_lyric"
:class="['lyric', $style.lyric, { [$style.draging]: isMsDown }, { [$style.lrcActiveZoom]: isZoomActiveLrc }]" :style="lrcStyles"
@wheel="handleWheel" @mousedown="handleLyricMouseDown"
@contextmenu.stop="handleShowLyricMenu"
>
<div :class="['pre', $style.lyricSpace]" />
<div ref="dom_lyric_text" />
<div :class="$style.lyricSpace" />
</div>
</transition>
<transition enter-active-class="animated fadeIn" leave-active-class="animated fadeOut">
<div v-if="isShowLyricProgressSetting" v-show="isStopScroll && !isShowLrcSelectContent" :class="$style.skip">
<div ref="dom_skip_line" :class="$style.line" />
<span :class="$style.label">{{ timeStr }}</span>
<base-btn :class="$style.skipBtn" @mouseenter="handleSkipMouseEnter" @mouseleave="handleSkipMouseLeave" @click="handleSkipPlay">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" height="50%" viewBox="0 0 170 170" space="preserve">
<use xlink:href="#icon-play" />
</svg>
</base-btn>
</div>
</transition>
<transition enter-active-class="animated fadeIn" leave-active-class="animated fadeOut">
<div v-if="isShowLrcSelectContent" :class="[$style.lyricSelectContent, 'select', 'scroll', 'lyricSelectContent']" @contextmenu="handleCopySelectText">
<div v-for="(info, index) in lyric.lines" :key="index" :class="[$style.lyricSelectline, { [$style.lrcActive]: lyric.line == index }]">
<span>{{ info.text }}</span>
<template v-for="(lrc, i) in info.extendedLyrics" :key="i">
<br>
<span :class="$style.lyricSelectlineExtended">{{ lrc }}</span>
</template>
</div>
</div>
</transition>
<LyricMenu v-model="lyricMenuVisible" :xy="lyricMenuXY" :lyric-info="lyricInfo" @update-lyric="handleUpdateLyric" />
</div>
</template>
<script>
import { clipboardWriteText } from '@common/utils/electron'
import { lyric } from '@renderer/store/player/lyric'
import { playProgress } from '@renderer/store/player/playProgress'
import { isFullscreen } from '@renderer/store'
import {
isPlay,
isShowLrcSelectContent,
isShowPlayComment,
musicInfo as playerMusicInfo,
playMusicInfo,
} from '@renderer/store/player/state'
import {
setMusicInfo,
} from '@renderer/store/player/action'
import { onMounted, onBeforeUnmount, computed, reactive, ref, nextTick, watch } from '@common/utils/vueTools'
import useLyric from '@renderer/utils/compositions/useLyric'
import LyricMenu from './components/LyricMenu'
import { appSetting } from '@renderer/store/setting'
import { setLyricOffset } from '@renderer/core/lyric'
export default {
components: {
LyricMenu,
},
setup() {
const isZoomActiveLrc = computed(() => appSetting['playDetail.isZoomActiveLrc'])
const isShowLyricProgressSetting = computed(() => appSetting['playDetail.isShowLyricProgressSetting'])
const {
dom_lyric,
dom_lyric_text,
dom_skip_line,
isMsDown,
isStopScroll,
timeStr,
handleLyricMouseDown,
handleWheel,
handleSkipPlay,
handleSkipMouseEnter,
handleSkipMouseLeave,
handleScrollLrc,
} = useLyric({ isPlay, lyric, playProgress, isShowLyricProgressSetting })
watch([isFullscreen, isShowPlayComment], () => {
setTimeout(handleScrollLrc, 400)
})
const lyricMenuVisible = ref(false)
const lyricMenuXY = reactive({
x: 0,
y: 0,
})
const lyricInfo = reactive({
lyric: '',
tlyric: '',
rlyric: '',
lxlyric: '',
rawlyric: '',
musicInfo: null,
})
const updateMusicInfo = () => {
lyricInfo.lyric = playerMusicInfo.lrc
lyricInfo.tlyric = playerMusicInfo.tlrc
lyricInfo.rlyric = playerMusicInfo.rlrc
lyricInfo.lxlyric = playerMusicInfo.lxlrc
lyricInfo.rawlyric = playerMusicInfo.rawlrc
lyricInfo.musicInfo = playMusicInfo.musicInfo
}
const handleShowLyricMenu = event => {
updateMusicInfo()
lyricMenuXY.x = event.pageX
lyricMenuXY.y = event.pageY
if (lyricMenuVisible.value) return
nextTick(() => {
lyricMenuVisible.value = true
})
}
const handleUpdateLyric = ({ lyric, tlyric, rlyric, lxlyric, offset }) => {
setMusicInfo({
lrc: lyric,
tlrc: tlyric,
rlrc: rlyric,
lxlrc: lxlyric,
})
console.log(offset)
setLyricOffset(offset)
}
const lrcStyles = computed(() => {
return {
textAlign: appSetting['playDetail.style.align'],
}
})
const lrcFontSize = computed(() => {
let size = appSetting['playDetail.style.fontSize'] / 100
if (isFullscreen.value) size = size *= 1.4
return {
'--playDetail-lrc-font-size': (isShowPlayComment.value ? size * 0.82 : size) + 'rem',
}
})
onMounted(() => {
window.app_event.on('lyricUpdated', updateMusicInfo)
})
onBeforeUnmount(() => {
window.app_event.off('lyricUpdated', updateMusicInfo)
})
return {
dom_lyric,
dom_lyric_text,
dom_skip_line,
isMsDown,
timeStr,
handleLyricMouseDown,
handleWheel,
handleSkipPlay,
handleSkipMouseEnter,
handleSkipMouseLeave,
lyric,
lrcStyles,
lrcFontSize,
isShowLrcSelectContent,
isShowLyricProgressSetting,
isZoomActiveLrc,
isStopScroll,
lyricMenuVisible,
lyricMenuXY,
handleShowLyricMenu,
handleUpdateLyric,
lyricInfo,
}
},
methods: {
handleCopySelectText() {
let str = window.getSelection().toString()
str = str.trim()
if (!str.length) return
clipboardWriteText(str)
},
},
}
</script>
<style lang="less" module>
@import '@renderer/assets/styles/layout.less';
.right {
flex: 0 0 60%;
// padding: 0 30px;
position: relative;
transition: flex-basis @transition-normal;
}
.lyric {
text-align: center;
height: 100%;
overflow: hidden;
font-size: var(--playDetail-lrc-font-size, 16px);
-webkit-mask-image: linear-gradient(transparent 0%, #fff 20%, #fff 80%, transparent 100%);
cursor: grab;
&.draging {
cursor: grabbing;
}
:global {
.font-lrc {
color: var(--color-200);
}
.line-content {
line-height: 1.2;
padding: calc(var(--playDetail-lrc-font-size, 16px) / 2) 1px;
overflow-wrap: break-word;
color: var(--color-200);
transition: @transition-normal;
transition-property: padding;
.extended {
font-size: 0.8em;
margin-top: 5px;
}
&.line-mode {
.font-lrc {
transition: @transition-fast;
transition-property: font-size, color;
}
}
&.line-mode.active .font-lrc, &.font-mode.played .font-lrc {
color: var(--color-primary-dark-200);
}
&.font-mode .extended .font-lrc {
transition: @transition-slow;
transition-property: font-size, color;
}
&.font-mode > .line > .font-lrc {
> span {
transition: @transition-normal;
transition-property: font-size;
font-size: 1em;
background-repeat: no-repeat;
background-color: var(--color-200);
background-image: -webkit-linear-gradient(top, var(--color-primary-dark-200), var(--color-primary-dark-200));
-webkit-text-fill-color: transparent;
-webkit-background-clip: text;
background-size: 0 100%;
}
}
}
}
// p {
// padding: 8px 0;
// line-height: 1.2;
// overflow-wrap: break-word;
// transition: @transition-normal !important;
// transition-property: color, font-size;
// }
// .lrc-active {
// color: var(--color-primary);
// font-size: 1.2em;
// }
}
.lrc-active-zoom {
:global {
.line-content {
&.active {
.extended {
font-size: .94em;
}
.line {
font-size: 1.1em;
}
}
}
}
}
.skip {
position: absolute;
top: calc(38% + var(--playDetail-lrc-font-size, 16px) + 4px);
left: 0;
// height: 6px;
width: 100%;
pointer-events: none;
// opacity: .5;
.line {
border-top: 2px dotted var(--color-primary-dark-100);
opacity: .15;
margin-right: 30px;
-webkit-mask-image: linear-gradient(90deg, transparent 0%, transparent 15%, #fff 100%);
}
.label {
position: absolute;
right: 30px;
top: -14px;
line-height: 1;
font-size: 12px;
color: var(--color-primary-dark-100);
opacity: .7;
}
.skipBtn {
position: absolute;
right: 0;
top: 0;
transform: translateY(-50%);
width: 30px;
height: 30px;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
background: none !important;
pointer-events: initial;
transition: @transition-normal;
transition-property: opacity;
opacity: .8;
&:hover {
opacity: .6;
}
}
}
.lyricSelectContent {
position: absolute;
left: 0;
top: 0;
// text-align: center;
height: 100%;
width: 100%;
font-size: 16px;
z-index: 10;
color: var(--color-400);
.lyricSelectline {
padding: 8px 0;
overflow-wrap: break-word;
transition: @transition-normal !important;
transition-property: color, font-size;
line-height: 1.3;
}
.lyricSelectlineExtended {
font-size: 14px;
}
.lrc-active {
color: var(--color-primary);
}
}
.lyric-space {
height: 70%;
}
</style>
@@ -0,0 +1,143 @@
<template>
<div :class="$style.footer">
<div :class="$style.footerLeft">
<control-btns />
<div :class="$style.progressContainer">
<div :class="$style.progressContent">
<common-progress-bar
:class-name="$style.progress"
:progress="progress"
:handle-transition-end="handleTransitionEnd"
:is-active-transition="isActiveTransition"
/>
</div>
</div>
<div :class="$style.timeLabel"><span :class="$style.status" style="margin-left: 15px">{{ status }}</span><span>{{ nowPlayTimeStr }}</span><span style="margin: 0 5px;">/</span><span>{{ maxPlayTimeStr }}</span></div>
</div>
<div :class="$style.playControl">
<div :class="$style.playBtn" style="transform: rotate(180deg);" :aria-label="$t('player__prev')" @click="playPrev()">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" height="100%" viewBox="0 0 220.847 220.847" space="preserve">
<use xlink:href="#icon-nextMusic" />
</svg>
</div>
<div :class="$style.playBtn" :aria-label="isPlay ? $t('player__pause') : $t('player__play')" @click="togglePlay">
<svg v-if="isPlay" version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" height="100%" viewBox="0 0 277.338 277.338" space="preserve">
<use xlink:href="#icon-pause" />
</svg>
<svg v-else version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" height="100%" viewBox="0 0 170 170" space="preserve">
<use xlink:href="#icon-play" />
</svg>
</div>
<div :class="$style.playBtn" :aria-label="$t('player__next')" @click="playNext()">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" height="100%" viewBox="0 0 220.847 220.847" space="preserve">
<use xlink:href="#icon-nextMusic" />
</svg>
</div>
</div>
</div>
</template>
<script setup>
import { playNext, playPrev, togglePlay } from '@renderer/core/player'
import { status, isPlay } from '@renderer/store/player/state'
import usePlayProgress from '@renderer/utils/compositions/usePlayProgress'
import ControlBtns from './components/ControlBtns'
const {
nowPlayTimeStr,
maxPlayTimeStr,
progress,
isActiveTransition,
handleTransitionEnd,
} = usePlayProgress()
</script>
<style lang="less" module>
@import '@renderer/assets/styles/layout.less';
.footer {
flex: 0 0 100px;
overflow: hidden;
display: flex;
align-items: center;
}
.footerLeft {
flex: auto;
display: flex;
flex-flow: column nowrap;
padding: 13px;
overflow: hidden;
}
.progress-container {
width: 100%;
position: relative;
padding: 3px 0;
}
.progress-content {
position: relative;
height: 16px;
padding: 5px 0;
width: 100%;
}
.progress {
height: 100%;
}
.bar-transition {
transition-property: transform;
transition-timing-function: ease-out;
transition-duration: 0.2s;
}
.time-label {
width: 100%;
height: 18px;
display: flex;
span {
font-size: 13px;
}
}
.status {
flex: auto;
}
.play-control {
flex: none;
height: 100%;
display: flex;
justify-content: flex-end;
align-items: center;
padding: 0 25px;
color: var(--color-button-font);
}
.play-btn {
height: 40%;
padding: 5px;
cursor: pointer;
flex: none;
// transition: @transition-normal;
// transition-property: color;
color: var(--color-button-font);
transition: opacity 0.2s ease;
opacity: 1;
cursor: pointer;
+.play-btn {
margin-left: 10px;
}
svg {
fill: currentColor;
filter: drop-shadow(0 0 1px rgba(0, 0, 0, 0.2));
}
&:hover {
opacity: 0.8;
}
&:active {
opacity: 0.6;
}
}
</style>
@@ -0,0 +1,62 @@
import { debounce } from '@common/utils/common'
let isAutoHide = false
let isLockedPointer = false
let dom = null
let event = null
let isMouseDown = false
const isControl = dom => {
if (!dom || dom === document.body) return false
// console.log(dom)
if (dom.getAttribute('aria-label') || dom.tagName == 'BUTTON') return true
return isControl(dom.parentNode)
}
const lockPointer = () => {
if (!isAutoHide || isMouseDown) return
if (event && isControl(document.elementFromPoint(event.clientX, event.clientY))) return
dom.requestPointerLock()
isLockedPointer = true
}
const unLockPointer = () => {
if (!isLockedPointer) return
document.exitPointerLock()
isLockedPointer = false
}
const startTimeout = debounce(lockPointer, 3000)
const handleMouseMove = (_event) => {
event = _event
startTimeout()
unLockPointer()
}
const handleMouseDown = () => {
isMouseDown = true
}
const handleMouseUp = () => {
isMouseDown = false
startTimeout()
}
export const registerAutoHideMounse = () => {
if (isAutoHide) return
if (!dom) dom = document.getElementById('root')
isAutoHide = true
document.addEventListener('mousemove', handleMouseMove)
document.addEventListener('mousedown', handleMouseDown)
document.addEventListener('mouseup', handleMouseUp)
startTimeout()
}
export const unregisterAutoHideMounse = () => {
if (!isAutoHide) return
isAutoHide = false
// console.log(dom)
dom.removeEventListener('mousemove', handleMouseMove)
dom.removeEventListener('mousedown', handleMouseDown)
dom.removeEventListener('mouseup', handleMouseUp)
unLockPointer()
}
@@ -0,0 +1,154 @@
<template lang="pug">
div(:class="$style.footerLeftControlBtns")
common-volume-bar
div(:class="[$style.footerLeftControlBtn, $style.lrcBtn]" @click="toggleDesktopLyric" @contextmenu="toggleLockDesktopLyric" :aria-label="toggleDesktopLyricBtnTitle")
svg(v-show="appSetting['desktopLyric.enable']" version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' height='125%' viewBox='0 0 512 512' space='preserve')
use(xlink:href='#icon-desktop-lyric-on')
svg(v-show="!appSetting['desktopLyric.enable']" version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' height='125%' viewBox='0 0 512 512' space='preserve')
use(xlink:href='#icon-desktop-lyric-off')
div(:class="[$style.footerLeftControlBtn, { [$style.active]: appSetting['player.audioVisualization'] }]" @click="toggleAudioVisualization" :aria-label="$t('audio_visualization')")
svg(version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' width='95%' viewBox='0 0 24 24' space='preserve')
use(xlink:href='#icon-audio-wave')
div(:class="[$style.footerLeftControlBtn, { [$style.active]: isShowLrcSelectContent }]" @click="toggleVisibleLrc" :aria-label="$t('lyric__select')")
svg(version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' width='95%' viewBox='0 0 24 24' space='preserve')
use(xlink:href='#icon-text')
div(:class="[$style.footerLeftControlBtn, {[$style.active]: isShowPlayComment}]" @click="toggleVisibleComment" :aria-label="$t('comment__show')")
svg(version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' width='95%' viewBox='0 0 24 24' space='preserve')
use(xlink:href='#icon-comment')
div(:class="$style.footerLeftControlBtn" @click="toggleNextPlayMode" :aria-label="nextTogglePlayName")
svg(v-show="appSetting['player.togglePlayMethod'] == 'listLoop'" version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' viewBox='0 0 24 24' space='preserve')
use(xlink:href='#icon-list-loop')
svg(v-show="appSetting['player.togglePlayMethod'] == 'random'" version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' viewBox='0 0 24 24' space='preserve')
use(xlink:href='#icon-list-random')
svg(v-show="appSetting['player.togglePlayMethod'] == 'list'" version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' width='120%' viewBox='0 0 24 24' space='preserve')
use(xlink:href='#icon-list-order')
svg(v-show="appSetting['player.togglePlayMethod'] == 'singleLoop'" version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' width='110%' viewBox='0 0 24 24' space='preserve')
use(xlink:href='#icon-single-loop')
svg(v-show="!appSetting['player.togglePlayMethod']" version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' width='120%' viewBox='0 0 24 24' space='preserve')
use(xlink:href='#icon-single')
div(:class="$style.footerLeftControlBtn" @click="isShowAddMusicTo = true" :aria-label="$t('player__add_music_to')")
svg(version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' viewBox='0 0 512 512' space='preserve')
use(xlink:href='#icon-add-2')
common-list-add-modal(v-model:show="isShowAddMusicTo" :musicInfo="playMusicInfo.musicInfo")
</template>
<script>
import { ref } from '@common/utils/vueTools'
import { useI18n } from '@renderer/plugins/i18n'
import {
isShowLrcSelectContent,
isShowPlayComment,
playMusicInfo,
} from '@renderer/store/player/state'
import {
setShowPlayLrcSelectContentLrc,
setShowPlayComment,
} from '@renderer/store/player/action'
import useNextTogglePlay from '@renderer/utils/compositions/useNextTogglePlay'
import useToggleDesktopLyric from '@renderer/utils/compositions/useToggleDesktopLyric'
import { dialog } from '@renderer/plugins/Dialog'
import { setMediaDeviceId } from '@renderer/plugins/player'
import { appSetting, saveMediaDeviceId, setEnableAudioVisualization } from '@renderer/store/setting'
export default {
setup() {
const t = useI18n()
// const setting = useRefGetter('setting')
// const setAudioVisualization = useCommit('setAudioVisualization')
// const saveMediaDeviceId = useCommit('setMediaDeviceId')
const toggleVisibleLrc = () => {
setShowPlayLrcSelectContentLrc(!isShowLrcSelectContent.value)
}
const toggleVisibleComment = () => {
setShowPlayComment(!isShowPlayComment.value)
}
const {
nextTogglePlayName,
toggleNextPlayMode,
} = useNextTogglePlay()
const {
toggleDesktopLyricBtnTitle,
toggleDesktopLyric,
toggleLockDesktopLyric,
} = useToggleDesktopLyric()
const isShowAddMusicTo = ref(false)
const toggleAudioVisualization = async() => {
const newSetting = !appSetting['player.audioVisualization']
if (newSetting && appSetting['player.mediaDeviceId'] != 'default') {
const confirm = await dialog.confirm({
message: t('setting__player_audio_visualization_tip'),
cancelButtonText: t('cancel_button_text'),
confirmButtonText: t('confirm_button_text'),
})
if (!confirm) return
saveMediaDeviceId('default')
await setMediaDeviceId('default').catch(_ => _)
}
setEnableAudioVisualization(newSetting)
}
return {
appSetting,
isShowLrcSelectContent,
toggleVisibleLrc,
isShowPlayComment,
toggleVisibleComment,
nextTogglePlayName,
toggleNextPlayMode,
toggleDesktopLyricBtnTitle,
toggleDesktopLyric,
toggleLockDesktopLyric,
toggleAudioVisualization,
isShowAddMusicTo,
playMusicInfo,
}
},
}
</script>
<style lang="less" module>
@import '@renderer/assets/styles/layout.less';
.footerLeftControlBtns {
color: var(--color-font);
display: flex;
flex-flow: row nowrap;
justify-content: flex-end;
.footerLeftControlBtn {
width: 18px;
height: 18px;
opacity: .5;
cursor: pointer;
transition: opacity @transition-normal;
display: flex;
align-items: center;
justify-content: center;
&:hover {
opacity: .9;
}
+.footerLeftControlBtn {
margin-left: 6px;
}
&.active {
color: var(--color-primary);
opacity: .8;
}
}
.lrcBtn {
width: 20px;
}
}
</style>
@@ -0,0 +1,296 @@
<template>
<teleport to="#root">
<div ref="dom_menu" :class="$style.container" :style="menuStyles" :aria-hidden="!modelValue">
<!-- <div :class="$style.group">
<div :class="$style.title">{{$t('lyric_menu__align')}}</div>
<div :class="$style.subGroup">
<div :class="[$style.btn, { [$style.active]: appSetting['playDetail.style.align'] == 'left' }]" role="button" @click="setFontAlign('left')" ignore-tip :aria-label="$t('lyric_menu__align_left')">{{$t('lyric_menu__align_left')}}</div>
<div :class="[$style.btn, { [$style.active]: appSetting['playDetail.style.align'] == 'center' }]" role="button" @click="setFontAlign('center')" ignore-tip :aria-label="$t('lyric_menu__align_center')">{{$t('lyric_menu__align_center')}}</div>
</div>
</div> -->
<div :class="$style.group">
<div :class="$style.subGroup">
<div :class="$style.title">{{ $t('lyric_menu__lrc_size', { size: appSetting['playDetail.style.fontSize'] }) }}</div>
<button :class="[$style.btn, $style.titleBtn]" :disabled="appSetting['playDetail.style.fontSize'] == 100" ignore-tip :aria-label="$t('lyric_menu__size_reset')" @click="fontSizeReset">{{ $t('lyric_menu__size_reset') }}</button>
</div>
<div :class="$style.subGroup">
<button :class="$style.btn" :aria-label="$t('lyric_menu__size_add')" @click="fontSizeUp(5)" @contextmenu="fontSizeUp(1)">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" height="18px" viewBox="0 0 24 24" space="preserve">
<use xlink:href="#icon-font-increase" />
</svg>
</button>
<button :class="$style.btn" :aria-label="$t('lyric_menu__size_dec')" @click="fontSizeDown(5)" @contextmenu="fontSizeDown(1)">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" height="18px" viewBox="0 0 24 24" space="preserve">
<use xlink:href="#icon-font-decrease" />
</svg>
</button>
</div>
</div>
<div :class="$style.group">
<div :class="$style.subGroup">
<div :class="$style.title">{{ $t('lyric_menu__offset', { offset }) }}</div>
<button :class="[$style.btn, $style.titleBtn]" :disabled="offsetDisabled || offset == originOffset" @click="offsetReset">{{ $t('lyric_menu__offset_reset') }}</button>
</div>
<div :class="$style.subGroup">
<button :class="$style.btn" :disabled="offsetDisabled" ignore-tip :aria-label="$t('lyric_menu__offset_add_10')" @click="setOffset(10)">+ 10ms</button>
<button :class="$style.btn" :disabled="offsetDisabled" ignore-tip :aria-label="$t('lyric_menu__offset_dec_10')" @click="setOffset(-10)">- 10ms</button>
</div>
<div :class="$style.subGroup">
<button :class="$style.btn" :disabled="offsetDisabled" ignore-tip :aria-label="$t('lyric_menu__offset_add_100')" @click="setOffset(100)">+ 100ms</button>
<button :class="$style.btn" :disabled="offsetDisabled" ignore-tip :aria-label="$t('lyric_menu__offset_dec_100')" @click="setOffset(-100)">- 100ms</button>
</div>
</div>
</div>
</teleport>
</template>
<script>
import { computed, ref, watch } from '@common/utils/vueTools'
import useMenuLocation from '@renderer/utils/compositions/useMenuLocation'
import { debounce } from '@common/utils/common'
import { saveLyricEdited, removeLyricEdited } from '@renderer/utils/ipc'
import { appSetting, setPlayDetailLyricFont, setPlayDetailLyricAlign } from '@renderer/store/setting'
const offsetTagRxp = /(?:^|\n)\s*\[offset:\s*(\S+(?:\d+)*)\s*\]/
const offsetTagAllRxp = /(^|\n)\s*\[offset:\s*(\S+(?:\d+)*)\s*\]/g
const saveLyric = debounce((musicInfo, lyricInfo) => {
saveLyricEdited(musicInfo, lyricInfo)
})
const removeLyric = debounce(musicInfo => {
removeLyricEdited(musicInfo)
})
const getOffset = lrc => {
let offset = offsetTagRxp.exec(lrc)
if (offset) {
offset = parseInt(offset[1])
if (Number.isNaN(offset)) offset = 0
} else offset = 0
return offset
}
export default {
name: 'LyricMenu',
props: {
modelValue: Boolean,
xy: {
type: Object,
required: true,
},
lyricInfo: {
type: Object,
required: true,
},
},
emits: ['updateLyric', 'update:modelValue'],
setup(props, { emit }) {
// const appSetting = useRefGetter('appSetting')
// const playDetailSetting = useRefGetter('playDetailSetting')
// const setPlayDetailLyricAlign = useCommit('setPlayDetailLyricAlign')
// const setPlayDetailLyricFont = useCommit('setPlayDetailLyricFont')
const offset = ref(0)
const offsetDisabled = ref(true)
const originOffset = ref(0)
const visible = computed(() => props.modelValue)
const location = computed(() => props.xy)
const onHide = () => {
emit('update:modelValue', false)
}
const setFontAlign = val => {
if (appSetting['playDetail.style.align'] == val) return
setPlayDetailLyricAlign(val)
}
const fontSizeUp = step => {
if (appSetting['playDetail.style.fontSize'] >= 200) return
setPlayDetailLyricFont(Math.min(appSetting['playDetail.style.fontSize'] + step, 200))
}
const fontSizeDown = step => {
if (appSetting['playDetail.style.fontSize'] <= 70) return
setPlayDetailLyricFont(Math.max(appSetting['playDetail.style.fontSize'] - step, 70))
}
const fontSizeReset = () => {
setPlayDetailLyricFont(100)
}
const updateLyric = offset => {
let lyric = props.lyricInfo.lyric
let tlyric = props.lyricInfo.tlyric
let rlyric = props.lyricInfo.rlyric
let lxlyric = props.lyricInfo.lxlyric
if (offsetTagRxp.test(lyric)) {
lyric = lyric.replace(offsetTagAllRxp, `$1[offset:${offset}]`)
if (tlyric) tlyric = tlyric.replace(offsetTagAllRxp, `$1[offset:${offset}]`)
if (lxlyric) lxlyric = lxlyric.replace(offsetTagAllRxp, `$1[offset:${offset}]`)
if (rlyric) rlyric = rlyric.replace(offsetTagAllRxp, `$1[offset:${offset}]`)
} else {
lyric = `[offset:${offset}]\n` + lyric
if (tlyric) tlyric = `[offset:${offset}]\n` + tlyric
if (lxlyric) lxlyric = `[offset:${offset}]\n` + lxlyric
if (rlyric) rlyric = `[offset:${offset}]\n` + rlyric
}
const musicInfo = 'progress' in props.lyricInfo.musicInfo ? props.lyricInfo.musicInfo.meta.musicInfo : props.lyricInfo.musicInfo
if (offset == originOffset.value) {
removeLyric(musicInfo)
} else {
saveLyric(musicInfo, {
lyric,
tlyric,
rlyric,
lxlyric,
})
}
emit('updateLyric', {
lyric,
tlyric,
rlyric,
lxlyric,
offset,
})
}
const setOffset = step => {
offset.value += step
updateLyric(offset.value)
}
const offsetReset = () => {
if (offset.value == originOffset.value) return
offset.value = originOffset.value
updateLyric(originOffset.value)
}
const parseLrcOffset = () => {
offset.value = getOffset(props.lyricInfo.lyric)
originOffset.value = getOffset(props.lyricInfo.rawlyric)
offsetDisabled.value = !props.lyricInfo.lyric
}
const { dom_menu, menuStyles } = useMenuLocation({
visible,
location,
onHide,
})
watch(() => props.lyricInfo.musicInfo, () => {
if (!props.modelValue) return
parseLrcOffset()
})
watch(visible, val => {
if (!val) return
parseLrcOffset()
})
return {
appSetting,
dom_menu,
menuStyles,
offset,
originOffset,
fontSizeUp,
fontSizeDown,
fontSizeReset,
setOffset,
offsetReset,
setFontAlign,
offsetDisabled,
}
},
}
</script>
<style lang="less" module>
@import '@renderer/assets/styles/layout.less';
.container {
font-size: 12px;
position: absolute;
opacity: 0;
transform: scale(0);
transform-origin: 0 0 0;
transition: .14s ease;
transition-property: transform, opacity;
border-radius: @radius-border;
background-color: var(--color-content-background);
box-shadow: 0 1px 8px 0 rgba(0,0,0,.2);
z-index: 10;
overflow: hidden;
}
.group {
display: flex;
flex-direction: column;
}
.title {
flex: auto;
padding: 10px 0 10px 10px;
color: var(--color-font-label);
white-space: nowrap;
min-width: 120px;
}
.subGroup {
display: flex;
flex-flow: row nowrap;
}
.btn {
flex: auto;
white-space: nowrap;
cursor: pointer;
min-width: 60px;
height: 34px;
display: flex;
align-items: center;
justify-content: center;
// color: @color-btn;
padding: 0 10px;
outline: none;
transition: @transition-normal;
transition-property: background-color, opacity;
box-sizing: border-box;
.mixin-ellipsis-1;
background-color: var(--color-content-background);
border: none;
&:hover {
background-color: var(--color-primary-background-hover);
}
&:active {
background-color: var(--color-primary-background-active);
}
&.active {
background-color: var(--color-content-background);
color: var(--color-button-font-selected);
cursor: default;
opacity: 1;
}
&[disabled] {
cursor: default;
opacity: .4;
&:hover {
background: none !important;
}
}
}
.titleBtn {
flex: none;
padding: 0 10;
min-width: 40px;
opacity: .7;
&[disabled] {
opacity: .3;
}
}
</style>
@@ -0,0 +1,144 @@
<template lang="pug">
div(:class="$style.container")
ul
li(v-for="(item, index) in comments" :key="item.id" :class="$style.listItem")
div(:class="$style.content")
div(:class="$style.left")
img( :class="$style.avatar" :src="item.avatar || commentDefImg" @error="handleUserImg")
div(:class="$style.right")
div(:class="$style.info")
div.select(:class="$style.name") {{item.userName}}
time(:class="$style.time" v-if="item.timeStr") {{timeFormat(item.timeStr)}}
div(:class="$style.likes" v-if="item.likedCount != null")
svg(:class="$style.likesIcon" version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' viewBox='0 0 512 512' space='preserve')
use(xlink:href='#icon-thumbs-up')
| {{item.likedCount}}
div.select(:class="$style.comment_text")
p(v-for="text in item.text") {{text}}
comment-floor(v-if="item.reply && item.reply.length" :class="$style.reply_floor" :comments="item.reply")
</template>
<script>
import commentDefImg from '@renderer/assets/images/defaultUser.jpg'
export default {
name: 'CommentFloor',
props: {
comments: {
type: Array,
default() {
return []
},
},
},
data() {
return {
commentDefImg,
}
},
methods: {
timeFormat(time) {
return time
// return formatTime(new Date(time), true)
},
handleUserImg(event) {
event.target.src = this.commentDefImg
},
},
}
</script>
<style lang="less" module>
@import '@renderer/assets/styles/layout.less';
@padding: 15px;
// .container {
// }
.listItem {
border-bottom: 1px dashed var(--color-primary-alpha-700);
}
.content {
padding: 12px 0;
font-size: 13px;
color: var(--color-font);
display: flex;
}
.left {
flex: none;
}
.avatar {
width: 40px;
border-radius: 4px;
box-shadow: 0 0 2px rgba(0, 0, 0, .15);
}
.right {
flex: auto;
min-width: 0;
margin-left: 10px;
}
.info {
display: flex;
flex-flow: row nowrap;
align-items: flex-end;
min-width: 0;
line-height: 1.3;
color: var(--color-font-label);
}
.name {
flex: 0 1 auto;
min-width: 0;
.mixin-ellipsis-1;
}
.time {
flex: none;
font-size: 11px;
margin-left: 5px;
}
.likes {
flex: 1 0 auto;
margin-left: 10px;
font-size: 11px;
align-self: flex-end;
text-align: right;
}
.likesIcon {
width: 12px;
height: 12px;
margin-right: 3px;
color: var(--color-primary-font);
}
.comment_text {
text-align: justify;
font-size: 14px;
padding-top: 5px;
p {
line-height: 1.5;
word-break: break-all;
overflow-wrap: break-word;
}
}
.reply_floor {
padding: 0 0 0 @padding;
margin-left: @padding * 2;
border-radius: .5rem;
&:last-child {
margin-bottom: 12px;
}
.listItem:last-child {
border-bottom: none;
}
.right {
margin-right: 10px;
}
background-color: var(--color-primary-light-500-alpha-700);
}
</style>
@@ -0,0 +1,380 @@
<template lang="pug">
div.comment(:class="$style.comment" ref="dom_container")
div(:class="$style.commentHeader")
h3 {{$t('comment__title', { name: title })}}
div(:class="$style.commentHeaderBtns")
div(:class="$style.commentHeaderBtn" @click="handleShowComment" :aria-label="$t('comment__refresh')")
svg(version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' style='transform: rotate(45deg);' viewBox='0 0 24 24' space='preserve')
use(xlink:href='#icon-refresh')
div(:class="$style.commentHeaderBtn" @click="$emit('close')")
svg(version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' viewBox='0 0 24 24' space='preserve')
use(xlink:href='#icon-close')
div(:class="$style.commentMain")
template(v-if="available")
header(:class="$style.tab_header")
button(type="button" @click="handleToggleTab('hot')" :class="[$style.commentType, { [$style.active]: tabActiveId == 'hot' }]") {{$t('comment__hot_title')}} ({{hotComment.total}})
button(type="button" @click="handleToggleTab('new')" :class="[$style.commentType, { [$style.active]: tabActiveId == 'new' }]") {{$t('comment__new_title')}} ({{newComment.total}})
main(:class="$style.tab_main" ref="dom_tabMain")
div(:class="$style.tab_content")
div.scroll(:class="$style.tab_content_scroll" ref="dom_commentHot")
p(:class="$style.commentLabel" style="cursor: pointer;" v-if="hotComment.isLoadError" @click="handleGetHotComment(currentMusicInfo, hotComment.nextPage, hotComment.limit)") {{$t('comment__hot_load_error')}}
p(:class="$style.commentLabel" v-else-if="hotComment.isLoading && !hotComment.list.length") {{$t('comment__hot_loading')}}
comment-floor(v-if="!hotComment.isLoadError && hotComment.list.length" :class="[$style.commentFloor, hotComment.isLoading ? $style.loading : null]" :comments="hotComment.list")
p(:class="$style.commentLabel" v-else-if="!hotComment.isLoadError && !hotComment.isLoading") {{$t('comment__no_content')}}
div(:class="$style.pagination")
material-pagination(:count="hotComment.total" :btnLength="5" :limit="hotComment.limit" :page="hotComment.page" @btn-click="handleToggleHotCommentPage")
div(:class="$style.tab_content")
div.scroll(:class="$style.tab_content_scroll" ref="dom_commentNew")
p(:class="$style.commentLabel" style="cursor: pointer;" v-if="newComment.isLoadError" @click="handleGetNewComment(currentMusicInfo, newComment.nextPage, newComment.limit)") {{$t('comment__new_load_error')}}
p(:class="$style.commentLabel" v-else-if="newComment.isLoading && !newComment.list.length") {{$t('comment__new_loading')}}
comment-floor(v-if="!newComment.isLoadError && newComment.list.length" :class="[$style.commentFloor, newComment.isLoading ? $style.loading : null]" :comments="newComment.list")
p(:class="$style.commentLabel" v-else-if="!newComment.isLoadError && !newComment.isLoading") {{$t('comment__no_content')}}
div(:class="$style.pagination")
material-pagination(:count="newComment.total" :btnLength="5" :limit="newComment.limit" :page="newComment.page" @btn-click="handleToggleCommentPage")
div(v-else :class="$style.unavailable")
p {{$t('comment__unavailable')}}
</template>
<script>
import { appSetting } from '@renderer/store/setting'
import { toOldMusicInfo } from '@renderer/utils'
import music from '@renderer/utils/musicSdk'
import CommentFloor from './CommentFloor'
export default {
name: 'MusicComment',
components: {
CommentFloor,
},
props: {
show: Boolean,
musicInfo: {
type: Object,
required: true,
},
},
emits: ['close'],
setup() {
return {
appSetting,
}
},
data() {
return {
available: false,
currentMusicInfo: {
name: '',
singer: '',
},
tabActiveId: 'hot',
newComment: {
isLoading: false,
isLoadError: false,
page: 1,
total: 0,
maxPage: 1,
nextPage: 1,
limit: 20,
list: [
// {
// text: ['123123hhh'],
// userName: 'dsads',
// avatar: 'http://img4.kuwo.cn/star/userhead/39/52/1602393411654_512039239s.jpg',
// time: '2020-10-22 22:14:17',
// timeStr: '2020-10-22 22:14:17',
// likedCount: 100,
// reply: [],
// },
],
},
hotComment: {
isLoading: true,
isLoadError: true,
page: 1,
total: 0,
maxPage: 1,
nextPage: 1,
limit: 20,
list: [
// {
// text: ['123123hhh'],
// userName: 'dsads',
// avatar: 'http://img4.kuwo.cn/star/userhead/39/52/1602393411654_512039239s.jpg',
// time: '2020-10-22 22:14:17',
// timeStr: '2020-10-22 22:14:17',
// likedCount: 100,
// reply: [
// {
// text: ['123123hhh'],
// userName: 'dsads',
// avatar: 'http://img4.kuwo.cn/star/userhead/39/52/1602393411654_512039239s.jpg',
// time: '2020-10-22 22:14:17',
// timeStr: '2020-10-22 22:14:17',
// likedCount: 100,
// },
// ],
// },
],
},
}
},
computed: {
title() {
return this.currentMusicInfo.name
? this.appSetting['download.fileName'].replace('歌名', this.currentMusicInfo.name).replace('歌手', this.currentMusicInfo.singer)
: '^-^'
},
},
watch: {
show(n) {
if (n) this.handleShowComment()
},
},
mounted() {
this.setWidth()
window.addEventListener('resize', this.setWidth)
},
beforeUnmount() {
window.removeEventListener('resize', this.setWidth)
},
methods: {
setWidth() {
setTimeout(() => {
this.$refs.dom_container.style.width = Math.floor(this.$refs.dom_container.parentNode.clientWidth * 0.5) + 'px'
setTimeout(() => {
this.handleToggleTab(this.tabActiveId, true)
})
})
},
async getComment(musicInfo, page, limit, retryNum = 0) {
let resp
try {
resp = await music[musicInfo.source].comment.getComment(musicInfo, page, limit)
} catch (error) {
if (error.message == '取消请求' || ++retryNum > 2) throw error
resp = await this.getComment(musicInfo, page, limit, retryNum)
}
return resp
},
async getHotComment(musicInfo, page, limit, retryNum = 0) {
let resp
try {
resp = await music[musicInfo.source].comment.getHotComment(musicInfo, page, limit)
} catch (error) {
if (error.message == '取消请求' || ++retryNum > 2) throw error
resp = await this.getHotComment(musicInfo, page, limit, retryNum)
}
return resp
},
handleGetNewComment(musicInfo, page, limit) {
this.newComment.isLoadError = false
this.newComment.isLoading = true
this.getComment(toOldMusicInfo(musicInfo), page, limit).then(comment => {
this.newComment.isLoading = false
this.newComment.total = comment.total
this.newComment.maxPage = comment.maxPage
this.newComment.page = page
this.newComment.list = comment.comments
this.$nextTick(() => {
this.$refs.dom_commentNew.scrollTo(0, 0)
})
}).catch(err => {
console.log(err)
if (err.message == '取消请求') return
this.newComment.isLoadError = true
this.newComment.isLoading = false
})
},
handleGetHotComment(musicInfo, page, limit) {
this.hotComment.isLoadError = false
this.hotComment.isLoading = true
this.getHotComment(toOldMusicInfo(musicInfo), page, limit).then(hotComment => {
this.hotComment.isLoading = false
this.hotComment.total = hotComment.total
this.hotComment.maxPage = hotComment.maxPage
this.hotComment.page = page
this.hotComment.list = hotComment.comments
this.$nextTick(() => {
this.$refs.dom_commentHot.scrollTo(0, 0)
})
}).catch(err => {
console.log(err)
if (err.message == '取消请求') return
this.hotComment.isLoadError = true
this.hotComment.isLoading = false
})
},
handleShowComment() {
this.currentMusicInfo = 'progress' in this.musicInfo ? this.musicInfo.metadata.musicInfo : this.musicInfo
if (this.currentMusicInfo.source == 'local' || !music[this.currentMusicInfo.source].comment) {
this.available = false
return
}
this.available = true
// if (this.musicInfo.songmid != this.currentMusicInfo.songmid) {
this.hotComment.page = 1
this.hotComment.total = 0
this.hotComment.maxPage = 1
this.hotComment.nextPage = 1
this.newComment.page = 1
this.newComment.total = 0
this.newComment.maxPage = 1
this.newComment.nextPage = 1
// }
this.isShowComment = true
this.handleGetHotComment(this.currentMusicInfo, this.hotComment.page, this.hotComment.limit)
this.handleGetNewComment(this.currentMusicInfo, this.newComment.page, this.newComment.limit)
},
handleToggleHotCommentPage(page) {
this.hotComment.nextPage = page
this.handleGetHotComment(this.currentMusicInfo, page, this.hotComment.limit)
},
handleToggleCommentPage(page) {
this.newComment.nextPage = page
this.handleGetNewComment(this.currentMusicInfo, page, this.newComment.limit)
},
handleToggleTab(id, force) {
if (!this.available || (!force && this.tabActiveId == id)) return
switch (id) {
case 'hot':
this.$refs.dom_tabMain.scrollLeft = 0
break
case 'new':
this.$refs.dom_tabMain.scrollLeft = this.$refs.dom_tabMain.clientWidth
break
}
this.tabActiveId = id
},
},
}
</script>
<style lang="less" module>
@import '@renderer/assets/styles/layout.less';
.comment {
display: flex;
flex-flow: column nowrap;
transition: @transition-normal;
transition-property: transform,opacity;
transform-origin: 100%;
overflow: hidden;
}
.commentHeader {
flex: none;
padding-bottom: 5px;
display: flex;
flex-flow: row nowrap;
align-items: center;
// border-bottom: 1px solid #eee;
h3 {
font-size: 14px;
.mixin-ellipsis-1;
line-height: 1.2;
}
}
.commentHeaderBtns {
flex: 1 0 auto;
display: flex;
flex-flow: row nowrap;
justify-content: flex-end;
color: var(--color-primary);
}
.commentHeaderBtn {
height: 22px;
width: 22px;
cursor: pointer;
transition: opacity @transition-normal;
+.commentHeaderBtn {
margin-left: 5px;
}
&:hover {
opacity: .7;
}
}
.commentMain {
flex: auto;
background-color: var(--color-primary-light-400-alpha-700);
border-radius: 4px;
display: flex;
flex-direction: column;
}
.tab_header {
display: flex;
flex-flow: row nowrap;
gap: 15px;
padding-left: 15px;
padding-right: 10px;
}
.tab_main {
flex: auto;
display: flex;
flex-flow: row nowrap;
overflow: hidden;
scroll-snap-type: x mandatory;
scroll-behavior: smooth;
}
.tab_content {
flex-shrink: 0;
width: 100%;
position: relative;
}
.tab_content_scroll {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
padding-left: 15px;
padding-right: 10px;
scroll-behavior: smooth;
}
.commentLabel {
padding: 15px;
color: var(--color-font-label);
font-size: 14px;
}
.commentType {
padding: 5px;
margin: 5px 0;
font-size: 13px;
background: none;
border: none;
cursor: pointer;
transition: @transition-normal;
transition-property: opacity, color;
&:hover {
opacity: .7;
}
&.active {
color: var(--color-primary);
}
}
.commentFloor {
opacity: 1;
transition: opacity @transition-normal;
&.loading {
opacity: .4;
}
}
.pagination {
padding: 10px 0;
}
.unavailable {
flex: auto;
padding-top: 10%;
text-align: center;
font-size: 14px;
color: var(--color-font-label);
}
</style>
@@ -0,0 +1,420 @@
<template lang="pug">
transition(enter-active-class="animated lightSpeedIn" leave-active-class="animated slideOutDown" @after-enter="handleAfterEnter" @after-leave="handleAfterLeave")
div(:class="[$style.container, { [$style.fullscreen]: isFullscreen }]" @contextmenu="handleContextMenu" v-if="isShowPlayerDetail")
div(:class="$style.bg")
//- div(:class="$style.bg" :style="bgStyle")
//- div(:class="$style.bg2")
div(:class="[$style.header, $style.controlBtnLeft]" v-if="appSetting['common.controlBtnPosition'] == 'left'")
div(:class="$style.controBtn")
button(type="button" :class="$style.hide" :aria-label="$t('player__hide_detail_tip')" @click="hide")
svg(:class="$style.controBtnIcon" version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' width='80%' viewBox='0 0 30.727 30.727' space='preserve')
use(xlink:href='#icon-window-hide')
button(type="button" :class="$style.fullscreenExit" :aria-label="$t('fullscreen_exit')" @click="fullscreenExit")
svg(:class="$style.controBtnIcon" version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' width='100%')
use(xlink:href='#icon-fullscreen-exit')
button(type="button" :class="$style.min" :aria-label="$t('min')" @click="min")
svg(:class="$style.controBtnIcon" version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' width='100%' viewBox='0 0 24 24' space='preserve')
use(xlink:href='#icon-window-minimize')
//- button(type="button" :class="$style.max" @click="max")
button(type="button" :class="$style.close" :aria-label="$t('close')" @click="close")
svg(:class="$style.controBtnIcon" version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' width='100%' viewBox='0 0 24 24' space='preserve')
use(xlink:href='#icon-window-close')
div(:class="[$style.header, $style.controlBtnRight]" v-else)
div(:class="$style.controBtn")
button(type="button" :class="$style.hide" :aria-label="$t('player__hide_detail_tip')" @click="hide")
svg(:class="$style.controBtnIcon" version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' height='35%' viewBox='0 0 30.727 30.727' space='preserve')
use(xlink:href='#icon-window-hide')
button(type="button" :class="$style.fullscreenExit" :aria-label="$t('fullscreen_exit')" @click="fullscreenExit")
svg(:class="$style.controBtnIcon" version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' height='60%')
use(xlink:href='#icon-fullscreen-exit')
button(type="button" :class="$style.min" :aria-label="$t('min')" @click="min")
svg(:class="$style.controBtnIcon" version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' height='60%' viewBox='0 0 24 24' space='preserve')
use(xlink:href='#icon-window-minimize-2')
//- button(type="button" :class="$style.max" @click="max")
button(type="button" :class="$style.close" :aria-label="$t('close')" @click="close")
svg(:class="$style.controBtnIcon" version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' height='60%' viewBox='0 0 24 24' space='preserve')
use(xlink:href='#icon-window-close-2')
div(:class="[$style.main, {[$style.showComment]: isShowPlayComment}]")
div.left(:class="$style.left")
//- div(:class="$style.info")
div(:class="$style.info")
img(:class="$style.img" :src="musicInfo.pic" v-if="musicInfo.pic")
div.description(:class="$style.description")
p {{$t('player__music_name')}}{{musicInfo.name}}
p {{$t('player__music_singer')}}{{musicInfo.singer}}
p(v-if="musicInfo.album") {{$t('player__music_album')}}{{musicInfo.album}}
transition(enter-active-class="animated fadeIn" leave-active-class="animated fadeOut")
LyricPlayer(v-if="visibled")
music-comment(:class="$style.comment" :show="isShowPlayComment" :musicInfo="playMusicInfo.musicInfo" @close="hideComment" v-if="visibled")
transition(enter-active-class="animated fadeIn" leave-active-class="animated fadeOut")
play-bar(v-if="visibled")
transition(enter-active-class="animated-slow fadeIn" leave-active-class="animated-slow fadeOut")
common-audio-visualizer(v-if="appSetting['player.audioVisualization'] && visibled")
</template>
<script>
import { ref, watch } from '@common/utils/vueTools'
import { isFullscreen } from '@renderer/store'
import {
isShowPlayerDetail,
isShowPlayComment,
musicInfo,
playMusicInfo,
} from '@renderer/store/player/state'
import {
setShowPlayerDetail,
setShowPlayComment,
setShowPlayLrcSelectContentLrc,
} from '@renderer/store/player/action'
import LyricPlayer from './LyricPlayer'
import PlayBar from './PlayBar'
import MusicComment from './components/MusicComment'
import { registerAutoHideMounse, unregisterAutoHideMounse } from './autoHideMounse'
import { appSetting } from '@renderer/store/setting'
import { closeWindow, maxWindow, minWindow, setFullScreen } from '@renderer/utils/ipc'
export default {
name: 'CorePlayDetail',
components: {
LyricPlayer,
PlayBar,
MusicComment,
},
setup() {
const visibled = ref(false)
let clickTime = 0
const hide = () => {
setShowPlayerDetail(false)
}
const handleContextMenu = () => {
if (window.performance.now() - clickTime > 400) {
clickTime = window.performance.now()
return
}
clickTime = 0
hide()
}
const hideComment = () => {
setShowPlayComment(false)
}
const handleAfterEnter = () => {
if (isFullscreen.value) registerAutoHideMounse()
visibled.value = true
}
const handleAfterLeave = () => {
setShowPlayLrcSelectContentLrc(false)
hideComment(false)
visibled.value = false
unregisterAutoHideMounse()
}
watch(isFullscreen, isFullscreen => {
(isFullscreen ? registerAutoHideMounse : unregisterAutoHideMounse)()
})
return {
appSetting,
playMusicInfo,
isShowPlayerDetail,
isShowPlayComment,
musicInfo,
hide,
handleContextMenu,
hideComment,
handleAfterEnter,
handleAfterLeave,
visibled,
isFullscreen,
fullscreenExit() {
setFullScreen(false).then((fullscreen) => {
isFullscreen.value = fullscreen
})
},
min() {
minWindow()
},
max() {
maxWindow()
},
close() {
closeWindow()
},
}
},
}
</script>
<style lang="less" module>
@import '@renderer/assets/styles/layout.less';
@control-btn-width: @height-toolbar * .26;
.container {
position: absolute;
display: flex;
flex-flow: column nowrap;
width: 100%;
height: 100%;
top: 0;
left: 0;
background-color: var(--color-content-background);
z-index: 10;
// -webkit-app-region: drag;
overflow: hidden;
border-radius: @radius-border;
color: var(--color-font);
border-left: 12px solid var(--color-primary-light-200-alpha-500);
-webkit-app-region: no-drag;
contain: strict;
box-sizing: border-box;
* {
box-sizing: border-box;
}
&.fullscreen {
.header {
-webkit-app-region: no-drag;
align-self: flex-start;
.controBtn {
.close, .min {
display: none;
}
.fullscreenExit {
display: flex;
}
}
}
}
}
.bg {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
background: var(--background-image) var(--background-image-position) no-repeat;
background-size: var(--background-image-size);
// background-size: 110% 110%;
// filter: blur(60px);
opacity: .7;
z-index: -1;
&:before {
content: '';
display: block;
width: 100%;
height: 100%;
background-color: var(--color-app-background);
}
&:after {
position: absolute;
left: 0;
top: 0;
content: '';
display: block;
width: 100%;
height: 100%;
background-color: var(--color-main-background);
}
}
// .bg2 {
// position: absolute;
// width: 100%;
// height: 100%;
// top: 0;
// left: 0;
// z-index: -1;
// background-color: rgba(255, 255, 255, .8);
// }
.header {
position: relative;
flex: 0 0 @height-toolbar;
-webkit-app-region: drag;
width: 100%;
.controBtn {
position: absolute;
top: 0;
display: flex;
-webkit-app-region: no-drag;
button {
display: flex;
position: relative;
background: none;
border: none;
outline: none;
padding: 1px;
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
}
.fullscreenExit {
display: none;
}
}
&.controlBtnLeft {
.controBtn {
align-items: center;
padding: 0 @control-btn-width;
left: 0;
flex-direction: row-reverse;
height: @height-toolbar * .7;
transition: opacity @transition-normal;
opacity: .5;
&:hover {
opacity: .8;
.controBtnIcon {
opacity: 1;
}
}
button {
width: @control-btn-width;
height: @control-btn-width;
border-radius: 50%;
color: var(--color-font);
+ button {
margin-right: (@control-btn-width / 2);
}
&.hide {
background-color: var(--color-btn-hide);
}
&.min, &.fullscreenExit {
background-color: var(--color-btn-min);
}
// &.max {
// background-color: var(--color-btn-max);
// }
&.close {
background-color: var(--color-btn-close);
}
}
}
.controBtnIcon {
opacity: 0;
transition: opacity 0.2s ease-in-out;
}
}
&.controlBtnRight {
align-self: flex-start;
.controBtn {
right: 0;
button {
width: 46px;
height: 30px;
color: var(--color-font-label);
transition: background-color 0.2s ease-in-out;
&:hover {
background-color: var(--color-button-background-hover);
&.close {
background-color: var(--color-btn-close);
}
}
}
}
}
}
.main {
flex: auto;
min-height: 0;
overflow: hidden;
display: flex;
margin: 0 30px;
position: relative;
&.showComment {
:global {
.left {
flex-basis: 18%;
.description p {
font-size: 12px;
}
}
.right {
flex-basis: 30%;
.lyricSelectContent {
font-size: 14px;
}
}
.comment {
opacity: 1;
transform: scaleX(1);
}
}
}
}
.left {
flex: 0 0 40%;
display: flex;
flex-flow: column nowrap;
align-items: center;
padding: 13px;
overflow: hidden;
transition: flex-basis @transition-normal;
}
.info {
display: flex;
flex-flow: column nowrap;
justify-content: flex-start;
max-width: 300px;
}
.img {
max-width: 100%;
max-height: 100%;
min-width: 100%;
box-shadow: 0 0 6px var(--color-primary-alpha-500);
border-radius: 6px;
opacity: .8;
}
.description {
max-width: 300px;
padding: 15px 0;
overflow: hidden;
p {
line-height: 1.5;
font-size: 14px;
overflow-wrap: break-word;
}
}
.comment {
position: absolute;
right: 0;
top: 0;
width: 50%;
height: 100%;
opacity: 1;
margin-left: 10px;
transform: scaleX(0);
}
</style>
@@ -0,0 +1,152 @@
<template>
<material-modal :show="sync.isShowSyncMode" :bg-close="false" :close-btn="false" @close="handleClose(false)">
<main :class="$style.main">
<h2>{{ $t('sync__title', { name: sync.deviceName }) }}</h2>
<div class="scroll" :class="$style.content">
<dl :class="$style.btnGroup">
<dt :class="$style.label">{{ $t('sync__merge_label') }}</dt>
<dd :class="$style.btns">
<base-btn :class="$style.btn" @click="handleSelectMode('merge_local_remote')">{{ $t('sync__merge_btn_local_remote') }}</base-btn>
<base-btn :class="$style.btn" @click="handleSelectMode('merge_remote_local')">{{ $t('sync__merge_btn_remote_local') }}</base-btn>
</dd>
</dl>
<dl :class="$style.btnGroup">
<dt :class="$style.label">{{ $t('sync__overwrite_label') }}</dt>
<dd :class="$style.btns">
<base-btn :class="$style.btn" @click="handleSelectMode('overwrite_local_remote')">{{ $t('sync__overwrite_btn_local_remote') }}</base-btn>
<base-btn :class="$style.btn" @click="handleSelectMode('overwrite_remote_local')">{{ $t('sync__overwrite_btn_remote_local') }}</base-btn>
</dd>
<dd style="font-size: 14px; margin-top: 5px;">
<base-checkbox id="sync_mode_modal_isOverwrite" v-model="isOverwrite" :label="$t('sync__overwrite')" />
</dd>
</dl>
<dl :class="$style.btnGroup">
<dt :class="$style.label">{{ $t('sync__other_label') }}</dt>
<dd :class="$style.btns">
<base-btn :class="$style.btn" @click="handleSelectMode('none')">{{ $t('sync__overwrite_btn_none') }}</base-btn>
<base-btn :class="$style.btn" @click="handleSelectMode('cancel')">{{ $t('sync__overwrite_btn_cancel') }}</base-btn>
</dd>
</dl>
<dl :class="$style.btnGroup">
<dd>
<section :class="$style.tipGroup">
<h3 :class="$style.title">{{ $t('sync__merge_tip') }}</h3>
<p :class="$style.tip">{{ $t('sync__merge_tip_desc') }}</p>
</section>
<section :class="$style.tipGroup">
<h3 :class="$style.title">{{ $t('sync__overwrite_tip') }}</h3>
<p :class="$style.tip">{{ $t('sync__overwrite_tip_desc') }}</p>
</section>
<section :class="$style.tipGroup">
<h3 :class="$style.title">{{ $t('sync__other_tip') }}</h3>
<p :class="$style.tip">{{ $t('sync__other_tip_desc') }}</p>
</section>
</dd>
</dl>
</div>
</main>
</material-modal>
</template>
<script>
import { ref } from '@common/utils/vueTools'
import { sync } from '@renderer/store'
import { sendSyncAction } from '@renderer/utils/ipc'
export default {
setup() {
const isOverwrite = ref(false)
const handleClose = () => {
sync.isShowSyncMode = false
}
const handleSelectMode = (mode) => {
if (mode.startsWith('overwrite') && this.isOverwrite) mode += '_full'
sendSyncAction({ action: 'select_mode', data: mode })
handleClose()
}
return {
sync,
isOverwrite,
handleClose,
handleSelectMode,
}
},
}
</script>
<style lang="less" module>
@import '@renderer/assets/styles/layout.less';
.main {
padding: 15px;
max-width: 700px;
min-width: 200px;
min-height: 0;
display: flex;
flex-flow: column nowrap;
justify-content: center;
h2 {
font-size: 16px;
color: var(--color-font);
line-height: 1.3;
text-align: center;
}
}
.content {
flex: auto;
padding: 15px 0 5px;
padding-right: 5px;
.btnGroup + .btnGroup {
margin-top: 10px;
}
.label {
color: var(--color-font-label);
font-size: 14px;
line-height: 2;
}
.desc {
line-height: 1.5;
font-size: 14px;
text-align: justify;
}
.tipGroup {
display: flex;
flex-direction: row;
font-size: 12px;
+ .tipGroup {
margin-top: 5px;
}
.title {
white-space: nowrap;
font-weight: bold;
margin-right: 5px;
}
.tip {
line-height: 1.3;
}
}
}
.btns {
display: flex;
align-items: center;
}
.btn {
display: block;
white-space: nowrap;
+.btn {
margin-left: 15px;
}
&:last-child {
margin-bottom: 0;
}
}
</style>
@@ -0,0 +1,57 @@
<template>
<div v-show="!isFullscreen" :class="$style.control">
<button type="button" :class="[$style.btn, $style.min]" :aria-label="$t('min')" @click="minWindow">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" height="60%" viewBox="0 0 24 24" space="preserve">
<use xlink:href="#icon-window-minimize-2" />
</svg>
</button>
<button type="button" :class="[$style.btn, $style.close]" :aria-label="$t('close')" @click="closeWindow">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" height="60%" viewBox="0 0 24 24" space="preserve">
<use xlink:href="#icon-window-close-2" />
</svg>
</button>
</div>
</template>
<script setup>
import { minWindow, closeWindow } from '@renderer/utils/ipc'
// import { getRandom } from '../../utils'
import { isFullscreen } from '@renderer/store'
</script>
<style lang="less" module>
@import '@renderer/assets/styles/layout.less';
.control {
display: flex;
align-self: flex-start;
-webkit-app-region: no-drag;
height: 30px;
.btn {
display: flex;
align-items: center;
justify-content: center;
position: relative;
width: 46px;
height: 30px;
background: none;
border: none;
outline: none;
padding: 1px;
cursor: pointer;
color: var(--color-font-label);
transition: background-color 0.2s ease-in-out;
&:hover {
&.min, &.max {
background-color: var(--color-button-background-hover);
}
&.close {
background-color: var(--color-btn-close);
}
}
}
}
</style>
@@ -0,0 +1,113 @@
<template>
<material-search-input v-model="searchText" :list="tipList" :visible-list="visibleList" @event="handleEvent" />
</template>
<script>
import music from '@renderer/utils/musicSdk'
import { debounce } from '@common/utils'
import {
ref,
watch,
nextTick,
} from '@common/utils/vueTools'
import { useRouter, useRoute } from '@common/utils/vueRouter'
import { appSetting } from '@renderer/store/setting'
import { searchText as _searchText } from '@renderer/store/search/state'
import { setSearchText } from '@renderer/store/search/action'
import { getSearchSetting } from '@renderer/utils/data'
export default {
setup() {
const searchText = ref('')
const visibleList = ref(false)
const tipList = ref([])
let isFocused = false
let prevTempSearchSource = ''
const route = useRoute()
const router = useRouter()
watch(() => route.name, (newValue, oldValue) => {
if (newValue.name != 'search') {
setTimeout(() => {
if (appSetting['odc.isAutoClearSearchInput'] && searchText.value) searchText.value = ''
if (appSetting['odc.isAutoClearSearchList']) setSearchText('')
})
}
})
watch(_searchText, (newValue, oldValue) => {
searchText.value = newValue
if (newValue !== searchText.value) searchText.value = newValue
})
watch(searchText, () => {
handleTipSearch()
})
const tipSearch = debounce(async() => {
if (searchText.value === '' && prevTempSearchSource) {
tipList.value = []
music[prevTempSearchSource].tempSearch.cancelTempSearch()
return
}
const { temp_source } = await getSearchSetting()
prevTempSearchSource = temp_source
music[prevTempSearchSource].tempSearch.search(searchText.value).then(list => {
tipList.value = list
}).catch(() => {})
}, 50)
const handleTipSearch = () => {
if (!visibleList.value && isFocused) visibleList.value = true
tipSearch()
}
const handleSearch = () => {
if (visibleList.value) visibleList.value = false
if (!searchText.value && route.path != '/search') {
setSearchText('')
return
}
setTimeout(() => {
router.push({
path: '/search',
query: {
text: searchText.value,
},
}).catch(_ => _)
}, searchText.value ? 200 : 0)
}
const handleEvent = ({ action, data }) => {
switch (action) {
case 'focus':
isFocused = true
if (!visibleList.value) visibleList.value = true
if (searchText.value) handleTipSearch()
break
case 'blur':
isFocused = false
setTimeout(() => {
if (visibleList.value) visibleList.value = false
}, 50)
break
case 'submit':
handleSearch()
break
case 'listClick':
searchText.value = tipList.value[data]
nextTick(handleSearch)
}
}
return {
searchText,
visibleList,
tipList,
handleEvent,
}
},
}
</script>
@@ -0,0 +1,59 @@
<template>
<div :class="[$style.toolbar, { [$style.fullscreen]: isFullscreen }, appSetting['common.controlBtnPosition'] == 'left' ? $style.controlBtnLeft : $style.controlBtnRight]">
<SearchInput />
<div v-if="appSetting['common.controlBtnPosition'] == 'left'" :class="$style.logo">L X</div>
<ControlBtns v-else />
</div>
</template>
<script setup>
import { isFullscreen } from '@renderer/store'
import { appSetting } from '@renderer/store/setting'
import ControlBtns from './ControlBtns'
import SearchInput from './SearchInput'
</script>
<style lang="less" module>
@import '@renderer/assets/styles/layout.less';
.toolbar {
display: flex;
height: @height-toolbar;
align-items: center;
justify-content: space-between;
padding-left: 15px;
-webkit-app-region: drag;
z-index: 2;
&.fullscreen {
-webkit-app-region: no-drag;
.logo {
display: none;
}
}
&.controlBtnLeft {
.control {
display: none;
}
}
&.controlBtnRight {
justify-content: space-between;
}
}
.logo {
box-sizing: border-box;
padding: 0 @height-toolbar * .4;
height: @height-toolbar;
color: var(--color-primary);
flex: none;
text-align: center;
line-height: @height-toolbar;
font-weight: bold;
// -webkit-app-region: no-drag;
}
</style>
@@ -0,0 +1,301 @@
<template lang="pug">
material-modal(:show="versionInfo.showModal" @close="handleClose")
main(:class="$style.main" v-if="versionInfo.isDownloaded")
h2 🚀程序更新🚀
div.scroll.select(:class="$style.info")
div(:class="$style.current")
h3 最新版本:{{versionInfo.newVersion?.version}}
h3 当前版本:{{versionInfo.version}}
h3 版本变化:
pre(:class="$style.desc" v-text="versionInfo.newVersion?.desc")
div(:class="[$style.history, $style.desc]" v-if="history.length")
h3 历史版本:
div(:class="$style.item" v-for="ver in history")
h4 v{{ver.version}}
pre(v-text="ver.desc")
div(:class="$style.footer")
div(:class="$style.desc")
p 新版本已下载完毕,
p
| 你可以选择
strong 立即重启更新
| 或稍后
strong 关闭程序时
| 自动更新~
base-btn(:class="$style.btn" @click.onec="handleRestartClick") 立即重启更新
main(:class="$style.main" v-else-if="versionInfo.isError && !versionInfo.isUnknow && versionInfo.newVersion?.version != versionInfo.version")
h2 ❌ 版本更新出错 ❌
div.scroll.select(:class="$style.info")
div(:class="$style.current")
h3 最新版本:{{versionInfo.newVersion?.version}}
h3 当前版本:{{versionInfo.version}}
h3 版本变化:
pre(:class="$style.desc" v-text="versionInfo.newVersion?.desc")
div(:class="[$style.history, $style.desc]" v-if="history.length")
h3 历史版本:
div(:class="$style.item" v-for="ver in history")
h4 v{{ver.version}}
pre(v-text="ver.desc")
div(:class="$style.footer")
div(:class="$style.desc")
p 发现有新版本啦,但是自动更新功能出问题了,
p
| 你可以去&nbsp;
strong.hover.underline(@click="handleOpenUrl('https://github.com/lyswhut/lx-music-desktop/releases')" aria-label="点击打开") 软件发布页
| &nbsp;或&nbsp;
strong.hover.underline(@click="handleOpenUrl('https://www.lanzoui.com/b0bf2cfa/')" aria-label="点击打开") 网盘
| (密码:
strong.hover(@click="handleCopy('glqw')" aria-label="点击复制") glqw
| )&nbsp;下载新版本,
p
| 国内Windows/MAC用户推荐到
strong 网盘
| 下载。
base-btn(:class="$style.btn" @click.onec="handleIgnoreClick") {{ isIgnored ? '恢复当前版本的更新失败提醒' : '忽略当前版本的更新失败提醒'}}
main(:class="$style.main" v-else-if="versionInfo.isDownloading && versionInfo.isTimeOut && !versionInfo.isUnknow")
h2 ❗️ 新版本下载超时 ❗️
div(:class="$style.desc")
p 你当前所在网络访问GitHub较慢,导致新版本下载超时(已经下了半个钟了😳),你仍可选择继续等,但墙裂建议手动更新版本!
p
| 你可以去
base-btn(min @click="handleOpenUrl('https://github.com/lyswhut/lx-music-desktop/releases')" aria-label="点击打开") 软件发布页
| 或
base-btn(min @click="handleOpenUrl('https://www.lanzoui.com/b0bf2cfa/')" aria-label="点击打开") 网盘
| (密码:
strong.hover(@click="handleCopy('glqw')" aria-label="点击复制") glqw
| )下载新版本,
p
| 国内Windows/MAC用户推荐到
strong 网盘
| 下载。
p 当前下载进度:{{progress}}
main(:class="$style.main" v-else-if="versionInfo.isUnknow")
h2 ❓ 获取最新版本信息失败 ❓
div.scroll.select(:class="$style.info")
div(:class="$style.current")
h3 当前版本:{{versionInfo.version}}
div(:class="$style.desc")
p 更新信息获取失败,可能是无法访问Github导致的,请手动检查更新!
p
| 检查方法:打开
base-btn(min @click="handleOpenUrl('https://github.com/lyswhut/lx-music-desktop/releases')" aria-label="点击打开") 软件发布页
| 或
base-btn(min @click="handleOpenUrl('https://www.lanzoui.com/b0bf2cfa/')" aria-label="点击打开") 网盘
| (密码:
strong.hover(@click="handleCopy('glqw')" aria-label="点击复制") glqw
| )查看它们的
strong 版本号
| 与当前版本({{versionInfo.version}})对比是否一样,
p 若一样则不必理会该弹窗,直接关闭即可,否则请手动下载新版本更新。
main(:class="$style.main" v-else)
h2 🌟发现新版本🌟
div.scroll.select(:class="$style.info")
div(:class="$style.current")
h3 最新版本:{{versionInfo.newVersion?.version}}
h3 当前版本:{{versionInfo.version}}
h3 版本变化:
pre(:class="$style.desc" v-text="versionInfo.newVersion?.desc")
div(:class="[$style.history, $style.desc]" v-if="history.length")
h3 历史版本:
div(:class="$style.item" v-for="ver in history")
h4 v{{ver.version}}
pre(v-text="ver.desc")
div(:class="$style.footer")
div(:class="$style.desc")
p 发现有新版本啦,正在努力更新中,若下载太慢可以手动更新哦~
p
| 你也可以关闭本弹窗继续使用软件,还可在
strong 设置-软件更新
| 重新打开本弹窗。
p
| 手动更新可以去&nbsp;
strong.hover.underline(@click="handleOpenUrl('https://github.com/lyswhut/lx-music-desktop/releases')" aria-label="点击打开") 软件发布页
| &nbsp;或&nbsp;
strong.hover.underline(@click="handleOpenUrl('https://www.lanzoui.com/b0bf2cfa/')" aria-label="点击打开") 网盘
| (密码:
strong.hover(@click="handleCopy('glqw')" aria-label="点击复制") glqw
| )&nbsp;下载,
p 国内Windows/MAC用户推荐到网盘下载。
p 当前下载进度:{{progress}}
</template>
<script>
import { compareVer, sizeFormate } from '@common/utils'
import { openUrl, clipboardWriteText } from '@common/utils/electron'
import { versionInfo } from '@renderer/store'
import { getIgnoreVersion, saveIgnoreVersion, quitUpdate } from '@renderer/utils/ipc'
export default {
setup() {
return {
versionInfo,
}
},
data() {
return {
ignoreVersion: null,
}
},
computed: {
history() {
if (!this.versionInfo.newVersion || !this.versionInfo.newVersion?.history) return []
let arr = []
let currentVer = this.versionInfo.version
this.versionInfo.newVersion?.history.forEach(ver => {
if (compareVer(currentVer, ver.version) < 0) arr.push(ver)
})
return arr
},
progress() {
return this.versionInfo.downloadProgress
? `${this.versionInfo.downloadProgress.percent.toFixed(2)}% - ${sizeFormate(this.versionInfo.downloadProgress.transferred)}/${sizeFormate(this.versionInfo.downloadProgress.total)} - ${sizeFormate(this.versionInfo.downloadProgress.bytesPerSecond)}/s`
: '处理更新中...'
},
isIgnored() {
return this.ignoreVersion == this.versionInfo.newVersion?.version
},
},
created() {
this.ignoreVersion = getIgnoreVersion()
},
methods: {
handleClose() {
versionInfo.showModal = false
},
handleOpenUrl(url) {
openUrl(url)
},
handleRestartClick(event) {
this.handleClose()
event.target.disabled = true
quitUpdate()
},
handleCopy(text) {
clipboardWriteText(text)
},
handleIgnoreClick() {
saveIgnoreVersion(this.isIgnored ? null : this.versionInfo.newVersion?.version)
this.handleClose()
},
},
}
</script>
<style lang="less" module>
@import '@renderer/assets/styles/layout.less';
.main {
position: relative;
padding: 15px;
max-width: 450px;
min-width: 300px;
display: flex;
flex-flow: column nowrap;
justify-content: center;
overflow: hidden;
// overflow-y: auto;
* {
box-sizing: border-box;
}
h2 {
flex: 0 0 none;
font-size: 16px;
color: var(--color-font);
line-height: 1.3;
text-align: center;
margin-bottom: 15px;
}
h3 {
font-size: 14px;
line-height: 1.3;
}
pre {
white-space: pre-wrap;
text-align: justify;
margin-top: 10px;
}
}
.info {
flex: 1 1 auto;
font-size: 13px;
line-height: 1.5;
overflow-y: auto;
height: 100%;
padding-right: 5px;
}
.current {
> p {
padding-left: 15px;
}
}
.desc {
h3, h4 {
font-weight: bold;
}
h3 {
padding: 5px 0 3px;
}
ul {
list-style: initial;
padding-inline-start: 30px;
}
p {
font-size: 14px;
line-height: 1.5;
}
}
.history {
h3 {
padding-top: 15px;
}
.item {
h3 {
padding: 5px 0 3px;
}
padding: 0 15px;
+ .item {
padding-top: 15px;
}
h4 {
font-weight: 700;
}
> p {
padding-left: 15px;
}
}
}
.footer {
flex: 0 0 none;
.desc {
padding-top: 10px;
font-size: 12px;
color: var(--color-primary-font);
line-height: 1.25;
p {
font-size: 12px;
color: var(--color-primary-font);
line-height: 1.25;
}
}
}
.btn {
margin-top: 10px;
display: block;
width: 100%;
}
</style>
+28
View File
@@ -0,0 +1,28 @@
<template>
<div :class="$style.view">
<router-view v-slot="{ Component }">
<!-- <transition enter-active-class="animated-fast fadeIn" leave-active-class="animated-fast fadeOut"> -->
<component :is="Component" class="view-container" />
<!-- </transition> -->
</router-view>
</div>
</template>
<style lang="less" module>
@import '@renderer/assets/styles/layout.less';
.view {
position: relative;
z-index: 1;
> :global(.view-container) {
position: absolute !important;
left: 0;
top: 0;
height: 100%;
width: 100%;
}
// background: #fff;
// overflow: hidden;
}
</style>