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
+66 -65
View File
@@ -1,42 +1,40 @@
<template lang="pug">
#container
core-aside#left
#right
core-toolbar#toolbar
#view
core-view#view-container
core-play-bar#player
core-icons
core-version-modal
core-pact-modal
core-sync-mode-modal
core-play-detail
<template>
<div id="container" class="view-container">
<layout-aside id="left" />
<div id="right">
<layout-toolbar id="toolbar" />
<layout-view id="view" />
<layout-play-bar id="player" />
</div>
<layout-icons />
<layout-version-modal />
<layout-pact-modal />
<layout-sync-mode-modal />
<layout-play-detail />
</div>
</template>
<script>
import { onMounted } from '@renderer/utils/vueTools'
<script setup>
import { onMounted } from '@common/utils/vueTools'
// import BubbleCursor from '@common/utils/cursor-effects/bubbleCursor'
import useApp from '@renderer/core/useApp'
export default {
setup() {
const dom_root = document.getElementById('root')
useApp()
useApp()
onMounted(() => {
document.getElementById('root').style.display = 'block'
// const styles = getComputedStyle(document.documentElement)
// window.lxData.bubbleCursor = new BubbleCursor({
// fillStyle: styles.getPropertyValue('--color-primary-alpha-900'),
// strokeStyle: styles.getPropertyValue('--color-primary-alpha-700'),
// })
})
// onBeforeUnmount(() => {
// window.lxData.bubbleCursor?.destroy()
// })
onMounted(() => {
// 隐藏等待界面
/* inited.value = true
const dom_mask = document.getElementById('waiting-mask')
if (dom_mask) {
dom_mask.addEventListener('transitionend', () => {
dom_mask.parentNode.removeChild(dom_mask)
})
dom_mask.classList.add('hide')
} */
dom_root.style.display = 'block'
})
},
}
</script>
@@ -44,24 +42,28 @@ export default {
@import './assets/styles/index.less';
@import './assets/styles/layout.less';
html {
height: 100vh;
}
html, body {
overflow: hidden;
// overflow: hidden;
box-sizing: border-box;
}
body {
user-select: none;
height: 100vh;
box-sizing: border-box;
height: 100%;
}
#root {
height: 100%;
position: relative;
overflow: hidden;
color: @color-theme_2-font;
background: @color-theme-bgimg @color-theme-bgposition no-repeat;
background-size: @color-theme-bgsize;
transition: background-color @transition-theme;
background-color: @color-theme;
color: var(--color-font);
background: var(--background-image) var(--background-image-position) no-repeat;
background-size: var(--background-image-size);
transition: background-color @transition-normal;
background-color: var(--color-content-background);
box-sizing: border-box;
}
.disableAnimation * {
@@ -70,6 +72,7 @@ body {
}
.transparent {
background: transparent;
padding: @shadow-app;
// #waiting-mask {
// border-radius: @radius-border;
@@ -78,18 +81,20 @@ body {
// top: @shadow-app;
// bottom: @shadow-app;
// }
#body {
border-radius: @radius-border;
}
#root {
box-shadow: 0 0 @shadow-app rgba(0, 0, 0, 0.5);
border-radius: @radius-border;
background-color: transparent;
}
#container {
border-radius: @radius-border;
background-color: transparent;
}
// #container {
// border-radius: @radius-border;
// background-color: transparent;
// }
}
.disableTransparent {
background-color: #fff;
background-color: var(--color-content-background);
#right {
border-top-left-radius: 0;
@@ -101,7 +106,7 @@ body {
// }
}
.fullscreen {
background-color: #fff;
background-color: var(--color-content-background);
#right {
border-top-left-radius: 0;
@@ -113,6 +118,7 @@ body {
position: relative;
display: flex;
height: 100%;
background-color: var(--color-app-background);
}
#left {
@@ -123,8 +129,8 @@ body {
flex: auto;
display: flex;
flex-flow: column nowrap;
transition: background-color @transition-theme;
background-color: @color-theme_2;
transition: background-color @transition-normal;
background-color: var(--color-main-background);
border-top-left-radius: @radius-border;
border-bottom-left-radius: @radius-border;
@@ -137,24 +143,19 @@ body {
#view {
position: relative;
flex: auto;
display: flex;
// display: flex;
min-height: 0;
}
#view-container {
flex: auto;
.view-container {
transition: opacity @transition-normal;
}
#root.show-modal > .view-container {
opacity: .9;
}
#view.show-modal > .view-container {
opacity: .2;
}
each(@themes, {
#root.@{value} {
color: ~'@{color-@{value}-theme_2-font}';
background-color: ~'@{color-@{value}-theme}';
background-image: ~'@{color-@{value}-theme-bgimg}';
background-size: ~'@{color-@{value}-theme-bgsize}';
background-position: ~'@{color-@{value}-theme-bgposition}';
#right {
background-color: ~'@{color-@{value}-theme_2}';
}
}
})
</style>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 353 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 997 KiB

+445 -253
View File
@@ -6,6 +6,280 @@
-webkit-user-drag: none;
}
:root {
--color-primary: rgb(77, 175, 124);
--color-primary-alpha-100: rgba(77, 175, 124, 0.90);
--color-primary-alpha-200: rgba(77, 175, 124, 0.80);
--color-primary-alpha-300: rgba(77, 175, 124, 0.70);
--color-primary-alpha-400: rgba(77, 175, 124, 0.60);
--color-primary-alpha-500: rgba(77, 175, 124, 0.50);
--color-primary-alpha-600: rgba(77, 175, 124, 0.40);
--color-primary-alpha-700: rgba(77, 175, 124, 0.30);
--color-primary-alpha-800: rgba(77, 175, 124, 0.20);
--color-primary-alpha-900: rgba(77, 175, 124, 0.10);
--color-primary-dark-100: rgb(69,158,112);
--color-primary-dark-100-alpha-100: rgba(69, 158, 112, 0.90);
--color-primary-dark-100-alpha-200: rgba(69, 158, 112, 0.80);
--color-primary-dark-100-alpha-300: rgba(69, 158, 112, 0.70);
--color-primary-dark-100-alpha-400: rgba(69, 158, 112, 0.60);
--color-primary-dark-100-alpha-500: rgba(69, 158, 112, 0.50);
--color-primary-dark-100-alpha-600: rgba(69, 158, 112, 0.40);
--color-primary-dark-100-alpha-700: rgba(69, 158, 112, 0.30);
--color-primary-dark-100-alpha-800: rgba(69, 158, 112, 0.20);
--color-primary-dark-100-alpha-900: rgba(69, 158, 112, 0.10);
--color-primary-dark-200: rgb(62,142,101);
--color-primary-dark-200-alpha-100: rgba(62, 142, 101, 0.90);
--color-primary-dark-200-alpha-200: rgba(62, 142, 101, 0.80);
--color-primary-dark-200-alpha-300: rgba(62, 142, 101, 0.70);
--color-primary-dark-200-alpha-400: rgba(62, 142, 101, 0.60);
--color-primary-dark-200-alpha-500: rgba(62, 142, 101, 0.50);
--color-primary-dark-200-alpha-600: rgba(62, 142, 101, 0.40);
--color-primary-dark-200-alpha-700: rgba(62, 142, 101, 0.30);
--color-primary-dark-200-alpha-800: rgba(62, 142, 101, 0.20);
--color-primary-dark-200-alpha-900: rgba(62, 142, 101, 0.10);
--color-primary-dark-300: rgb(56,128,91);
--color-primary-dark-300-alpha-100: rgba(56, 128, 91, 0.90);
--color-primary-dark-300-alpha-200: rgba(56, 128, 91, 0.80);
--color-primary-dark-300-alpha-300: rgba(56, 128, 91, 0.70);
--color-primary-dark-300-alpha-400: rgba(56, 128, 91, 0.60);
--color-primary-dark-300-alpha-500: rgba(56, 128, 91, 0.50);
--color-primary-dark-300-alpha-600: rgba(56, 128, 91, 0.40);
--color-primary-dark-300-alpha-700: rgba(56, 128, 91, 0.30);
--color-primary-dark-300-alpha-800: rgba(56, 128, 91, 0.20);
--color-primary-dark-300-alpha-900: rgba(56, 128, 91, 0.10);
--color-primary-dark-400: rgb(50,115,82);
--color-primary-dark-400-alpha-100: rgba(50, 115, 82, 0.90);
--color-primary-dark-400-alpha-200: rgba(50, 115, 82, 0.80);
--color-primary-dark-400-alpha-300: rgba(50, 115, 82, 0.70);
--color-primary-dark-400-alpha-400: rgba(50, 115, 82, 0.60);
--color-primary-dark-400-alpha-500: rgba(50, 115, 82, 0.50);
--color-primary-dark-400-alpha-600: rgba(50, 115, 82, 0.40);
--color-primary-dark-400-alpha-700: rgba(50, 115, 82, 0.30);
--color-primary-dark-400-alpha-800: rgba(50, 115, 82, 0.20);
--color-primary-dark-400-alpha-900: rgba(50, 115, 82, 0.10);
--color-primary-dark-500: rgb(45,104,74);
--color-primary-dark-500-alpha-100: rgba(45, 104, 74, 0.90);
--color-primary-dark-500-alpha-200: rgba(45, 104, 74, 0.80);
--color-primary-dark-500-alpha-300: rgba(45, 104, 74, 0.70);
--color-primary-dark-500-alpha-400: rgba(45, 104, 74, 0.60);
--color-primary-dark-500-alpha-500: rgba(45, 104, 74, 0.50);
--color-primary-dark-500-alpha-600: rgba(45, 104, 74, 0.40);
--color-primary-dark-500-alpha-700: rgba(45, 104, 74, 0.30);
--color-primary-dark-500-alpha-800: rgba(45, 104, 74, 0.20);
--color-primary-dark-500-alpha-900: rgba(45, 104, 74, 0.10);
--color-primary-dark-600: rgb(41,94,67);
--color-primary-dark-600-alpha-100: rgba(41, 94, 67, 0.90);
--color-primary-dark-600-alpha-200: rgba(41, 94, 67, 0.80);
--color-primary-dark-600-alpha-300: rgba(41, 94, 67, 0.70);
--color-primary-dark-600-alpha-400: rgba(41, 94, 67, 0.60);
--color-primary-dark-600-alpha-500: rgba(41, 94, 67, 0.50);
--color-primary-dark-600-alpha-600: rgba(41, 94, 67, 0.40);
--color-primary-dark-600-alpha-700: rgba(41, 94, 67, 0.30);
--color-primary-dark-600-alpha-800: rgba(41, 94, 67, 0.20);
--color-primary-dark-600-alpha-900: rgba(41, 94, 67, 0.10);
--color-primary-dark-700: rgb(37,85,60);
--color-primary-dark-700-alpha-100: rgba(37, 85, 60, 0.90);
--color-primary-dark-700-alpha-200: rgba(37, 85, 60, 0.80);
--color-primary-dark-700-alpha-300: rgba(37, 85, 60, 0.70);
--color-primary-dark-700-alpha-400: rgba(37, 85, 60, 0.60);
--color-primary-dark-700-alpha-500: rgba(37, 85, 60, 0.50);
--color-primary-dark-700-alpha-600: rgba(37, 85, 60, 0.40);
--color-primary-dark-700-alpha-700: rgba(37, 85, 60, 0.30);
--color-primary-dark-700-alpha-800: rgba(37, 85, 60, 0.20);
--color-primary-dark-700-alpha-900: rgba(37, 85, 60, 0.10);
--color-primary-dark-800: rgb(33,77,54);
--color-primary-dark-800-alpha-100: rgba(33, 77, 54, 0.90);
--color-primary-dark-800-alpha-200: rgba(33, 77, 54, 0.80);
--color-primary-dark-800-alpha-300: rgba(33, 77, 54, 0.70);
--color-primary-dark-800-alpha-400: rgba(33, 77, 54, 0.60);
--color-primary-dark-800-alpha-500: rgba(33, 77, 54, 0.50);
--color-primary-dark-800-alpha-600: rgba(33, 77, 54, 0.40);
--color-primary-dark-800-alpha-700: rgba(33, 77, 54, 0.30);
--color-primary-dark-800-alpha-800: rgba(33, 77, 54, 0.20);
--color-primary-dark-800-alpha-900: rgba(33, 77, 54, 0.10);
--color-primary-dark-900: rgb(30,69,49);
--color-primary-dark-900-alpha-100: rgba(30, 69, 49, 0.90);
--color-primary-dark-900-alpha-200: rgba(30, 69, 49, 0.80);
--color-primary-dark-900-alpha-300: rgba(30, 69, 49, 0.70);
--color-primary-dark-900-alpha-400: rgba(30, 69, 49, 0.60);
--color-primary-dark-900-alpha-500: rgba(30, 69, 49, 0.50);
--color-primary-dark-900-alpha-600: rgba(30, 69, 49, 0.40);
--color-primary-dark-900-alpha-700: rgba(30, 69, 49, 0.30);
--color-primary-dark-900-alpha-800: rgba(30, 69, 49, 0.20);
--color-primary-dark-900-alpha-900: rgba(30, 69, 49, 0.10);
--color-primary-dark-1000: rgb(27,62,44);
--color-primary-dark-1000-alpha-100: rgba(27, 62, 44, 0.90);
--color-primary-dark-1000-alpha-200: rgba(27, 62, 44, 0.80);
--color-primary-dark-1000-alpha-300: rgba(27, 62, 44, 0.70);
--color-primary-dark-1000-alpha-400: rgba(27, 62, 44, 0.60);
--color-primary-dark-1000-alpha-500: rgba(27, 62, 44, 0.50);
--color-primary-dark-1000-alpha-600: rgba(27, 62, 44, 0.40);
--color-primary-dark-1000-alpha-700: rgba(27, 62, 44, 0.30);
--color-primary-dark-1000-alpha-800: rgba(27, 62, 44, 0.20);
--color-primary-dark-1000-alpha-900: rgba(27, 62, 44, 0.10);
--color-primary-light-100: rgb(113,191,150);
--color-primary-light-100-alpha-100: rgba(113, 191, 150, 0.90);
--color-primary-light-100-alpha-200: rgba(113, 191, 150, 0.80);
--color-primary-light-100-alpha-300: rgba(113, 191, 150, 0.70);
--color-primary-light-100-alpha-400: rgba(113, 191, 150, 0.60);
--color-primary-light-100-alpha-500: rgba(113, 191, 150, 0.50);
--color-primary-light-100-alpha-600: rgba(113, 191, 150, 0.40);
--color-primary-light-100-alpha-700: rgba(113, 191, 150, 0.30);
--color-primary-light-100-alpha-800: rgba(113, 191, 150, 0.20);
--color-primary-light-100-alpha-900: rgba(113, 191, 150, 0.10);
--color-primary-light-200: rgb(141,204,171);
--color-primary-light-200-alpha-100: rgba(141, 204, 171, 0.90);
--color-primary-light-200-alpha-200: rgba(141, 204, 171, 0.80);
--color-primary-light-200-alpha-300: rgba(141, 204, 171, 0.70);
--color-primary-light-200-alpha-400: rgba(141, 204, 171, 0.60);
--color-primary-light-200-alpha-500: rgba(141, 204, 171, 0.50);
--color-primary-light-200-alpha-600: rgba(141, 204, 171, 0.40);
--color-primary-light-200-alpha-700: rgba(141, 204, 171, 0.30);
--color-primary-light-200-alpha-800: rgba(141, 204, 171, 0.20);
--color-primary-light-200-alpha-900: rgba(141, 204, 171, 0.10);
--color-primary-light-300: rgb(164,214,188);
--color-primary-light-300-alpha-100: rgba(164, 214, 188, 0.90);
--color-primary-light-300-alpha-200: rgba(164, 214, 188, 0.80);
--color-primary-light-300-alpha-300: rgba(164, 214, 188, 0.70);
--color-primary-light-300-alpha-400: rgba(164, 214, 188, 0.60);
--color-primary-light-300-alpha-500: rgba(164, 214, 188, 0.50);
--color-primary-light-300-alpha-600: rgba(164, 214, 188, 0.40);
--color-primary-light-300-alpha-700: rgba(164, 214, 188, 0.30);
--color-primary-light-300-alpha-800: rgba(164, 214, 188, 0.20);
--color-primary-light-300-alpha-900: rgba(164, 214, 188, 0.10);
--color-primary-light-400: rgb(182,222,201);
--color-primary-light-400-alpha-100: rgba(182, 222, 201, 0.90);
--color-primary-light-400-alpha-200: rgba(182, 222, 201, 0.80);
--color-primary-light-400-alpha-300: rgba(182, 222, 201, 0.70);
--color-primary-light-400-alpha-400: rgba(182, 222, 201, 0.60);
--color-primary-light-400-alpha-500: rgba(182, 222, 201, 0.50);
--color-primary-light-400-alpha-600: rgba(182, 222, 201, 0.40);
--color-primary-light-400-alpha-700: rgba(182, 222, 201, 0.30);
--color-primary-light-400-alpha-800: rgba(182, 222, 201, 0.20);
--color-primary-light-400-alpha-900: rgba(182, 222, 201, 0.10);
--color-primary-light-500: rgb(197,229,212);
--color-primary-light-500-alpha-100: rgba(197, 229, 212, 0.90);
--color-primary-light-500-alpha-200: rgba(197, 229, 212, 0.80);
--color-primary-light-500-alpha-300: rgba(197, 229, 212, 0.70);
--color-primary-light-500-alpha-400: rgba(197, 229, 212, 0.60);
--color-primary-light-500-alpha-500: rgba(197, 229, 212, 0.50);
--color-primary-light-500-alpha-600: rgba(197, 229, 212, 0.40);
--color-primary-light-500-alpha-700: rgba(197, 229, 212, 0.30);
--color-primary-light-500-alpha-800: rgba(197, 229, 212, 0.20);
--color-primary-light-500-alpha-900: rgba(197, 229, 212, 0.10);
--color-primary-light-600: rgb(209,234,221);
--color-primary-light-600-alpha-100: rgba(209, 234, 221, 0.90);
--color-primary-light-600-alpha-200: rgba(209, 234, 221, 0.80);
--color-primary-light-600-alpha-300: rgba(209, 234, 221, 0.70);
--color-primary-light-600-alpha-400: rgba(209, 234, 221, 0.60);
--color-primary-light-600-alpha-500: rgba(209, 234, 221, 0.50);
--color-primary-light-600-alpha-600: rgba(209, 234, 221, 0.40);
--color-primary-light-600-alpha-700: rgba(209, 234, 221, 0.30);
--color-primary-light-600-alpha-800: rgba(209, 234, 221, 0.20);
--color-primary-light-600-alpha-900: rgba(209, 234, 221, 0.10);
--color-primary-light-700: rgb(218,238,228);
--color-primary-light-700-alpha-100: rgba(218, 238, 228, 0.90);
--color-primary-light-700-alpha-200: rgba(218, 238, 228, 0.80);
--color-primary-light-700-alpha-300: rgba(218, 238, 228, 0.70);
--color-primary-light-700-alpha-400: rgba(218, 238, 228, 0.60);
--color-primary-light-700-alpha-500: rgba(218, 238, 228, 0.50);
--color-primary-light-700-alpha-600: rgba(218, 238, 228, 0.40);
--color-primary-light-700-alpha-700: rgba(218, 238, 228, 0.30);
--color-primary-light-700-alpha-800: rgba(218, 238, 228, 0.20);
--color-primary-light-700-alpha-900: rgba(218, 238, 228, 0.10);
--color-primary-light-800: rgb(225,241,233);
--color-primary-light-800-alpha-100: rgba(225, 241, 233, 0.90);
--color-primary-light-800-alpha-200: rgba(225, 241, 233, 0.80);
--color-primary-light-800-alpha-300: rgba(225, 241, 233, 0.70);
--color-primary-light-800-alpha-400: rgba(225, 241, 233, 0.60);
--color-primary-light-800-alpha-500: rgba(225, 241, 233, 0.50);
--color-primary-light-800-alpha-600: rgba(225, 241, 233, 0.40);
--color-primary-light-800-alpha-700: rgba(225, 241, 233, 0.30);
--color-primary-light-800-alpha-800: rgba(225, 241, 233, 0.20);
--color-primary-light-800-alpha-900: rgba(225, 241, 233, 0.10);
--color-primary-light-900: rgb(231,244,237);
--color-primary-light-900-alpha-100: rgba(231, 244, 237, 0.90);
--color-primary-light-900-alpha-200: rgba(231, 244, 237, 0.80);
--color-primary-light-900-alpha-300: rgba(231, 244, 237, 0.70);
--color-primary-light-900-alpha-400: rgba(231, 244, 237, 0.60);
--color-primary-light-900-alpha-500: rgba(231, 244, 237, 0.50);
--color-primary-light-900-alpha-600: rgba(231, 244, 237, 0.40);
--color-primary-light-900-alpha-700: rgba(231, 244, 237, 0.30);
--color-primary-light-900-alpha-800: rgba(231, 244, 237, 0.20);
--color-primary-light-900-alpha-900: rgba(231, 244, 237, 0.10);
--color-primary-light-1000: rgb(255,255,255);
--color-primary-light-1000-alpha-100: rgba(255, 255, 255, 0.90);
--color-primary-light-1000-alpha-200: rgba(255, 255, 255, 0.80);
--color-primary-light-1000-alpha-300: rgba(255, 255, 255, 0.70);
--color-primary-light-1000-alpha-400: rgba(255, 255, 255, 0.60);
--color-primary-light-1000-alpha-500: rgba(255, 255, 255, 0.50);
--color-primary-light-1000-alpha-600: rgba(255, 255, 255, 0.40);
--color-primary-light-1000-alpha-700: rgba(255, 255, 255, 0.30);
--color-primary-light-1000-alpha-800: rgba(255, 255, 255, 0.20);
--color-primary-light-1000-alpha-900: rgba(255, 255, 255, 0.10);
--color-theme: rgb(77, 175, 124);
// --color-scrollbar-track:
// --color-900: #fff;
// --color-800: #fafafa;
// --color-700: #f5f5f5;
// --color-600: #eeeeee;
// --color-500: #e0e0e0;
// --color-400: #bdbdbd;
// --color-300: #9e9e9e;
// --color-200: #757575;
// --color-100: #616161;
// --color-050: #424242;
// --color-000: #212121;
--color-000: #fff;
--color-050: #fafafa;
--color-100: #f5f5f5;
--color-200: #eeeeee;
--color-300: #e0e0e0;
--color-400: #bdbdbd;
--color-500: #9e9e9e;
--color-600: #757575;
--color-700: #616161;
--color-800: #424242;
--color-900: #212121;
--color-app-background: var(--color-primary-light-600-alpha-600);
--color-main-background: rgba(255, 255, 255, 0.9);
--color-nav-font: var(--color-primary);
// --color-app-background: rgba(0, 0, 0, .5);
// --color-main-background: rgba(0, 0, 0, 0.26);
--color-btn-hide: #3bc2b2;
--color-btn-min: #85c43b;
// --color-btn-max: #e7aa36;
--color-btn-close: #fab4a0;
--color-badge-primary: var(--color-primary);
--color-badge-secondary: #4baed5;
--color-badge-tertiary: #e7aa36;
--color-font: var(--color-700);
--color-font-label: var(--color-200);
--color-primary-font: var(--color-primary);
--color-primary-font-hover: var(--color-primary-alpha-300);
--color-primary-font-active: var(--color-primary-dark-100-alpha-200);
--color-primary-background: var(--color-primary-light-400-alpha-700);
--color-primary-background-hover: var(--color-primary-light-300-alpha-800);
--color-primary-background-active: var(--color-primary-light-100-alpha-800);
--color-button-font: var(--color-primary-alpha-100);
--color-button-font-selected: var(--color-primary-dark-100-alpha-100);
--color-button-background: var(--color-primary-light-400-alpha-700);
--color-button-background-selected: var(--color-primary-alpha-600);
--color-button-background-hover: var(--color-primary-light-300-alpha-600);
--color-button-background-active: var(--color-primary-light-100-alpha-600);
--color-list-header-border-bottom: 1px solid var(--color-primary-alpha-900);
--color-content-background: var(--color-primary-light-1000);
--background-image: none;
--background-image-position: center;
--background-image-size: cover;
}
html {
font-size: 16px;
}
@@ -33,90 +307,6 @@ html {
user-select: none;
}
.thead {
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
}
table {
width: 100%;
border-spacing: 0;
border-collapse: collapse;
overflow: hidden;
color: @color-theme_2-font;
th {
font-size: 12px;
text-align: left;
line-height: 38px;
padding: 0 6px;
}
tbody {
tr {
// border-top: 1px solid rgba(0, 0, 0, 0.12);
transition: background-color 0.2s ease;
+ tr {
border-top: 1px solid @color-theme_2-line;
}
&:hover {
background-color: @color-theme_2-hover;
}
&.active {
background-color: @color-theme_2-active;
}
&.selected {
background-color: @color-theme_2-hover;
}
td {
padding: 6px;
position: relative;
transition: 0.3s cubic-bezier(0.4, 0, 0.2, 1);
font-size: 13px;
line-height: 16px;
vertical-align: middle;
}
}
}
}
.list {
width: 100%;
overflow: hidden;
color: @color-theme_2-font;
.list-item {
height: 100%;
display: flex;
flex-flow: row nowrap;
align-items: center;
// border-top: 1px solid rgba(0, 0, 0, 0.12);
transition: 0.2s ease;
transition-property: background-color, color;
border-bottom: 1px solid @color-theme_2-line;
box-sizing: border-box;
&:hover {
background-color: @color-theme_2-hover;
}
&.active {
background-color: @color-theme_2-active;
}
&.selected {
background-color: @color-theme_2-hover;
}
.list-item-cell {
flex: none;
padding: 0 6px;
position: relative;
// transition: 0.3s cubic-bezier(0.4, 0, 0.2, 1);
font-size: 13px;
line-height: 16px;
vertical-align: middle;
box-sizing: border-box;
.mixin-ellipsis-1;
&.auto {
flex: auto;
}
}
}
}
.badge {
display: inline-block;
padding: 0.25em 0.4em;
@@ -125,39 +315,41 @@ table {
line-height: 1;
text-align: center;
white-space: nowrap;
vertical-align: baseline;
border-radius: 2px;
// vertical-align: baseline;
vertical-align: text-top;
// border-radius: 2px;
&.badge-light {
background-color: #f8f9fa;
// &.badge-light {
// background-color: #f8f9fa;
// }
// &.badge-secondary {
// color: #fff;
// background-color: #6c757d;
// }
// &.badge-info {
// color: #fff;
// background-color: #4baed5;
// }
// &.badge-warning {
// color: #fff;
// background-color: #ffa45a;
// }
// &.badge-danger {
// color: #fff;
// background-color: #ff705a;
// }
// &.badge-success {
// color: #fff;
// background-color: #32bc63;
// }
&.badge-theme-primary {
color: var(--color-badge-primary);
}
&.badge-secondary {
color: #fff;
background-color: #6c757d;
&.badge-theme-secondary {
color: var(--color-badge-secondary);
}
&.badge-info {
color: #fff;
background-color: #4baed5;
}
&.badge-warning {
color: #fff;
background-color: #ffa45a;
}
&.badge-danger {
color: #fff;
background-color: #ff705a;
}
&.badge-success {
color: #fff;
background-color: #32bc63;
}
&.badge-theme-success {
color: @color-badge-success-font;
background-color: @color-badge-success;
}
&.badge-theme-info {
color: @color-badge-info-font;
background-color: @color-badge-info;
&.badge-theme-tertiary {
color: var(--color-badge-tertiary);
}
}
@@ -168,7 +360,7 @@ small {
font-size: .9em;
}
.tip {
color: @color-theme_2-font-label;
color: var(--color-label);
}
strong {
font-weight: bold;
@@ -179,32 +371,32 @@ strong {
}
svg {
transition: @transition-theme;
transition: @transition-normal;
transition-property: fill;
}
button, input, textarea, a {
color: @color-theme_2-font;
color: var(--color-font);
}
input, textarea {
&::placeholder {
color: @color-theme_2-font-label;
color: var(--color-font-label);
}
}
::selection {
color: @color-theme_2;
background: @color-btn;
color: var(--color-primary-dark-500-alpha-200);
background: var(--color-primary-light-100-alpha-500);
}
.hover, a {
cursor: pointer;
transition: color .2s ease;
&:hover {
color: @color-theme;
color: var(--color-primary-font-hover);
}
&:active {
color: @color-theme-active;
color: var(--color-primary-font-active);
}
}
@@ -216,24 +408,150 @@ input, textarea {
background-color: rgba(0, 0, 0, 0);
}
&::-webkit-scrollbar-track {
background-color: @color-scrollbar-track;
background-color: var(--color-primary-light-100-alpha-800);
border-radius: 3px;
// background-color: rgba(0, 0, 0, 0.1);
}
&::-webkit-scrollbar-thumb {
border-radius: 3px;
background-color: @color-scrollbar-thumb;
background-color: var(--color-primary-alpha-600);
// background-color: rgba(0, 0, 0, 0.2);
transition: background-color 0.4s ease;
}
&::-webkit-scrollbar-thumb:hover {
border-radius: 3px;
background-color: @color-scrollbar-thumb-hover;
background-color: var(--color-primary-alpha-400);
// background-color: rgba(0, 0, 0, 0.4);
transition: background-color 0.4s ease;
}
}
.thead {
border-bottom: var(--color-list-header-border-bottom);
padding-right: 6px;
flex: none;
.num {
.nobreak;
.center;
color: var(--color-font-label);
}
// box-shadow: 0 0 2px var(--color-primary-dark-500-alpha-800);
// position: relative;
// z-index: 2;
}
table {
width: 100%;
border-spacing: 0;
border-collapse: collapse;
overflow: hidden;
color: var(--color-font);
th {
font-size: 12px;
text-align: left;
line-height: 38px;
padding: 0 6px;
}
}
.list {
width: 100%;
overflow: hidden;
color: var(--color-font);
flex: auto;
.list-item {
height: 100%;
display: flex;
flex-flow: row nowrap;
align-items: center;
// border-top: 1px solid rgba(0, 0, 0, 0.12);
transition: 0.2s ease;
transition-property: background-color, color;
// border-bottom: 1px solid @color-theme_2-line;
box-sizing: border-box;
font-size: 12px;
&:hover {
background-color: var(--color-primary-background-hover);
// .list-item-cell-action {
// display: block;
// }
}
&.active {
background-color: var(--color-primary-background-active);
}
&.selected {
background-color: var(--color-primary-background-hover);
}
&.disabled {
opacity: .5;
}
.list-item-cell {
flex: none;
padding: 0 6px;
position: relative;
// transition: 0.3s cubic-bezier(0.4, 0, 0.2, 1);
line-height: 16px;
vertical-align: middle;
box-sizing: border-box;
.mixin-ellipsis-1;
&.auto {
flex: auto;
}
&.num {
.nobreak;
.center;
padding-left: 3px;
padding-right: 3px;
font-size: 11px;
color: var(--color-font-label);
}
&.name {
display: flex;
flex-flow: row nowrap;
overflow: hidden;
white-space: initial;
text-overflow: initial;
align-items: center;
>.name {
.mixin-ellipsis-1;
}
}
.badge {
margin-left: 3px;
opacity: .85;
}
}
// .list-item-cell-action {
// white-space: nowrap;
// display: none;
// flex: auto;
// text-align: right;
// // position: absolute;
// // right: 5px;
// // top: -2px;
// // opacity: 0;
// // transition: opacity .1s ease;
// }
}
}
.copying {
.no-select {
display: none !important;
}
}
.gap-left {
+ .gap-left {
margin-left: 20px;
@@ -249,132 +567,6 @@ input, textarea {
}
}
/*
#waiting-mask {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
background-color: #fff;
z-index: 999999;
transition: all .8s ease-in-out;
display: flex;
align-items: center;
justify-content: center;
-webkit-app-region: drag;
background-color: @color-theme;
&.hide {
opacity: 0;
}
.color-picker {
border-radius: @radius-border !important;
}
#logo-path-1 {fill: #5ed698; filter: drop-shadow(0 0 1px rgba(0, 0, 0, 0.2));}
#logo-path-2 {fill: #4daf7c; filter: drop-shadow(0 0 1px rgba(0, 0, 0, 0.2));}
each(@themes, {
body.@{value} #waiting-mask {
background-color: ~'@{color-@{value}-theme_2-background_1}';
#logo-path-1 {fill: ~'@{color-@{value}-btn}'; filter: drop-shadow(0 0 1px rgba(0, 0, 0, 0.2));}
#logo-path-2 {fill: ~'@{color-@{value}-theme}'; filter: drop-shadow(0 0 1px rgba(0, 0, 0, 0.2));}
}
}) */
// each(@themes, {
// body.@{value} #waiting-mask {
// background-color: ~'@{color-@{value}-theme}';
// #logo-path-1 {fill: ~'@{color-@{value}-theme_2-background_1}'; filter: drop-shadow(0 0 1px rgba(0, 0, 0, 0.2));}
// #logo-path-2 {fill: ~'@{color-@{value}-theme-font-label}'; filter: drop-shadow(0 0 1px rgba(0, 0, 0, 0.2));}
// }
// })
each(@themes, {
#root.@{value} {
button, input, textarea, a {
color: ~'@{color-@{value}-theme_2-font}';
}
.tip {
color: ~'@{color-@{value}-theme_2-font-label}';
}
.hover, a {
&:hover {
color: ~'@{color-@{value}-theme}';
}
&:active {
color: ~'@{color-@{value}-theme-active}';
}
}
.scroll {
&::-webkit-scrollbar {
background-color: rgba(0, 0, 0, 0);
}
&::-webkit-scrollbar-track {
background-color: ~'@{color-@{value}-scrollbar-track}';
}
&::-webkit-scrollbar-thumb {
background-color: ~'@{color-@{value}-scrollbar-thumb}';
}
&::-webkit-scrollbar-thumb:hover {
background-color: ~'@{color-@{value}-scrollbar-thumb-hover}';
}
}
table {
color: ~'@{color-@{value}-theme_2-font}';
tbody {
tr {
border-top-color: ~'@{color-@{value}-theme_2-line}';
&:hover {
background-color: ~'@{color-@{value}-theme_2-hover}';
}
&.active {
background-color: ~'@{color-@{value}-theme_2-active}';
}
&.selected {
background-color: ~'@{color-@{value}-theme_2-hover}';
}
}
}
}
.list {
color: ~'@{color-@{value}-theme_2-font}';
.list-item {
border-bottom-color: ~'@{color-@{value}-theme_2-line}';
&:hover {
background-color: ~'@{color-@{value}-theme_2-hover}';
}
&.active {
background-color: ~'@{color-@{value}-theme_2-active}';
}
&.selected {
background-color: ~'@{color-@{value}-theme_2-hover}';
}
}
}
input, textarea {
&::placeholder {
color: ~'@{color-@{value}-theme_2-font-label}';
}
}
::selection {
color: ~'@{color-@{value}-theme_2}';
background: ~'@{color-@{value}-btn}';
}
.badge {
&.badge-theme-success {
color: ~'@{color-@{value}-badge-success-font}';
background-color: ~'@{color-@{value}-badge-success}';
}
&.badge-theme-info {
color: ~'@{color-@{value}-badge-info-font}';
background-color: ~'@{color-@{value}-badge-info}';
}
}
}
})
+7 -2
View File
@@ -7,14 +7,14 @@
white-space: nowrap;
text-overflow: ellipsis;
}
.mixin-ellipsis() {
.mixin-ellipsis(@n: 1) {
display: -webkit-box;
overflow: hidden;
text-overflow: ellipsis;
word-wrap: break-word;
word-break: break-all;
white-space: normal !important;
-webkit-line-clamp: 1;
-webkit-line-clamp: @n;
-webkit-box-orient: vertical;
}
.mixin-ellipsis-2() {
@@ -28,3 +28,8 @@
-webkit-box-orient: vertical;
}
.mixin-after() {
display: block;
position: absolute;
content: '';
}
+8 -5
View File
@@ -16,16 +16,19 @@ time, mark, audio, video {
border: 0;
font-size: 100%;
font: inherit;
font-size: inherit;
vertical-align: baseline;
}
/* HTML5 display-role reset for older browsers */
article, aside, details, figcaption, figure,
footer, header, hgroup, menu, nav, section {
display: block;
}
html {
font-family: "Microsoft YaHei", "Microsoft Jhenghei", "PingFang SC", -apple-system, BlinkMacSystemFont, Roboto, "Helvetica Neue", Helvetica, Arial, "Hiragino Sans GB", "Source Han Sans", "Noto Sans CJK Sc", sans-serif;
}
/* HTML5 display-role reset for older browsers */
article, aside, details, figcaption, figure,
footer, header, hgroup, menu, nav, section {
display: block;
}
// html {
// }
body {
line-height: 1;
}
+4 -975
View File
@@ -1,985 +1,12 @@
@import './colors.less';
@themes: green, yellow, blue, red, pink, purple, orange, grey, ming, blue2, black, mid_autumn, naruto, happy_new_year;
// Colors
// @color-theme: #03a678;
@color-theme: #4daf7c;
@color-theme-bgimg: none;
@color-theme-bgposition: center;
@color-theme-bgsize: cover;
@color-theme-hover: fadeout(lighten(@color-theme, 10%), 30%);
@color-theme-active: fadeout(darken(@color-theme, 20%), 60%);
@color-theme-font: #fff;
@color-theme-font-label: lighten(@color-theme, 35%);
@color-theme_2: rgba(255, 255, 255, .9);
@color-theme_2-background_1: #fff;
@color-theme_2-background_2: fadeout(@color-theme_2-background_1, 2%);
@color-theme_2-hover: fadeout(lighten(@color-theme, 10%), 80%);
@color-theme_2-active: fadeout(lighten(@color-theme, 10%), 70%);
@color-theme_2-font:desaturate( darken(@color-theme_2, 70%), 90%);
@color-theme_2-font-label: fadeout(@color-theme_2-font, 55%);
// @color-theme_2-line: transparent;
@color-theme_2-line: lighten(@color-theme, 35%);
@color-reply-floor: fadeout(@color-theme, 95%);
@color-theme-sidebar: @color-theme;
@color-btn: lighten(@color-theme, 5%);
@color-btn-background: fadeout(lighten(@color-theme, 35%), 70%);
@color-btn-hover: fadeout(lighten(@color-theme, 5%), 70%);
@color-btn-active: fadeout(darken(@color-theme, 5%), 70%);
@color-btn-select: fadeout(lighten(@color-theme, 5%), 50%);
@color-pagination-background: fadeout(lighten(@color-theme, 45%), 30%);
@color-pagination-hover: fadeout(lighten(@color-theme, 10%), 70%);
@color-pagination-active: fadeout(darken(@color-theme, 10%), 70%);
@color-pagination-select: fadeout(lighten(@color-theme, 10%), 50%);
@color-search-form-background: fadeout(lighten(@color-theme, 35%), 10%);
// @color-search-list-background: fadeout(lighten(@color-theme, 35%), 10%);
@color-search-list-hover: fadeout(darken(@color-theme, 10%), 70%);
@color-scrollbar-track: fadeout(lighten(@color-theme, 10%), 65%);
@color-scrollbar-thumb: fadeout(lighten(@color-theme, 10%), 45%);
@color-scrollbar-thumb-hover: fadeout(lighten(@color-theme, 10%), 25%);
@color-player-pic-c1: fadeout(@color-theme_2, 50%);
@color-player-pic-c2: darken(@color-theme_2, 30%);
@color-player-progress: darken(@color-theme_2, 6%);
@color-player-progress-bar1: darken(@color-theme_2, 12%);
@color-player-progress-bar2: fadeout(lighten(@color-theme, 12%), 20%);
@color-player-status-text: lighten(@color-theme_2-font, 32%);
@color-player-detail-lyric: fadeout(@color-theme_2-font, 40%);
@color-player-detail-lyric-active: darken(@color-theme, 2%);
@color-player-detail-play-btn: lighten(@color-theme, 7%);
@color-tab-btn-background: fadeout(lighten(@color-theme, 10%), 80%);
@color-tab-btn-background-hover: @color-theme_2-hover;
@color-tab-border-top: fadeout(lighten(@color-theme, 5%), 50%);
@color-tab-border-bottom: lighten(@color-theme, 5%);
@color-hideBtn: #3bc2b2;
@color-minBtn: #85c43b;
@color-maxBtn: #e7aa36;
@color-closeBtn: #ea6e4d;
@color-hideBtn-hover: fadeout(@color-hideBtn, 10%);
@color-minBtn-hover: fadeout(@color-minBtn, 10%);
@color-maxBtn-hover: fadeout(@color-maxBtn, 10%);
@color-closeBtn-hover: fadeout(@color-closeBtn, 10%);
@color-green-badge-success: #32bc63;
@color-green-badge-success-font: #fff;
@color-green-badge-info: #4baed5;
@color-green-badge-info-font: #fff;
@color-badge-success: @color-theme;
@color-badge-success-font: #fff;
@color-badge-info: #4baed5;
@color-badge-info-font: #fff;
@color-green-theme: #4daf7c;
@color-green-theme-bgimg: none;
@color-green-theme-bgposition: center;
@color-green-theme-bgsize: cover;
@color-green-theme-hover: fadeout(lighten(@color-green-theme, 10%), 30%);
@color-green-theme-active: fadeout(darken(@color-green-theme, 20%), 60%);
@color-green-theme-font: #fff;
@color-green-theme-font-label: lighten(@color-green-theme, 35%);
@color-green-theme_2: #fff;
@color-green-theme_2-background_1: #fff;
@color-green-theme_2-background_2: fadeout(@color-green-theme_2-background_1, 2%);
@color-green-theme_2-hover: fadeout(lighten(@color-green-theme, 10%), 80%);
@color-green-theme_2-active: fadeout(lighten(@color-green-theme, 10%), 70%);
@color-green-theme_2-font: darken(desaturate(@color-green-theme_2, 70%), 90%);
@color-green-theme_2-font-label: fadeout(@color-green-theme_2-font, 55%);
// @color-green-theme_2-line: transparent;
@color-green-theme_2-line: lighten(@color-green-theme, 45%);
@color-green-reply-floor: fadeout(@color-green-theme, 95%);
@color-green-theme-sidebar: @color-green-theme;
@color-green-btn: lighten(@color-green-theme, 5%);
@color-green-btn-background: fadeout(lighten(@color-green-theme, 35%), 70%);
@color-green-btn-hover: fadeout(lighten(@color-green-theme, 5%), 70%);
@color-green-btn-active: fadeout(darken(@color-green-theme, 5%), 70%);
@color-green-btn-select: fadeout(lighten(@color-green-theme, 5%), 50%);
@color-green-pagination-background: fadeout(lighten(@color-green-theme, 45%), 30%);
@color-green-pagination-hover: fadeout(lighten(@color-green-theme, 10%), 70%);
@color-green-pagination-active: fadeout(darken(@color-green-theme, 10%), 70%);
@color-green-pagination-select: fadeout(lighten(@color-green-theme, 10%), 50%);
@color-green-search-form-background: fadeout(lighten(@color-green-theme, 35%), 10%);
@color-green-search-list-hover: fadeout(darken(@color-green-theme, 10%), 70%);
@color-green-scrollbar-track: fadeout(lighten(@color-green-theme, 10%), 75%);
@color-green-scrollbar-thumb: fadeout(lighten(@color-green-theme, 10%), 50%);
@color-green-scrollbar-thumb-hover: fadeout(lighten(@color-green-theme, 10%), 30%);
@color-green-player-pic-c1: fadeout(@color-green-theme_2, 50%);
@color-green-player-pic-c2: darken(@color-green-theme_2, 30%);
@color-green-player-progress: darken(@color-green-theme_2, 6%);
@color-green-player-progress-bar1: darken(@color-green-theme_2, 12%);
@color-green-player-progress-bar2: fadeout(lighten(@color-green-theme, 12%), 20%);
@color-green-player-status-text: lighten(@color-green-theme_2-font, 32%);
@color-green-player-detail-lyric: fadeout(@color-green-theme_2-font, 40%);
@color-green-player-detail-lyric-active: darken(@color-green-theme, 2%);
@color-green-player-detail-play-btn: lighten(@color-green-theme, 7%);
@color-green-tab-btn-background: fadeout(lighten(@color-green-theme, 10%), 80%);
@color-green-tab-btn-background-hover: @color-green-theme_2-hover;
@color-green-tab-border-top: fadeout(lighten(@color-green-theme, 5%), 50%);
@color-green-tab-border-bottom: lighten(@color-green-theme, 5%);
@color-green-hideBtn: #3bc2b2;
@color-green-minBtn: #85c43b;
@color-green-maxBtn: #e7aa36;
@color-green-closeBtn: #ea6e4d;
@color-green-hideBtn-hover: fadeout(@color-hideBtn, 10%);
@color-green-minBtn-hover: fadeout(@color-green-minBtn, 10%);
@color-green-maxBtn-hover: fadeout(@color-green-maxBtn, 10%);
@color-green-closeBtn-hover: fadeout(@color-green-closeBtn, 10%);
@color-green-badge-success: lighten(@color-green-theme, 7%);
@color-green-badge-success-font: #fff;
@color-green-badge-info: #66a2bd;
@color-green-badge-info-font: #fff;
@color-yellow-theme: #e9d460;
@color-yellow-theme-bgimg: none;
@color-yellow-theme-bgposition: center;
@color-yellow-theme-bgsize: cover;
@color-yellow-theme-hover: fadeout(lighten(@color-yellow-theme, 10%), 30%);
@color-yellow-theme-active: fadeout(darken(@color-yellow-theme, 20%), 60%);
@color-yellow-theme-font: #fff;
@color-yellow-theme-font-label: lighten(@color-yellow-theme, 35%);
@color-yellow-theme_2: #fff;
@color-yellow-theme_2-background_1: #fff;
@color-yellow-theme_2-background_2: fadeout(@color-yellow-theme_2-background_1, 2%);
@color-yellow-theme_2-hover: fadeout(lighten(@color-yellow-theme, 10%), 70%);
@color-yellow-theme_2-active: fadeout(lighten(@color-yellow-theme, 10%), 60%);
@color-yellow-theme_2-font: darken(desaturate(@color-yellow-theme_2, 70%), 90%);
@color-yellow-theme_2-font-label: fadeout(@color-yellow-theme_2-font, 55%);
// @color-yellow-theme_2-line: transparent;
@color-yellow-theme_2-line: lighten(@color-yellow-theme, 28%);
@color-yellow-reply-floor: fadeout(@color-yellow-theme, 94%);
@color-yellow-theme-sidebar: @color-yellow-theme;
@color-yellow-btn: darken(@color-yellow-theme, 5%);
@color-yellow-btn-background: fadeout(lighten(@color-yellow-theme, 25%), 60%);
@color-yellow-btn-hover: fadeout(lighten(@color-yellow-theme, 5%), 70%);
@color-yellow-btn-active: fadeout(darken(@color-yellow-theme, 5%), 70%);
@color-yellow-btn-select: fadeout(lighten(@color-yellow-theme, 5%), 50%);
@color-yellow-pagination-background: fadeout(lighten(@color-yellow-theme, 30%), 30%);
@color-yellow-pagination-hover: fadeout(lighten(@color-yellow-theme, 5%), 70%);
@color-yellow-pagination-active: fadeout(darken(@color-yellow-theme, 5%), 70%);
@color-yellow-pagination-select: fadeout(lighten(@color-yellow-theme, 5%), 50%);
@color-yellow-search-form-background: fadeout(lighten(@color-yellow-theme, 25%), 10%);
@color-yellow-search-list-hover: fadeout(darken(@color-yellow-theme, 10%), 60%);
@color-yellow-scrollbar-track: fadeout(lighten(@color-yellow-theme, 5%), 65%);
@color-yellow-scrollbar-thumb: fadeout(lighten(@color-yellow-theme, 5%), 30%);
@color-yellow-scrollbar-thumb-hover: fadeout(lighten(@color-yellow-theme, 5%), 15%);
@color-yellow-player-pic-c1: fadeout(@color-yellow-theme_2, 50%);
@color-yellow-player-pic-c2: darken(@color-yellow-theme_2, 30%);
@color-yellow-player-progress: darken(@color-yellow-theme_2, 6%);
@color-yellow-player-progress-bar1: darken(@color-yellow-theme_2, 12%);
@color-yellow-player-progress-bar2: fadeout(lighten(@color-yellow-theme, 2%), 20%);
@color-yellow-player-status-text: lighten(@color-yellow-theme_2-font, 32%);
@color-yellow-player-detail-lyric: fadeout(@color-yellow-theme_2-font, 40%);
@color-yellow-player-detail-lyric-active: darken(@color-yellow-theme, 7%);
@color-yellow-player-detail-play-btn: lighten(@color-yellow-theme, 7%);
@color-yellow-tab-btn-background: fadeout(lighten(@color-yellow-theme, 10%), 70%);
@color-yellow-tab-btn-background-hover: @color-yellow-theme_2-hover;
@color-yellow-tab-border-top: fadeout(lighten(@color-yellow-theme, 5%), 40%);
@color-yellow-tab-border-bottom: @color-yellow-theme;
@color-yellow-hideBtn: #3bc2b2;
@color-yellow-minBtn: #85c43b;
@color-yellow-maxBtn: #e7aa36;
@color-yellow-closeBtn: #ea6e4d;
@color-yellow-hideBtn-hover: fadeout(@color-hideBtn, 10%);
@color-yellow-minBtn-hover: fadeout(@color-yellow-minBtn, 10%);
@color-yellow-maxBtn-hover: fadeout(@color-yellow-maxBtn, 10%);
@color-yellow-closeBtn-hover: fadeout(@color-yellow-closeBtn, 10%);
@color-yellow-badge-success: lighten(@color-yellow-theme, 10%);
@color-yellow-badge-success-font: #fff;
@color-yellow-badge-info: #7cd295;
@color-yellow-badge-info-font: #fff;
@color-orange-theme: #f5ab35;
@color-orange-theme-bgimg: none;
@color-orange-theme-bgposition: center;
@color-orange-theme-bgsize: cover;
@color-orange-theme-hover: fadeout(lighten(@color-orange-theme, 10%), 30%);
@color-orange-theme-active: fadeout(darken(@color-orange-theme, 20%), 60%);
@color-orange-theme-font: #fff;
@color-orange-theme-font-label: lighten(@color-orange-theme, 35%);
@color-orange-theme_2: #fff;
@color-orange-theme_2-background_1: #fff;
@color-orange-theme_2-background_2: fadeout(@color-orange-theme_2-background_1, 2%);
@color-orange-theme_2-hover: fadeout(darken(@color-orange-theme, 5%), 80%);
@color-orange-theme_2-active: fadeout(darken(@color-orange-theme, 5%), 70%);
@color-orange-theme_2-font: darken(desaturate(@color-orange-theme_2, 70%), 90%);
@color-orange-theme_2-font-label: fadeout(@color-orange-theme_2-font, 55%);
// @color-orange-theme_2-line: transparent;
@color-orange-theme_2-line: lighten(@color-orange-theme, 36%);
@color-orange-reply-floor: fadeout(@color-orange-theme, 95%);
@color-orange-theme-sidebar: @color-orange-theme;
@color-orange-btn: lighten(@color-orange-theme, 5%);
@color-orange-btn-background: fadeout(lighten(@color-orange-theme, 25%), 60%);
@color-orange-btn-hover: fadeout(lighten(@color-orange-theme, 5%), 70%);
@color-orange-btn-active: fadeout(darken(@color-orange-theme, 5%), 70%);
@color-orange-btn-select: fadeout(lighten(@color-orange-theme, 5%), 50%);
@color-orange-pagination-background: fadeout(lighten(@color-orange-theme, 25%), 40%);
@color-orange-pagination-hover: fadeout(lighten(@color-orange-theme, 10%), 70%);
@color-orange-pagination-active: fadeout(darken(@color-orange-theme, 10%), 70%);
@color-orange-pagination-select: fadeout(lighten(@color-orange-theme, 10%), 50%);
@color-orange-search-form-background: fadeout(lighten(@color-orange-theme, 30%), 10%);
@color-orange-search-list-hover: fadeout(darken(@color-orange-theme, 10%), 70%);
@color-orange-scrollbar-track: fadeout(lighten(@color-orange-theme, 10%), 75%);
@color-orange-scrollbar-thumb: fadeout(lighten(@color-orange-theme, 10%), 45%);
@color-orange-scrollbar-thumb-hover: fadeout(lighten(@color-orange-theme, 10%), 25%);
@color-orange-player-pic-c1: fadeout(@color-orange-theme_2, 50%);
@color-orange-player-pic-c2: darken(@color-orange-theme_2, 30%);
@color-orange-player-progress: darken(@color-orange-theme_2, 6%);
@color-orange-player-progress-bar1: darken(@color-orange-theme_2, 12%);
@color-orange-player-progress-bar2: fadeout(lighten(@color-orange-theme, 12%), 20%);
@color-orange-player-status-text: lighten(@color-orange-theme_2-font, 32%);
@color-orange-player-detail-lyric: fadeout(@color-orange-theme_2-font, 40%);
@color-orange-player-detail-lyric-active: darken(@color-orange-theme, 7%);
@color-orange-player-detail-play-btn: lighten(@color-orange-theme, 7%);
@color-orange-tab-btn-background: fadeout(lighten(@color-orange-theme, 10%), 80%);
@color-orange-tab-btn-background-hover: @color-orange-theme_2-hover;
@color-orange-tab-border-top: fadeout(lighten(@color-orange-theme, 5%), 50%);
@color-orange-tab-border-bottom: lighten(@color-orange-theme, 5%);
@color-orange-hideBtn: #3bc2b2;
@color-orange-minBtn: #85c43b;
@color-orange-maxBtn: #e7aa36;
@color-orange-closeBtn: #ea6e4d;
@color-orange-hideBtn-hover: fadeout(@color-hideBtn, 10%);
@color-orange-minBtn-hover: fadeout(@color-orange-minBtn, 10%);
@color-orange-maxBtn-hover: fadeout(@color-orange-maxBtn, 10%);
@color-orange-closeBtn-hover: fadeout(@color-orange-closeBtn, 10%);
@color-orange-badge-success: lighten(@color-orange-theme, 10%);
@color-orange-badge-success-font: #fff;
@color-orange-badge-info: #9ed458;
@color-orange-badge-info-font: #fff;
@color-blue-theme: #3498db;
@color-blue-theme-bgimg: none;
@color-blue-theme-bgposition: center;
@color-blue-theme-bgsize: cover;
@color-blue-theme-hover: fadeout(lighten(@color-blue-theme, 10%), 30%);
@color-blue-theme-active: fadeout(darken(@color-blue-theme, 20%), 60%);
@color-blue-theme-font: #fff;
@color-blue-theme-font-label: lighten(@color-blue-theme, 35%);
@color-blue-theme_2: #fff;
@color-blue-theme_2-background_1: #fff;
@color-blue-theme_2-background_2: fadeout(@color-blue-theme_2-background_1, 2%);
@color-blue-theme_2-hover: fadeout(lighten(@color-blue-theme, 10%), 80%);
@color-blue-theme_2-active: fadeout(lighten(@color-blue-theme, 10%), 70%);
@color-blue-theme_2-font: darken(desaturate(@color-blue-theme_2, 70%), 90%);
@color-blue-theme_2-font-label: fadeout(@color-blue-theme_2-font, 55%);
// @color-blue-theme_2-line: transparent;
@color-blue-theme_2-line: lighten(@color-blue-theme, 42%);
@color-blue-reply-floor: fadeout(@color-blue-theme, 95%);
@color-blue-theme-sidebar: @color-blue-theme;
@color-blue-btn: lighten(@color-blue-theme, 5%);
@color-blue-btn-background: fadeout(lighten(@color-blue-theme, 35%), 50%);
@color-blue-btn-hover: fadeout(lighten(@color-blue-theme, 5%), 70%);
@color-blue-btn-active: fadeout(darken(@color-blue-theme, 5%), 70%);
@color-blue-btn-select: fadeout(lighten(@color-blue-theme, 5%), 50%);
@color-blue-pagination-background: fadeout(lighten(@color-blue-theme, 40%), 30%);
@color-blue-pagination-hover: fadeout(lighten(@color-blue-theme, 15%), 70%);
@color-blue-pagination-active: fadeout(darken(@color-blue-theme, 15%), 70%);
@color-blue-pagination-select: fadeout(lighten(@color-blue-theme, 15%), 50%);
@color-blue-search-form-background: fadeout(lighten(@color-blue-theme, 35%), 10%);
@color-blue-search-list-hover: fadeout(darken(@color-blue-theme, 10%), 70%);
@color-blue-scrollbar-track: fadeout(lighten(@color-blue-theme, 10%), 75%);
@color-blue-scrollbar-thumb: fadeout(lighten(@color-blue-theme, 10%), 50%);
@color-blue-scrollbar-thumb-hover: fadeout(lighten(@color-blue-theme, 10%), 35%);
@color-blue-player-pic-c1: fadeout(@color-blue-theme_2, 50%);
@color-blue-player-pic-c2: darken(@color-blue-theme_2, 30%);
@color-blue-player-progress: darken(@color-blue-theme_2, 6%);
@color-blue-player-progress-bar1: darken(@color-blue-theme_2, 12%);
@color-blue-player-progress-bar2: fadeout(lighten(@color-blue-theme, 12%), 20%);
@color-blue-player-status-text: lighten(@color-blue-theme_2-font, 32%);
@color-blue-player-detail-lyric: fadeout(@color-blue-theme_2-font, 40%);
@color-blue-player-detail-lyric-active: darken(@color-blue-theme, 2%);
@color-blue-player-detail-play-btn: lighten(@color-blue-theme, 7%);
@color-blue-tab-btn-background: fadeout(lighten(@color-blue-theme, 10%), 80%);
@color-blue-tab-btn-background-hover: @color-blue-theme_2-hover;
@color-blue-tab-border-top: fadeout(lighten(@color-blue-theme, 5%), 50%);
@color-blue-tab-border-bottom: lighten(@color-blue-theme, 5%);
@color-blue-hideBtn: #3bc2b2;
@color-blue-minBtn: #85c43b;
@color-blue-maxBtn: #e7aa36;
@color-blue-closeBtn: #ea6e4d;
@color-blue-hideBtn-hover: fadeout(@color-hideBtn, 10%);
@color-blue-minBtn-hover: fadeout(@color-blue-minBtn, 10%);
@color-blue-maxBtn-hover: fadeout(@color-blue-maxBtn, 10%);
@color-blue-closeBtn-hover: fadeout(@color-blue-closeBtn, 10%);
@color-blue-badge-success: #5cbf9b;
@color-blue-badge-success-font: #fff;
@color-blue-badge-info: lighten(@color-blue-theme, 10%);
@color-blue-badge-info-font: #fff;
@color-red-theme: #d64541;
@color-red-theme-bgimg: none;
@color-red-theme-bgposition: center;
@color-red-theme-bgsize: cover;
@color-red-theme-hover: fadeout(lighten(@color-red-theme, 10%), 30%);
@color-red-theme-active: fadeout(darken(@color-red-theme, 20%), 60%);
@color-red-theme-font: #fff;
@color-red-theme-font-label: lighten(@color-red-theme, 35%);
@color-red-theme_2: #fff;
@color-red-theme_2-hover: fadeout(lighten(@color-red-theme, 10%), 80%);
@color-red-theme_2-active: fadeout(lighten(@color-red-theme, 10%), 70%);
@color-red-theme_2-background_1: #fff;
@color-red-theme_2-background_2: fadeout(@color-red-theme_2-background_1, 2%);
@color-red-theme_2-font: darken(desaturate(@color-red-theme_2, 70%), 90%);
@color-red-theme_2-font-label: fadeout(@color-red-theme_2-font, 55%);
// @color-red-theme_2-line: transparent;
@color-red-theme_2-line: lighten(@color-red-theme, 42%);
@color-red-reply-floor: fadeout(@color-red-theme, 95%);
@color-red-theme-sidebar: @color-red-theme;
@color-red-btn: lighten(@color-red-theme, 5%);
@color-red-btn-background: fadeout(lighten(@color-red-theme, 35%), 70%);
@color-red-btn-hover: fadeout(lighten(@color-red-theme, 5%), 70%);
@color-red-btn-active: fadeout(darken(@color-red-theme, 5%), 70%);
@color-red-btn-select: fadeout(lighten(@color-red-theme, 5%), 50%);
@color-red-pagination-background: fadeout(lighten(@color-red-theme, 40%), 30%);
@color-red-pagination-hover: fadeout(lighten(@color-red-theme, 15%), 70%);
@color-red-pagination-active: fadeout(darken(@color-red-theme, 15%), 70%);
@color-red-pagination-select: fadeout(lighten(@color-red-theme, 15%), 50%);
@color-red-search-form-background: fadeout(lighten(@color-red-theme, 35%), 10%);
@color-red-search-list-hover: fadeout(darken(@color-red-theme, 10%), 70%);
@color-red-scrollbar-track: fadeout(lighten(@color-red-theme, 10%), 75%);
@color-red-scrollbar-thumb: fadeout(lighten(@color-red-theme, 10%), 45%);
@color-red-scrollbar-thumb-hover: fadeout(lighten(@color-red-theme, 10%), 25%);
@color-red-player-pic-c1: fadeout(@color-red-theme_2, 50%);
@color-red-player-pic-c2: darken(@color-red-theme_2, 30%);
@color-red-player-progress: darken(@color-red-theme_2, 6%);
@color-red-player-progress-bar1: darken(@color-red-theme_2, 12%);
@color-red-player-progress-bar2: fadeout(lighten(@color-red-theme, 12%), 20%);
@color-red-player-status-text: lighten(@color-red-theme_2-font, 32%);
@color-red-player-detail-lyric: fadeout(@color-red-theme_2-font, 40%);
@color-red-player-detail-lyric-active: lighten(@color-red-theme, 2%);
@color-red-player-detail-play-btn: lighten(@color-red-theme, 7%);
@color-red-tab-border-top: fadeout(lighten(@color-red-theme, 25%), 70%);
@color-red-tab-border-bottom: lighten(@color-red-theme, 35%);
@color-red-tab-btn-background: fadeout(lighten(@color-red-theme, 10%), 80%);
@color-red-tab-btn-background-hover: @color-red-theme_2-hover;
@color-red-tab-border-top: fadeout(lighten(@color-red-theme, 5%), 50%);
@color-red-tab-border-bottom: lighten(@color-red-theme, 5%);
@color-red-hideBtn: #3bc2b2;
@color-red-minBtn: #85c43b;
@color-red-maxBtn: #e7aa36;
@color-red-closeBtn: #ea6e4d;
@color-red-hideBtn-hover: fadeout(@color-hideBtn, 10%);
@color-red-minBtn-hover: fadeout(@color-red-minBtn, 10%);
@color-red-maxBtn-hover: fadeout(@color-red-maxBtn, 10%);
@color-red-closeBtn-hover: fadeout(@color-red-closeBtn, 10%);
@color-red-badge-success: lighten(@color-red-theme, 10%);
@color-red-badge-success-font: #fff;
@color-red-badge-info: #dfbb6b;
@color-red-badge-info-font: #fff;
@color-pink-theme: #f1828d;
@color-pink-theme-bgimg: none;
@color-pink-theme-bgposition: center;
@color-pink-theme-bgsize: cover;
@color-pink-theme-hover: fadeout(lighten(@color-pink-theme, 10%), 30%);
@color-pink-theme-active: fadeout(darken(@color-pink-theme, 20%), 60%);
@color-pink-theme-font: #fff;
@color-pink-theme-font-label: lighten(@color-pink-theme, 35%);
@color-pink-theme_2: #fff;
@color-pink-theme_2-background_1: #fff;
@color-pink-theme_2-background_2: fadeout(@color-pink-theme_2-background_1, 2%);
@color-pink-theme_2-hover: fadeout(lighten(@color-pink-theme, 10%), 75%);
@color-pink-theme_2-active: fadeout(lighten(@color-pink-theme, 10%), 60%);
@color-pink-theme_2-font: darken(desaturate(@color-pink-theme_2, 70%), 90%);
@color-pink-theme_2-font-label: fadeout(@color-pink-theme_2-font, 55%);
// @color-pink-theme_2-line: transparent;
@color-pink-theme_2-line: lighten(@color-pink-theme, 25%);
@color-pink-reply-floor: fadeout(@color-pink-theme, 93%);
@color-pink-theme-sidebar: @color-pink-theme;
@color-pink-btn: darken(@color-pink-theme, 3%);
@color-pink-btn-background: fadeout(lighten(@color-pink-theme, 20%), 50%);
@color-pink-btn-hover: fadeout(lighten(@color-pink-theme, 5%), 70%);
@color-pink-btn-active: fadeout(darken(@color-pink-theme, 5%), 70%);
@color-pink-btn-select: fadeout(lighten(@color-pink-theme, 5%), 50%);
@color-pink-pagination-background: fadeout(lighten(@color-pink-theme, 23%), 25%);
@color-pink-pagination-hover: fadeout(lighten(@color-pink-theme, 5%), 70%);
@color-pink-pagination-active: fadeout(darken(@color-pink-theme, 5%), 70%);
@color-pink-pagination-select: fadeout(lighten(@color-pink-theme, 5%), 50%);
@color-pink-search-form-background: fadeout(lighten(@color-pink-theme, 20%), 10%);
@color-pink-search-list-hover: fadeout(darken(@color-pink-theme, 10%), 60%);
@color-pink-scrollbar-track: fadeout(lighten(@color-pink-theme, 6%), 75%);
@color-pink-scrollbar-thumb: fadeout(lighten(@color-pink-theme, 6%), 35%);
@color-pink-scrollbar-thumb-hover: fadeout(lighten(@color-pink-theme, 6%), 20%);
@color-pink-player-pic-c1: fadeout(@color-pink-theme_2, 50%);
@color-pink-player-pic-c2: darken(@color-pink-theme_2, 30%);
@color-pink-player-progress: darken(@color-pink-theme_2, 6%);
@color-pink-player-progress-bar1: darken(@color-pink-theme_2, 12%);
@color-pink-player-progress-bar2: fadeout(lighten(@color-pink-theme, 2%), 20%);
@color-pink-player-status-text: lighten(@color-pink-theme_2-font, 32%);
@color-pink-player-detail-lyric: fadeout(@color-pink-theme_2-font, 40%);
@color-pink-player-detail-lyric-active: darken(@color-pink-theme, 2%);
@color-pink-player-detail-play-btn: lighten(@color-pink-theme, 7%);
@color-pink-tab-btn-background: fadeout(lighten(@color-pink-theme, 10%), 70%);
@color-pink-tab-btn-background-hover: @color-pink-theme_2-hover;
@color-pink-tab-border-top: fadeout(lighten(@color-pink-theme, 5%), 40%);
@color-pink-tab-border-bottom: @color-pink-theme;
@color-pink-hideBtn: #3bc2b2;
@color-pink-minBtn: #85c43b;
@color-pink-maxBtn: #e7aa36;
@color-pink-closeBtn: #ea6e4d;
@color-pink-hideBtn-hover: fadeout(@color-hideBtn, 10%);
@color-pink-minBtn-hover: fadeout(@color-pink-minBtn, 10%);
@color-pink-maxBtn-hover: fadeout(@color-pink-maxBtn, 10%);
@color-pink-closeBtn-hover: fadeout(@color-pink-closeBtn, 10%);
@color-pink-badge-success: lighten(@color-pink-theme, 10%);
@color-pink-badge-success-font: #fff;
@color-pink-badge-info: #f5b684;
@color-pink-badge-info-font: #fff;
@color-purple-theme: #9b59b6;
@color-purple-theme-bgimg: none;
@color-purple-theme-bgposition: center;
@color-purple-theme-bgsize: cover;
@color-purple-theme-hover: fadeout(lighten(@color-purple-theme, 10%), 30%);
@color-purple-theme-active: fadeout(darken(@color-purple-theme, 20%), 60%);
@color-purple-theme-font: #fff;
@color-purple-theme-font-label: lighten(@color-purple-theme, 35%);
@color-purple-theme_2: #fff;
@color-purple-theme_2-background_1: #fff;
@color-purple-theme_2-background_2: fadeout(@color-purple-theme_2-background_1, 2%);
@color-purple-theme_2-hover: fadeout(lighten(@color-purple-theme, 10%), 80%);
@color-purple-theme_2-active: fadeout(lighten(@color-purple-theme, 10%), 70%);
@color-purple-theme_2-font: darken(desaturate(@color-purple-theme_2, 70%), 90%);
@color-purple-theme_2-font-label: fadeout(@color-purple-theme_2-font, 55%);
// @color-purple-theme_2-line: transparent;
@color-purple-theme_2-line: lighten(@color-purple-theme, 43%);
@color-purple-reply-floor: fadeout(@color-purple-theme, 95%);
@color-purple-theme-sidebar: @color-purple-theme;
@color-purple-btn: lighten(@color-purple-theme, 5%);
@color-purple-btn-background: fadeout(lighten(@color-purple-theme, 35%), 70%);
@color-purple-btn-hover: fadeout(lighten(@color-purple-theme, 5%), 70%);
@color-purple-btn-active: fadeout(darken(@color-purple-theme, 5%), 70%);
@color-purple-btn-select: fadeout(lighten(@color-purple-theme, 5%), 50%);
@color-purple-pagination-background: fadeout(lighten(@color-purple-theme, 40%), 30%);
@color-purple-pagination-hover: fadeout(lighten(@color-purple-theme, 15%), 70%);
@color-purple-pagination-active: fadeout(darken(@color-purple-theme, 15%), 70%);
@color-purple-pagination-select: fadeout(lighten(@color-purple-theme, 15%), 50%);
@color-purple-search-form-background: fadeout(lighten(@color-purple-theme, 35%), 10%);
@color-purple-search-list-hover: fadeout(darken(@color-purple-theme, 10%), 70%);
@color-purple-scrollbar-track: fadeout(lighten(@color-purple-theme, 10%), 77%);
@color-purple-scrollbar-thumb: fadeout(lighten(@color-purple-theme, 10%), 50%);
@color-purple-scrollbar-thumb-hover: fadeout(lighten(@color-purple-theme, 10%), 35%);
@color-purple-player-pic-c1: fadeout(@color-purple-theme_2, 50%);
@color-purple-player-pic-c2: darken(@color-purple-theme_2, 30%);
@color-purple-player-progress: darken(@color-purple-theme_2, 6%);
@color-purple-player-progress-bar1: darken(@color-purple-theme_2, 12%);
@color-purple-player-progress-bar2: fadeout(lighten(@color-purple-theme, 12%), 20%);
@color-purple-player-status-text: lighten(@color-purple-theme_2-font, 32%);
@color-purple-player-detail-lyric: fadeout(@color-purple-theme_2-font, 40%);
@color-purple-player-detail-lyric-active: darken(@color-purple-theme, 2%);
@color-purple-player-detail-play-btn: lighten(@color-purple-theme, 7%);
@color-purple-tab-btn-background: fadeout(lighten(@color-purple-theme, 10%), 80%);
@color-purple-tab-btn-background-hover: @color-purple-theme_2-hover;
@color-purple-tab-border-top: fadeout(lighten(@color-purple-theme, 5%), 50%);
@color-purple-tab-border-bottom: lighten(@color-purple-theme, 5%);
@color-purple-hideBtn: #3bc2b2;
@color-purple-minBtn: #85c43b;
@color-purple-maxBtn: #e7aa36;
@color-purple-closeBtn: #ea6e4d;
@color-purple-hideBtn-hover: fadeout(@color-hideBtn, 10%);
@color-purple-minBtn-hover: fadeout(@color-purple-minBtn, 10%);
@color-purple-maxBtn-hover: fadeout(@color-purple-maxBtn, 10%);
@color-purple-closeBtn-hover: fadeout(@color-purple-closeBtn, 10%);
@color-purple-badge-success: lighten(@color-purple-theme, 15%);
@color-purple-badge-success-font: #fff;
@color-purple-badge-info: #e5a39f;
@color-purple-badge-info-font: #fff;
@color-grey-theme: #6c7a89;
@color-grey-theme-bgimg: none;
@color-grey-theme-bgposition: center;
@color-grey-theme-bgsize: cover;
@color-grey-theme-hover: fadeout(lighten(@color-grey-theme, 10%), 30%);
@color-grey-theme-active: fadeout(darken(@color-grey-theme, 20%), 60%);
@color-grey-theme-font: #fff;
@color-grey-theme-font-label: lighten(@color-grey-theme, 35%);
@color-grey-theme_2: #fff;
@color-grey-theme_2-background_1: #fff;
@color-grey-theme_2-background_2: fadeout(@color-grey-theme_2-background_1, 2%);
@color-grey-theme_2-hover: fadeout(lighten(@color-grey-theme, 10%), 80%);
@color-grey-theme_2-active: fadeout(lighten(@color-grey-theme, 10%), 70%);
@color-grey-theme_2-font: darken(desaturate(@color-grey-theme_2, 70%), 90%);
@color-grey-theme_2-font-label: fadeout(@color-grey-theme_2-font, 55%);
// @color-grey-theme_2-line: transparent;
@color-grey-theme_2-line: lighten(@color-grey-theme, 47%);
@color-grey-reply-floor: fadeout(@color-grey-theme, 95%);
@color-grey-theme-sidebar: @color-grey-theme;
@color-grey-btn: lighten(@color-grey-theme, 5%);
@color-grey-btn-background: fadeout(lighten(@color-grey-theme, 35%), 70%);
@color-grey-btn-hover: fadeout(lighten(@color-grey-theme, 5%), 70%);
@color-grey-btn-active: fadeout(darken(@color-grey-theme, 5%), 70%);
@color-grey-btn-select: fadeout(lighten(@color-grey-theme, 5%), 50%);
@color-grey-pagination-background: fadeout(lighten(@color-grey-theme, 45%), 30%);
@color-grey-pagination-hover: fadeout(lighten(@color-grey-theme, 10%), 70%);
@color-grey-pagination-active: fadeout(darken(@color-grey-theme, 10%), 70%);
@color-grey-pagination-select: fadeout(lighten(@color-grey-theme, 10%), 50%);
@color-grey-search-form-background: fadeout(lighten(@color-grey-theme, 35%), 10%);
@color-grey-search-list-hover: fadeout(darken(@color-grey-theme, 10%), 70%);
@color-grey-scrollbar-track: fadeout(lighten(@color-grey-theme, 10%), 75%);
@color-grey-scrollbar-thumb: fadeout(lighten(@color-grey-theme, 10%), 50%);
@color-grey-scrollbar-thumb-hover: fadeout(lighten(@color-grey-theme, 10%), 35%);
@color-grey-player-pic-c1: fadeout(@color-grey-theme_2, 50%);
@color-grey-player-pic-c2: darken(@color-grey-theme_2, 30%);
@color-grey-player-progress: darken(@color-grey-theme_2, 6%);
@color-grey-player-progress-bar1: darken(@color-grey-theme_2, 12%);
@color-grey-player-progress-bar2: fadeout(lighten(@color-grey-theme, 12%), 20%);
@color-grey-player-status-text: lighten(@color-grey-theme_2-font, 32%);
@color-grey-player-detail-lyric: fadeout(@color-grey-theme_2-font, 55%);
@color-grey-player-detail-lyric-active: darken(@color-grey-theme, 3%);
@color-grey-player-detail-play-btn: lighten(@color-grey-theme, 7%);
@color-grey-tab-btn-background: fadeout(lighten(@color-grey-theme, 10%), 80%);
@color-grey-tab-btn-background-hover: @color-grey-theme_2-hover;
@color-grey-tab-border-top: fadeout(lighten(@color-grey-theme, 5%), 50%);
@color-grey-tab-border-bottom: lighten(@color-grey-theme, 5%);
@color-grey-hideBtn: #3bc2b2;
@color-grey-minBtn: #85c43b;
@color-grey-maxBtn: #e7aa36;
@color-grey-closeBtn: #ea6e4d;
@color-grey-hideBtn-hover: fadeout(@color-hideBtn, 10%);
@color-grey-minBtn-hover: fadeout(@color-grey-minBtn, 10%);
@color-grey-maxBtn-hover: fadeout(@color-grey-maxBtn, 10%);
@color-grey-closeBtn-hover: fadeout(@color-grey-closeBtn, 10%);
@color-grey-badge-success: lighten(@color-grey-theme, 15%);
@color-grey-badge-success-font: #fff;
@color-grey-badge-info: #b19b9f;
@color-grey-badge-info-font: #fff;
@color-ming-theme: #336e7b;
@color-ming-theme-bgimg: none;
@color-ming-theme-bgposition: center;
@color-ming-theme-bgsize: cover;
@color-ming-theme-hover: fadeout(lighten(@color-ming-theme, 7%), 30%);
@color-ming-theme-active: fadeout(darken(@color-ming-theme, 20%), 60%);
@color-ming-theme-font: #fff;
@color-ming-theme-font-label: lighten(@color-ming-theme, 35%);
@color-ming-theme_2: #fff;
@color-ming-theme_2-hover: fadeout(lighten(@color-ming-theme, 10%), 85%);
@color-ming-theme_2-active: fadeout(lighten(@color-ming-theme, 10%), 75%);
@color-ming-theme_2-background_1: #fff;
@color-ming-theme_2-background_2: fadeout(@color-ming-theme_2-background_1, 2%);
@color-ming-theme_2-font: darken(desaturate(@color-ming-theme_2, 70%), 90%);
@color-ming-theme_2-font-label: fadeout(@color-ming-theme_2-font, 55%);
// @color-ming-theme_2-line: transparent;
@color-ming-theme_2-line: lighten(@color-ming-theme, 60%);
@color-ming-reply-floor: fadeout(@color-ming-theme, 95%);
@color-ming-theme-sidebar: @color-ming-theme;
@color-ming-btn: lighten(@color-ming-theme, 5%);
@color-ming-btn-background: fadeout(lighten(@color-ming-theme, 35%), 75%);
@color-ming-btn-hover: fadeout(lighten(@color-ming-theme, 5%), 70%);
@color-ming-btn-active: fadeout(darken(@color-ming-theme, 5%), 70%);
@color-ming-btn-select: fadeout(lighten(@color-ming-theme, 5%), 50%);
@color-ming-pagination-background: fadeout(lighten(@color-ming-theme, 50%), 35%);
@color-ming-pagination-hover: fadeout(lighten(@color-ming-theme, 15%), 70%);
@color-ming-pagination-active: fadeout(darken(@color-ming-theme, 15%), 70%);
@color-ming-pagination-select: fadeout(lighten(@color-ming-theme, 15%), 50%);
@color-ming-search-form-background: fadeout(lighten(@color-ming-theme, 45%), 15%);
@color-ming-search-list-hover: fadeout(darken(@color-ming-theme, 10%), 70%);
@color-ming-scrollbar-track: fadeout(lighten(@color-ming-theme, 10%), 75%);
@color-ming-scrollbar-thumb: fadeout(lighten(@color-ming-theme, 10%), 50%);
@color-ming-scrollbar-thumb-hover: fadeout(lighten(@color-ming-theme, 10%), 35%);
@color-ming-player-pic-c1: fadeout(@color-ming-theme_2, 50%);
@color-ming-player-pic-c2: darken(@color-ming-theme_2, 30%);
@color-ming-player-progress: darken(@color-ming-theme_2, 6%);
@color-ming-player-progress-bar1: darken(@color-ming-theme_2, 12%);
@color-ming-player-progress-bar2: fadeout(lighten(@color-ming-theme, 12%), 20%);
@color-ming-player-status-text: lighten(@color-ming-theme_2-font, 32%);
@color-ming-player-detail-lyric: fadeout(@color-ming-theme_2-font, 50%);
@color-ming-player-detail-lyric-active: lighten(@color-ming-theme, 2%);
@color-ming-player-detail-play-btn: lighten(@color-ming-theme, 10%);
@color-ming-tab-border-top: fadeout(lighten(@color-ming-theme, 25%), 70%);
@color-ming-tab-border-bottom: lighten(@color-ming-theme, 35%);
@color-ming-tab-btn-background: fadeout(lighten(@color-ming-theme, 10%), 80%);
@color-ming-tab-btn-background-hover: @color-ming-theme_2-hover;
@color-ming-tab-border-top: fadeout(lighten(@color-ming-theme, 5%), 50%);
@color-ming-tab-border-bottom: lighten(@color-ming-theme, 5%);
@color-ming-hideBtn: #3bc2b2;
@color-ming-minBtn: #85c43b;
@color-ming-maxBtn: #e7aa36;
@color-ming-closeBtn: #ea6e4d;
@color-ming-hideBtn-hover: fadeout(@color-hideBtn, 10%);
@color-ming-minBtn-hover: fadeout(@color-ming-minBtn, 10%);
@color-ming-maxBtn-hover: fadeout(@color-ming-maxBtn, 10%);
@color-ming-closeBtn-hover: fadeout(@color-ming-closeBtn, 10%);
@color-ming-badge-success: lighten(@color-ming-theme, 10%);
@color-ming-badge-success-font: #fff;
@color-ming-badge-info: #6376a2;
@color-ming-badge-info-font: #fff;
@color-blue2-theme: #4f62d0;
@color-blue2-theme-bgimg: none;
@color-blue2-theme-bgposition: center;
@color-blue2-theme-bgsize: cover;
@color-blue2-theme-hover: fadeout(lighten(@color-blue2-theme, 10%), 30%);
@color-blue2-theme-active: fadeout(darken(@color-blue2-theme, 20%), 60%);
@color-blue2-theme-font: #fff;
@color-blue2-theme-font-label: lighten(@color-blue2-theme, 35%);
@color-blue2-theme_2: #fff;
@color-blue2-theme_2-background_1: #fff;
@color-blue2-theme_2-background_2: fadeout(@color-blue2-theme_2-background_1, 2%);
@color-blue2-theme_2-hover: fadeout(lighten(@color-blue2-theme, 10%), 80%);
@color-blue2-theme_2-active: fadeout(lighten(@color-blue2-theme, 10%), 70%);
@color-blue2-theme_2-font: darken(desaturate(@color-blue2-theme_2, 70%), 90%);
@color-blue2-theme_2-font-label: fadeout(@color-blue2-theme_2-font, 55%);
// @color-blue2-theme_2-line: transparent;
@color-blue2-theme_2-line: lighten(@color-blue2-theme, 41%);
@color-blue2-reply-floor: fadeout(@color-blue2-theme, 95%);
@color-blue2-theme-sidebar: @color-blue2-theme;
@color-blue2-btn: lighten(@color-blue2-theme, 5%);
@color-blue2-btn-background: fadeout(lighten(@color-blue2-theme, 30%), 70%);
@color-blue2-btn-hover: fadeout(lighten(@color-blue2-theme, 5%), 70%);
@color-blue2-btn-active: fadeout(darken(@color-blue2-theme, 5%), 70%);
@color-blue2-btn-select: fadeout(lighten(@color-blue2-theme, 5%), 50%);
@color-blue2-pagination-background: fadeout(lighten(@color-blue2-theme, 35%), 30%);
@color-blue2-pagination-hover: fadeout(lighten(@color-blue2-theme, 10%), 70%);
@color-blue2-pagination-active: fadeout(darken(@color-blue2-theme, 10%), 70%);
@color-blue2-pagination-select: fadeout(lighten(@color-blue2-theme, 10%), 50%);
@color-blue2-search-form-background: fadeout(lighten(@color-blue2-theme, 35%), 10%);
@color-blue2-search-list-hover: fadeout(darken(@color-blue2-theme, 10%), 70%);
@color-blue2-scrollbar-track: fadeout(lighten(@color-blue2-theme, 10%), 75%);
@color-blue2-scrollbar-thumb: fadeout(lighten(@color-blue2-theme, 10%), 50%);
@color-blue2-scrollbar-thumb-hover: fadeout(lighten(@color-blue2-theme, 10%), 35%);
@color-blue2-player-pic-c1: fadeout(@color-blue2-theme_2, 50%);
@color-blue2-player-pic-c2: darken(@color-blue2-theme_2, 30%);
@color-blue2-player-progress: darken(@color-blue2-theme_2, 6%);
@color-blue2-player-progress-bar1: darken(@color-blue2-theme_2, 12%);
@color-blue2-player-progress-bar2: fadeout(lighten(@color-blue2-theme, 12%), 20%);
@color-blue2-player-status-text: lighten(@color-blue2-theme_2-font, 32%);
@color-blue2-player-detail-lyric: fadeout(@color-blue2-theme_2-font, 50%);
@color-blue2-player-detail-lyric-active: darken(@color-blue2-theme, 2%);
@color-blue2-player-detail-play-btn: lighten(@color-blue2-theme, 7%);
@color-blue2-tab-btn-background: fadeout(lighten(@color-blue2-theme, 10%), 80%);
@color-blue2-tab-btn-background-hover: @color-blue2-theme_2-hover;
@color-blue2-tab-border-top: fadeout(lighten(@color-blue2-theme, 5%), 50%);
@color-blue2-tab-border-bottom: lighten(@color-blue2-theme, 5%);
@color-blue2-hideBtn: #3bc2b2;
@color-blue2-minBtn: #85c43b;
@color-blue2-maxBtn: #e7aa36;
@color-blue2-closeBtn: #ea6e4d;
@color-blue2-hideBtn-hover: fadeout(@color-hideBtn, 10%);
@color-blue2-minBtn-hover: fadeout(@color-blue2-minBtn, 10%);
@color-blue2-maxBtn-hover: fadeout(@color-blue2-maxBtn, 10%);
@color-blue2-closeBtn-hover: fadeout(@color-blue2-closeBtn, 10%);
@color-blue2-badge-success: lighten(@color-blue2-theme, 15%);
@color-blue2-badge-success-font: #fff;
@color-blue2-badge-info: #b080db;
@color-blue2-badge-info-font: #fff;
@color-black-theme: #272727;
@color-black-theme-bgimg: url(../images/landingMoon.png);
@color-black-theme-bgposition: center;
@color-black-theme-bgsize: cover;
@color-black-theme-hover: fadeout(lighten(@color-black-theme, 13%), 30%);
@color-black-theme-active: fadeout(lighten(@color-black-theme, 8%), 30%);
@color-black-theme-font: lighten(@color-black-theme, 55%);
@color-black-theme-font-label: lighten(@color-black-theme, 35%);
@color-black-theme_2: rgba(19, 19, 19, 0.93);
@color-black-theme_2-background_1: #080808;
@color-black-theme_2-background_2: #1f1f1f;
@color-black-theme_2-hover: fadeout(lighten(@color-black-theme, 10%), 80%);
@color-black-theme_2-active: fadeout(lighten(@color-black-theme, 10%), 70%);
@color-black-theme_2-font: lighten(@color-black-theme_2, 73%);
@color-black-theme_2-font-label: darken(@color-black-theme_2-font, 30%);
// @color-black-theme_2-line: transparent;
@color-black-theme_2-line: lighten(@color-black-theme_2, 3.2%);
@color-black-reply-floor: fadeout(lighten(@color-black-theme, 60%), 95%);
@color-black-theme-sidebar: @color-black-theme;
@color-black-btn: lighten(@color-black-theme, 40%);
@color-black-btn-background: fadeout(lighten(@color-black-theme, 30%), 70%);
@color-black-btn-hover: fadeout(lighten(@color-black-theme, 40%), 70%);
@color-black-btn-active: fadeout(lighten(@color-black-theme, 20%), 70%);
@color-black-btn-select: fadeout(lighten(@color-black-theme, 25%), 70%);
@color-black-pagination-background: fadeout(lighten(@color-black-theme, 25%), 20%);
@color-black-pagination-hover: fadeout(lighten(@color-black-theme, 1%), 70%);
@color-black-pagination-active: fadeout(darken(@color-black-theme, 5%), 60%);
@color-black-pagination-select: fadeout(darken(@color-black-theme, 3%), 60%);
@color-black-search-form-background: fadeout(lighten(@color-black-theme, 8%), 6%);
@color-black-search-list-hover: fadeout(darken(@color-black-theme, 10%), 70%);
@color-black-scrollbar-track: fadeout(lighten(@color-black-theme, 10%), 75%);
@color-black-scrollbar-thumb: fadeout(lighten(@color-black-theme, 10%), 50%);
@color-black-scrollbar-thumb-hover: fadeout(lighten(@color-black-theme, 10%), 35%);
@color-black-player-pic-c1: fadeout(@color-black-theme_2, 50%);
@color-black-player-pic-c2: lighten(@color-black-theme_2, 30%);
@color-black-player-progress: fadeout(lighten(@color-black-theme, 20%), 80%);
@color-black-player-progress-bar1: lighten(@color-black-theme_2, 12%);
@color-black-player-progress-bar2: fadeout(lighten(@color-black-theme, 12%), 20%);
@color-black-player-status-text: darken(@color-black-theme_2-font, 20%);
@color-black-player-detail-lyric: fadeout(darken(@color-black-theme_2-font, 30%), 10%);
@color-black-player-detail-lyric-active: lighten(@color-black-theme, 50%);
@color-black-player-detail-play-btn: lighten(@color-black-theme, 30%);
@color-black-tab-btn-background: fadeout(lighten(@color-black-theme, 10%), 80%);
@color-black-tab-btn-background-hover: @color-black-theme_2-hover;
@color-black-tab-border-top: fadeout(lighten(@color-black-theme, 5%), 50%);
@color-black-tab-border-bottom: lighten(@color-black-theme, 5%);
@color-black-hideBtn: #4e4e4e;
@color-black-minBtn: #5e5e5e;
@color-black-maxBtn: #535353;
@color-black-closeBtn: #6e6e6e;
@color-black-hideBtn-hover: fadeout(@color-hideBtn, 10%);
@color-black-minBtn-hover: fadeout(@color-black-minBtn, 10%);
@color-black-maxBtn-hover: fadeout(@color-black-maxBtn, 10%);
@color-black-closeBtn-hover: fadeout(@color-black-closeBtn, 10%);
@color-black-badge-success: lighten(@color-black-theme, 10%);
@color-black-badge-success-font: #fff;
@color-black-badge-info: lighten(@color-black-theme, 25%);
@color-black-badge-info-font: #fff;
@color-mid_autumn-theme: rgba(74, 55, 82, 1);
@color-mid_autumn-theme-bgimg: url(../images/jqbg.jpg);
@color-mid_autumn-theme-bgposition: center;
@color-mid_autumn-theme-bgsize: cover;
@color-mid_autumn-theme-hover: fadeout(lighten(@color-mid_autumn-theme, 10%), 40%);
@color-mid_autumn-theme-active: fadeout(lighten(@color-mid_autumn-theme, 15%), 60%);
@color-mid_autumn-theme-font: rgba(246, 233, 255, 0.9);
@color-mid_autumn-theme-font-label: fadeout(lighten(@color-mid_autumn-theme, 20%), 20%);
@color-mid_autumn-theme_2: rgba(255, 255, 255, .9);
@color-mid_autumn-theme_2-background_1: #ececec;
@color-mid_autumn-theme_2-background_2: fadeout(@color-mid_autumn-theme_2-background_1, 2%);
@color-mid_autumn-theme_2-hover: fadeout(lighten(@color-mid_autumn-theme, 15%), 80%);
@color-mid_autumn-theme_2-active: fadeout(lighten(@color-mid_autumn-theme, 15%), 70%);
@color-mid_autumn-theme_2-font: darken(desaturate(@color-mid_autumn-theme_2, 70%), 90%);
@color-mid_autumn-theme_2-font-label: desaturate(lighten(@color-mid_autumn-theme, 30%), 45%);
// @color-mid_autumn-theme_2-line: transparent;
@color-mid_autumn-theme_2-line: lighten(@color-mid_autumn-theme, 61%);
@color-mid_autumn-reply-floor: fadeout(@color-mid_autumn-theme, 95%);
@color-mid_autumn-theme-sidebar: rgba(255, 255, 255, 0);
@color-mid_autumn-btn: lighten(@color-mid_autumn-theme, 10%);
@color-mid_autumn-btn-background: fadeout(lighten(@color-mid_autumn-theme, 35%), 70%);
@color-mid_autumn-btn-hover: fadeout(lighten(@color-mid_autumn-theme, 5%), 70%);
@color-mid_autumn-btn-active: fadeout(darken(@color-mid_autumn-theme, 5%), 70%);
@color-mid_autumn-btn-select: fadeout(lighten(@color-mid_autumn-theme, 5%), 50%);
@color-mid_autumn-pagination-background: fadeout(lighten(@color-mid_autumn-theme, 45%), 50%);
@color-mid_autumn-pagination-hover: fadeout(lighten(@color-mid_autumn-theme, 10%), 70%);
@color-mid_autumn-pagination-active: fadeout(darken(@color-mid_autumn-theme, 10%), 70%);
@color-mid_autumn-pagination-select: fadeout(lighten(@color-mid_autumn-theme, 10%), 55%);
@color-mid_autumn-search-form-background: fadeout(lighten(@color-mid_autumn-theme, 50%), 10%);
@color-mid_autumn-search-list-hover: fadeout(darken(@color-mid_autumn-theme, 10%), 70%);
@color-mid_autumn-scrollbar-track: fadeout(lighten(@color-mid_autumn-theme, 10%), 80%);
@color-mid_autumn-scrollbar-thumb: fadeout(lighten(@color-mid_autumn-theme, 10%), 60%);
@color-mid_autumn-scrollbar-thumb-hover: fadeout(lighten(@color-mid_autumn-theme, 10%), 40%);
@color-mid_autumn-player-pic-c1: fadeout(@color-mid_autumn-theme_2, 50%);
@color-mid_autumn-player-pic-c2: darken(@color-mid_autumn-theme_2, 30%);
@color-mid_autumn-player-progress: fadeout(lighten(@color-mid_autumn-theme, 20%), 82%);
@color-mid_autumn-player-progress-bar1: darken(@color-mid_autumn-theme_2, 12%);
@color-mid_autumn-player-progress-bar2: fadeout(lighten(@color-mid_autumn-theme, 12%), 20%);
@color-mid_autumn-player-status-text: lighten(@color-mid_autumn-theme_2-font, 32%);
@color-mid_autumn-player-detail-lyric: fadeout(@color-mid_autumn-theme_2-font, 40%);
@color-mid_autumn-player-detail-lyric-active: lighten(@color-mid_autumn-theme, 7%);
@color-mid_autumn-player-detail-play-btn: lighten(@color-mid_autumn-theme, 7%);
@color-mid_autumn-tab-btn-background: fadeout(lighten(@color-mid_autumn-theme, 10%), 80%);
@color-mid_autumn-tab-btn-background-hover: @color-mid_autumn-theme_2-hover;
@color-mid_autumn-tab-border-top: fadeout(lighten(@color-mid_autumn-theme, 5%), 50%);
@color-mid_autumn-tab-border-bottom: lighten(@color-mid_autumn-theme, 5%);
@color-mid_autumn-hideBtn: #3bc2b2;
@color-mid_autumn-minBtn: #85c43b;
@color-mid_autumn-maxBtn: #e7aa36;
@color-mid_autumn-closeBtn: #ea6e4d;
@color-mid_autumn-hideBtn-hover: fadeout(@color-hideBtn, 10%);
@color-mid_autumn-minBtn-hover: fadeout(@color-mid_autumn-minBtn, 10%);
@color-mid_autumn-maxBtn-hover: fadeout(@color-mid_autumn-maxBtn, 10%);
@color-mid_autumn-closeBtn-hover: fadeout(@color-mid_autumn-closeBtn, 10%);
@color-mid_autumn-badge-success: lighten(@color-mid_autumn-theme, 25%);
@color-mid_autumn-badge-success-font: #fff;
@color-mid_autumn-badge-info: #af9479;
@color-mid_autumn-badge-info-font: #fff;
/*
@color-mid_autumn-theme: #312438;
@color-mid_autumn-theme-bgimg: url(../images/jqbg.jpg);
@color-mid_autumn-theme-bgposition: center;
@color-mid_autumn-theme-bgsize: cover;
@color-mid_autumn-theme-hover: fadeout(lighten(@color-mid_autumn-theme, 5%), 60%);
@color-mid_autumn-theme-active: fadeout(lighten(@color-mid_autumn-theme, 10%), 70%);
@color-mid_autumn-theme-font: rgba(246, 233, 255, 0.9);
@color-mid_autumn-theme-font-label: fadeout(lighten(@color-mid_autumn-theme, 20%), 20%);
@color-mid_autumn-theme_2: rgba(0, 0, 0, .8);
@color-mid_autumn-theme_2-background_1: #030107;
@color-mid_autumn-theme_2-background_2: fadeout(darken(desaturate(@color-mid_autumn-theme, 10%), 7%), 2%);
@color-mid_autumn-theme_2-hover: fadeout(lighten(@color-mid_autumn-theme, 15%), 80%);
@color-mid_autumn-theme_2-active: fadeout(lighten(@color-mid_autumn-theme, 15%), 70%);
@color-mid_autumn-theme_2-font: lighten(@color-mid_autumn-theme_2, 90%);
@color-mid_autumn-theme_2-font-label: desaturate(lighten(@color-mid_autumn-theme, 30%), 45%);
// @color-mid_autumn-theme_2-line: transparent;
@color-mid_autumn-theme_2-line: darken(@color-mid_autumn-theme, 18%);
@color-mid_autumn-reply-floor: fadeout(@color-mid_autumn-theme, 95%);
@color-mid_autumn-theme-sidebar: rgba(255, 255, 255, 0);
@color-mid_autumn-btn: darken(@color-mid_autumn-theme, 10%);
@color-mid_autumn-btn-background: fadeout(lighten(@color-mid_autumn-theme, 35%), 70%);
@color-mid_autumn-btn-hover: fadeout(lighten(@color-mid_autumn-theme, 5%), 70%);
@color-mid_autumn-btn-active: fadeout(darken(@color-mid_autumn-theme, 5%), 70%);
@color-mid_autumn-btn-select: fadeout(lighten(@color-mid_autumn-theme, 5%), 50%);
@color-mid_autumn-pagination-background: fadeout(lighten(@color-mid_autumn-theme, 45%), 50%);
@color-mid_autumn-pagination-hover: fadeout(lighten(@color-mid_autumn-theme, 10%), 70%);
@color-mid_autumn-pagination-active: fadeout(darken(@color-mid_autumn-theme, 10%), 70%);
@color-mid_autumn-pagination-select: fadeout(lighten(@color-mid_autumn-theme, 10%), 55%);
@color-mid_autumn-search-form-background: fadeout(lighten(@color-mid_autumn-theme, 50%), 10%);
@color-mid_autumn-search-list-hover: fadeout(darken(@color-mid_autumn-theme, 10%), 70%);
@color-mid_autumn-scrollbar-track: fadeout(lighten(@color-mid_autumn-theme, 10%), 80%);
@color-mid_autumn-scrollbar-thumb: fadeout(lighten(@color-mid_autumn-theme, 10%), 60%);
@color-mid_autumn-scrollbar-thumb-hover: fadeout(lighten(@color-mid_autumn-theme, 10%), 40%);
@color-mid_autumn-player-pic-c1: fadeout(@color-mid_autumn-theme_2, 50%);
@color-mid_autumn-player-pic-c2: darken(@color-mid_autumn-theme_2, 30%);
@color-mid_autumn-player-progress: darken(@color-mid_autumn-theme_2, 10%);
@color-mid_autumn-player-progress-bar1: darken(@color-mid_autumn-theme_2, 12%);
@color-mid_autumn-player-progress-bar2: fadeout(lighten(@color-mid_autumn-theme, 12%), 20%);
@color-mid_autumn-player-status-text: lighten(@color-mid_autumn-theme_2-font, 32%);
@color-mid_autumn-player-detail-lyric: lighten(@color-mid_autumn-theme, 7%);
@color-mid_autumn-player-detail-lyric-active: lighten(@color-mid_autumn-theme, 7%);
@color-mid_autumn-player-detail-play-btn: lighten(@color-mid_autumn-theme, 7%);
@color-mid_autumn-tab-btn-background: fadeout(lighten(@color-mid_autumn-theme, 10%), 80%);
@color-mid_autumn-tab-btn-background-hover: @color-mid_autumn-theme_2-hover;
@color-mid_autumn-tab-border-top: fadeout(lighten(@color-mid_autumn-theme, 5%), 50%);
@color-mid_autumn-tab-border-bottom: lighten(@color-mid_autumn-theme, 5%);
@color-mid_autumn-hideBtn: #3bc2b2;
@color-mid_autumn-minBtn: #85c43b;
@color-mid_autumn-maxBtn: #e7aa36;
@color-mid_autumn-closeBtn: #ea6e4d;
@color-mid_autumn-hideBtn-hover: fadeout(@color-hideBtn, 10%);
@color-mid_autumn-minBtn-hover: fadeout(@color-mid_autumn-minBtn, 10%);
@color-mid_autumn-maxBtn-hover: fadeout(@color-mid_autumn-maxBtn, 10%);
@color-mid_autumn-closeBtn-hover: fadeout(@color-mid_autumn-closeBtn, 10%);
@color-mid_autumn-badge-success: lighten(@color-mid_autumn-theme, 25%);
@color-mid_autumn-badge-success-font: #fff;
@color-mid_autumn-badge-info: #af9479;
@color-mid_autumn-badge-info-font: #fff; */
@color-naruto-theme: rgb(87, 144, 167);
@color-naruto-theme-bgimg: url(../images/myzcbg.jpg);
@color-naruto-theme-bgposition: center;
@color-naruto-theme-bgsize: cover;
@color-naruto-theme-hover: fadeout(lighten(@color-naruto-theme, 10%), 45%);
@color-naruto-theme-active: fadeout(lighten(@color-naruto-theme, 15%), 60%);
@color-naruto-theme-font: rgba(67, 118, 148, 0.9);
@color-naruto-theme-font-label: fadeout(darken(@color-naruto-theme, 20%), 20%);
@color-naruto-theme_2: rgba(255, 255, 255, 0.8);
@color-naruto-theme_2-background_1: #eafaff;
@color-naruto-theme_2-background_2: fadeout(@color-naruto-theme_2-background_1, 2%);
@color-naruto-theme_2-hover: fadeout(lighten(@color-naruto-theme, 10%), 75%);
@color-naruto-theme_2-active: fadeout(lighten(@color-naruto-theme, 10%), 65%);
@color-naruto-theme_2-font: darken(desaturate(@color-naruto-theme_2, 70%), 90%);
@color-naruto-theme_2-font-label: desaturate(lighten(@color-naruto-theme, 10%), 45%);
// @color-naruto-theme_2-line: transparent;
@color-naruto-theme_2-line: fadeout(lighten(@color-naruto-theme, 36%), 70%);
@color-naruto-reply-floor: fadeout(lighten(@color-naruto-theme, 12%), 90%);
@color-naruto-theme-sidebar: rgba(255, 255, 255, .3);
@color-naruto-btn: lighten(@color-naruto-theme, 2%);
@color-naruto-btn-background: fadeout(lighten(@color-naruto-theme, 35%), 70%);
@color-naruto-btn-hover: fadeout(lighten(@color-naruto-theme, 5%), 70%);
@color-naruto-btn-active: fadeout(darken(@color-naruto-theme, 5%), 70%);
@color-naruto-btn-select: fadeout(lighten(@color-naruto-theme, 5%), 50%);
@color-naruto-pagination-background: fadeout(lighten(@color-naruto-theme, 20%), 70%);
@color-naruto-pagination-hover: fadeout(lighten(@color-naruto-theme, 10%), 60%);
@color-naruto-pagination-active: fadeout(darken(@color-naruto-theme, 10%), 70%);
@color-naruto-pagination-select: fadeout(lighten(@color-naruto-theme, 10%), 50%);
@color-naruto-search-form-background: fadeout(lighten(@color-naruto-theme, 30%), 20%);
@color-naruto-search-list-hover: fadeout(darken(@color-naruto-theme, 10%), 65%);
@color-naruto-scrollbar-track: fadeout(lighten(@color-naruto-theme, 10%), 75%);
@color-naruto-scrollbar-thumb: fadeout(lighten(@color-naruto-theme, 10%), 50%);
@color-naruto-scrollbar-thumb-hover: fadeout(lighten(@color-naruto-theme, 10%), 35%);
@color-naruto-player-pic-c1: fadeout(@color-naruto-theme_2, 50%);
@color-naruto-player-pic-c2: darken(@color-naruto-theme_2, 30%);
@color-naruto-player-progress: fadeout(lighten(@color-naruto-theme, 20%), 70%);
@color-naruto-player-progress-bar1: darken(@color-naruto-theme_2, 12%);
@color-naruto-player-progress-bar2: fadeout(lighten(@color-naruto-theme, 12%), 20%);
@color-naruto-player-status-text: lighten(@color-naruto-theme_2-font, 32%);
@color-naruto-player-detail-lyric: fadeout(@color-naruto-theme_2-font, 36%);
@color-naruto-player-detail-lyric-active: darken(@color-naruto-theme, 7%);
@color-naruto-player-detail-play-btn: lighten(@color-naruto-theme, 7%);
@color-naruto-tab-btn-background: fadeout(lighten(@color-naruto-theme, 10%), 80%);
@color-naruto-tab-btn-background-hover: @color-naruto-theme_2-hover;
@color-naruto-tab-border-top: fadeout(lighten(@color-naruto-theme, 5%), 50%);
@color-naruto-tab-border-bottom: lighten(@color-naruto-theme, 5%);
@color-naruto-hideBtn: #3bc2b2;
@color-naruto-minBtn: #85c43b;
@color-naruto-maxBtn: #e7aa36;
@color-naruto-closeBtn: #ea6e4d;
@color-naruto-hideBtn-hover: fadeout(@color-hideBtn, 10%);
@color-naruto-minBtn-hover: fadeout(@color-naruto-minBtn, 10%);
@color-naruto-maxBtn-hover: fadeout(@color-naruto-maxBtn, 10%);
@color-naruto-closeBtn-hover: fadeout(@color-naruto-closeBtn, 10%);
@color-naruto-badge-success: #7fb575;
@color-naruto-badge-success-font: #fff;
@color-naruto-badge-info: lighten(@color-naruto-theme, 10%);
@color-naruto-badge-info-font: #fff;
@color-happy_new_year-theme: rgba(192, 57, 43, 1);
@color-happy_new_year-theme-bgimg: url(../images/xnkl.png);
@color-happy_new_year-theme-bgposition: center;
@color-happy_new_year-theme-bgsize: cover;
@color-happy_new_year-theme-hover: fadeout(lighten(@color-happy_new_year-theme, 10%), 45%);
@color-happy_new_year-theme-active: fadeout(lighten(@color-happy_new_year-theme, 15%), 60%);
@color-happy_new_year-theme-font: rgba(148, 67, 67, 0.9);
@color-happy_new_year-theme-font-label: fadeout(darken(@color-happy_new_year-theme, 20%), 20%);
@color-happy_new_year-theme_2: rgba(255, 255, 255, 0.8);
@color-happy_new_year-theme_2-background_1: #fef9f2;
@color-happy_new_year-theme_2-background_2: fadeout(@color-happy_new_year-theme_2-background_1, 2%);
@color-happy_new_year-theme_2-hover: fadeout(lighten(@color-happy_new_year-theme, 10%), 75%);
@color-happy_new_year-theme_2-active: fadeout(lighten(@color-happy_new_year-theme, 10%), 65%);
@color-happy_new_year-theme_2-font: darken(desaturate(@color-happy_new_year-theme_2, 70%), 90%);
@color-happy_new_year-theme_2-font-label: desaturate(lighten(@color-happy_new_year-theme, 10%), 45%);
// @color-happy_new_year-theme_2-line: transparent;
@color-happy_new_year-theme_2-line: fadeout(lighten(@color-happy_new_year-theme, 36%), 80%);
@color-happy_new_year-reply-floor: fadeout(lighten(@color-happy_new_year-theme, 12%), 90%);
@color-happy_new_year-theme-sidebar: rgba(255, 255, 255, .3);
@color-happy_new_year-btn: lighten(@color-happy_new_year-theme, 2%);
@color-happy_new_year-btn-background: fadeout(lighten(@color-happy_new_year-theme, 35%), 70%);
@color-happy_new_year-btn-hover: fadeout(lighten(@color-happy_new_year-theme, 5%), 70%);
@color-happy_new_year-btn-active: fadeout(darken(@color-happy_new_year-theme, 5%), 70%);
@color-happy_new_year-btn-select: fadeout(lighten(@color-happy_new_year-theme, 5%), 50%);
@color-happy_new_year-pagination-background: fadeout(lighten(@color-happy_new_year-theme, 20%), 70%);
@color-happy_new_year-pagination-hover: fadeout(lighten(@color-happy_new_year-theme, 10%), 60%);
@color-happy_new_year-pagination-active: fadeout(darken(@color-happy_new_year-theme, 10%), 70%);
@color-happy_new_year-pagination-select: fadeout(lighten(@color-happy_new_year-theme, 10%), 50%);
@color-happy_new_year-search-form-background: fadeout(lighten(@color-happy_new_year-theme, 32%), 20%);
@color-happy_new_year-search-list-hover: fadeout(darken(@color-happy_new_year-theme, 1%), 70%);
@color-happy_new_year-scrollbar-track: fadeout(lighten(@color-happy_new_year-theme, 10%), 70%);
@color-happy_new_year-scrollbar-thumb: fadeout(lighten(@color-happy_new_year-theme, 10%), 50%);
@color-happy_new_year-scrollbar-thumb-hover: fadeout(lighten(@color-happy_new_year-theme, 10%), 35%);
@color-happy_new_year-player-pic-c1: fadeout(@color-happy_new_year-theme_2, 50%);
@color-happy_new_year-player-pic-c2: darken(@color-happy_new_year-theme_2, 30%);
@color-happy_new_year-player-progress: fadeout(lighten(@color-happy_new_year-theme, 20%), 82%);
@color-happy_new_year-player-progress-bar1: darken(@color-happy_new_year-theme_2, 5%);
@color-happy_new_year-player-progress-bar2: fadeout(lighten(@color-happy_new_year-theme, 5%), 20%);
@color-happy_new_year-player-status-text: lighten(@color-happy_new_year-theme_2-font, 32%);
@color-happy_new_year-player-detail-lyric: fadeout(@color-happy_new_year-theme_2-font, 36%);
@color-happy_new_year-player-detail-lyric-active: darken(@color-happy_new_year-theme, 7%);
@color-happy_new_year-player-detail-play-btn: lighten(@color-happy_new_year-theme, 7%);
@color-happy_new_year-tab-btn-background: fadeout(lighten(@color-happy_new_year-theme, 10%), 80%);
@color-happy_new_year-tab-btn-background-hover: @color-happy_new_year-theme_2-hover;
@color-happy_new_year-tab-border-top: fadeout(lighten(@color-happy_new_year-theme, 5%), 50%);
@color-happy_new_year-tab-border-bottom: lighten(@color-happy_new_year-theme, 5%);
@color-happy_new_year-hideBtn: #3bc2b2;
@color-happy_new_year-minBtn: #85c43b;
@color-happy_new_year-maxBtn: #e7aa36;
@color-happy_new_year-closeBtn: #ea6e4d;
@color-happy_new_year-hideBtn-hover: fadeout(@color-hideBtn, 10%);
@color-happy_new_year-minBtn-hover: fadeout(@color-happy_new_year-minBtn, 10%);
@color-happy_new_year-maxBtn-hover: fadeout(@color-happy_new_year-maxBtn, 10%);
@color-happy_new_year-closeBtn-hover: fadeout(@color-happy_new_year-closeBtn, 10%);
@color-happy_new_year-badge-success: lighten(@color-happy_new_year-theme, 10%);
@color-happy_new_year-badge-success-font: #fff;
@color-happy_new_year-badge-info: #dfbb6b;
@color-happy_new_year-badge-info-font: #fff;
// Width
@width-app-left: 6.6%;
// Height
@height-toolbar: 54px;
@height-player: 60px;
@height-player: 70px;
// Shadow
@@ -990,6 +17,8 @@
@radius-progress-border: 5px;
@radius-border: 4px;
@transition-theme: .4s ease;
@transition-slow: .6s ease;
@transition-normal: .4s ease;
@transition-fast: .3s ease;
@form-radius: 3px;
@@ -0,0 +1,5 @@
<?xml version="1.0" ?><svg fill="none" height="20" viewBox="0 0 20 20" width="20" xmlns="http://www.w3.org/2000/svg">
<path
d="M3 5C3 3.89543 3.89543 3 5 3H15C16.1046 3 17 3.89543 17 5V15C17 16.1046 16.1046 17 15 17H5C3.89543 17 3 16.1046 3 15V5ZM5 4C4.44772 4 4 4.44772 4 5V15C4 15.5523 4.44772 16 5 16H15C15.5523 16 16 15.5523 16 15V5C16 4.44772 15.5523 4 15 4H5Z"
fill="currentColor" />
</svg>

After

Width:  |  Height:  |  Size: 407 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M19,13H13V19H11V13H5V11H11V5H13V11H19V13Z" /></svg>

After

Width:  |  Height:  |  Size: 120 B

+12 -27
View File
@@ -1,9 +1,11 @@
<template>
<button
:class="[$style.btn, {[$style.min]: min}, {[$style.outline]: outline}]"
:disabled="disabled">
<slot />
</button>
<button
:class="[$style.btn, {[$style.min]: min}, {[$style.outline]: outline}]"
tabindex="0"
:disabled="disabled"
>
<slot />
</button>
</template>
<script>
@@ -34,13 +36,14 @@ export default {
border-radius: @form-radius;
cursor: pointer;
padding: 8px 15px;
color: @color-btn;
color: var(--color-button-font);
outline: none;
transition: background-color 0.2s ease;
background-color: @color-btn-background;
background-color: var(--color-button-background);
font-size: 14px;
&[disabled] {
opacity: .4;
cursor: default;
}
&.outline {
@@ -48,10 +51,10 @@ export default {
}
&:hover {
background-color: @color-btn-hover;
background-color: var(--color-button-background-hover);
}
&:active {
background-color: @color-btn-active;
background-color: var(--color-button-background-active);
}
}
@@ -60,22 +63,4 @@ export default {
font-size: 12px;
}
each(@themes, {
:global(#root.@{value}) {
.btn {
color: ~'@{color-@{value}-btn}';
background-color: ~'@{color-@{value}-btn-background}';
&.outline {
background-color: transparent;
}
&:hover {
background-color: ~'@{color-@{value}-btn-hover}';
}
&:active {
background-color: ~'@{color-@{value}-btn-active}';
}
}
}
})
</style>
+60 -96
View File
@@ -1,50 +1,60 @@
<template lang="pug">
div(:class="$style.checkbox")
input(:type="need ? 'radio' : 'checkbox'" :class="$style.input" :id="id" :disabled="disabled" :value="value" :name="name" @change="change" v-model="bool")
label(:for="id" :class="$style.content")
div(v-if="indeterminate" :class="$style.container")
svg(v-show="indeterminate" :class="$style.icon" version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' height='100%' width="100%" viewBox='0 32 448 448' space='preserve')
use(xlink:href='#icon-check-indeterminate')
svg(v-show="!indeterminate" :class="$style.icon" version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' height='100%' width="100%" viewBox='0 0 448 512' space='preserve')
use(xlink:href='#icon-check-true')
div(v-else :class="$style.container")
svg(version='1.1' :class="$style.icon" xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' height='100%' width="100%" viewBox='0 32 448 448' space='preserve')
use(xlink:href='#icon-check-true')
span(v-if="label != null" v-html="label" :class="$style.label")
<template>
<div :class="$style.checkbox">
<input
:id="id" :type="need ? 'radio' : 'checkbox'" :aria-hidden="true" :checked="checked"
:class="$style.input" :disabled="disabled" :value="value" :name="name" @input="handleInput"
>
<label :for="id" :class="$style.content">
<div :class="$style.container" role="checkbox" tabindex="0" :aria-label="ariaLabel || label" :aria-checked="checked" :aria-disabled="disabled">
<svg version="1.1" :class="$style.icon" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" height="100%" width="100%" viewBox="0 32 448 448" space="preserve">
<use xlink:href="#icon-check-true" />
</svg>
</div>
<span v-if="label != null" :class="$style.label" v-text="label" />
</label>
</div>
</template>
<script>
export default {
props: {
modelValue: {},
value: {},
modelValue: {
type: [String, Boolean, Number, Array],
required: true,
},
value: {
type: [String, Boolean, Number, Array],
default: undefined,
},
id: {
type: String,
required: true,
},
name: {
type: String,
default: undefined,
},
need: {
type: Boolean,
default: false,
},
ariaLabel: {
type: String,
default: undefined,
},
label: {
type: String,
required: true,
},
disabled: {
type: Boolean,
default: false,
},
indeterminate: {
type: Boolean,
default: false,
},
},
emits: ['update:modelValue', 'change'],
data() {
return {
bool: false,
checked: false,
}
},
watch: {
@@ -56,57 +66,38 @@ export default {
this.setValue(this.modelValue)
},
methods: {
change() {
let checked
handleInput(event) {
const checked = event.target.checked
let modelValue
if (Array.isArray(this.modelValue)) {
checked = [...this.modelValue]
const index = checked.indexOf(this.value)
if (index < 0) {
checked.push(this.value)
} else {
checked.splice(index, 1)
modelValue = [...this.modelValue]
if (checked) modelValue.push(this.value)
else {
const index = modelValue.indexOf(this.value)
if (index > -1) modelValue.splice(index, 1)
}
} else {
let bool = this.bool
switch (typeof this.modelValue) {
case 'boolean':
if (this.indeterminate) {
bool = true
this.$nextTick(() => {
this.bool = bool
})
}
checked = bool
break
case 'number':
checked = this.value
break
default:
checked = bool ? this.value : ''
break
}
if (typeof this.modelValue == 'boolean') {
modelValue = checked
} else modelValue = checked ? this.value : ''
}
this.$emit('update:modelValue', checked)
this.$emit('change', checked)
this.$emit('update:modelValue', modelValue)
this.$emit('change', modelValue)
},
setValue(value) {
let bool
let checked
if (Array.isArray(value)) {
bool = value.includes(this.value)
checked = value.includes(this.value)
} else {
switch (typeof value) {
case 'boolean':
bool = value
break
// case 'string':
// case 'number':
default:
bool = value === this.value
break
}
if (typeof this.modelValue == 'boolean') {
checked = this.modelValue
} else if (value == null) checked = this.modelValue != ''
else checked = this.modelValue == this.value
}
this.bool = this.need ? bool && this.value : bool
// console.log(this.need, this.value, checked)
// this.checked = this.need ? checked && this.value : checked
if (this.checked == checked) return
this.checked = checked
},
},
}
@@ -125,13 +116,16 @@ export default {
&[disabled] {
+ .content {
opacity: .5;
.container, .label {
cursor: default;
}
}
}
&:checked {
+ .content {
.container {
&:after {
border-color: @color-theme;
border-color: var(--color-primary-font);
}
}
.icon {
@@ -143,19 +137,16 @@ export default {
}
.content {
display: flex;
justify-content: center;
align-items: center;
}
.container {
flex: none;
position: relative;
display: inline-block;
width: 1em;
height: 1em;
cursor: pointer;
display: flex;
color: @color-theme;
margin-top: .25em;
color: var(--color-primary);
// border: 1px solid #ccc;
&:after {
position: absolute;
@@ -164,7 +155,7 @@ export default {
bottom: 0;
left: 0;
right: 0;
border: 1px solid @color-theme_2-font-label;
border: 1px solid var(--color-font-label);
transition: border-color 0.2s ease;
border-radius: 2px;
}
@@ -179,36 +170,9 @@ export default {
.label {
flex: auto;
line-height: 1;
margin-left: 5px;
line-height: 1.5;
cursor: pointer;
}
each(@themes, {
:global(#root.@{value}) {
.checkbox {
> input {
&:checked {
+ .content {
.container {
&:after {
border-color: ~'@{color-@{value}-theme}';
}
}
}
}
}
}
.content {
.container {
color: ~'@{color-@{value}-theme}';
// border: 1px solid #ccc;
&:after {
border-color: ~'@{color-@{value}-theme_2-font-label}';
}
}
}
}
})
</style>
+16 -30
View File
@@ -1,15 +1,16 @@
<template>
<input
:class="$style.input"
ref="dom_input"
:type="type"
:placeholder="placeholder"
:value="modelValue"
:disabled="disabled"
@input="handleInput"
@change="$emit('change', $event.target.value.trim())"
@keyup.enter="$emit('submit', $event.target.value.trim())"
/>
<input
ref="dom_input"
:class="$style.input"
:type="type"
:placeholder="placeholder"
:value="modelValue"
:disabled="disabled"
tabindex="0"
@input="handleInput"
@change="$emit('change', $event.target.value.trim())"
@keyup.enter="$emit('submit', $event.target.value.trim())"
>
</template>
<script>
@@ -55,10 +56,10 @@ export default {
border: none;
border-radius: @form-radius;
padding: 7px 8px;
color: @color-btn;
color: var(--color-button-font);
outline: none;
transition: background-color 0.2s ease;
background-color: @color-btn-background;
background-color: var(--color-primary-background);
font-size: 13.3px;
&::-webkit-outer-spin-button,
@@ -72,10 +73,10 @@ export default {
}
&:hover, &:focus {
background-color: @color-theme_2-hover;
background-color: var(--color-primary-background-hover);
}
&:active {
background-color: @color-theme_2-active;
background-color: var(--color-primary-background-active);
}
}
@@ -84,19 +85,4 @@ export default {
font-size: 12px;
}
each(@themes, {
:global(#root.@{value}) {
.input {
color: ~'@{color-@{value}-btn}';
background-color: ~'@{color-@{value}-btn-background}';
&:hover, &:focus {
background-color: ~'@{color-@{value}-theme_2-hover}';
}
&:active {
background-color: ~'@{color-@{value}-theme_2-active}';
}
}
}
})
</style>
+81 -147
View File
@@ -1,22 +1,42 @@
<template>
<ul :class="$style.list" :style="listStyles" ref="dom_list" role="toolbar" :aria-hidden="!show">
<li v-for="item in menus"
role="tab"
tabindex="0"
:aria-label="item[itemName]"
ignore-tip
:key="item.action"
@click="handleClick(item)"
v-show="!item.hide && (item.action == 'download' ? setting.download.enable : true)"
:disabled="item.disabled ? true : null">{{item[itemName]}}</li>
</ul>
<teleport to="#root">
<ul ref="dom_menu" :class="$style.list" :style="menuStyles" role="toolbar" :aria-hidden="!modelValue">
<li
v-for="item in menus"
v-show="!item.hide && (item.action == 'download' ? appSetting['download.enable'] : true)"
:key="item.action"
:class="$style.listItem"
role="tab"
tabindex="0"
:aria-label="item[itemName]"
ignore-tip
:disabled="item.disabled ? true : null"
@click="menuClick(item)"
>
{{ item[itemName] }}
</li>
</ul>
</teleport>
</template>
<script>
import { mapGetters } from 'vuex'
import { computed } from '@common/utils/vueTools'
import useMenuLocation from '@renderer/utils/compositions/useMenuLocation'
import { appSetting } from '@renderer/store/setting'
export default {
name: 'MenuToolBar',
props: {
modelValue: {
type: Boolean,
required: true,
},
xy: {
type: Object,
required: true,
},
menus: {
type: Array,
default() {
@@ -27,103 +47,34 @@ export default {
type: String,
default: 'name',
},
isShow: {
type: Boolean,
default: false,
},
location: {
type: Object,
default() {
return { x: 0, y: 0 }
},
},
},
emits: ['menu-click'],
computed: {
...mapGetters(['setting']),
},
watch: {
isShow: {
handler(n) {
n ? this.handleShow() : this.handleHide()
},
immediate: true,
},
location: {
handler(n) {
this.listStyles.left = n.x + 2 + 'px'
this.listStyles.top = n.y + 'px'
if (this.show) {
if (this.listStyles.transitionProperty != this.transition2) this.listStyles.transitionProperty = this.transition2
this.listStyles.transform = `scaleY(1) translate(${this.handleGetOffsetXY(n.x, n.y)})`
}
},
deep: true,
},
},
data() {
return {
show: false,
listStyles: {
left: 0,
top: 0,
opacity: 0,
transitionProperty: 'transform, opacity',
transform: 'scale(0) translate(0,0)',
},
transition1: 'transform, opacity',
transition2: 'transform, opacity, top, left',
transitionProperty: 'transform, opacity',
emits: ['update:modelValue', 'menu-click'],
setup(props, { emit }) {
const visible = computed(() => props.modelValue)
const location = computed(() => props.xy)
const onHide = () => {
emit('update:modelValue', false)
menuClick(null)
}
},
mounted() {
document.addEventListener('click', this.handleDocumentClick)
},
beforeUnmount() {
document.removeEventListener('click', this.handleDocumentClick)
},
methods: {
handleShow() {
this.show = true
this.listStyles.opacity = 1
this.listStyles.transform = `scaleY(1) translate(${this.handleGetOffsetXY(this.location.x, this.location.y)})`
},
handleHide(e) {
this.listStyles.opacity = 0
this.listStyles.transform = 'scale(0) translate(0, 0)'
this.show = false
},
handleGetOffsetXY(left, top) {
const listWidth = this.$refs.dom_list.clientWidth
const listHeight = this.$refs.dom_list.clientHeight
const dom_container = this.$refs.dom_list.offsetParent
const containerWidth = dom_container.clientWidth
const containerHeight = dom_container.clientHeight
const offsetWidth = containerWidth - left - listWidth
const offsetHeight = containerHeight - top - listHeight
let x = 0
let y = 0
if (containerWidth > listWidth && offsetWidth < 17) {
x = offsetWidth - 17
}
if (containerHeight > listHeight && offsetHeight < 5) {
y = offsetHeight - 5
}
return `${x}px, ${y}px`
},
handleDocumentClick(event) {
if (!this.show) return
if (event.target == this.$refs.dom_list || this.$refs.dom_list.contains(event.target)) return
const { dom_menu, menuStyles } = useMenuLocation({
visible,
location,
onHide,
})
if (this.show && this.listStyles.transitionProperty != this.transition1) this.listStyles.transitionProperty = this.transition1
this.handleClick(null)
},
handleClick(item) {
const menuClick = (item) => {
if (item && item.disabled) return
emit('menu-click', item)
}
this.$emit('menu-click', item)
},
return {
dom_menu,
menuStyles,
menuClick,
appSetting,
}
},
}
</script>
@@ -138,60 +89,43 @@ export default {
opacity: 0;
transform: scale(0);
transform-origin: 0 0 0;
transition: .25s ease;
transition: .14s ease;
transition-property: transform, opacity;
border-radius: @radius-border;
background-color: @color-theme_2-background_2;
background-color: var(--color-content-background);
box-shadow: 0 1px 8px 0 rgba(0,0,0,.2);
z-index: 10;
overflow: hidden;
// will-change: transform;
li {
cursor: pointer;
min-width: 96px;
line-height: 34px;
// color: @color-btn;
padding: 0 10px;
text-align: center;
outline: none;
transition: @transition-theme;
transition-property: background-color, opacity;
box-sizing: border-box;
.mixin-ellipsis-1;
}
.listItem {
cursor: pointer;
min-width: 96px;
line-height: 34px;
// color: var(--color-button-font);
padding: 0 10px;
text-align: center;
outline: none;
transition: @transition-normal;
transition-property: background-color, opacity;
box-sizing: border-box;
.mixin-ellipsis-1;
// background-color: var(--color-primary-light-600-alpha-800);
&:hover {
background-color: var(--color-primary-background-hover);
}
&:active {
background-color: var(--color-primary-background-active);
}
&[disabled] {
cursor: default;
opacity: .4;
&:hover {
background-color: @color-theme_2-hover;
}
&:active {
background-color: @color-theme_2-active;
}
&[disabled] {
cursor: default;
opacity: .4;
&:hover {
background: none !important;
}
background: none !important;
}
}
}
each(@themes, {
:global(#root.@{value}) {
.list {
background-color: ~'@{color-@{value}-theme_2-background_2}';
li {
// color: ~'@{color-@{value}-btn}';
&:hover {
background-color: ~'@{color-@{value}-theme_2-hover}';
}
&:active {
background-color: ~'@{color-@{value}-theme_2-active}';
}
}
}
}
})
</style>
+301
View File
@@ -0,0 +1,301 @@
<template>
<component :is="containerEl" ref="dom_scrollContainer" :class="containerClass" tabindex="0" style="outline: none; height: 100%; overflow-y: auto; position: relative; display: block; contain: strict;">
<component :is="contentEl" ref="dom_list" :class="contentClass" :style="contentStyle">
<!-- <div v-for="item in views" :key="item.key" :style="item.style">
<slot name="default" v-bind="{ item: item.item, index: item.index }" />
</div> -->
</component>
<slot name="footer" />
</component>
</template>
<script>
import {
computed,
ref,
nextTick,
watch,
onMounted,
onBeforeUnmount,
render,
// h,
} from 'vue'
const easeInOutQuad = (t, b, c, d) => {
t /= d / 2
if (t < 1) return (c / 2) * t * t + b
t--
return (-c / 2) * (t * (t - 2) - 1) + b
}
const handleScroll = (element, to, duration = 300, callback = () => {}, onCancel = () => {}) => {
if (!element) return callback()
const start = element.scrollTop || element.scrollY || 0
let cancel = false
if (to > start) {
let maxScrollTop = element.scrollHeight - element.clientHeight
if (to > maxScrollTop) to = maxScrollTop
} else if (to < start) {
if (to < 0) to = 0
} else return callback()
const change = to - start
const increment = 10
if (!change) return callback()
let currentTime = 0
let val
let cancelCallback
const animateScroll = () => {
currentTime += increment
val = parseInt(easeInOutQuad(currentTime, start, change, duration))
if (element.scrollTo) {
element.scrollTo(0, val)
} else {
element.scrollTop = val
}
if (currentTime < duration) {
if (cancel) {
cancelCallback()
onCancel()
return
}
setTimeout(animateScroll, increment)
} else {
callback()
}
}
animateScroll()
return (callback) => {
cancelCallback = callback
cancel = true
}
}
export default {
name: 'VirtualizedList',
props: {
containerEl: {
type: String,
default: 'div',
},
containerClass: {
type: String,
default: 'virtualized-list',
},
contentEl: {
type: String,
default: 'div',
},
contentClass: {
type: String,
default: 'virtualized-list-content',
},
itemHeight: {
type: Number,
required: true,
},
keyName: {
type: String,
required: true,
},
list: {
type: Array,
required: true,
},
},
emits: ['scroll'],
setup(props, { emit, slots }) {
const views = ref([])
const dom_scrollContainer = ref(null)
const dom_list = ref(null)
let startIndex = -1
let endIndex = -1
let scrollTop = -1
let cachedList = []
let cancelScroll = null
let isScrolling = false
let scrollToValue = 0
const createList = (startIndex, endIndex) => {
if (startIndex == endIndex) return []
console.log(startIndex, endIndex)
const cache = cachedList.slice(startIndex, endIndex)
const list = props.list.slice(startIndex, endIndex).map((item, i) => {
if (cache[i]) return cache[i]
const top = (startIndex + i) * props.itemHeight
const index = startIndex + i
return cachedList[index] = {
item,
top,
style: { position: 'absolute', left: 0, right: 0, top: top + 'px', height: props.itemHeight + 'px' },
index,
key: item[props.keyName],
}
})
return list
}
const renderListItem = (list, type) => {
if (!list.length) return
console.log(list)
// const dom = document.createDocumentFragment()
switch (type) {
case 'up':
break
case 'down':
break
default:
console.log(render(null, slots.default(list[0])[0]))
break
}
}
const updateView = (force = false, currentScrollTop = dom_scrollContainer.value.scrollTop) => {
// const currentScrollTop = this.$refs.dom_scrollContainer.scrollTop
const itemHeight = props.itemHeight
const currentStartIndex = Math.floor(currentScrollTop / itemHeight)
const scrollContainerHeight = dom_scrollContainer.value.clientHeight
const currentEndIndex = currentStartIndex + Math.ceil(scrollContainerHeight / itemHeight)
const continuous = currentStartIndex <= endIndex && currentEndIndex >= startIndex
const currentStartRenderIndex = Math.max(currentStartIndex, 0)
const currentEndRenderIndex = currentEndIndex + 1
// console.log(continuous, currentStartIndex, endIndex, currentEndIndex, startIndex)
// debugger
if (!force && continuous) {
// if (Math.abs(currentScrollTop - this.scrollTop) < this.itemHeight * 0.6) return
// console.log('update')
if (currentScrollTop > scrollTop) { // scroll down
console.log('scroll down')
renderListItem(createList(endIndex + 1, currentEndRenderIndex))
// // views.value.push(...list.slice(list.indexOf(views.value[views.value.length - 1]) + 1))
// // // if (this.views.length > 100) {
// // nextTick(() => {
// // views.value.splice(0, views.value.indexOf(list[0]))
// // })
// // }
} else if (currentScrollTop < scrollTop) { // scroll up
console.log('scroll up')
renderListItem(createList(currentStartRenderIndex, startIndex))
// views.value = createList(currentStartRenderIndex, currentEndRenderIndex)
} else return
// if (currentScrollTop == scrollTop && endIndex >= currentEndIndex) return
// views.value = createList(currentStartRenderIndex, currentEndRenderIndex)
} else {
renderListItem(createList(currentStartRenderIndex, currentEndRenderIndex))
}
startIndex = currentStartIndex
endIndex = currentEndIndex
scrollTop = currentScrollTop
}
const onScroll = event => {
const currentScrollTop = dom_scrollContainer.value.scrollTop
if (Math.abs(currentScrollTop - scrollTop) > props.itemHeight * 0.6) {
updateView(false, currentScrollTop)
}
emit('scroll', event)
}
const scrollTo = (scrollTop, animate = false) => {
return new Promise(resolve => {
if (cancelScroll) {
cancelScroll(resolve)
} else {
resolve()
}
}).then(() => {
return new Promise((resolve, reject) => {
if (animate) {
isScrolling = true
scrollToValue = scrollTop
cancelScroll = handleScroll(dom_scrollContainer.value, scrollTop, 300, () => {
cancelScroll = null
isScrolling = false
resolve()
}, () => {
cancelScroll = null
isScrolling = false
reject('canceled')
})
} else {
dom_scrollContainer.value.scrollTop = scrollTop
}
})
})
}
const scrollToIndex = (index, offset = 0, animate = false) => {
return scrollTo(Math.max(index * props.itemHeight + offset, 0), animate)
}
const getScrollTop = () => {
return isScrolling ? scrollToValue : dom_scrollContainer.value.scrollTop
}
const handleResize = () => {
setTimeout(updateView)
}
const contentStyle = computed(() => ({
display: 'block',
height: props.list.length * props.itemHeight + 'px',
}))
const handleReset = list => {
cachedList = Array(list.length)
startIndex = -1
endIndex = -1
nextTick(() => {
updateView(true)
})
}
watch(() => props.itemHeight, () => {
handleReset(props.list)
})
watch(() => props.list, (list) => {
handleReset(list)
}, {
deep: true,
})
onMounted(() => {
dom_scrollContainer.value.addEventListener('scroll', onScroll, false)
cachedList = Array(props.list.length)
startIndex = -1
endIndex = -1
updateView(true)
window.addEventListener('resize', handleResize)
})
onBeforeUnmount(() => {
dom_scrollContainer.value.removeEventListener('scroll', onScroll)
window.removeEventListener('resize', handleResize)
if (cancelScroll) cancelScroll()
})
return {
views,
dom_scrollContainer,
dom_list,
contentStyle,
scrollTo,
scrollToIndex,
getScrollTop,
}
},
// render() {
// console.log('render')
// // const list = this.views.map((item) => {
// // return h('div', { style: item.style, key: item.key }, this.$slots.default(item))
// // })
// return h(this.containerEl, {
// ref: 'dom_scrollContainer',
// class: this.containerClass,
// style: 'outline: none; height: 100%; overflow-y: auto; position: relative; display: block; contain: strict;',
// }, [
// h(this.contentEl, { style: this.contentStyle, class: this.contentClass }, this.views.map((item) => {
// return h('div', { style: item.style, key: item.key }, 'this.$slots.default(item)')
// })),
// ])
// },
}
</script>
-167
View File
@@ -1,167 +0,0 @@
<template>
<div :class="[$style.select, {[$style.active]: show}]">
<div :class="$style.label" ref="dom_btn" @click="handleShow">{{modelValue ? itemName ? list.find(l => l.id == modelValue).name : modelValue : ''}}</div>
<ul :class="$style.list">
<li v-for="(item, index) in list" :key="index" @click="handleClick(itemKey ? item[itemKey] : item)">{{itemName ? item[itemName] : item}}</li>
</ul>
</div>
</template>
<script>
export default {
props: {
list: {
type: Array,
default() {
return []
},
},
modelValue: {
type: [String, Number],
},
itemName: {
type: String,
},
itemKey: {
type: String,
},
},
data() {
return {
show: false,
}
},
mounted() {
document.addEventListener('click', this.handleHide)
},
beforeUnmount() {
document.removeEventListener('click', this.handleHide)
},
methods: {
handleHide(e) {
// if (e && e.target.parentNode != this.$refs.dom_list && this.show) return this.show = false
if (e && e.target == this.$refs.dom_btn) return
setTimeout(() => {
this.show = false
}, 50)
},
handleClick(item) {
if (item === this.modelValue) return
this.$emit('update:modelValue', item)
this.$emit('change', item)
},
handleShow() {
this.show = !this.show
},
},
}
</script>
<style lang="less" module>
@import '@renderer/assets/styles/layout.less';
.select {
font-size: 12px;
position: relative;
&.active {
.list {
transform: scaleY(1);
opacity: 1;
}
}
}
.label {
padding: 8px 15px;
// background-color: @color-btn-background;
transition: background-color @transition-theme;
border-top: 2px solid @color-tab-border-bottom;
border-left: 2px solid @color-tab-border-bottom;
box-sizing: border-box;
text-align: center;
border-top-left-radius: @radius-border;
color: @color-btn;
cursor: pointer;
.mixin-ellipsis-1;
&:hover {
background-color: @color-btn-hover;
}
&:active {
background-color: @color-btn-active;
}
}
.list {
position: absolute;
width: 100%;
top: 100%;
left: 0;
border-bottom: 2px solid @color-tab-border-bottom;
border-left: 2px solid @color-tab-border-bottom;
border-bottom-left-radius: @radius-border;
background-color: @color-theme_2-background_2;
opacity: 0;
transform: scaleY(0);
transform-origin: 0 0 0;
transition: .25s ease;
transition-property: transform, opacity;
z-index: 10;
li {
cursor: pointer;
padding: 8px 15px;
// color: @color-btn;
text-align: center;
outline: none;
transition: background-color @transition-theme;
background-color: @color-btn-background;
box-sizing: border-box;
.mixin-ellipsis-1;
&:hover {
background-color: @color-btn-hover;
}
&:active {
background-color: @color-btn-active;
}
}
}
each(@themes, {
:global(#root.@{value}) {
.label {
border-top-color: ~'@{color-@{value}-tab-border-bottom}';
border-left-color: ~'@{color-@{value}-tab-border-bottom}';
color: ~'@{color-@{value}-btn}';
&:hover {
background-color: ~'@{color-@{value}-btn-hover}';
}
&:active {
background-color: ~'@{color-@{value}-btn-active}';
}
}
.list {
border-bottom-color: ~'@{color-@{value}-tab-border-bottom}';
border-left-color: ~'@{color-@{value}-tab-border-bottom}';
background-color: ~'@{color-@{value}-theme_2-background_2}';
li {
// color: ~'@{color-@{value}-btn}';
background-color: ~'@{color-@{value}-btn-background}';
&:hover {
background-color: ~'@{color-@{value}-btn-hover}';
}
&:active {
background-color: ~'@{color-@{value}-btn-active}';
}
}
}
}
})
</style>
+58 -79
View File
@@ -1,18 +1,22 @@
<template>
<div class="content" :class="[$style.select, show ? $style.active : '']">
<div class="label-content" :class="$style.label" ref="dom_btn" @click="handleShow">
<span class="label">{{label}}</span>
<div class="icon" :class="$style.icon">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" height="100%" viewBox="0 0 451.847 451.847" space="preserve">
<use xlink:href="#icon-down"></use>
</svg>
<div class="content" :class="[$style.select, show ? $style.active : '']">
<div ref="dom_btn" class="label-content" :class="$style.label" @click="handleShow">
<span class="label">{{ label }}</span>
<div class="icon" :class="$style.icon">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" height="100%" viewBox="0 0 451.847 451.847" space="preserve">
<use xlink:href="#icon-down" />
</svg>
</div>
</div>
<ul v-if="show" ref="dom_list" class="selection-list scroll" :class="$style.list" :style="listStyles">
<li
v-for="(item, index) in list" :key="index" :class="[$style.listItem, (itemKey ? item[itemKey] : item) == modelValue ? $style.active : null]"
:aria-label="itemName ? item[itemName] : item" @click="handleClick(item)"
>
{{ itemName ? item[itemName] : item }}
</li>
</ul>
</div>
<ul class="selection-list scroll" :class="$style.list" :style="listStyles" ref="dom_list">
<li v-for="(item, index) in list" :key="index" :class="(itemKey ? item[itemKey] : item) == modelValue ? $style.active : null"
@click="handleClick(item)" :aria-label="itemName ? item[itemName] : item">{{itemName ? item[itemName] : item}}</li>
</ul>
</div>
</template>
<script>
@@ -27,14 +31,18 @@ export default {
},
modelValue: {
type: [String, Number],
required: true,
},
itemName: {
type: String,
default: '',
},
itemKey: {
type: String,
default: '',
},
},
emits: ['update:modelValue', 'change'],
data() {
return {
show: false,
@@ -43,12 +51,6 @@ export default {
},
}
},
mounted() {
document.addEventListener('click', this.handleHide)
},
beforeUnmount() {
document.removeEventListener('click', this.handleHide)
},
computed: {
label() {
if (this.modelValue == null) return ''
@@ -58,8 +60,15 @@ export default {
return item[this.itemName]
},
},
mounted() {
document.addEventListener('click', this.handleHide, true)
},
beforeUnmount() {
document.removeEventListener('click', this.handleHide, true)
},
methods: {
handleHide(e) {
if (!this.show) return
// if (e && e.target.parentNode != this.$refs.dom_list && this.show) return this.show = false
if (e && (e.target == this.$refs.dom_btn || this.$refs.dom_btn.contains(e.target))) return
this.listStyles.transform = 'scaleY(0) translateY(0)'
@@ -75,7 +84,9 @@ export default {
},
handleShow() {
this.show = true
this.listStyles.transform = `scaleY(1) translateY(${this.handleGetOffset()}px)`
this.$nextTick(() => {
this.listStyles.transform = `scaleY(1) translateY(${this.handleGetOffset()}px)`
})
},
handleGetOffset() {
const listHeight = this.$refs.dom_list.clientHeight
@@ -105,7 +116,7 @@ export default {
&.active {
.label {
background-color: @color-btn-background;
background-color: var(--color-button-background);
}
.list {
opacity: 1;
@@ -119,16 +130,17 @@ export default {
}
.label {
background-color: @color-btn-background;
background-color: var(--color-button-background);
padding: 0 10px;
transition: background-color @transition-theme;
transition: background-color @transition-normal;
height: @selection-height;
line-height: 27px;
// line-height: 27px;
box-sizing: border-box;
color: @color-btn;
color: var(--color-button-font);
border-radius: @form-radius;
cursor: pointer;
display: flex;
align-items: center;
span {
flex: auto;
@@ -146,10 +158,10 @@ export default {
}
&:hover {
background-color: @color-btn-hover;
background-color: var(--color-button-background-hover);
}
&:active {
background-color: @color-btn-active;
background-color: var(--color-button-background-active);
}
}
@@ -158,7 +170,7 @@ export default {
top: 0;
left: 0;
width: 100%;
background-color: @color-theme_2-background_2;
background-color: var(--color-content-background);
opacity: 0;
transform: scaleY(0) translateY(0);
transform-origin: 0 (@selection-height / 2) 0;
@@ -169,61 +181,28 @@ export default {
box-shadow: 0 0 4px rgba(0, 0, 0, .15);
overflow: auto;
max-height: 200px;
}
.listItem {
cursor: pointer;
padding: 0 10px;
line-height: @selection-height;
// color: var(--color-button-font);
outline: none;
transition: background-color @transition-normal;
background-color: var(--color-button-font)-background;
box-sizing: border-box;
.mixin-ellipsis-1;
li {
cursor: pointer;
padding: 0 10px;
line-height: @selection-height;
// color: @color-btn;
outline: none;
transition: background-color @transition-theme;
background-color: @color-btn-background;
box-sizing: border-box;
.mixin-ellipsis-1;
&:hover {
background-color: @color-btn-hover;
}
&:active {
background-color: @color-btn-active;
}
&.active {
color: @color-btn;
}
&:hover {
background-color: var(--color-button-background-hover);
}
&:active {
background-color: var(--color-button-background-active);
}
&.active {
color: var(--color-button-font);
}
}
each(@themes, {
:global(#root.@{value}) {
.label {
color: ~'@{color-@{value}-btn}';
background-color: ~'@{color-@{value}-btn-background}';
&:hover {
background-color: ~'@{color-@{value}-btn-hover}';
}
&:active {
background-color: ~'@{color-@{value}-btn-active}';
}
}
.list {
background-color: ~'@{color-@{value}-theme_2-background_2}';
li {
// color: ~'@{color-@{value}-btn}';
background-color: ~'@{color-@{value}-btn-background}';
&:hover {
background-color: ~'@{color-@{value}-btn-hover}';
}
&:active {
background-color: ~'@{color-@{value}-btn-active}';
}
&.active {
color: ~'@{color-@{value}-btn}';
}
}
}
}
})
</style>
+87 -192
View File
@@ -1,20 +1,17 @@
<template lang="pug">
div.scroll(:class="$style.tab")
//- div(:class="$style.content")
ul
li.ignore-to-rem(v-for="item in list" :key="itemKey ? item[itemKey] : item" :class="modelValue === (itemKey ? item[itemKey] : item) ? $style.active : ''")
button(type="button"
@click="handleClick(itemKey ? item[itemKey] : item)") {{ itemName ? item[itemName] : item }}
//- div(:class="$style.control")
div(:class="$style.left")
svg(version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' height='100%' viewBox='0 0 451.846 451.847' space='preserve')
use(xlink:href='#icon-left')
div(:class="$style.right")
svg(version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' height='100%' viewBox='0 0 451.846 451.847' space='preserve')
use(xlink:href='#icon-right')
<template>
<ul :class="[$style.list, $style[align]]" role="tablist">
<li
v-for="item in list" :key="item[itemKey]" :class="[$style.listItem, {[$style.active]: modelValue == item[itemKey]}]" tabindex="-1"
role="tab" :aria-label="item[itemLabel]" ignore-tip :aria-selected="modelValue == item[itemKey]"
@click="handleToggle(item[itemKey])"
>
<span :class="$style.label">{{ item[itemLabel] }}</span>
</li>
</ul>
</template>
<script>
export default {
props: {
list: {
@@ -23,203 +20,101 @@ export default {
return []
},
},
modelValue: {
type: [String, Number],
},
itemName: {
align: {
type: String,
default: 'left',
},
itemKey: {
type: String,
default: 'id',
},
itemLabel: {
type: String,
default: 'label',
},
modelValue: {
type: [String, Number],
default: '',
},
},
methods: {
handleClick(item) {
if (item === this.modelValue) return
this.$emit('update:modelValue', item)
this.$emit('change', item)
},
emits: ['update:modelValue', 'change'],
setup(props, { emit }) {
const handleToggle = id => {
if (id == props.modelValue) return
emit('update:modelValue', id)
emit('change', id)
}
return {
handleToggle,
}
},
}
</script>
<style lang="less" module>
@import '@renderer/assets/styles/layout.less';
.tab {
// overflow: hidden;
// display: flex;
// flex-flow: row nowrap;
overflow-x: auto;
ul {
display: flex;
flex: none;
border-bottom: 2px solid @color-tab-border-bottom;
transition: border-bottom-color @transition-theme;
li {
position: relative;
flex: none;
border-top: 2px solid @color-tab-border-top;
border-left: 2px solid @color-tab-btn-background;
border-right: 2px solid @color-tab-btn-background;
// box-sizing: border-box;
transition: border-color @transition-theme;
.list {
display: flex;
flex-flow: row nowrap;
font-size: 12px;
gap: 25px;
padding: 0 15px;
&:global(.ignore-to-rem) {
margin-left: -2px;
margin-bottom: -2px;
&:after, &:before {
height: 2px;
}
}
&.left {
justify-content: flex-start;
}
&.center {
justify-content: center;
}
&.right {
justify-content: flex-end;
}
}
.listItem {
display: block;
// padding: 5px 15px;
cursor: pointer;
transition: color @transition-normal;
&:hover {
color: var(--color-primary);
}
&.active {
color: var(--color-primary);
cursor: default;
>.label {
&:after {
content: ' ';
display: block;
width: 50%;
position: absolute;
bottom: 0;
left: 0;
background-color: @color-tab-border-bottom;
transition: width @transition-theme;
}
&:before {
content: ' ';
display: block;
width: 50%;
position: absolute;
bottom: 0;
right: 0;
background-color: @color-tab-border-bottom;
transition: width @transition-theme;
}
&:first-child {
border-left: none;
margin-left: 0;
button {
border-top-left-radius: 3px;
// border-bottom-left-radius: @radius-border;
}
}
&:last-child {
border-right: 2px solid @color-tab-border-top;
border-top-right-radius: @radius-border;
button {
border-top-right-radius: 3px;
// border-bottom-right-radius: @radius-border;
}
}
button {
display: inline-block;
border: none;
cursor: pointer;
padding: 0 10px;
font-size: 12px;
min-width: 56px;
// color: @color-btn;
outline: none;
transition: background-color @transition-theme;
background-color: @color-tab-btn-background;
line-height: 28px;
}
&:hover {
// border-left-color: @color-theme_2-hover;
// border-right-color: @color-theme_2-hover;
button {
background-color: @color-tab-btn-background-hover;
}
}
&:active {
// border-left-color: @color-theme_2-active;
// border-right-color: @color-theme_2-active;
button {
background-color: @color-theme_2-active;
}
}
&.active {
border-bottom-color: @color-theme_2-background_1;
border-top-color: @color-tab-border-bottom;
border-left-color: @color-tab-border-bottom;
border-right-color: @color-tab-border-bottom;
&::after {
width: 0;
}
&::before {
width: 0;
}
button {
background-color: @color-theme_2-background_1;
}
// background-color: var(--color-primary);
opacity: 1;
transform: translateY(0);
}
}
}
}
// .control {
// flex: none;
// border-bottom: 2px solid @color-tab-border-bottom;
// width: 1em;
// svg {
// width: .85em;
// }
// }
// .left, .right {
// line-height: 0;
// }
// .content {
// flex: auto;
// overflow: hidden;
// }
each(@themes, {
:global(#root.@{value}) {
.tab {
ul {
border-bottom-color: ~'@{color-@{value}-tab-border-bottom}';
li {
border-top-color: ~'@{color-@{value}-tab-border-top}';
border-left-color: ~'@{color-@{value}-tab-btn-background}';
border-right-color: ~'@{color-@{value}-tab-btn-background}';
&::after {
background-color: ~'@{color-@{value}-tab-border-bottom}';
}
&::before {
background-color: ~'@{color-@{value}-tab-border-bottom}';
}
&:last-child {
border-right-color: ~'@{color-@{value}-tab-border-top}';
}
button {
// color: ~'@{color-@{value}-btn}';
background-color: ~'@{color-@{value}-tab-btn-background}';
}
&:hover {
// border-left-color: ~'@{color-@{value}-theme_2-hover}';
// border-right-color: ~'@{color-@{value}-theme_2-hover}';
button {
background-color: ~'@{color-@{value}-tab-btn-background-hover}';
}
}
&:active {
// border-left-color: ~'@{color-@{value}-theme_2-active}';
// border-right-color: ~'@{color-@{value}-theme_2-active}';
button {
background-color: ~'@{color-@{value}-theme_2-active}';
}
}
&.active {
border-bottom-color: ~'@{color-@{value}-theme_2}';
border-top-color: ~'@{color-@{value}-tab-border-bottom}';
border-left-color: ~'@{color-@{value}-tab-border-bottom}';
border-right-color: ~'@{color-@{value}-tab-border-bottom}';
button {
background-color: ~'@{color-@{value}-theme_2-background_1}';
}
}
}
}
}
.label {
display: block;
position: relative;
padding: 8px 0;
&:after {
.mixin-after;
left: 0;
bottom: 0;
width: 100%;
height: 2px;
border-radius: 20px;
background-color: transparent;
transform: translateY(-4px);
opacity: 0;
background-color: var(--color-primary-alpha-300);
transition: @transition-fast;
transition-property: transform, opacity;
}
})
}
</style>
@@ -1,5 +1,5 @@
<template>
<component :is="containerEl" :class="containerClass" tabindex="0" ref="dom_scrollContainer" style="outline: none; height: 100%; overflow: auto; position: relative; display: block; contain: strict;">
<component :is="containerEl" ref="dom_scrollContainer" :class="containerClass" tabindex="0" style="outline: none; height: 100%; overflow-y: auto; position: relative; display: block; contain: strict;">
<component :is="contentEl" :class="contentClass" :style="contentStyle">
<div v-for="item in views" :key="item.key" :style="item.style">
<slot name="default" v-bind="{ item: item.item, index: item.index }" />
@@ -57,7 +57,7 @@ const handleScroll = (element, to, duration = 300, callback = () => {}, onCancel
onCancel()
return
}
setTimeout(animateScroll, increment)
window.setTimeout(animateScroll, increment)
} else {
callback()
}
@@ -88,21 +88,17 @@ export default {
type: String,
default: 'virtualized-list-content',
},
outsideNum: {
type: Number,
default: 1,
},
itemHeight: {
type: Number,
required: true,
},
keyName: {
type: String,
require: true,
required: true,
},
list: {
type: Array,
require: true,
required: true,
},
},
emits: ['scroll'],
@@ -141,12 +137,12 @@ export default {
const scrollContainerHeight = dom_scrollContainer.value.clientHeight
const currentEndIndex = currentStartIndex + Math.ceil(scrollContainerHeight / itemHeight)
const continuous = currentStartIndex <= endIndex && currentEndIndex >= startIndex
const currentStartRenderIndex = Math.max(currentStartIndex - props.outsideNum, 0)
const currentEndRenderIndex = currentEndIndex + props.outsideNum + 1
const currentStartRenderIndex = Math.max(currentStartIndex, 0)
const currentEndRenderIndex = currentEndIndex + 1
// console.log(continuous)
// debugger
if (continuous) {
// if (Math.abs(currentScrollTop - this.scrollTop) < this.itemHeight * this.outsideNum * 0.6) return
// if (Math.abs(currentScrollTop - this.scrollTop) < this.itemHeight * 0.6) return
// console.log('update')
// if (currentScrollTop > scrollTop) { // scroll down
// // console.log('scroll down')
@@ -162,9 +158,13 @@ export default {
// views.value = createList(currentStartRenderIndex, currentEndRenderIndex)
// } else return
if (currentScrollTop == scrollTop && endIndex >= currentEndIndex) return
views.value = createList(currentStartRenderIndex, currentEndRenderIndex)
requestAnimationFrame(() => {
views.value = createList(currentStartRenderIndex, currentEndRenderIndex)
})
} else {
views.value = createList(currentStartRenderIndex, currentEndRenderIndex)
requestAnimationFrame(() => {
views.value = createList(currentStartRenderIndex, currentEndRenderIndex)
})
}
startIndex = currentStartIndex
endIndex = currentEndIndex
@@ -173,42 +173,47 @@ export default {
const onScroll = event => {
const currentScrollTop = dom_scrollContainer.value.scrollTop
if (Math.abs(currentScrollTop - scrollTop) > props.itemHeight * props.outsideNum * 0.6) {
if (Math.abs(currentScrollTop - scrollTop) > props.itemHeight * 0.6) {
updateView(currentScrollTop)
}
emit('scroll', event)
}
const scrollTo = (scrollTop, animate = false) => {
return new Promise(resolve => {
if (cancelScroll) {
cancelScroll(resolve)
} else {
resolve()
}
}).then(() => {
return new Promise((resolve, reject) => {
const scrollTo = (scrollTop, animate = false, onScrollEnd) => {
if (onScrollEnd) {
new Promise(resolve => {
if (cancelScroll) {
cancelScroll(resolve)
} else {
resolve()
}
}).then(() => {
if (animate) {
isScrolling = true
scrollToValue = scrollTop
cancelScroll = handleScroll(dom_scrollContainer.value, scrollTop, 300, () => {
cancelScroll = null
isScrolling = false
resolve()
onScrollEnd(true)
}, () => {
cancelScroll = null
isScrolling = false
reject('canceled')
onScrollEnd('canceled')
})
} else {
dom_scrollContainer.value.scrollTop = scrollTop
}
})
})
} else {
dom_scrollContainer.value.scrollTo({
top: scrollTop,
behavior: animate ? 'smooth' : 'instant',
})
}
}
const scrollToIndex = (index, offset = 0, animate = false) => {
return scrollTo(Math.max(index * props.itemHeight + offset, 0), animate)
const scrollToIndex = (index, offset = 0, animate = false, onScrollEnd) => {
scrollTo(Math.max(index * props.itemHeight + offset, 0), animate, onScrollEnd)
}
const getScrollTop = () => {
@@ -216,7 +221,7 @@ export default {
}
const handleResize = () => {
setTimeout(updateView)
window.setTimeout(updateView)
}
const contentStyle = computed(() => ({
@@ -0,0 +1,227 @@
// import {
// computed,
// ref,
// nextTick,
// watch,
// onMounted,
// onBeforeUnmount,
// } from 'vue'
// import { scrollTo } from '@common/utils/renderer'
// interface ListItem {
// item: LX.Music.MusicInfo
// top: number
// style: {
// position: string
// left: number
// right: number
// top: string
// height: string
// }
// index: number
// key: string
// }
// export default (props: { list: LX.Music.MusicInfo[], itemHeight: number }) => {
// const dom_scrollContainer = ref<HTMLElement | null>(null)
// const dom_list = ref<HTMLElement | null>(null)
// let startIndex = -1
// let endIndex = -1
// let scrollTop = -1
// let cachedList: ListItem[] = []
// let cancelScroll: null | (() => void) = null
// let isScrolling = false
// let scrollToValue = 0
// const createList = (startIndex: number, endIndex: number) => {
// if (startIndex == endIndex) return []
// console.log(startIndex, endIndex)
// const cache = cachedList.slice(startIndex, endIndex)
// const list = props.list.slice(startIndex, endIndex).map((item, i) => {
// if (cache[i]) return cache[i]
// const top = (startIndex + i) * props.itemHeight
// const index = startIndex + i
// return cachedList[index] = {
// item,
// top,
// style: { position: 'absolute', left: 0, right: 0, top: `${top}px`, height: `${props.itemHeight}px` },
// index,
// key: item.id,
// }
// })
// return list
// }
// // div.list-item(@click="handleListItemClick($event, index)" @contextmenu="handleListItemRightClick($event, index)"
// // :class="[{ selected: rightClickSelectedIndex == index }, { active: selectedList.includes(item) }]")
// // div.list-item-cell.nobreak.center(:style="{ width: rowWidth.r1 }" style="padding-left: 3px; padding-right: 3px;" :class="$style.noSelect" @click.stop) {{index + 1}}
// // div.list-item-cell.auto(:style="{ width: rowWidth.r2 }" :aria-label="item.name + (item.meta._qualitys.flac32bit ? ` - ${$t('tag__lossless_24bit')}` : (item.meta._qualitys.ape || item.meta._qualitys.flac || item.meta._qualitys.wav) ? ` - ${$t('tag__lossless')}` : item.meta._qualitys['320k'] ? ` - ${$t('tag__high_quality')}` : '') + (sourceTag ? ` - ${item.source}` : '')")
// // span.select {{item.name}}
// // span.badge.badge-theme-primary(:class="[$style.labelQuality, $style.noSelect]" v-if="item.meta._qualitys.flac32bit") {{$t('tag__lossless_24bit')}}
// // span.badge.badge-theme-primary(:class="[$style.labelQuality, $style.noSelect]" v-else-if="item.meta._qualitys.ape || item.meta._qualitys.flac || item.meta._qualitys.wav") {{$t('tag__lossless')}}
// // span.badge.badge-theme-secondary(:class="[$style.labelQuality, $style.noSelect]" v-else-if="item.meta._qualitys['320k']") {{$t('tag__high_quality')}}
// // span.badge.badge-theme-tertiary(:class="[$style.labelQuality, $style.noSelect]" v-if="sourceTag") {{item.source}}
// // div.list-item-cell(:style="{ width: rowWidth.r3 }" :aria-label="item.singer")
// // span.select {{item.singer}}
// // div.list-item-cell(:style="{ width: rowWidth.r4 }" :aria-label="item.albumName")
// // span.select {{item.meta.albumName}}
// // div.list-item-cell(:style="{ width: rowWidth.r5 }")
// // span(:class="[$style.time, $style.noSelect]") {{item.meta.interval || '--/--'}}
// // div.list-item-cell(:style="{ width: rowWidth.r6 }" style="padding-left: 0; padding-right: 0;")
// // material-list-buttons(:index="index" :class="$style.btns"
// // :remove-btn="false" @btn-click="handleListBtnClick"
// // :download-btn="assertApiSupport(item.source)")
// const renderListItem = (list: ListItem) => {
// const dom_listItem = document.createElement('div')
// dom_listItem.className = 'list-item'
// }
// const renderList = (list: ListItem[], type?: 'up' | 'down') => {
// if (!list.length) return
// console.log(list)
// const dom = document.createDocumentFragment()
// for (const item of list) {
// dom.appendChild(renderListItem(item))
// }
// switch (type) {
// case 'up':
// break
// case 'down':
// break
// default:
// // console.log()
// break
// }
// }
// const updateView = (force = false, currentScrollTop = dom_scrollContainer.value.scrollTop) => {
// // const currentScrollTop = this.$refs.dom_scrollContainer.scrollTop
// const itemHeight = props.itemHeight
// const currentStartIndex = Math.floor(currentScrollTop / itemHeight)
// const scrollContainerHeight = dom_scrollContainer.value.clientHeight
// const currentEndIndex = currentStartIndex + Math.ceil(scrollContainerHeight / itemHeight)
// const continuous = currentStartIndex <= endIndex && currentEndIndex >= startIndex
// const currentStartRenderIndex = Math.max(currentStartIndex, 0)
// const currentEndRenderIndex = currentEndIndex + 1
// // console.log(continuous, currentStartIndex, endIndex, currentEndIndex, startIndex)
// // debugger
// if (!force && continuous) {
// // if (Math.abs(currentScrollTop - this.scrollTop) < this.itemHeight * 0.6) return
// // console.log('update')
// if (currentScrollTop > scrollTop) { // scroll down
// console.log('scroll down')
// renderList(createList(endIndex + 1, currentEndRenderIndex))
// // // views.value.push(...list.slice(list.indexOf(views.value[views.value.length - 1]) + 1))
// // // // if (this.views.length > 100) {
// // // nextTick(() => {
// // // views.value.splice(0, views.value.indexOf(list[0]))
// // // })
// // // }
// } else if (currentScrollTop < scrollTop) { // scroll up
// console.log('scroll up')
// renderList(createList(currentStartRenderIndex, startIndex))
// // views.value = createList(currentStartRenderIndex, currentEndRenderIndex)
// } else return
// // if (currentScrollTop == scrollTop && endIndex >= currentEndIndex) return
// // views.value = createList(currentStartRenderIndex, currentEndRenderIndex)
// } else {
// renderList(createList(currentStartRenderIndex, currentEndRenderIndex))
// }
// startIndex = currentStartIndex
// endIndex = currentEndIndex
// scrollTop = currentScrollTop
// }
// const onScroll = event => {
// const currentScrollTop = dom_scrollContainer.value.scrollTop
// if (Math.abs(currentScrollTop - scrollTop) > props.itemHeight * 0.6) {
// updateView(false, currentScrollTop)
// }
// emit('scroll', event)
// }
// const scrollTo = async(scrollTop, animate = false) => {
// return await new Promise(resolve => {
// if (cancelScroll) {
// cancelScroll(resolve)
// } else {
// resolve()
// }
// }).then(async() => {
// return await new Promise((resolve, reject) => {
// if (animate) {
// isScrolling = true
// scrollToValue = scrollTop
// cancelScroll = handleScroll(dom_scrollContainer.value, scrollTop, 300, () => {
// cancelScroll = null
// isScrolling = false
// resolve()
// }, () => {
// cancelScroll = null
// isScrolling = false
// reject('canceled')
// })
// } else {
// dom_scrollContainer.value.scrollTop = scrollTop
// }
// })
// })
// }
// const scrollToIndex = async(index, offset = 0, animate = false) => {
// return await scrollTo(Math.max(index * props.itemHeight + offset, 0), animate)
// }
// const getScrollTop = () => {
// return isScrolling ? scrollToValue : dom_scrollContainer.value.scrollTop
// }
// const handleResize = () => {
// setTimeout(updateView)
// }
// const contentStyle = computed(() => ({
// display: 'block',
// height: props.list.length * props.itemHeight + 'px',
// }))
// const handleReset = list => {
// cachedList = Array(list.length)
// startIndex = -1
// endIndex = -1
// void nextTick(() => {
// updateView(true)
// })
// }
// watch(() => props.itemHeight, () => {
// handleReset(props.list)
// })
// watch(() => props.list, (list) => {
// handleReset(list)
// }, {
// deep: true,
// })
// onMounted(() => {
// dom_scrollContainer.value!.addEventListener('scroll', onScroll, false)
// cachedList = Array(props.list.length)
// startIndex = -1
// endIndex = -1
// updateView(true)
// window.addEventListener('resize', handleResize)
// })
// onBeforeUnmount(() => {
// dom_scrollContainer.value!.removeEventListener('scroll', onScroll)
// window.removeEventListener('resize', handleResize)
// if (cancelScroll) cancelScroll()
// })
// return {
// dom_scrollContainer,
// dom_list,
// contentStyle,
// scrollTo,
// scrollToIndex,
// getScrollTop,
// }
// }
@@ -1,31 +1,31 @@
<template>
<div :class="$style.content">
<canvas :class="$style.canvas" ref="dom_canvas"></canvas>
<canvas ref="dom_canvas" :class="$style.canvas" />
</div>
</template>
<script>
import { ref, onBeforeUnmount, onMounted, useRefGetter, watch } from '@renderer/utils/vueTools'
import { ref, onBeforeUnmount, onMounted } from '@common/utils/vueTools'
import { getAnalyser } from '@renderer/plugins/player'
import { isPlay } from '@renderer/core/share/player'
import { player as eventPlayerNames } from '@renderer/event/names'
import { isPlay } from '@renderer/store/player/state'
// import { appSetting } from '@renderer/store/setting'
const themes = {
green: 'rgba(77,175,124,.16)',
blue: 'rgba(52,152,219,.16)',
yellow: 'rgba(233,212,96,.22)',
orange: 'rgba(245,171,53,.16)',
red: 'rgba(214,69,65,.12)',
pink: 'rgba(241,130,141,.16)',
purple: 'rgba(155,89,182,.14)',
grey: 'rgba(108,122,137,.16)',
ming: 'rgba(51,110,123,.14)',
blue2: 'rgba(79,98,208,.14)',
black: 'rgba(39,39,39,.4)',
mid_autumn: 'rgba(74,55,82,.1)',
naruto: 'rgba(87,144,167,.15)',
happy_new_year: 'rgba(192,57,43,.1)',
}
// const themes = {
// green: 'rgba(77,175,124,.16)',
// blue: 'rgba(52,152,219,.16)',
// yellow: 'rgba(233,212,96,.22)',
// orange: 'rgba(245,171,53,.16)',
// red: 'rgba(214,69,65,.12)',
// pink: 'rgba(241,130,141,.16)',
// purple: 'rgba(155,89,182,.14)',
// grey: 'rgba(108,122,137,.16)',
// ming: 'rgba(51,110,123,.14)',
// blue2: 'rgba(79,98,208,.14)',
// black: 'rgba(39,39,39,.4)',
// mid_autumn: 'rgba(74,55,82,.1)',
// naruto: 'rgba(87,144,167,.15)',
// happy_new_year: 'rgba(192,57,43,.1)',
// }
const getBarWidth = canvasWidth => {
let barWidth = (canvasWidth / 128) * 2.5
@@ -60,18 +60,15 @@ export default {
const maxNum = 255
let frequencyAvg = 0
const theme = useRefGetter('theme')
// const theme = useRefGetter('theme')
// const setting = useRefGetter('setting')
let themeColor = themes[theme.value || 'green']
watch(theme, theme => {
themeColor = themes[theme || 'green']
})
let themeColor = getComputedStyle(document.documentElement).getPropertyValue('--color-primary-light-200-alpha-800')
// watch(theme, theme => {
// themeColor = themes[theme || 'green']
// })
// https://developer.mozilla.org/zh-CN/docs/Web/API/AnalyserNode/smoothingTimeConstant
const renderFrame = () => {
animationFrameId = null
if (isPlaying) animationFrameId = window.requestAnimationFrame(renderFrame)
x = 0
analyser.getByteFrequencyData(dataArray)
@@ -107,11 +104,14 @@ export default {
x += barWidth
}
animationFrameId = null
if (isPlaying) animationFrameId = window.requestAnimationFrame(renderFrame)
}
const handlePlay = () => {
isPlaying = true
analyser.fftSize = 256
// analyser.fftSize = 256
bufferLength = analyser.frequencyBinCount
// console.log(bufferLength)
barWidth = getBarWidth(WIDTH)
@@ -134,15 +134,15 @@ export default {
barWidth = getBarWidth(WIDTH)
}
window.eventHub.on(eventPlayerNames.play, handlePlay)
window.eventHub.on(eventPlayerNames.pause, handlePause)
window.eventHub.on(eventPlayerNames.error, handlePause)
window.app_event.on('play', handlePlay)
window.app_event.on('pause', handlePause)
window.app_event.on('error', handlePause)
window.addEventListener('resize', handleResize)
onBeforeUnmount(() => {
handlePause()
window.eventHub.off(eventPlayerNames.play, handlePlay)
window.eventHub.off(eventPlayerNames.pause, handlePause)
window.eventHub.off(eventPlayerNames.error, handlePause)
window.app_event.off('play', handlePlay)
window.app_event.off('pause', handlePause)
window.app_event.off('error', handlePause)
window.removeEventListener('resize', handleResize)
})
@@ -1,16 +1,17 @@
<template>
<material-modal :show="show" :bg-close="bgClose" @close="handleClose" :teleport="teleport">
<main :class="$style.main">
<h2>{{ info.name }}<br/>{{ info.singer }}</h2>
<base-btn :class="$style.btn" :key="type.type" @click="handleClick(type.type)" v-for="type in types"
>{{getTypeName(type.type)}}{{ type.size && ` - ${type.size.toUpperCase()}` }}</base-btn>
</main>
</material-modal>
<material-modal :show="show" :bg-close="bgClose" :teleport="teleport" @close="handleClose">
<main :class="$style.main">
<h2>{{ info.name }}<br>{{ info.singer }}</h2>
<base-btn v-for="quality in qualitys" :key="quality.type" :class="$style.btn" @click="handleClick(quality.type)">
{{ getTypeName(quality.type) }}{{ quality.size && ` - ${quality.size.toUpperCase()}` }}
</base-btn>
</main>
</material-modal>
</template>
<script>
import { mapActions } from 'vuex'
import { qualityList } from '@renderer/core/share'
import { qualityList } from '@renderer/store'
import { createDownloadTasks } from '@renderer/store/download/action'
export default {
props: {
@@ -19,13 +20,17 @@ export default {
default: false,
},
musicInfo: {
type: Object,
type: [Object, null],
required: true,
},
bgClose: {
type: Boolean,
default: true,
},
teleport: String,
teleport: {
type: String,
default: '#root',
},
},
emits: ['update:show'],
setup() {
@@ -40,36 +45,35 @@ export default {
sourceQualityList() {
return this.qualityList[this.musicInfo.source] || []
},
types() {
return this.info?.types?.filter(type => this.checkSource(type.type)) || []
qualitys() {
return this.info.meta?.qualitys?.filter(quality => this.checkSource(quality.type)) || []
},
},
methods: {
...mapActions('download', ['createDownload']),
handleClick(type) {
this.createDownload({ musicInfo: this.musicInfo, type })
handleClick(quality) {
createDownloadTasks([this.musicInfo], quality)
this.handleClose()
},
handleClose() {
this.$emit('update:show', false)
},
getTypeName(type) {
switch (type) {
case 'flac32bit':
getTypeName(quality) {
switch (quality) {
case 'flac24bit':
return this.$t('download__lossless') + ' FLAC Hires'
case 'flac':
case 'ape':
case 'wav':
return this.$t('download__lossless') + ' ' + type.toUpperCase()
return this.$t('download__lossless') + ' ' + quality.toUpperCase()
case '320k':
return this.$t('download__high_quality') + ' ' + type.toUpperCase()
return this.$t('download__high_quality') + ' ' + quality.toUpperCase()
case '192k':
case '128k':
return this.$t('download__normal') + ' ' + type.toUpperCase()
return this.$t('download__normal') + ' ' + quality.toUpperCase()
}
},
checkSource(type) {
return this.sourceQualityList.includes(type)
checkSource(quality) {
return this.sourceQualityList.includes(quality)
},
},
}
@@ -88,7 +92,7 @@ export default {
justify-content: center;
h2 {
font-size: 13px;
color: @color-theme_2-font;
color: var(--color-font);
line-height: 1.3;
text-align: center;
margin-bottom: 15px;
@@ -103,14 +107,4 @@ export default {
}
}
each(@themes, {
:global(#root.@{value}) {
.main {
h2 {
color: ~'@{color-@{value}-theme_2-font}';
}
}
}
})
</style>
@@ -1,17 +1,17 @@
<template>
<material-modal :show="show" :bg-close="bgClose" @close="handleClose" :teleport="teleport">
<main :class="$style.main">
<h2>{{$t('download__multiple_tip', { len: list.length })}}<br/>{{$t('download__multiple_tip2')}}</h2>
<base-btn :class="$style.btn" @click="handleClick('128k')">{{$t('download__normal')}} - 128K</base-btn>
<base-btn :class="$style.btn" @click="handleClick('320k')">{{$t('download__high_quality')}} - 320K</base-btn>
<base-btn :class="$style.btn" @click="handleClick('flac')">{{$t('download__lossless')}} - FLAC</base-btn>
<base-btn :class="$style.btn" @click="handleClick('flac32bit')">{{$t('download__lossless')}} - FLAC Hires</base-btn>
</main>
</material-modal>
<material-modal :show="show" :bg-close="bgClose" :teleport="teleport" @close="handleClose">
<main :class="$style.main">
<h2>{{ $t('download__multiple_tip', { len: list.length }) }}<br>{{ $t('download__multiple_tip2') }}</h2>
<base-btn :class="$style.btn" @click="handleClick('128k')">{{ $t('download__normal') }} - 128K</base-btn>
<base-btn :class="$style.btn" @click="handleClick('320k')">{{ $t('download__high_quality') }} - 320K</base-btn>
<base-btn :class="$style.btn" @click="handleClick('flac')">{{ $t('download__lossless') }} - FLAC</base-btn>
<base-btn :class="$style.btn" @click="handleClick('flac24bit')">{{ $t('download__lossless') }} - FLAC Hires</base-btn>
</main>
</material-modal>
</template>
<script>
import { mapActions } from 'vuex'
import { createDownloadTasks } from '@renderer/store/download/action'
export default {
props: {
@@ -29,13 +29,15 @@ export default {
return []
},
},
teleport: String,
teleport: {
type: String,
default: '#root',
},
},
emits: ['update:show', 'confirm'],
methods: {
...mapActions('download', ['createDownloadMultiple']),
handleClick(type) {
this.createDownloadMultiple({ list: [...this.list], type })
handleClick(quality) {
createDownloadTasks(this.list.filter(item => item.source != 'local'), quality)
this.handleClose()
this.$emit('confirm')
},
@@ -59,7 +61,7 @@ export default {
justify-content: center;
h2 {
font-size: 13px;
color: @color-theme_2-font;
color: var(--color-font);
line-height: 1.3;
text-align: center;
margin-bottom: 15px;
@@ -74,14 +76,4 @@ export default {
}
}
each(@themes, {
:global(#root.@{value}) {
.main {
h2 {
color: ~'@{color-@{value}-theme_2-font}';
}
}
}
})
</style>
+61 -54
View File
@@ -1,26 +1,26 @@
<template>
<material-modal :show="show" :bg-close="bgClose" @close="handleClose" :teleport="teleport" max-width="70%">
<main :class="$style.main">
<h2>{{$t('list_add__' + (isMove ? 'title_first_move' : 'title_first_add'))}}&nbsp;<span :class="$style.name">{{this.musicInfo && `${musicInfo.name}`}}</span>&nbsp;{{$t('list_add__title_last')}}</h2>
<div class="scroll" :class="$style.btnContent">
<base-btn :class="$style.btn" :aria-label="$t('list_add__btn_title', { name: item.name })" :key="item.id" :disabled="item.isExist" @click="handleClick(index)" v-for="(item, index) in lists">{{item.name}}</base-btn>
<base-btn :class="[$style.btn, $style.newList, isEditing ? $style.editing : null]" @click="handleEditing($event)" :aria-label="$t('lists__new_list_btn')">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" viewBox="0 0 42 42" space="preserve">
<use xlink:href="#icon-addTo"></use>
</svg>
<input :class="$style.newListInput" :value="newListName" type="text" :placeholder="$t('lists__new_list_input')" @keyup.enter="handleSaveList($event)" @blur="handleSaveList($event)"/>
</base-btn>
<span :class="$style.btn" :key="i" v-for="i in spaceNum"></span>
</div>
</main>
</material-modal>
<material-modal :show="show" :bg-close="bgClose" :teleport="teleport" max-width="70%" min-width="200px" @close="handleClose">
<main :class="$style.main">
<h2>{{ $t('list_add__' + (isMove ? 'title_first_move' : 'title_first_add')) }}&nbsp;<span :class="$style.name">{{ currentMusicInfo.name }}</span>&nbsp;{{ $t('list_add__title_last') }}</h2>
<div class="scroll" :class="$style.btnContent">
<base-btn v-for="(item, index) in lists" :key="item.id" :class="$style.btn" :aria-label="$t('list_add__btn_title', { name: item.name })" :disabled="item.isExist" @click="handleClick(index)">{{ item.name }}</base-btn>
<base-btn :class="[$style.btn, $style.newList, isEditing ? $style.editing : null]" :aria-label="$t('lists__new_list_btn')" @click="handleEditing($event)">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" viewBox="0 0 42 42" space="preserve">
<use xlink:href="#icon-addTo" />
</svg>
<input :class="$style.newListInput" :value="newListName" type="text" :placeholder="$t('lists__new_list_input')" @keyup.enter="handleSaveList($event)" @blur="handleSaveList($event)">
</base-btn>
<span v-for="i in spaceNum" :key="i" :class="$style.btn" />
</div>
</main>
</material-modal>
</template>
<script>
import { mapMutations } from 'vuex'
import { computed } from '@renderer/utils/vueTools'
import { defaultList, loveList, userLists } from '@renderer/core/share/list'
import { getList } from '@renderer/core/share/utils'
// import { mapMutations } from 'vuex'
import { watch, ref } from '@common/utils/vueTools'
import { defaultList, loveList, userLists } from '@renderer/store/list/state'
import { addListMusics, moveListMusics, createUserList, getMusicExistListIds } from '@renderer/store/list/action'
import useKeyDown from '@renderer/utils/compositions/useKeyDown'
export default {
@@ -30,7 +30,8 @@ export default {
default: false,
},
musicInfo: {
type: Object,
type: [Object, null],
required: true,
},
bgClose: {
type: Boolean,
@@ -54,24 +55,48 @@ export default {
type: Boolean,
default: false,
},
teleport: String,
teleport: {
type: String,
default: '#root',
},
},
emits: ['update:show'],
setup(props) {
const keyModDown = useKeyDown('mod')
const lists = ref([])
const lists = computed(() => {
if (!props.musicInfo) return []
const targetMid = props.musicInfo.songmid
return [
const currentMusicInfo = ref({})
const checkMusicExist = (musicInfo) => {
const mid = musicInfo.id
getMusicExistListIds(mid).then(ids => {
if (mid != musicInfo.id) return
for (const list of lists.value) {
if (ids.includes(list.id)) list.isExist = true
}
})
}
watch(() => props.show, show => {
if (!show) return
if (!props.musicInfo) return lists.value = []
currentMusicInfo.value = 'progress' in props.musicInfo ? props.musicInfo.metadata.musicInfo : props.musicInfo
lists.value = [
defaultList,
loveList,
...userLists,
].filter(l => !props.excludeListId.includes(l.id)).map(l => ({ ...l, isExist: getList(l.id).some(s => s.songmid == targetMid) }))
].filter(l => !props.excludeListId.includes(l.id)).map(l => ({ ...l, isExist: false }))
checkMusicExist(currentMusicInfo.value)
})
return {
keyModDown,
lists,
checkMusicExist,
currentMusicInfo,
}
},
data() {
@@ -94,7 +119,6 @@ export default {
window.removeEventListener('resize', this.handleResize)
},
methods: {
...mapMutations('list', ['listAdd', 'listMove', 'createUserList']),
handleResize() {
const width = window.innerWidth
this.rowNum = width < 1920
@@ -105,9 +129,10 @@ export default {
},
handleClick(index) {
this.isMove
? this.listMove({ fromId: this.fromListId, toId: this.lists[index].id, musicInfo: this.musicInfo })
: this.listAdd({ id: this.lists[index].id, musicInfo: this.musicInfo })
? moveListMusics(this.fromListId, this.lists[index].id, [this.currentMusicInfo])
: addListMusics(this.lists[index].id, [this.currentMusicInfo])
this.lists[index].isExist = true
if (this.keyModDown && !this.isMove) return
this.$nextTick(() => {
this.handleClose()
@@ -127,7 +152,7 @@ export default {
this.newListName = event.target.value = ''
this.isEditing = false
if (!name) return
this.createUserList({ name })
createUserList({ name })
},
},
}
@@ -140,7 +165,7 @@ export default {
.main {
// padding: 15px 0;
// max-width: 70%;
min-width: 200px;
// min-width: 200px;
display: flex;
flex-flow: column nowrap;
justify-content: center;
@@ -149,7 +174,7 @@ export default {
// overflow: hidden;
h2 {
font-size: 13px;
color: @color-theme_2-font;
color: var(--color-font);
line-height: 1.3;
text-align: center;
padding: 15px;
@@ -157,7 +182,7 @@ export default {
}
.name {
color: @color-theme;
color: var(--color-primary);
}
.btn-content {
@@ -184,9 +209,9 @@ export default {
}
.newList {
border: 1px dashed @color-theme-hover;
background-color: @color-theme_2-background_2;
color: @color-theme-hover;
border: 1px dashed var(--color-primary-font-hover);
// background-color: var(--color-main-background);
color: var(--color-primary-font-hover);
opacity: .7;
svg {
@@ -243,22 +268,4 @@ export default {
}
}
each(@themes, {
:global(#root.@{value}) {
.main {
h2 {
color: ~'@{color-@{value}-theme_2-font}';
}
}
.name {
color: ~'@{color-@{value}-theme}';
}
.newList {
border-color: ~'@{color-@{value}-theme-hover}';
color: ~'@{color-@{value}-theme-hover}';
background-color: ~'@{color-@{value}-theme_2-background_2}';
}
}
})
</style>
@@ -1,25 +1,25 @@
<template>
<material-modal :show="show" :bg-close="bgClose" @close="handleClose" max-width="70%">
<main :class="$style.main">
<h2>{{$t('list_add__multiple_' + (isMove ? 'title_move' : 'title_add'), { num: musicList.length })}}</h2>
<div class="scroll" :class="$style.btnContent">
<base-btn :class="$style.btn" :aria-label="$t('list_add__multiple_btn_title', { name: item.name })" :key="item.id" @click="handleClick(index)" v-for="(item, index) in lists">{{item.name}}</base-btn>
<base-btn :class="[$style.btn, $style.newList, isEditing ? $style.editing : null]" @click="handleEditing($event)" :aria-label="$t('lists__new_list_btn')">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" viewBox="0 0 42 42" space="preserve">
<use xlink:href="#icon-addTo"></use>
</svg>
<input :class="$style.newListInput" :value="newListName" type="text" :placeholder="$t('lists__new_list_input')" @keyup.enter="handleSaveList($event)" @blur="handleSaveList($event)"/>
</base-btn>
<span :class="$style.btn" :key="i" v-for="i in spaceNum"></span>
</div>
</main>
</material-modal>
<material-modal :show="show" :bg-close="bgClose" max-width="70%" :teleport="teleport" @close="handleClose">
<main :class="$style.main">
<h2>{{ $t('list_add__multiple_' + (isMove ? 'title_move' : 'title_add'), { num: musicList.length }) }}</h2>
<div class="scroll" :class="$style.btnContent">
<base-btn v-for="(item, index) in lists" :key="item.id" :class="$style.btn" :aria-label="$t('list_add__multiple_btn_title', { name: item.name })" @click="handleClick(index)">{{ item.name }}</base-btn>
<base-btn :class="[$style.btn, $style.newList, isEditing ? $style.editing : null]" :aria-label="$t('lists__new_list_btn')" @click="handleEditing($event)">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" viewBox="0 0 42 42" space="preserve">
<use xlink:href="#icon-addTo" />
</svg>
<input :class="$style.newListInput" :value="newListName" type="text" :placeholder="$t('lists__new_list_input')" @keyup.enter="handleSaveList($event)" @blur="handleSaveList($event)">
</base-btn>
<span v-for="i in spaceNum" :key="i" :class="$style.btn" />
</div>
</main>
</material-modal>
</template>
<script>
import { mapMutations } from 'vuex'
import { computed } from '@renderer/utils/vueTools'
import { defaultList, loveList, userLists } from '@renderer/core/share/list'
import { computed } from '@common/utils/vueTools'
import { defaultList, loveList, userLists } from '@renderer/store/list/state'
import { addListMusics, moveListMusics, createUserList } from '@renderer/store/list/action'
import useKeyDown from '@renderer/utils/compositions/useKeyDown'
export default {
@@ -56,7 +56,10 @@ export default {
type: Boolean,
default: false,
},
teleport: String,
teleport: {
type: String,
default: '#root',
},
},
emits: ['update:show', 'confirm'],
setup(props) {
@@ -95,7 +98,6 @@ export default {
window.removeEventListener('resize', this.handleResize)
},
methods: {
...mapMutations('list', ['listAddMultiple', 'listMoveMultiple', 'createUserList']),
handleResize() {
const width = window.innerWidth
this.rowNum = width < 1920
@@ -105,9 +107,11 @@ export default {
: width < 3840 ? 5 : 6
},
handleClick(index) {
const list = 'progress' in this.musicList[0] ? this.musicList.map(t => t.metadata.musicInfo) : this.musicList
this.isMove
? this.listMoveMultiple({ fromId: this.fromListId, toId: this.lists[index].id, list: this.musicList })
: this.listAddMultiple({ id: this.lists[index].id, list: this.musicList })
? moveListMusics(this.fromListId, this.lists[index].id, list)
: addListMusics(this.lists[index].id, list)
if (this.keyModDown && !this.isMove) return
this.$nextTick(() => {
@@ -129,7 +133,7 @@ export default {
this.newListName = event.target.value = ''
this.isEditing = false
if (!name) return
this.createUserList({ name })
createUserList({ name })
},
},
}
@@ -151,7 +155,7 @@ export default {
// overflow: hidden;
h2 {
font-size: 13px;
color: @color-theme_2-font;
color: var(--color-font);
line-height: 1.3;
text-align: center;
padding: 15px;
@@ -182,9 +186,9 @@ export default {
}
.newList {
border: 1px dashed @color-theme-hover;
background-color: @color-theme_2-background_2;
color: @color-theme-hover;
border: 1px dashed var(--color-primary-font-hover);
// background-color: var(--color-main-background);
color: var(--color-primary-font-hover);
opacity: .7;
svg {
@@ -241,20 +245,4 @@ export default {
}
}
each(@themes, {
:global(#root.@{value}) {
.main {
h2 {
color: ~'@{color-@{value}-theme_2-font}';
}
}
.newList {
border-color: ~'@{color-@{value}-theme-hover}';
color: ~'@{color-@{value}-theme-hover}';
background-color: ~'@{color-@{value}-theme_2-background_2}';
}
}
})
</style>
+18 -32
View File
@@ -1,19 +1,21 @@
<template>
<div :class="[$style.progress, className]">
<div :class="[$style.progressBar, $style.progressBar2, {[$style.barTransition]: isActiveTransition}]" @transitionend="handleTransitionEnd" :style="{ transform: `scaleX(${progress || 0})` }"></div>
<div v-show="dragging" :class="[$style.progressBar, $style.progressBar3]" :style="{ transform: `scaleX(${dragProgress || 0})` }"></div>
</div>
<div :class="$style.progressMask" @mousedown="handleMsDown" ref="dom_progress"></div>
<div :class="[$style.progress, className]">
<div :class="[$style.progressBar, $style.progressBar2, {[$style.barTransition]: isActiveTransition}]" :style="{ transform: `scaleX(${progress || 0})` }" @transitionend="handleTransitionEnd" />
<div v-show="dragging" :class="[$style.progressBar, $style.progressBar3]" :style="{ transform: `scaleX(${dragProgress || 0})` }" />
</div>
<div ref="dom_progress" :class="$style.progressMask" @mousedown="handleMsDown" />
</template>
<script>
import { ref, onBeforeUnmount } from '@renderer/utils/vueTools'
import { player as eventPlayerNames } from '@renderer/event/names'
import { playProgress } from '@renderer/core/share/playProgress'
import { ref, onBeforeUnmount } from '@common/utils/vueTools'
import { playProgress } from '@renderer/store/player/playProgress'
export default {
props: {
className: String,
className: {
type: String,
default: '',
},
progress: {
type: Number,
required: true,
@@ -70,7 +72,7 @@ export default {
})
const setProgress = num => {
window.eventHub.emit(eventPlayerNames.setProgress, num)
window.app_event.setProgress(num)
}
// const handleSetProgress = event => {
@@ -93,11 +95,11 @@ export default {
.progress {
width: 100%;
height: 4px;
height: 5px;
// overflow: hidden;
transition: @transition-theme;
transition: @transition-normal;
transition-property: background-color;
background-color: @color-player-progress;
background-color: var(--color-primary-light-100-alpha-800);
// background-color: #f5f5f5;
position: relative;
border-radius: 20px;
@@ -120,16 +122,16 @@ export default {
border-radius: 20px;
}
.progress-bar1 {
background-color: @color-player-progress-bar1;
background-color: var(--color-primary-light-100-alpha-600);
}
.progress-bar2 {
background-color: @color-player-progress-bar2;
background-color: var(--color-primary-light-100-alpha-400);
will-change: transform;
}
.progress-bar3 {
background-color: @color-player-progress-bar2;
background-color: var(--color-primary-light-100-alpha-200);
box-shadow: 0 0 2px rgba(0, 0, 0, 0.3);
opacity: 0.5;
}
@@ -140,20 +142,4 @@ export default {
transition-duration: 0.2s;
}
each(@themes, {
:global(#root.@{value}) {
.progress {
background-color: ~'@{color-@{value}-player-progress}';
}
.progress-bar1 {
background-color: ~'@{color-@{value}-player-progress-bar1}';
}
.progress-bar2 {
background-color: ~'@{color-@{value}-player-progress-bar2}';
}
.progress-bar3 {
background-color: ~'@{color-@{value}-player-progress-bar2}';
}
}
})
</style>
+18 -30
View File
@@ -1,25 +1,24 @@
<template>
<div :class="[$style.volumeContent, className]">
<div :class="[$style.volume, {[$style.muted]: setting.player.isMute} ]">
<div :class="$style.volumeBar" ref="dom_volumeBar" :style="{ transform: `scaleX(${volume || 0})` }"></div>
<div :class="[$style.volumeContent, className]">
<div :class="[$style.volume, {[$style.muted]: appSetting['player.isMute']} ]">
<div ref="dom_volumeBar" :class="$style.volumeBar" :style="{ transform: `scaleX(${volume || 0})` }" />
</div>
<div :class="$style.volumeMask" :aria-label="`${$t('player__volume')}${parseInt(volume * 100)}%`" @mousedown="handleVolumeMsDown" />
</div>
<div :class="$style.volumeMask" @mousedown="handleVolumeMsDown" :aria-label="`${$t('player__volume')}${parseInt(volume * 100)}%`"></div>
</div>
</template>
<script>
import { ref, onBeforeUnmount } from '@renderer/utils/vueTools'
import { ref, onBeforeUnmount } from '@common/utils/vueTools'
// import { player as eventPlayerNames } from '@renderer/event/names'
import { volume, isMute, setVolume, setMute } from '@renderer/core/share/volume'
import { volume, isMute } from '@renderer/store/player/volume'
import { appSetting } from '@renderer/store/setting'
export default {
props: {
className: {
type: String,
},
setting: {
type: Object,
required: true,
default: '',
},
},
setup(props) {
@@ -38,9 +37,9 @@ export default {
if (val < 0) val = 0
if (val > 1) val = 1
setVolume(volumeEvent.msDownVolume = val)
window.app_event.setVolume(volumeEvent.msDownVolume = val)
if (isMute.value) setMute(false)
if (isMute.value) window.app_event.setVolumeIsMute(false)
}
const handleVolumeMsUp = () => {
volumeEvent.isMsDown = false
@@ -50,7 +49,7 @@ export default {
let volume = volumeEvent.msDownVolume + (event.clientX - volumeEvent.msDownX) / dom_volumeBar.value.clientWidth
if (volume > 1) volume = 1
else if (volume < 0) volume = 0
setVolume(volume)
window.app_event.setVolume(volume)
}
document.addEventListener('mousemove', handleVolumeMsMove)
@@ -65,6 +64,7 @@ export default {
dom_volumeBar,
volume,
isMute,
appSetting,
}
},
}
@@ -81,7 +81,7 @@ export default {
display: flex;
align-items: center;
opacity: .5;
transition: opacity @transition-theme;
transition: opacity @transition-normal;
&:hover {
opacity: 1;
}
@@ -93,9 +93,9 @@ export default {
height: 0.25em;
border-radius: 10px;
// overflow: hidden;
transition: @transition-theme;
transition: @transition-normal;
transition-property: background-color, opacity;
background-color: @color-player-progress-bar1;
background-color: var(--color-button-background);
// background-color: #f5f5f5;
position: relative;
border-radius: @radius-progress-border;
@@ -117,7 +117,7 @@ export default {
height: 100%;
border-radius: @radius-progress-border;
transition-duration: 0.2s;
background-color: @color-btn;
background-color: var(--color-button-background);
box-shadow: 0 0 2px rgba(0, 0, 0, 0.2);
}
@@ -129,16 +129,4 @@ export default {
cursor: pointer;
}
each(@themes, {
:global(#root.@{value}) {
.volume {
background-color: ~'@{color-@{value}-player-progress-bar1}';
}
.volume-bar {
background-color: ~'@{color-@{value}-btn}';
}
}
})
</style>
@@ -1,113 +0,0 @@
<template>
<div :class="$style.controlBtn" v-show="!isFullscreen">
<button type="button" :class="[$style.btn, $style.close]" :aria-label="$t('close')" @click="close">
<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"></use>
</svg>
</button>
<button type="button" :class="[$style.btn, $style.min]" :aria-label="$t('min')" @click="min">
<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"></use>
</svg>
</button>
</div>
</template>
<script>
import { base as eventBaseName } from '@renderer/event/names'
// import { getRandom } from '../../utils'
import { isFullscreen } from '@renderer/core/share'
export default {
setup() {
return {
isFullscreen,
min() {
window.eventHub.emit(eventBaseName.min)
},
max() {
window.eventHub.emit(eventBaseName.max)
},
close() {
window.eventHub.emit(eventBaseName.close)
},
}
},
}
</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-theme;
&: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: @color-theme_2;
&.min {
background-color: @color-minBtn;
}
&.max {
background-color: @color-maxBtn;
}
&.close {
background-color: @color-closeBtn;
}
}
.controlBtniIcon {
opacity: 0;
transition: opacity 0.2s ease-in-out;
}
each(@themes, {
:global(#root.@{value}) {
.btn {
color: ~'@{color-@{value}-theme_2}';
&.min {
background-color: ~'@{color-@{value}-minBtn}';
}
&.max {
background-color: ~'@{color-@{value}-maxBtn}';
}
&.close {
background-color: ~'@{color-@{value}-closeBtn}';
}
}
}
})
</style>
@@ -1,77 +0,0 @@
<template>
<div :class="[$style.aside, { [$style.fullscreen]: isFullscreen }]">
<ControlBtns v-if="setting.controlBtnPosition == 'left'" />
<div :class="$style.logo" v-else>L X</div>
<NavBar />
</div>
</template>
<script>
import { mapGetters } from 'vuex'
import { isFullscreen } from '@renderer/core/share'
import ControlBtns from './ControlBtns'
import NavBar from './NavBar'
export default {
name: 'CoreAside',
components: { ControlBtns, NavBar },
setup() {
return {
isFullscreen,
}
},
computed: {
...mapGetters(['setting']),
},
}
</script>
<style lang="less" module>
@import '@renderer/assets/styles/layout.less';
.aside {
// box-shadow: 0 0 5px rgba(0, 0, 0, .3);
transition: @transition-theme;
transition-property: background-color;
background-color: @color-theme-sidebar;
// background-color: @color-aside-background;
// border-right: 2px solid @color-theme;
-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: @color-theme-font;
flex: none;
text-align: center;
line-height: 50px;
font-weight: bold;
// -webkit-app-region: no-drag;
}
each(@themes, {
:global(#root.@{value}) {
.aside {
background-color: ~'@{color-@{value}-theme-sidebar}';
}
.logo {
color: ~'@{color-@{value}-theme-font}';
}
}
})
</style>
File diff suppressed because one or more lines are too long
-186
View File
@@ -1,186 +0,0 @@
<template lang="pug">
material-modal(:show="!setting.isAgreePact || isShowPact" @close="handleClose(false)" :bgClose="setting.isAgreePact" :close-btn="setting.isAgreePact")
main(:class="$style.main")
h2 许可协议
div.select.scroll(:class="$style.content")
p
| 本项目(软件)基于
strong.hover.underline(@click="openUrl('http://www.apache.org/licenses/LICENSE-2.0')") Apache License 2.0
| &nbsp;许可证发行,在使用本软件前,你(使用者)需签署本协议才可继续使用,以下协议是对于 Apache License 2.0 的补充,如有冲突,以以下协议为准。
br
p 词语约定:本协议中的“本软件”指洛雪音乐桌面版项目;“使用者”指签署本协议的使用者;“官方音乐平台”指对本软件内置的包括酷我、酷狗、咪咕等音乐源的官方平台统称;“版权数据”指包括但不限于图像、音频、名字等在内的他人拥有所属版权的数据。
br
p
strong 1、
| 本软件的数据来源原理是从各官方音乐平台的公开服务器中拉取数据,经过对数据简单地筛选与合并后进行展示,因此本软件不对数据的准确性负责。
p
strong 2、
| 使用本软件的过程中可能会产生版权数据,对于这些版权数据,本软件不拥有它们的所有权,为了避免造成侵权,使用者务必在
strong 24小时内
| 清除使用本软件的过程中所产生的版权数据。
p
strong 3、
| 本软件内的官方音乐平台别名为本软件内对官方音乐平台的一个称呼,不包含恶意,如果官方音乐平台觉得不妥,可联系本软件更改或移除。
p
strong 4、
| 本软件内使用的部分包括但不限于字体、图片等资源来源于互联网,如果出现侵权可联系本软件移除。
p
strong 5、
| 由于使用本软件产生的包括由于本协议或由于使用或无法使用本软件而引起的任何性质的任何直接、间接、特殊、偶然或结果性损害(包括但不限于因商誉损失、停工、计算机故障或故障引起的损害赔偿,或任何及所有其他商业损害或损失)由使用者负责。
p
strong 6、
| 本项目完全免费,且开源发布于&nbsp;
span.hover.underline(@click="openUrl('https://github.com/lyswhut/lx-music-desktop#readme')") GitHub
| &nbsp;面向全世界人用作对技术的学习交流,本软件不对项目内的技术可能存在违反当地法律法规的行为作保证,
strong 禁止在违反当地法律法规的情况下使用本软件
|,对于使用者在明知或不知当地法律法规不允许的情况下使用本软件所造成的任何违法违规行为由使用者承担,本软件不承担由此造成的任何直接、间接、特殊、偶然或结果性责任。
br
p
strong *
| &nbsp;若协议更新,恕不另行通知,可到开源地址查看。
p
strong *
| &nbsp;本软件的初衷是帮助官方音乐平台简化数据后代为展示,帮助使用者根据歌曲名、艺术家等关键字快速地定位所需内容所在的音乐平台。
p
strong *
| &nbsp;音乐平台不易,建议到对应音乐平台支持正版资源。
br
p(v-if="!setting.isAgreePact")
strong 若你(使用者)接受以上协议,请点击下面的“接受”按钮签署本协议,若不接受,请点击“不接受”后退出软件并清除本软件的所有数据。
div(:class="$style.btns" v-if="!setting.isAgreePact")
base-btn(:class="$style.btn" @click="handleClose(true)") {{$t('not_agree')}}
base-btn(:class="$style.btn" :disabled="!btnEnable" @click="handleClick()") {{$t('agree')}} {{timeStr}}
</template>
<script>
import { mapGetters, mapMutations } from 'vuex'
import { rendererSend, NAMES } from '@common/ipc'
import { openUrl } from '@renderer/utils'
import { isShowPact } from '@renderer/core/share'
export default {
setup() {
return {
isShowPact,
}
},
data() {
return {
time: 20,
}
},
computed: {
...mapGetters(['setting']),
btnEnable() {
return this.time == 0
},
timeStr() {
return this.btnEnable ? '' : `(${this.time})`
},
},
watch: {
'setting.isAgreePact'(n) {
if (n) return
this.time = 5
this.startTimeout()
},
},
mounted() {
this.$nextTick(() => {
if (!this.setting.isAgreePact) {
this.startTimeout()
}
})
},
methods: {
...mapMutations(['setAgreePact']),
handleClick() {
this.setAgreePact()
setTimeout(() => {
this.$dialog({
message: Buffer.from('e69cace8bdafe4bbb6e5ae8ce585a8e5858de8b4b9e4b894e5bc80e6ba90efbc8ce5a682e69e9ce4bda0e698afe88ab1e992b1e8b4ade4b9b0e79a84efbc8ce8afb7e79bb4e68ea5e7bb99e5b7aee8af84efbc810a0a5468697320736f667477617265206973206672656520616e64206f70656e20736f757263652e', 'hex').toString(),
confirmButtonText: Buffer.from('e5a5bde79a8420284f4b29', 'hex').toString(),
})
}, 2e3)
},
handleClose(isExit) {
if (isExit) return rendererSend(NAMES.mainWindow.close, true)
isShowPact.value = false
},
openUrl(url) {
openUrl(url)
},
startTimeout() {
setTimeout(() => {
if (--this.time > 0) this.startTimeout()
}, 1e3)
},
},
}
</script>
<style lang="less" module>
@import '@renderer/assets/styles/layout.less';
.main {
padding: 15px;
max-width: 550px;
min-width: 200px;
min-height: 0;
display: flex;
flex-flow: column nowrap;
justify-content: center;
h2 {
font-size: 16px;
color: @color-theme_2-font;
line-height: 1.3;
text-align: center;
}
}
.content {
flex: auto;
margin: 15px 0;
padding-right: 5px;
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;
}
}
each(@themes, {
:global(#root.@{value}) {
.main {
h2 {
color: ~'@{color-@{value}-theme_2-font}';
}
}
.name {
color: ~'@{color-@{value}-theme}';
}
}
})
</style>
@@ -1,129 +0,0 @@
<template>
<div :class="$style.controlBtn">
<common-volume-bar :setting="setting" />
<div :class="$style.titleBtn" @click="toggleDesktopLyric" @contextmenu="toggleLockDesktopLyric" :aria-label="toggleDesktopLyricBtnTitle">
<svg v-show="setting.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"></use>
</svg>
<svg v-show="!setting.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"></use>
</svg>
</div>
<div :class="$style.titleBtn" @click="toggleNextPlayMode" :aria-label="nextTogglePlayName">
<svg v-show="setting.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"></use>
</svg>
<svg v-show="setting.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"></use>
</svg>
<svg v-show="setting.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"></use>
</svg>
<svg v-show="setting.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"></use>
</svg>
<svg v-show="!setting.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"></use>
</svg>
</div>
<div :class="$style.titleBtn" @click="addMusicTo" :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" height="80%" viewBox="0 0 512 512" space="preserve">
<use xlink:href="#icon-add-2"></use>
</svg>
</div>
<common-list-add-modal v-model:show="isShowAddMusicTo" :musicInfo="musicInfoItem" />
</div>
</template>
<script>
import { useI18n, ref, useRefGetter } from '@renderer/utils/vueTools'
import useNextTogglePlay from '@renderer/utils/compositions/useNextTogglePlay'
import useToggleDesktopLyric from '@renderer/utils/compositions/useToggleDesktopLyric'
import { musicInfo, musicInfoItem } from '@renderer/core/share/player'
export default {
setup() {
const { t } = useI18n()
const isShowAddMusicTo = ref(false)
const setting = useRefGetter('setting')
const {
nextTogglePlayName,
toggleNextPlayMode,
} = useNextTogglePlay({ setting, t })
const {
toggleDesktopLyricBtnTitle,
toggleDesktopLyric,
toggleLockDesktopLyric,
} = useToggleDesktopLyric({ setting, t })
const addMusicTo = () => {
if (!musicInfo.songmid) return
isShowAddMusicTo.value = true
}
return {
setting,
isShowAddMusicTo,
nextTogglePlayName,
toggleNextPlayMode,
toggleDesktopLyricBtnTitle,
toggleDesktopLyric,
toggleLockDesktopLyric,
addMusicTo,
musicInfoItem,
}
},
}
</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: @color-btn;
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;
}
}
each(@themes, {
:global(#root.@{value}) {
.titleBtn {
color: ~'@{color-@{value}-btn}';
}
}
})
</style>
@@ -1,376 +0,0 @@
<template lang="pug">
div(:class="$style.player")
div(:class="$style.left" @contextmenu="handleToMusicLocation" @click="showPlayerDetail" :aria-label="$t('player__pic_tip')")
img(v-if="musicInfo.img" :src="musicInfo.img" @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}`')
div(:class="$style.middle")
div(:class="$style.column1")
div(:class="$style.container")
div(:class="$style.title" @click="handleCopy(title)" :aria-label="title + $t('copy_tip')") {{title}}
control-btns
div(:class="$style.column2")
common-progress-bar(:progress="progress" :handleTransitionEnd="handleTransitionEnd" :isActiveTransition="isActiveTransition" v-if="!isShowPlayerDetail")
div(:class="$style.column3")
template(v-if="!isShowPlayerDetail")
span(:class="$style.statusText") {{statusText}}
span {{nowPlayTimeStr}}
span(style="margin: 0 5px;") /
span {{maxPlayTimeStr}}
div(:class="$style.right")
div(:class="$style.playBtn" @click='playPrev' :aria-label="$t('player__prev')" style="transform: rotate(180deg);")
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')
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(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')
div(:class="$style.playBtn" @click='playNext' :aria-label="$t('player__next')")
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(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')
</template>
<script>
import { useRefGetter, computed, useRouter } from '@renderer/utils/vueTools'
import { clipboardWriteText } from '@renderer/utils'
import { player as eventPlayerNames } from '@renderer/event/names'
import ControlBtns from './ControlBtns'
import usePlayProgress from '@renderer/utils/compositions/usePlayProgress'
// import { lyric } from '@renderer/core/share/lyric'
import {
statusText,
musicInfo,
setMusicInfo,
isShowPlayerDetail,
setShowPlayerDetail,
isPlay,
musicInfoItem,
playInfo,
playMusicInfo,
} from '@renderer/core/share/player'
export default {
name: 'CorePlayBar',
components: {
ControlBtns,
},
data() {
return {
isShowAddMusicTo: false,
}
},
setup() {
const setting = useRefGetter('setting')
const router = useRouter()
const {
nowPlayTimeStr,
maxPlayTimeStr,
progress,
isActiveTransition,
handleTransitionEnd,
} = usePlayProgress()
const showPlayerDetail = () => {
if (!musicInfoItem.value.songmid) return
setShowPlayerDetail(true)
}
const handleCopy = (text) => {
clipboardWriteText(text)
}
const imgError = () => {
// console.log(e)
setMusicInfo({ img: null })
}
const handleToMusicLocation = () => {
const listId = playMusicInfo.listId
if (!listId || listId == '__temp__' || listId == 'download' || !musicInfoItem.value.songmid) return
if (playInfo.playIndex == -1) return
router.push({
path: 'list',
query: {
id: listId,
scrollIndex: playInfo.playIndex,
},
})
}
const title = computed(() => {
return musicInfo.name
? setting.value.download.fileName.replace('歌名', musicInfo.name).replace('歌手', musicInfo.singer)
: '^-^'
})
const togglePlay = () => {
window.eventHub.emit(eventPlayerNames.setTogglePlay)
}
const playNext = () => {
window.eventHub.emit(eventPlayerNames.setPlayNext)
}
const playPrev = () => {
window.eventHub.emit(eventPlayerNames.setPlayPrev)
}
// onBeforeUnmount(() => {
// window.eventHub.emit(eventPlayerNames.setTogglePlay)
// })
return {
musicInfo,
nowPlayTimeStr,
maxPlayTimeStr,
progress,
isActiveTransition,
handleTransitionEnd,
handleCopy,
imgError,
statusText,
title,
showPlayerDetail,
isPlay,
togglePlay,
setting,
playNext,
playPrev,
handleToMusicLocation,
isShowPlayerDetail,
}
},
}
</script>
<style lang="less" module>
@import '@renderer/assets/styles/layout.less';
.player {
height: @height-player;
background-color: @color-theme_2;
transition: @transition-theme;
transition-property: border-color;
border-top: 2px solid @color-theme;
box-sizing: border-box;
display: flex;
contain: strict;
z-index: 2;
* {
box-sizing: border-box;
}
}
.left {
width: @height-player - 2;
height: 100%;
color: @color-theme;
transition: @transition-theme;
transition-property: color;
flex: none;
padding: 2PX;
opacity: 1;
transition: @transition-theme;
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-theme;
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: 5px 10px 5px 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-theme;
// transition-property: color;
// color: @color-theme;
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-theme;
transition-property: color;
color: @color-theme_2-font;
flex: none;
font-size: 12px;
display: flex;
padding-top: 2px;
// justify-content: space-between;
height: 16px;
align-items: center;
}
.status-text {
font-size: 0.98em;
transition: @transition-theme;
transition-property: color;
color: @color-player-status-text;
.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: @color-theme;
.c1 {
transition: @transition-theme;
transition-property: fill;
fill: @color-player-pic-c1;
}
.c2 {
transition: @transition-theme;
transition-property: fill;
fill: @color-player-pic-c2;
}
}
each(@themes, {
:global(#root.@{value}) {
.player {
background-color: ~'@{color-@{value}-theme_2}';
border-top-color: ~'@{color-@{value}-theme}';
}
.left {
color: ~'@{color-@{value}-theme}';
// img {
// border-color: ~'@{color-@{value}-theme_2-background_1}';
// }
}
.titleBtn {
color: ~'@{color-@{value}-btn}';
}
.play-btn {
color: ~'@{color-@{value}-btn}';
svg {
filter: drop-shadow(0 0 1px rgba(0, 0, 0, 0.3));
}
}
.volume {
background-color: ~'@{color-@{value}-player-progress-bar1}';
}
.volume-bar {
background-color: ~'@{color-@{value}-btn}';
}
.column3 {
color: ~'@{color-@{value}-theme_2-font}';
}
.status-text {
color: ~'@{color-@{value}-player-status-text}';
}
#icon-pic {
color: ~'@{color-@{value}-theme}';
.c1 {
fill: ~'@{color-@{value}-player-pic-c1}';
}
.c2 {
fill: ~'@{color-@{value}-player-pic-c2}';
}
}
}
})
</style>
@@ -1,378 +0,0 @@
<template>
<div :class="['right', $style.right]" :style="lrcFontSize">
<transition enter-active-class="animated fadeIn" leave-active-class="animated fadeOut">
<div v-show="!isShowLrcSelectContent"
:class="['lyric', $style.lyric, { [$style.draging]: isMsDown }, { [$style.lrcActiveZoom]: isZoomActiveLrc }]"
:style="lrcStyles" @wheel="handleWheel"
@mousedown="handleLyricMouseDown" ref="dom_lyric"
@contextmenu.stop="handleShowLyricMenu"
>
<div :class="['pre', $style.lyricSpace]"></div>
<div ref="dom_lyric_text"></div>
<div :class="$style.lyricSpace"></div>
</div>
</transition>
<transition enter-active-class="animated fadeIn" leave-active-class="animated fadeOut">
<div :class="$style.skip" v-if="isShowLyricProgressSetting" v-show="isStopScroll && !isShowLrcSelectContent">
<div :class="$style.line" ref="dom_skip_line"></div>
<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"></use>
</svg>
</base-btn>
</div>
</transition>
<transition enter-active-class="animated fadeIn" leave-active-class="animated fadeOut">
<div :class="[$style.lyricSelectContent, 'select', 'scroll', 'lyricSelectContent']" v-if="isShowLrcSelectContent" @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, index) in info.extendedLyrics" :key="index">
<br />
<span :class="$style.lyricSelectlineExtended">{{lrc}}</span>
</template>
</div>
</div>
</transition>
<LyricMenu v-model="lyricMenuVisible" :xy="lyricMenuXY" :lyricInfo="lyricInfo" @updateLyric="handleUpdateLyric" />
</div>
</template>
<script>
import { clipboardWriteText } from '@renderer/utils'
import { lyric } from '@renderer/core/share/lyric'
import { isFullscreen } from '@renderer/core/share'
import { isPlay, isShowLrcSelectContent, isShowPlayComment, musicInfo as playerMusicInfo, musicInfoItem, setMusicInfo } from '@renderer/core/share/player'
import { onMounted, onBeforeUnmount, useRefGetter, computed, reactive, ref } from '@renderer/utils/vueTools'
import useLyric from '@renderer/utils/compositions/useLyric'
import LyricMenu from './components/LyricMenu'
import { player as eventPlayerNames } from '@renderer/event/names'
export default {
components: {
LyricMenu,
},
setup() {
const setting = useRefGetter('setting')
const isZoomActiveLrc = computed(() => setting.value.playDetail.isZoomActiveLrc)
const isShowLyricProgressSetting = computed(() => setting.value.playDetail.isShowLyricProgressSetting)
const {
dom_lyric,
dom_lyric_text,
dom_skip_line,
isMsDown,
isStopScroll,
timeStr,
handleLyricMouseDown,
handleWheel,
handleSkipPlay,
handleSkipMouseEnter,
handleSkipMouseLeave,
} = useLyric({ isPlay, lyric, isShowLyricProgressSetting })
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 = musicInfoItem.value
}
const handleShowLyricMenu = event => {
updateMusicInfo()
lyricMenuXY.x = event.pageX
lyricMenuXY.y = event.pageY
lyricMenuVisible.value = true
}
const handleUpdateLyric = ({ lyric, tlyric, rlyric, lxlyric, offset }) => {
setMusicInfo({
lrc: lyric,
tlrc: tlyric,
rlrc: rlyric,
lxlrc: lxlyric,
})
// console.log(offset)
window.eventHub.emit(eventPlayerNames.updateLyricOffset, offset)
}
const lrcStyles = computed(() => {
return {
textAlign: setting.value.playDetail.style.align,
}
})
const lrcFontSize = computed(() => {
let size = setting.value.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.eventHub.on(eventPlayerNames.updateLyric, updateMusicInfo)
})
onBeforeUnmount(() => {
window.eventHub.off(eventPlayerNames.updateLyric, 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-theme;
}
.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 {
.lrc-content {
line-height: 1.2;
padding: calc(var(--playDetail-lrc-font-size, 16px) / 2) 1px;
overflow-wrap: break-word;
color: @color-player-detail-lyric;
transition: @transition-theme;
transition-property: padding;
.extended {
transition: @transition-theme !important;
transition-property: font-size, color;
font-size: .9em;
margin-top: 5px;
}
.line {
transition-property: font-size, color !important;
background: none !important;
-webkit-text-fill-color: unset;
// -webkit-text-fill-color: none !important;
}
&.active {
.line {
color: @color-theme;
}
.extended {
color: @color-theme;
}
// span {
// // color: @color-theme;
// font-size: 1.1em;
// }
}
span {
transition: @transition-theme !important;
transition-property: font-size !important;
font-size: 1em;
background-repeat: no-repeat;
background-color: rgba(77, 77, 77, 0.9);
background-image: -webkit-linear-gradient(top, @color-theme, @color-theme);
-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-theme !important;
// transition-property: color, font-size;
// }
// .lrc-active {
// color: @color-theme;
// font-size: 1.2em;
// }
}
.lrcActiveZoom {
:global {
.lrc-content {
&.active {
.extended {
font-size: .94em;
}
span {
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 @color-player-detail-lyric-active;
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: @color-player-detail-lyric-active;
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-theme;
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: @color-player-detail-lyric;
.lyricSelectline {
padding: 8px 0;
overflow-wrap: break-word;
transition: @transition-theme !important;
transition-property: color, font-size;
line-height: 1.3;
}
.lyricSelectlineExtended {
font-size: 14px;
}
.lrc-active {
color: @color-theme;
}
}
.lyric-space {
height: 70%;
}
each(@themes, {
:global(#root.@{value}) {
.lyric {
:global {
.lrc-content {
color: ~'@{color-@{value}-player-detail-lyric}';
&.active {
.extended {
color: ~'@{color-@{value}-player-detail-lyric-active}';
}
.line {
color: ~'@{color-@{value}-player-detail-lyric-active}';
}
}
span {
background-color: ~'@{color-@{value}-player-detail-lyric}';
background-image: -webkit-linear-gradient(top, ~'@{color-@{value}-player-detail-lyric-active}', ~'@{color-@{value}-player-detail-lyric-active}');
}
}
}
}
// .lrc-active {
// color: ~'@{color-@{value}-theme}';
// }
.skip {
.line {
border-top-color: ~'@{color-@{value}-player-detail-lyric-active}';
}
.label {
color:~'@{color-@{value}-player-detail-lyric-active}';
}
}
.lyricSelectContent {
color: ~'@{color-@{value}-player-detail-lyric}';
.lrc-active {
color: ~'@{color-@{value}-theme}';
}
}
}
})
</style>
@@ -1,161 +0,0 @@
<template lang="pug">
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" :handleTransitionEnd="handleTransitionEnd" :isActiveTransition="isActiveTransition")
div(:class="$style.timeLabel")
span(:class="$style.status" style="margin-left: 15px") {{status}}
span {{nowPlayTimeStr}}
span(style="margin: 0 5px;") /
span {{maxPlayTimeStr}}
div(:class="$style.playControl")
div(:class="$style.playBtn" @click="playPrev" style="transform: rotate(180deg);" :aria-label="$t('player__prev')")
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')
div(:class="$style.playBtn" @click="togglePlay" :aria-label="isPlay ? $t('player__pause') : $t('player__play')")
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(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')
div(:class="$style.playBtn" @click="playNext" :aria-label="$t('player__next')")
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')
</template>
<script>
import { status, isPlay } from '@renderer/core/share/player'
import usePlayProgress from '@renderer/utils/compositions/usePlayProgress'
import { player as eventPlayerNames } from '@renderer/event/names'
import ControlBtns from './components/ControlBtns'
export default {
components: {
ControlBtns,
},
setup() {
const {
nowPlayTimeStr,
maxPlayTimeStr,
progress,
isActiveTransition,
handleTransitionEnd,
} = usePlayProgress()
return {
nowPlayTimeStr,
maxPlayTimeStr,
status,
progress,
isActiveTransition,
handleTransitionEnd,
isPlay,
togglePlay() {
window.eventHub.emit(eventPlayerNames.setTogglePlay)
},
playNext() {
window.eventHub.emit(eventPlayerNames.setPlayNext)
},
playPrev() {
window.eventHub.emit(eventPlayerNames.setPlayPrev)
},
}
},
}
</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: @color-btn;
}
.play-btn {
height: 40%;
padding: 5px;
cursor: pointer;
flex: none;
// transition: @transition-theme;
// transition-property: color;
color: @color-player-detail-play-btn;
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;
}
}
each(@themes, {
:global(#root.@{value}) {
.play-btn {
color: ~'@{color-@{value}-player-detail-play-btn}';
}
}
})
</style>
@@ -1,159 +0,0 @@
<template lang="pug">
material-modal(:show="sync.isShowSyncMode" @close="handleClose(false)" :bgClose="false" :close-btn="false")
main(:class="$style.main")
h2 {{$t('sync__title', { name: sync.deviceName })}}
div.scroll(:class="$style.content")
dl(:class="$style.btnGroup")
dt(:class="$style.label") {{$t('sync__merge_label')}}
dd(:class="$style.btns")
base-btn(:class="$style.btn" @click="handleSelectMode('merge_local_remote')") {{$t('sync__merge_btn_local_remote')}}
base-btn(:class="$style.btn" @click="handleSelectMode('merge_remote_local')") {{$t('sync__merge_btn_remote_local')}}
dl(:class="$style.btnGroup")
dt(:class="$style.label") {{$t('sync__overwrite_label')}}
dd(:class="$style.btns")
base-btn(:class="$style.btn" @click="handleSelectMode('overwrite_local_remote')") {{$t('sync__overwrite_btn_local_remote')}}
base-btn(:class="$style.btn" @click="handleSelectMode('overwrite_remote_local')") {{$t('sync__overwrite_btn_remote_local')}}
dd(style="font-size: 14px; margin-top: 5px;")
base-checkbox(id="sync_mode_modal_isOverwrite" v-model="isOverwrite" :label="$t('sync__overwrite')")
dl(:class="$style.btnGroup")
dt(:class="$style.label") {{$t('sync__other_label')}}
dd(:class="$style.btns")
base-btn(:class="$style.btn" @click="handleSelectMode('none')") {{$t('sync__overwrite_btn_none')}}
base-btn(:class="$style.btn" @click="handleSelectMode('cancel')") {{$t('sync__overwrite_btn_cancel')}}
dl(:class="$style.btnGroup")
dd
section(:class="$style.tipGroup")
h3(:class="$style.title") {{$t('sync__merge_tip')}}
p(:class="$style.tip") {{$t('sync__merge_tip_desc')}}
section(:class="$style.tipGroup")
h3(:class="$style.title") {{$t('sync__overwrite_tip')}}
p(:class="$style.tip") {{$t('sync__overwrite_tip_desc')}}
section(:class="$style.tipGroup")
h3(:class="$style.title") {{$t('sync__other_tip')}}
p(:class="$style.tip") {{$t('sync__other_tip_desc')}}
</template>
<script>
import { sync as eventSyncName } from '@renderer/event/names'
import { sync } from '@renderer/core/share'
export default {
setup() {
return {
sync,
}
},
data() {
return {
isOverwrite: false,
}
},
computed: {
},
methods: {
handleSelectMode(mode) {
if (mode.startsWith('overwrite') && this.isOverwrite) mode += '_full'
window.eventHub.emit(eventSyncName.send_sync_list, {
action: 'selectMode',
data: mode,
})
this.handleClose()
},
handleClose() {
sync.isShowSyncMode = false
},
},
}
</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: @color-theme_2-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: @color-theme_2-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;
}
}
each(@themes, {
:global(#root.@{value}) {
.main {
h2 {
color: ~'@{color-@{value}-theme_2-font}';
}
}
.name {
color: ~'@{color-@{value}-theme}';
}
}
})
</style>
@@ -1,90 +0,0 @@
<template>
<div :class="$style.control" v-show="!isFullscreen">
<button type="button" :class="[$style.btn, $style.min]" :aria-label="$t('min')" @click="min">
<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"></use>
</svg>
</button>
<button type="button" :class="[$style.btn, $style.close]" :aria-label="$t('close')" @click="close">
<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"></use>
</svg>
</button>
</div>
</template>
<script>
import { base as eventBaseName } from '@renderer/event/names'
import { isFullscreen } from '@renderer/core/share'
// import { getRandom } from '../../utils'
export default {
setup() {
return {
min() {
window.eventHub.emit(eventBaseName.min)
},
max() {
window.eventHub.emit(eventBaseName.max)
},
close() {
window.eventHub.emit(eventBaseName.close)
},
isFullscreen,
}
},
}
</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: @color-theme;
transition: background-color 0.2s ease-in-out;
&:hover {
&.min, &.max {
background-color: @color-btn-hover;
}
&.close {
background-color: @color-closeBtn;
}
}
}
}
each(@themes, {
:global(#root.@{value}) {
.btn {
color: ~'@{color-@{value}-theme_2-font-label}';
&:hover {
&.min, &.max {
background-color: ~'@{color-@{value}-btn-hover}';
}
&.close {
background-color: ~'@{color-@{value}-closeBtn}';
}
}
}
}
})
</style>
-28
View File
@@ -1,28 +0,0 @@
<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" />
</transition>
</router-view>
</div>
</template>
<style lang="less" module>
@import '@renderer/assets/styles/layout.less';
.view {
position: relative;
z-index: 1;
> * {
position: absolute !important;
left: 0;
top: 0;
height: 100%;
width: 100%;
}
// background: #fff;
// overflow: hidden;
}
</style>
@@ -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>
@@ -1,68 +1,79 @@
<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" :active-class="$style.active" role="tab" :aria-selected="$route.name == item.to" :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"></use>
</svg>
</div>
</router-link>
</li>
</ul>
</div>
<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 { mapGetters } from 'vuex'
import { appSetting } from '@renderer/store/setting'
export default {
name: 'Nav',
name: 'NavBar',
setup() {
return {
appSetting,
}
},
computed: {
...mapGetters(['setting']),
menus() {
return [
{
to: 'search',
to: '/search',
tips: this.$t('search'),
icon: '#icon-search-2',
iconSize: '0 0 425.2 425.2',
name: 'Search',
enable: true,
},
{
to: 'songList',
to: '/songList/list',
tips: this.$t('song_list'),
icon: '#icon-album',
iconSize: '0 0 425.2 425.2',
name: 'SongList',
enable: true,
},
{
to: 'leaderboard',
to: '/leaderboard',
tips: this.$t('leaderboard'),
icon: '#icon-leaderboard',
iconSize: '0 0 425.22 425.2',
name: 'Leaderboard',
enable: true,
},
{
to: 'list',
to: '/list',
tips: this.$t('my_list'),
icon: '#icon-love',
iconSize: '0 0 444.87 391.18',
name: 'List',
enable: true,
},
{
to: 'download',
to: '/download',
tips: this.$t('download'),
icon: '#icon-download-2',
iconSize: '0 0 425.2 425.2',
enable: this.setting.download.enable,
enable: this.appSetting['download.enable'],
name: 'Download',
},
{
to: 'setting',
to: '/setting',
tips: this.$t('setting'),
icon: '#icon-setting',
iconSize: '0 0 493.23 436.47',
enable: true,
name: 'Setting',
},
].filter(m => m.enable)
},
@@ -89,10 +100,11 @@ export default {
&:last-child {
margin-bottom: 0;
}
// background-color: pink;
// dt {
// padding-left: 5px;
// font-size: 11px;
// transition: @transition-theme;
// transition: @transition-normal;
// transition-property: color;
// color: @color-theme-font-label;
// .mixin-ellipsis-1;
@@ -109,20 +121,25 @@ export default {
}
.link {
position: absolute;
left: 0;
top: 0;
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;
// text-decoration: none;
// border-radius: 20%;
// padding: 18px 3px;
// margin: 5px 0;
// border-left: 5px solid transparent;
transition: @transition-theme;
transition: @transition-normal;
transition-property: color;
color: @color-theme-font !important;
color: var(--color-nav-font);
cursor: pointer;
font-size: 11.5px;
text-align: center;
@@ -131,20 +148,32 @@ export default {
align-items: center;
justify-content: center;
transition: background-color 0.3s ease;
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: @color-theme-active;
background-color: var(--color-primary-light-400-alpha-600);
&:hover {
background-color: var(--color-primary-light-300-alpha-600);
}
}
&:hover:not(.active) {
background-color: @color-theme-hover;
&:hover {
color: var(--color-nav-font);
&:not(.active) {
opacity: .8;
background-color: var(--color-primary-light-500-alpha-600);
}
}
&:hover:not(.active) {
background-color: @color-theme-active;
&:active:not(.active) {
opacity: .6;
background-color: var(--color-primary-light-200-alpha-600);
}
}
@@ -155,21 +184,4 @@ export default {
}
}
each(@themes, {
:global(#root.@{value}) {
.link {
color: ~'@{color-@{value}-theme-font}' !important;
&.active {
background-color: ~'@{color-@{value}-theme-active}';
}
&:hover:not(.active) {
background-color: ~'@{color-@{value}-theme-hover}';
}
&:active:not(.active) {
background-color: ~'@{color-@{value}-theme-active}';
}
}
}
})
</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>
@@ -1,4 +1,4 @@
import { debounce } from '@renderer/utils/index'
import { debounce } from '@common/utils/common'
let isAutoHide = false
let isLockedPointer = false
let dom = null
@@ -1,12 +1,12 @@
<template lang="pug">
div(:class="$style.footerLeftControlBtns")
common-volume-bar(:setting="setting")
common-volume-bar
div(:class="[$style.footerLeftControlBtn, $style.lrcBtn]" @click="toggleDesktopLyric" @contextmenu="toggleLockDesktopLyric" :aria-label="toggleDesktopLyricBtnTitle")
svg(v-show="setting.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')
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="!setting.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')
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]: setting.player.audioVisualization }]" @click="toggleAudioVisualization" :aria-label="$t('audio_visualization')")
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')")
@@ -16,45 +16,49 @@ div(:class="$style.footerLeftControlBtns")
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="setting.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')
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="setting.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')
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="setting.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')
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="setting.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')
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="!setting.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')
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="musicInfoItem")
common-list-add-modal(v-model:show="isShowAddMusicTo" :musicInfo="playMusicInfo.musicInfo")
</template>
<script>
import { useI18n, useRefGetter, ref, useCommit } from '@renderer/utils/vueTools'
import { ref } from '@common/utils/vueTools'
import { useI18n } from '@renderer/plugins/i18n'
import {
isShowLrcSelectContent,
setShowPlayLrcSelectContentLrc,
isShowPlayComment,
playMusicInfo,
} from '@renderer/store/player/state'
import {
setShowPlayLrcSelectContentLrc,
setShowPlayComment,
musicInfoItem,
} from '@renderer/core/share/player'
} 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 t = useI18n()
// const setting = useRefGetter('setting')
// const setAudioVisualization = useCommit('setAudioVisualization')
// const saveMediaDeviceId = useCommit('setMediaDeviceId')
const toggleVisibleLrc = () => {
setShowPlayLrcSelectContentLrc(!isShowLrcSelectContent.value)
@@ -65,19 +69,19 @@ export default {
const {
nextTogglePlayName,
toggleNextPlayMode,
} = useNextTogglePlay({ setting, t })
} = useNextTogglePlay()
const {
toggleDesktopLyricBtnTitle,
toggleDesktopLyric,
toggleLockDesktopLyric,
} = useToggleDesktopLyric({ setting, t })
} = useToggleDesktopLyric()
const isShowAddMusicTo = ref(false)
const toggleAudioVisualization = async() => {
const newSetting = !setting.value.player.audioVisualization
if (newSetting && setting.value.player.mediaDeviceId != 'default') {
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'),
@@ -87,11 +91,11 @@ export default {
saveMediaDeviceId('default')
await setMediaDeviceId('default').catch(_ => _)
}
setAudioVisualization(newSetting)
setEnableAudioVisualization(newSetting)
}
return {
setting,
appSetting,
isShowLrcSelectContent,
toggleVisibleLrc,
isShowPlayComment,
@@ -103,7 +107,7 @@ export default {
toggleLockDesktopLyric,
toggleAudioVisualization,
isShowAddMusicTo,
musicInfoItem,
playMusicInfo,
}
},
}
@@ -113,7 +117,7 @@ export default {
@import '@renderer/assets/styles/layout.less';
.footerLeftControlBtns {
color: @color-theme_2-font;
color: var(--color-font);
display: flex;
flex-flow: row nowrap;
justify-content: flex-end;
@@ -123,7 +127,7 @@ export default {
height: 18px;
opacity: .5;
cursor: pointer;
transition: opacity @transition-theme;
transition: opacity @transition-normal;
display: flex;
align-items: center;
justify-content: center;
@@ -137,7 +141,7 @@ export default {
}
&.active {
color: @color-theme;
color: var(--color-primary);
opacity: .8;
}
}
@@ -147,18 +151,4 @@ export default {
}
}
each(@themes, {
:global(#root.@{value}) {
.footerLeftControlBtns {
color: ~'@{color-@{value}-theme_2-font}';
}
.footerLeftControlBtn {
&.active {
color: ~'@{color-@{value}-theme}';
}
}
}
})
</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>
@@ -22,7 +22,7 @@ div(:class="$style.container")
import commentDefImg from '@renderer/assets/images/defaultUser.jpg'
export default {
name: 'commentFloor',
name: 'CommentFloor',
props: {
comments: {
type: Array,
@@ -53,18 +53,18 @@ export default {
@padding: 15px;
.container {
// .container {
}
// }
.listItem {
border-bottom: 1px dashed @color-theme_2-line;
border-bottom: 1px dashed var(--color-primary-alpha-700);
}
.content {
padding: 12px 0;
font-size: 13px;
color: @color-theme_2-font;
color: var(--color-font);
display: flex;
}
.left {
@@ -87,7 +87,7 @@ export default {
align-items: flex-end;
min-width: 0;
line-height: 1.3;
color: @color-theme_2-font-label;
color: var(--color-font-label);
}
.name {
flex: 0 1 auto;
@@ -110,7 +110,7 @@ export default {
width: 12px;
height: 12px;
margin-right: 3px;
color: @color-theme-active;
color: var(--color-primary-font);
}
.comment_text {
text-align: justify;
@@ -137,28 +137,8 @@ export default {
margin-right: 10px;
}
background-color: @color-reply-floor;
background-color: var(--color-primary-light-500-alpha-700);
}
each(@themes, {
:global(#root.@{value}) {
.listItem {
border-bottom-color: ~'@{color-@{value}-theme_2-active}';
}
.content {
color: ~'@{color-@{value}-theme_2-font}';
}
.info {
color: ~'@{color-@{value}-theme_2-font-label}';
}
.likesIcon {
color: ~'@{color-@{value}-theme-active}';
}
.reply_floor {
background-color: ~'@{color-@{value}-reply-floor}';
}
}
})
</style>
@@ -11,35 +11,42 @@ div.comment(:class="$style.comment" ref="dom_container")
use(xlink:href='#icon-close')
div(:class="$style.commentMain")
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")
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 { mapGetters } from 'vuex'
import music from '@renderer/utils/music'
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: {
@@ -47,12 +54,15 @@ export default {
required: true,
},
},
components: {
CommentFloor,
},
emits: ['close'],
setup() {
return {
appSetting,
}
},
data() {
return {
available: false,
currentMusicInfo: {
name: '',
singer: '',
@@ -110,13 +120,17 @@ export default {
}
},
computed: {
...mapGetters(['setting']),
title() {
return this.currentMusicInfo.name
? this.setting.download.fileName.replace('歌名', this.currentMusicInfo.name).replace('歌手', this.currentMusicInfo.singer)
? 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)
@@ -124,11 +138,6 @@ export default {
beforeUnmount() {
window.removeEventListener('resize', this.setWidth)
},
watch: {
show(n) {
if (n) this.handleShowComment()
},
},
methods: {
setWidth() {
setTimeout(() => {
@@ -162,7 +171,7 @@ export default {
handleGetNewComment(musicInfo, page, limit) {
this.newComment.isLoadError = false
this.newComment.isLoading = true
this.getComment(musicInfo, page, limit).then(comment => {
this.getComment(toOldMusicInfo(musicInfo), page, limit).then(comment => {
this.newComment.isLoading = false
this.newComment.total = comment.total
this.newComment.maxPage = comment.maxPage
@@ -181,7 +190,7 @@ export default {
handleGetHotComment(musicInfo, page, limit) {
this.hotComment.isLoadError = false
this.hotComment.isLoading = true
this.getHotComment(musicInfo, page, limit).then(hotComment => {
this.getHotComment(toOldMusicInfo(musicInfo), page, limit).then(hotComment => {
this.hotComment.isLoading = false
this.hotComment.total = hotComment.total
this.hotComment.maxPage = hotComment.maxPage
@@ -198,7 +207,13 @@ export default {
})
},
handleShowComment() {
if (!this.musicInfo.songmid || !music[this.musicInfo.source].comment) return
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
@@ -211,7 +226,6 @@ export default {
this.newComment.nextPage = 1
// }
this.isShowComment = true
this.currentMusicInfo = this.musicInfo
this.handleGetHotComment(this.currentMusicInfo, this.hotComment.page, this.hotComment.limit)
this.handleGetNewComment(this.currentMusicInfo, this.newComment.page, this.newComment.limit)
@@ -225,7 +239,7 @@ export default {
this.handleGetNewComment(this.currentMusicInfo, page, this.newComment.limit)
},
handleToggleTab(id, force) {
if (!force && this.tabActiveId == id) return
if (!this.available || (!force && this.tabActiveId == id)) return
switch (id) {
case 'hot':
this.$refs.dom_tabMain.scrollLeft = 0
@@ -246,7 +260,7 @@ export default {
.comment {
display: flex;
flex-flow: column nowrap;
transition: @transition-theme;
transition: @transition-normal;
transition-property: transform,opacity;
transform-origin: 100%;
overflow: hidden;
@@ -269,13 +283,13 @@ export default {
display: flex;
flex-flow: row nowrap;
justify-content: flex-end;
color: @color-theme;
color: var(--color-primary);
}
.commentHeaderBtn {
height: 22px;
width: 22px;
cursor: pointer;
transition: opacity @transition-theme;
transition: opacity @transition-normal;
+.commentHeaderBtn {
margin-left: 5px;
@@ -287,7 +301,7 @@ export default {
}
.commentMain {
flex: auto;
background-color: @color-reply-floor;
background-color: var(--color-primary-light-400-alpha-700);
border-radius: 4px;
display: flex;
flex-direction: column;
@@ -324,7 +338,7 @@ export default {
}
.commentLabel {
padding: 15px;
color: @color-theme_2-font-label;
color: var(--color-font-label);
font-size: 14px;
}
.commentType {
@@ -334,18 +348,18 @@ export default {
background: none;
border: none;
cursor: pointer;
transition: @transition-theme;
transition: @transition-normal;
transition-property: opacity, color;
&:hover {
opacity: .7;
}
&.active {
color: @color-theme;
color: var(--color-primary);
}
}
.commentFloor {
opacity: 1;
transition: opacity @transition-theme;
transition: opacity @transition-normal;
&.loading {
opacity: .4;
@@ -355,22 +369,12 @@ export default {
padding: 10px 0;
}
each(@themes, {
:global(#root.@{value}) {
.commentHeaderBtns {
color: ~'@{color-@{value}-theme}';
}
.commentMain {
background-color: ~'@{color-@{value}-reply-floor}';
}
.commentLabel {
color: ~'@{color-@{value}-theme_2-font-label}';
}
.commentType {
&.active {
color: ~'@{color-@{value}-theme}';
}
}
}
})
.unavailable {
flex: auto;
padding-top: 10%;
text-align: center;
font-size: 14px;
color: var(--color-font-label);
}
</style>
@@ -4,7 +4,7 @@ transition(enter-active-class="animated lightSpeedIn" leave-active-class="animat
div(:class="$style.bg")
//- div(:class="$style.bg" :style="bgStyle")
//- div(:class="$style.bg2")
div(:class="[$style.header, $style.controlBtnLeft]" v-if="setting.controlBtnPosition == 'left'")
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')
@@ -40,7 +40,7 @@ transition(enter-active-class="animated lightSpeedIn" leave-active-class="animat
div.left(:class="$style.left")
//- div(:class="$style.info")
div(:class="$style.info")
img(:class="$style.img" :src="musicInfo.img" v-if="musicInfo.img")
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}}
@@ -48,31 +48,34 @@ transition(enter-active-class="animated lightSpeedIn" leave-active-class="animat
transition(enter-active-class="animated fadeIn" leave-active-class="animated fadeOut")
LyricPlayer(v-if="visibled")
music-comment(:class="$style.comment" :musicInfo="musicInfoItem" :show="isShowPlayComment" @close="hideComment" 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="setting.player.audioVisualization && visibled")
common-audio-visualizer(v-if="appSetting['player.audioVisualization'] && visibled")
</template>
<script>
import { useRefGetter, ref, watch } from '@renderer/utils/vueTools'
import { isFullscreen } from '@renderer/core/share'
import { base as eventBaseName } from '@renderer/event/names'
import { ref, watch } from '@common/utils/vueTools'
import { isFullscreen } from '@renderer/store'
import {
isShowPlayerDetail,
setShowPlayerDetail,
isShowPlayComment,
musicInfo,
playMusicInfo,
} from '@renderer/store/player/state'
import {
setShowPlayerDetail,
setShowPlayComment,
setShowPlayLrcSelectContentLrc,
musicInfoItem,
musicInfo,
} from '@renderer/core/share/player'
} 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',
@@ -82,7 +85,6 @@ export default {
MusicComment,
},
setup() {
const setting = useRefGetter('setting')
const visibled = ref(false)
let clickTime = 0
@@ -121,11 +123,12 @@ export default {
(isFullscreen ? registerAutoHideMounse : unregisterAutoHideMounse)()
})
return {
setting,
appSetting,
playMusicInfo,
isShowPlayerDetail,
isShowPlayComment,
musicInfoItem,
musicInfo,
hide,
handleContextMenu,
@@ -135,16 +138,18 @@ export default {
visibled,
isFullscreen,
fullscreenExit() {
window.eventHub.emit(eventBaseName.fullscreenToggle, false)
setFullScreen(false).then((fullscreen) => {
isFullscreen.value = fullscreen
})
},
min() {
window.eventHub.emit(eventBaseName.min)
minWindow()
},
max() {
window.eventHub.emit(eventBaseName.max)
maxWindow()
},
close() {
window.eventHub.emit(eventBaseName.close)
closeWindow()
},
}
},
@@ -165,13 +170,13 @@ export default {
height: 100%;
top: 0;
left: 0;
background-color: @color-theme_2-background_1;
background-color: var(--color-content-background);
z-index: 10;
// -webkit-app-region: drag;
overflow: hidden;
border-radius: @radius-border;
color: @color-theme_2-font;
border-left: 12px solid @color-theme;
color: var(--color-font);
border-left: 12px solid var(--color-primary-light-200-alpha-500);
-webkit-app-region: no-drag;
contain: strict;
@@ -203,17 +208,28 @@ export default {
height: 100%;
top: 0;
left: 0;
background-image: @color-theme-bgimg;
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;
&:after {
&:before {
content: '';
display: block;
width: 100%;
height: 100%;
background-color: @color-theme_2;
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 {
@@ -263,7 +279,10 @@ export default {
left: 0;
flex-direction: row-reverse;
height: @height-toolbar * .7;
transition: opacity @transition-normal;
opacity: .5;
&:hover {
opacity: .8;
.controBtnIcon {
opacity: 1;
}
@@ -273,22 +292,22 @@ export default {
width: @control-btn-width;
height: @control-btn-width;
border-radius: 50%;
color: @color-theme_2;
color: var(--color-font);
+ button {
margin-right: (@control-btn-width / 2);
}
&.hide {
background-color: @color-hideBtn;
background-color: var(--color-btn-hide);
}
&.min, &.fullscreenExit {
background-color: @color-minBtn;
}
&.max {
background-color: @color-maxBtn;
background-color: var(--color-btn-min);
}
// &.max {
// background-color: var(--color-btn-max);
// }
&.close {
background-color: @color-closeBtn;
background-color: var(--color-btn-close);
}
}
}
@@ -305,14 +324,14 @@ export default {
button {
width: 46px;
height: 30px;
color: @color-theme;
color: var(--color-font-label);
transition: background-color 0.2s ease-in-out;
&:hover {
background-color: @color-btn-hover;
background-color: var(--color-button-background-hover);
&.close {
background-color: @color-closeBtn;
background-color: var(--color-btn-close);
}
}
}
@@ -356,7 +375,7 @@ export default {
align-items: center;
padding: 13px;
overflow: hidden;
transition: flex-basis @transition-theme;
transition: flex-basis @transition-normal;
}
.info {
@@ -370,12 +389,9 @@ export default {
max-width: 100%;
max-height: 100%;
min-width: 100%;
box-shadow: 0 0 4px @color-theme-hover;
box-shadow: 0 0 6px var(--color-primary-alpha-500);
border-radius: 6px;
opacity: .8;
// border: 5px solid @color-theme-hover;
// border-radius: @radius-border;
// border: 5px solid #fff;
}
.description {
max-width: 300px;
@@ -401,65 +417,4 @@ export default {
}
each(@themes, {
:global(#root.@{value}) {
.container {
border-left-color: ~'@{color-@{value}-theme}';
background-color: ~'@{color-@{value}-theme_2-background_1}';
// color: ~'@{color-@{value}-theme_2-font}';
}
.bg {
// background-color: ~'@{color-@{value}-theme}';
background-image: ~'@{color-@{value}-theme-bgimg}';
background-size: ~'@{color-@{value}-theme-bgsize}';
background-position: ~'@{color-@{value}-theme-bgposition}';
&:after {
background-color: ~'@{color-@{value}-theme_2}';
}
}
.header {
&.controlBtnLeft {
.controBtn {
button {
color: ~'@{color-@{value}-theme_2}';
// &.hide:after {
// background-color: ~'@{color-@{value}-hideBtn}';
// }
&.hide {
background-color: ~'@{color-@{value}-hideBtn}';
}
&.min, &.fullscreenExit {
background-color: ~'@{color-@{value}-minBtn}';
}
&.max {
background-color: ~'@{color-@{value}-maxBtn}';
}
&.close {
background-color: ~'@{color-@{value}-closeBtn}';
}
}
}
}
&.controlBtnRight {
.controBtn {
button {
color: ~'@{color-@{value}-theme_2-font-label}';
&:hover {
background-color: ~'@{color-@{value}-btn-hover}';
&.close {
background-color: ~'@{color-@{value}-closeBtn}';
}
}
}
}
}
}
.img {
box-shadow: 0 0 4px ~'@{color-@{value}-theme-hover}';
// border-color: ~'@{color-@{value}-theme-hover}';
}
}
})
</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>
@@ -1,19 +1,20 @@
<template>
<material-search-input @event="handleEvent" :list="tipList" :visibleList="visibleList" v-model="searchText" />
<material-search-input v-model="searchText" :list="tipList" :visible-list="visibleList" @event="handleEvent" />
</template>
<script>
import music from '@renderer/utils/music'
import { debounce } from '@renderer/utils'
import music from '@renderer/utils/musicSdk'
import { debounce } from '@common/utils'
import {
ref,
useRoute,
watch,
useRefGetter,
useRouter,
nextTick,
useCommit,
} from '@renderer/utils/vueTools'
} 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() {
@@ -21,23 +22,21 @@ export default {
const visibleList = ref(false)
const tipList = ref([])
let isFocused = false
let prevTempSearchSource = ''
const route = useRoute()
const router = useRouter()
const setting = useRefGetter('setting')
const clearSearchList = useCommit('search', 'clearList')
watch(() => route.name, (newValue, oldValue) => {
if (newValue.name != 'search') {
if (setting.value.odc.isAutoClearSearchInput && searchText.value) searchText.value = ''
if (setting.value.odc.isAutoClearSearchList) clearSearchList()
setTimeout(() => {
if (appSetting['odc.isAutoClearSearchInput'] && searchText.value) searchText.value = ''
if (appSetting['odc.isAutoClearSearchList']) setSearchText('')
})
}
})
const storeSearchText = useRefGetter('search', 'searchText')
watch(storeSearchText, (newValue, oldValue) => {
watch(_searchText, (newValue, oldValue) => {
searchText.value = newValue
if (newValue !== searchText.value) searchText.value = newValue
})
@@ -46,13 +45,15 @@ export default {
})
const tipSearch = debounce(() => {
if (searchText.value === '') {
const tipSearch = debounce(async() => {
if (searchText.value === '' && prevTempSearchSource) {
tipList.value = []
music[setting.value.search.tempSearchSource].tempSearch.cancelTempSearch()
music[prevTempSearchSource].tempSearch.cancelTempSearch()
return
}
music[setting.value.search.tempSearchSource].tempSearch.search(searchText.value).then(list => {
const { temp_source } = await getSearchSetting()
prevTempSearchSource = temp_source
music[prevTempSearchSource].tempSearch.search(searchText.value).then(list => {
tipList.value = list
}).catch(() => {})
}, 50)
@@ -64,14 +65,18 @@ export default {
const handleSearch = () => {
if (visibleList.value) visibleList.value = false
if (!searchText.value && route.path != '/search') {
setSearchText('')
return
}
setTimeout(() => {
router.push({
path: 'search',
path: '/search',
query: {
text: searchText.value,
},
}).catch(_ => _)
}, 200)
}, searchText.value ? 200 : 0)
}
const handleEvent = ({ action, data }) => {
@@ -1,30 +1,17 @@
<template>
<div :class="[$style.toolbar, { [$style.fullscreen]: isFullscreen }, setting.controlBtnPosition == 'left' ? $style.controlBtnLeft : $style.controlBtnRight]">
<SearchInput />
<div :class="$style.logo" v-if="setting.controlBtnPosition == 'left'">L X</div>
<ControlBtns v-else />
</div>
<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>
import { mapGetters } from 'vuex'
<script setup>
import { isFullscreen } from '@renderer/store'
import { appSetting } from '@renderer/store/setting'
import ControlBtns from './ControlBtns'
import SearchInput from './SearchInput'
import { isFullscreen } from '@renderer/core/share'
export default {
name: 'CoreToolBar',
components: { SearchInput, ControlBtns },
setup() {
return {
isFullscreen,
}
},
computed: {
...mapGetters(['setting']),
},
}
</script>
@@ -61,7 +48,7 @@ export default {
box-sizing: border-box;
padding: 0 @height-toolbar * .4;
height: @height-toolbar;
color: @color-theme;
color: var(--color-primary);
flex: none;
text-align: center;
line-height: @height-toolbar;
@@ -69,11 +56,4 @@ export default {
// -webkit-app-region: no-drag;
}
each(@themes, {
:global(#root.@{value}) {
.logo {
color: ~'@{color-@{value}-theme}';
}
}
})
</style>
@@ -126,10 +126,10 @@ material-modal(:show="versionInfo.showModal" @close="handleClose")
</template>
<script>
import { mapGetters, mapMutations } from 'vuex'
import { rendererSend, NAMES } from '@common/ipc'
import { compareVer, openUrl, clipboardWriteText, sizeFormate } from '@renderer/utils'
import { versionInfo } from '@renderer/core/share'
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() {
@@ -137,8 +137,12 @@ export default {
versionInfo,
}
},
data() {
return {
ignoreVersion: null,
}
},
computed: {
...mapGetters(['setting']),
history() {
if (!this.versionInfo.newVersion || !this.versionInfo.newVersion?.history) return []
let arr = []
@@ -155,11 +159,13 @@ export default {
: '处理更新中...'
},
isIgnored() {
return this.setting.ignoreVersion == this.versionInfo.newVersion?.version
return this.ignoreVersion == this.versionInfo.newVersion?.version
},
},
created() {
this.ignoreVersion = getIgnoreVersion()
},
methods: {
...mapMutations(['setIgnoreVersion']),
handleClose() {
versionInfo.showModal = false
},
@@ -169,13 +175,13 @@ export default {
handleRestartClick(event) {
this.handleClose()
event.target.disabled = true
rendererSend(NAMES.mainWindow.quit_update)
quitUpdate()
},
handleCopy(text) {
clipboardWriteText(text)
},
handleIgnoreClick() {
this.setIgnoreVersion(this.isIgnored ? null : this.versionInfo.newVersion?.version)
saveIgnoreVersion(this.isIgnored ? null : this.versionInfo.newVersion?.version)
this.handleClose()
},
},
@@ -202,7 +208,7 @@ export default {
h2 {
flex: 0 0 none;
font-size: 16px;
color: @color-theme_2-font;
color: var(--color-font);
line-height: 1.3;
text-align: center;
margin-bottom: 15px;
@@ -276,12 +282,12 @@ export default {
.desc {
padding-top: 10px;
font-size: 12px;
color: @color-theme;
color: var(--color-primary-font);
line-height: 1.25;
p {
font-size: 12px;
color: @color-theme;
color: var(--color-primary-font);
line-height: 1.25;
}
}
@@ -292,22 +298,4 @@ export default {
width: 100%;
}
each(@themes, {
:global(#root.@{value}) {
.main {
h2 {
color: ~'@{color-@{value}-theme_2-font}';
}
}
.footer {
.desc {
color: ~'@{color-@{value}-theme}';
p {
color: ~'@{color-@{value}-theme}';
}
}
}
}
})
</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>
@@ -1,37 +1,50 @@
<template lang="pug">
div(:class="$style.btns")
button(type="button" v-if="playBtn" @contextmenu.capture.stop :aria-label="$t('list__play')" @click.stop="handleClick('play')")
svg(version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' height='100%' viewBox='0 0 287.386 287.386' space='preserve' v-once)
use(xlink:href='#icon-testPlay')
button(type="button" v-if="listAddBtn" @contextmenu.capture.stop :aria-label="$t('list__add_to')" @click.stop="handleClick('listAdd')")
svg(version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' height='100%' viewBox='0 0 42 42' space='preserve' v-once)
use(xlink:href='#icon-addTo')
button(type="button" v-if="downloadBtn && setting.download.enable" @contextmenu.capture.stop :aria-label="$t('list__download')" @click.stop="handleClick('download')")
svg(version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' height='100%' viewBox='0 0 475.078 475.077' space='preserve' v-once)
use(xlink:href='#icon-download')
//- button(type="button" :aria-label="$t('list__add')" v-if="userInfo" @click.stop="handleClick('add')")
svg(version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' height='100%' viewBox='0 0 42 42' space='preserve')
use(xlink:href='#icon-addTo')
button(type="button" v-if="startBtn" @contextmenu.capture.stop :aria-label="$t('list__start')" @click.stop="handleClick('start')")
svg(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' v-once)
use(xlink:href='#icon-play')
button(type="button" v-if="pauseBtn" @contextmenu.capture.stop :aria-label="$t('list__pause')" @click.stop="handleClick('pause')")
svg(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' v-once)
use(xlink:href='#icon-pause')
button(type="button" v-if="fileBtn" @contextmenu.capture.stop :aria-label="$t('list__file')" @click.stop="handleClick('file')")
svg(version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' height='100%' viewBox='-61 0 512 512' space='preserve' v-once)
use(xlink:href='#icon-musicFile')
button(type="button" v-if="searchBtn" @contextmenu.capture.stop :aria-label="$t('list__search')" @click.stop="handleClick('search')")
svg(version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' height='100%' viewBox='0 0 30.239 30.239' space='preserve' v-once)
use(xlink:href='#icon-search')
button(type="button" v-if="removeBtn" :aria-label="$t('list__remove')" @click.stop="handleClick('remove')")
svg(version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' height='100%' viewBox='0 0 212.982 212.982' space='preserve' v-once)
use(xlink:href='#icon-delete')
<template>
<div :class="$style.btns">
<button v-if="playBtn" type="button" :aria-label="$t('list__play')" @contextmenu.capture.stop @click.stop="handleClick('play')">
<svg v-once version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" height="100%" viewBox="0 0 287.386 287.386" space="preserve">
<use xlink:href="#icon-testPlay" />
</svg>
</button>
<button v-if="listAddBtn" type="button" :aria-label="$t('list__add_to')" @contextmenu.capture.stop @click.stop="handleClick('listAdd')">
<svg v-once version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" height="100%" viewBox="0 0 42 42" space="preserve">
<use xlink:href="#icon-addTo" />
</svg>
</button>
<button v-if="downloadBtn && appSetting['download.enable']" type="button" :aria-label="$t('list__download')" @contextmenu.capture.stop @click.stop="handleClick('download')">
<svg v-once version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" height="100%" viewBox="0 0 475.078 475.077" space="preserve">
<use xlink:href="#icon-download" />
</svg>
</button>
<button v-if="startBtn" type="button" :aria-label="$t('list__start')" @contextmenu.capture.stop @click.stop="handleClick('start')">
<svg v-once 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>
</button>
<button v-if="pauseBtn" type="button" :aria-label="$t('list__pause')" @contextmenu.capture.stop @click.stop="handleClick('pause')">
<svg v-once 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>
</button>
<button v-if="fileBtn" type="button" :aria-label="$t('list__file')" @contextmenu.capture.stop @click.stop="handleClick('file')">
<svg v-once version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" height="100%" viewBox="-61 0 512 512" space="preserve">
<use xlink:href="#icon-musicFile" />
</svg>
</button>
<button v-if="searchBtn" type="button" :aria-label="$t('list__search')" @contextmenu.capture.stop @click.stop="handleClick('search')">
<svg v-once version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" height="100%" viewBox="0 0 30.239 30.239" space="preserve">
<use xlink:href="#icon-search" />
</svg>
</button>
<button v-if="removeBtn" type="button" :aria-label="$t('list__remove')" @click.stop="handleClick('remove')">
<svg v-once version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" height="100%" viewBox="0 0 212.982 212.982" space="preserve">
<use xlink:href="#icon-delete" />
</svg>
</button>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
import { appSetting } from '@renderer/store/setting'
export default {
props: {
@@ -72,8 +85,11 @@ export default {
default: false,
},
},
computed: {
...mapGetters(['setting']),
emits: ['btn-click'],
setup() {
return {
appSetting,
}
},
methods: {
handleClick(action) {
@@ -97,7 +113,7 @@ export default {
margin-right: 5px;
cursor: pointer;
padding: 4px 7px;
color: @color-btn;
color: var(--color-button-font);
outline: none;
transition: background-color 0.2s ease;
line-height: 0;
@@ -110,27 +126,12 @@ export default {
}
&:hover {
background-color: @color-theme_2-hover;
background-color: var(--color-button-background-hover);
}
&:active {
background-color: @color-theme_2-active;
background-color: var(--color-button-background-active);
}
}
}
each(@themes, {
:global(#root.@{value}) {
.btns {
button {
color: ~'@{color-@{value}-btn}';
&:hover {
background-color: ~'@{color-@{value}-theme_2-hover}';
}
&:active {
background-color: ~'@{color-@{value}-theme_2-active}';
}
}
}
}
})
</style>
+158 -130
View File
@@ -1,21 +1,31 @@
<template lang="pug">
teleport(:to="teleport")
div(:class="$style.container" v-if="showModal")
transition(enter-active-class="animated fadeIn" leave-active-class="animated fadeOut")
div(:class="$style.modal" v-show="showContent" @click="bgClose && close()")
transition(:enter-active-class="inClass" :leave-active-class="outClass" @after-enter="$emit('after-enter', $event)" @after-leave="handleAfterLeave")
div(:class="$style.content" v-show="showContent" @click.stop :style="contentStyle")
header(:class="$style.header")
button(type="button" @click="close" v-if="closeBtn")
svg(version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' height='100%' viewBox='0 0 212.982 212.982' space='preserve')
use(xlink:href='#icon-delete')
slot
<template>
<teleport :to="teleport">
<div v-if="showModal" ref="dom_container" :class="$style.container">
<transition enter-active-class="animated fadeIn" leave-active-class="animated fadeOut">
<div v-show="showContent" :class="[$style.modal, {[$style.filter]: teleport == '#root'}]" @click="bgClose && close()">
<transition :enter-active-class="inClass" :leave-active-class="outClass" @after-enter="$emit('after-enter', $event)" @after-leave="handleAfterLeave">
<div v-show="showContent" :class="$style.content" :style="contentStyle" @click.stop>
<header :class="$style.header">
<button v-if="closeBtn" type="button" @click="close">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" height="100%" viewBox="0 0 212.982 212.982" space="preserve">
<use xlink:href="#icon-delete" />
</svg>
</button>
</header>
<slot />
</div>
</transition>
</div>
</transition>
</div>
</teleport>
</template>
<script>
import { getRandom } from '@renderer/utils'
import { mapGetters } from 'vuex'
import { nextTick } from '@renderer/utils/vueTools'
import { getRandom } from '@common/utils/common'
import { nextTick } from '@common/utils/vueTools'
import { appSetting } from '@renderer/store/setting'
export default {
props: {
show: {
@@ -38,81 +48,125 @@ export default {
type: String,
default: '76%',
},
minWidth: {
type: String,
default: '280px',
},
maxHeight: {
type: String,
default: '80%',
},
width: {
type: String,
default: 'auto',
},
},
emits: ['after-enter', 'after-leave', 'close'],
data() {
return {
animateIn: [
'flipInX',
'flipInY',
'fadeIn',
'bounceIn',
'lightSpeedIn',
'rotateInDownLeft',
'rotateInDownRight',
'rotateInUpLeft',
'rotateInUpRight',
'rollIn',
'zoomIn',
'zoomInDown',
'zoomInLeft',
'zoomInRight',
'zoomInUp',
'slideInDown',
'slideInLeft',
'slideInRight',
'slideInUp',
'jackInTheBox',
],
animateOut: [
'flipOutX',
'flipOutY',
'fadeOut',
'bounceOut',
'lightSpeedOut',
'rotateOutDownLeft',
'rotateOutDownRight',
'rotateOutUpLeft',
'rotateOutUpRight',
'rollOut',
'zoomOut',
'zoomOutDown',
'zoomOutLeft',
'zoomOutRight',
'zoomOutUp',
'slideOutDown',
'slideOutLeft',
'slideOutRight',
'slideOutUp',
// 'hinge',
animates: [
[['jackInTheBox', 'flipInX', 'flipInY', 'lightSpeedIn'], ['flipOutX', 'flipOutY', 'lightSpeedOut']],
// [['jackInTheBox', 'lightSpeedIn'], ['lightSpeedOut']],
[['rotateInDownLeft', 'rotateInDownRight', 'rotateInUpLeft', 'rotateInUpRight'], ['rotateOutDownLeft', 'rotateOutDownRight', 'rotateOutUpLeft', 'rotateOutUpRight']],
[['jackInTheBox', 'zoomInDown', 'zoomInUp'], ['zoomOutDown', 'zoomOutUp']],
[['slideInDown', 'slideInLeft', 'slideInRight', 'slideInUp'], ['slideOutDown', 'slideOutLeft', 'slideOutRight', 'slideOutUp']],
// ['flipInX', 'flipOutX'],
// ['flipInY', 'flipOutY'],
// ['lightSpeedIn', 'lightSpeedOut'],
// ['rotateInDownLeft', 'rotateOutDownLeft'],
// ['rotateInDownRight', 'rotateOutDownRight'],
// ['rotateInUpLeft', 'rotateOutUpLeft'],
// ['rotateInUpRight', 'rotateOutUpRight'],
// // ['rollIn', 'rollOut'],
// // ['zoomIn', 'zoomOut'],
// ['zoomInDown', 'zoomOutDown'],
// // ['zoomInLeft', 'zoomOutLeft'],
// // ['zoomInRight', 'zoomOutRight'],
// ['zoomInUp', 'zoomOutUp'],
// ['slideInDown', 'slideOutDown'],
// ['slideInLeft', 'slideOutLeft'],
// ['slideInRight', 'slideOutRight'],
// ['slideInUp', 'slideOutUp'],
// // ['jackInTheBox', 'hinge'],
],
// animateIn: [
// 'flipInX',
// 'flipInY',
// // 'fadeIn',
// // 'bounceIn',
// 'lightSpeedIn',
// 'rotateInDownLeft',
// 'rotateInDownRight',
// 'rotateInUpLeft',
// 'rotateInUpRight',
// 'rollIn',
// 'zoomIn',
// 'zoomInDown',
// 'zoomInLeft',
// 'zoomInRight',
// 'zoomInUp',
// 'slideInDown',
// 'slideInLeft',
// 'slideInRight',
// 'slideInUp',
// 'jackInTheBox',
// ],
// animateOut: [
// 'flipOutX',
// 'flipOutY',
// // 'fadeOut',
// // 'bounceOut',
// 'lightSpeedOut',
// 'rotateOutDownLeft',
// 'rotateOutDownRight',
// 'rotateOutUpLeft',
// 'rotateOutUpRight',
// 'rollOut',
// 'zoomOut',
// 'zoomOutDown',
// 'zoomOutLeft',
// 'zoomOutRight',
// 'zoomOutUp',
// 'slideOutDown',
// 'slideOutLeft',
// 'slideOutRight',
// 'slideOutUp',
// 'hinge',
// ],
inClass: 'animated jackInTheBox',
outClass: 'animated flipOutX',
unwatchFn: null,
outClass: 'animated slideOutRight',
showModal: false,
showContent: false,
// ai: 0,
}
},
computed: {
...mapGetters(['setting']),
contentStyle() {
return {
maxWidth: this.maxWidth,
minWidth: this.minWidth,
width: this.width,
maxHeight: this.maxHeight,
}
},
},
watch: {
'setting.randomAnimate'(n) {
n ? this.createWatch() : this.removeWatch()
},
show: {
handler(val) {
if (val) {
// const dom = document.getElementById(this.teleport)
// if (dom) {
// // dom.t
// }
this.setRandomAnimation()
this.showModal = true
nextTick(() => {
this.$refs.dom_container.parentNode.classList.add('show-modal')
this.showContent = true
})
} else {
this.$refs.dom_container?.parentNode.classList.remove('show-modal')
this.showContent = false
}
},
@@ -120,37 +174,24 @@ export default {
},
},
mounted() {
if (this.setting.randomAnimate) this.createWatch()
this.setRandomAnimation()
},
beforeUnmount() {
this.removeWatch()
this.$refs.dom_container?.parentNode.classList.remove('show-modal')
},
methods: {
check(isShow) {
if (isShow) {
this.showModal = true
nextTick(() => {
this.showContent = true
})
} else {
this.showContent = false
setRandomAnimation() {
if (appSetting['common.randomAnimate']) {
const [animIn, animOut] = this.animates[getRandom(0, this.animates.length)]
// const [animIn, animOut] = this.animates[this.ai]
// if (++this.ai >= this.animates.length) this.ai = 0
// console.log(animIn, animOut)
// this.inClass = 'animated ' + animIn
// this.outClass = 'animated ' + animOut
this.inClass = 'animated ' + animIn[getRandom(0, animIn.length)]
this.outClass = 'animated ' + animOut[getRandom(0, animOut.length)]
}
},
createWatch() {
this.removeWatch()
this.unwatchFn = this.$watch('show', function(n) {
if (!n) return
this.inClass = 'animated ' + this.animateIn[getRandom(0, this.animateIn.length)]
this.outClass = 'animated ' + this.animateOut[getRandom(0, this.animateOut.length)]
})
this.inClass = 'animated ' + this.animateIn[getRandom(0, this.animateIn.length)]
this.outClass = 'animated ' + this.animateOut[getRandom(0, this.animateOut.length)]
},
removeWatch() {
if (!this.unwatchFn) return
this.unwatchFn()
this.unwatchFn = null
},
close() {
this.$emit('close')
},
@@ -178,34 +219,50 @@ export default {
.modal {
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, .3);
// background-color: rgba(0, 0, 0, .2);
// background-color: rgba(255, 255, 255, .6);
// background-color: var(--color-primary-light-600-alpha-900);
// backdrop-filter: blur(4px);
// backdrop-filter: grayscale(70%);
display: grid;
align-items: center;
justify-items: center;
// will-change: transform;
&.filter {
backdrop-filter: grayscale(70%);
}
// &:before {
// .mixin-after;
// position: absolute;
// left: 0;
// top: 0;
// width: 100%;
// height: 100%;
// background-color: var(--color-000);
// opacity: .6;
// }
}
.content {
position: relative;
border-radius: 5px;
box-shadow: 0 0 3px rgba(0, 0, 0, .3);
border-radius: 4px;
box-shadow: 0 0 4px rgba(0, 0, 0, .25);
overflow: hidden;
max-height: 80%;
// max-height: 80%;
// max-width: 76%;
min-width: 220px;
position: relative;
display: flex;
flex-flow: column nowrap;
z-index: 100;
> * {
background-color: @color-theme_2-background_2;
}
background-color: var(--color-content-background);
}
.header {
flex: none;
background-color: @color-theme;
background-color: var(--color-primary-light-100-alpha-100);
display: flex;
align-items: center;
justify-content: flex-end;
@@ -216,7 +273,7 @@ export default {
cursor: pointer;
padding: 4px 7px;
background-color: transparent;
color: @color-theme-font-label;
color: var(--color-primary-dark-500-alpha-500);
outline: none;
transition: background-color 0.2s ease;
line-height: 0;
@@ -226,41 +283,12 @@ export default {
}
&:hover {
background-color: @color-theme-hover;
background-color: var(--color-primary-dark-100-alpha-600);
}
&:active {
background-color: @color-theme_2-active;
background-color: var(--color-primary-dark-200-alpha-600);
}
}
}
each(@themes, {
:global(#root.@{value}) {
.modal {
background-color: rgba(0, 0, 0, .3);
}
.content {
box-shadow: 0 0 3px rgba(0, 0, 0, .3);
> * {
background-color: ~'@{color-@{value}-theme_2-background_2}';
}
}
.header {
background-color: ~'@{color-@{value}-theme}';
button {
color: ~'@{color-@{value}-theme-font-label}';
&:hover {
background-color: ~'@{color-@{value}-theme-hover}';
}
&:active {
background-color: ~'@{color-@{value}-theme_2-active}';
}
}
}
}
})
</style>
@@ -1,63 +1,114 @@
<template lang="pug">
div(:class="$style.songList")
transition(enter-active-class="animated-fast fadeIn" leave-active-class="animated-fast fadeOut")
div(:class="$style.list")
div.thead(:class="$style.thead")
table
thead
tr
th.nobreak.center(:class="$style.th" :style="{ width: rowWidth.r1 }") #
th.nobreak(:class="$style.th" :style="{ width: rowWidth.r2 }") {{$t('music_name')}}
th.nobreak(:class="$style.th" :style="{ width: rowWidth.r3 }") {{$t('music_singer')}}
th.nobreak(:class="$style.th" :style="{ width: rowWidth.r4 }") {{$t('music_album')}}
th.nobreak(:class="$style.th" :style="{ width: rowWidth.r5 }") {{$t('music_time')}}
th.nobreak(:class="$style.th" :style="{ width: rowWidth.r6 }") {{$t('action')}}
div(:class="$style.content")
div(v-show="list.length" :class="$style.content" ref="dom_listContent")
base-virtualized-list(:list="list" key-name="songmid" ref="listRef" :item-height="listItemHeight"
containerClass="scroll" contentClass="list" @contextmenu.capture="handleListRightClick")
template(#default="{ item, index }")
div.list-item(@click="handleListItemClick($event, index)" @contextmenu="handleListItemRightClick($event, index)"
:class="[{ selected: rightClickSelectedIndex == index }, { active: selectedList.includes(item) }]")
div.list-item-cell.nobreak.center(:style="{ width: rowWidth.r1 }" style="padding-left: 3px; padding-right: 3px;" :class="$style.noSelect" @click.stop) {{index + 1}}
div.list-item-cell.auto(:style="{ width: rowWidth.r2 }" :aria-label="item.name + (item._types.flac32bit ? ` - ${$t('tag__lossless_24bit')}` : (item._types.ape || item._types.flac || item._types.wav) ? ` - ${$t('tag__lossless')}` : item._types['320k'] ? ` - ${$t('tag__high_quality')}` : '')")
span.select {{item.name}}
span.badge.badge-theme-success(:class="[$style.labelQuality, $style.noSelect]" v-if="item._types.flac32bit") {{$t('tag__lossless_24bit')}}
span.badge.badge-theme-success(:class="[$style.labelQuality, $style.noSelect]" v-else-if="item._types.ape || item._types.flac || item._types.wav") {{$t('tag__lossless')}}
span.badge.badge-theme-info(:class="[$style.labelQuality, $style.noSelect]" v-else-if="item._types['320k']") {{$t('tag__high_quality')}}
div.list-item-cell(:style="{ width: rowWidth.r3 }" :aria-label="item.singer")
span.select {{item.singer}}
div.list-item-cell(:style="{ width: rowWidth.r4 }" :aria-label="item.albumName")
span.select {{item.albumName}}
div.list-item-cell(:style="{ width: rowWidth.r5 }")
span(:class="[$style.time, $style.noSelect]") {{item.interval || '--/--'}}
div.list-item-cell(:style="{ width: rowWidth.r6 }" style="padding-left: 0; padding-right: 0;")
material-list-buttons(:index="index" :class="$style.btns"
:remove-btn="false" @btn-click="handleListBtnClick"
:download-btn="assertApiSupport(item.source)")
template(#footer)
div(:class="$style.pagination")
material-pagination(:count="total" :limit="limit" :page="page" @btn-click="$emit('togglePage', $event)")
transition(enter-active-class="animated-fast fadeIn" leave-active-class="animated-fast fadeOut")
div(v-show="!list.length" :class="$style.noitem")
p(v-html="noItem")
//- material-flow-btn(:show="isShowEditBtn && assertApiSupport(source)" :remove-btn="false" @btn-click="handleFlowBtnClick")
common-download-modal(v-model:show="isShowDownload" :musicInfo="selectedDownloadMusicInfo" teleport="#view")
common-download-multiple-modal(v-model:show="isShowDownloadMultiple" :list="selectedList" @confirm="removeAllSelect" teleport="#view")
common-list-add-modal(v-model:show="isShowListAdd" :musicInfo="selectedAddMusicInfo" teleport="#view")
common-list-add-multiple-modal(v-model:show="isShowListAddMultiple" :musicList="selectedList" @confirm="removeAllSelect" teleport="#view")
base-menu(:menus="menus" :location="menuLocation" item-name="name" :isShow="isShowItemMenu" @menu-click="handleMenuClick")
<template>
<div :class="$style.songList">
<transition enter-active-class="animated-fast fadeIn" leave-active-class="animated-fast fadeOut">
<div :class="$style.list">
<div class="thead">
<table>
<thead>
<tr v-if="actionButtonsVisible">
<th class="num" style="width: 5%;">#</th>
<th class="nobreak">{{ $t('music_name') }}</th>
<th class="nobreak" style="width: 22%;">{{ $t('music_singer') }}</th>
<th class="nobreak" style="width: 22%;">{{ $t('music_album') }}</th>
<th class="nobreak" style="width: 9%;">{{ $t('music_time') }}</th>
<th class="nobreak" style="width: 16%;">{{ $t('action') }}</th>
</tr>
<tr v-else>
<th class="num" style="width: 5%;">#</th>
<th class="nobreak">{{ $t('music_name') }}</th>
<th class="nobreak" style="width: 24%;">{{ $t('music_singer') }}</th>
<th class="nobreak" style="width: 27%;">{{ $t('music_album') }}</th>
<th class="nobreak" style="width: 10%;">{{ $t('music_time') }}</th>
</tr>
</thead>
</table>
</div>
<div :class="$style.content">
<div v-show="!noItem" ref="dom_listContent" :class="$style.content">
<base-virtualized-list v-if="actionButtonsVisible" ref="listRef" :list="list" key-name="id" :item-height="listItemHeight" container-class="scroll" content-class="list" @contextmenu.capture="handleListRightClick">
<template #default="{ item, index }">
<div
class="list-item" :class="[{ selected: rightClickSelectedIndex == index }, { active: selectedList.includes(item) }]"
@click="handleListItemClick($event, index)" @contextmenu="handleListItemRightClick($event, index)"
>
<div class="list-item-cell no-select num" style="flex: 0 0 5%;" @click.stop>{{ index + 1 }}</div>
<div class="list-item-cell auto name">
<span class="select name" :aria-label="item.name">{{ item.name }}</span>
<span v-if="item.meta._qualitys.flac24bit" class="no-select badge badge-theme-primary">{{ $t('tag__lossless_24bit') }}</span>
<span v-else-if="item.meta._qualitys.ape || item.meta._qualitys.flac || item.meta._qualitys.wav" class="no-select badge badge-theme-primary">{{ $t('tag__lossless') }}</span>
<span v-else-if="item.meta._qualitys['320k']" class="no-select badge badge-theme-secondary">{{ $t('tag__high_quality') }}</span>
<span v-if="sourceTag" class="no-select badge badge-theme-tertiary">{{ item.source }}</span>
</div>
<div class="list-item-cell" style="flex: 0 0 22%;"><span class="select" :aria-label="item.singer">{{ item.singer }}</span></div>
<div class="list-item-cell" style="flex: 0 0 22%;"><span class="select" :aria-label="item.meta.albumName">{{ item.meta.albumName }}</span></div>
<div class="list-item-cell" style="flex: 0 0 9%;"><span class="no-select">{{ item.interval || '--/--' }}</span></div>
<div class="list-item-cell" style="flex: 0 0 16%; padding-left: 0; padding-right: 0;">
<material-list-buttons :index="index" :remove-btn="false" :download-btn="assertApiSupport(item.source)" :play-btn="checkApiSource ? assertApiSupport(item.source) : true" @btn-click="handleListBtnClick" />
</div>
</div>
</template>
<template #footer>
<div :class="$style.pagination">
<material-pagination :count="total" :limit="limit" :page="page" @btn-click="$emit('togglePage', $event)" />
</div>
</template>
</base-virtualized-list>
<base-virtualized-list v-else ref="listRef" :list="list" key-name="id" :item-height="listItemHeight" container-class="scroll" content-class="list" @contextmenu.capture="handleListRightClick">
<template #default="{ item, index }">
<div
class="list-item" :class="[{ selected: rightClickSelectedIndex == index }, { active: selectedList.includes(item) }]"
@click="handleListItemClick($event, index)" @contextmenu="handleListItemRightClick($event, index)"
>
<div class="list-item-cell no-select num" style="flex: 0 0 5%;" @click.stop>{{ index + 1 }}</div>
<div class="list-item-cell auto name">
<span class="select name" :aria-label="item.name">{{ item.name }}</span>
<span v-if="item.meta._qualitys.flac24bit" class="no-select badge badge-theme-primary">{{ $t('tag__lossless_24bit') }}</span>
<span v-else-if="item.meta._qualitys.ape || item.meta._qualitys.flac || item.meta._qualitys.wav" class="no-select badge badge-theme-primary">{{ $t('tag__lossless') }}</span>
<span v-else-if="item.meta._qualitys['320k']" class="no-select badge badge-theme-secondary">{{ $t('tag__high_quality') }}</span>
<span v-if="sourceTag" class="no-select badge badge-theme-tertiary">{{ item.source }}</span>
</div>
<div class="list-item-cell" style="flex: 0 0 24%;"><span class="select" :aria-label="item.singer">{{ item.singer }}</span></div>
<div class="list-item-cell" style="flex: 0 0 27%;"><span class="select" :aria-label="item.meta.albumName">{{ item.meta.albumName }}</span></div>
<div class="list-item-cell" style="flex: 0 0 10%;"><span class="no-select">{{ item.interval || '--/--' }}</span></div>
</div>
</template>
<template #footer>
<div :class="$style.pagination">
<material-pagination :count="total" :limit="limit" :page="page" @btn-click="$emit('togglePage', $event)" />
</div>
</template>
</base-virtualized-list>
</div>
<transition enter-active-class="animated fadeIn" leave-active-class="animated fadeOut">
<div v-show="noItem" :class="$style.noitem">
<p v-text="noItem" />
</div>
</transition>
</div>
</div>
</transition>
<!-- <material-flow-btn :show="isShowEditBtn && assertApiSupport(source)" :remove-btn="false" @btn-click="handleFlowBtnClick" /> -->
<!-- <common-download-modal v-model:show="isShowDownload" :music-info="selectedDownloadMusicInfo" teleport="#view" />
<common-download-multiple-modal v-model:show="isShowDownloadMultiple" :list="selectedList" teleport="#view" @confirm="removeAllSelect" /> -->
<common-list-add-modal v-model:show="isShowListAdd" :music-info="selectedAddMusicInfo" teleport="#view" />
<common-list-add-multiple-modal v-model:show="isShowListAddMultiple" :music-list="selectedList" teleport="#view" @confirm="removeAllSelect" />
<common-download-modal v-model:show="isShowDownload" :music-info="selectedDownloadMusicInfo" teleport="#view" />
<common-download-multiple-modal v-model:show="isShowDownloadMultiple" :list="selectedList" teleport="#view" @confirm="removeAllSelect" />
<base-menu v-model="isShowItemMenu" :menus="menus" :xy="menuLocation" item-name="name" @menu-click="handleMenuClick" />
</div>
</template>
<script>
import { clipboardWriteText, assertApiSupport } from '@renderer/utils'
import { ref, useCssModule, useRefGetter } from '@renderer/utils/vueTools'
import { clipboardWriteText } from '@common/utils/electron'
import { assertApiSupport } from '@renderer/store/utils'
import { ref } from '@common/utils/vueTools'
import useList from './useList'
import useMenu from './useMenu'
import usePlay from './usePlay'
import useMusicDownload from './useMusicDownload'
import useMusicAdd from './useMusicAdd'
import useMusicActions from './useMusicActions'
import { appSetting } from '@renderer/store/setting'
export default {
name: 'MaterialOnlineList',
props: {
@@ -79,33 +130,26 @@ export default {
type: Number,
required: true,
},
rowWidth: {
type: Object,
default() {
return {
r1: '5%',
r2: 'auto',
r3: '22%',
r4: '22%',
r5: '8%',
r6: '13%',
}
},
sourceTag: {
type: Boolean,
default: false,
},
noItem: {
type: String,
default: '',
},
checkApiSource: {
type: Boolean,
default: false,
},
},
emits: ['show-menu', 'play-list', 'togglePage'],
setup(props, { emit }) {
const actionButtonsVisible = appSetting['list.actionButtonsVisible']
const rightClickSelectedIndex = ref(-1)
const dom_listContent = ref(null)
const listRef = ref(null)
const styles = useCssModule()
const setting = useRefGetter('setting')
const {
selectedList,
listItemHeight,
@@ -117,7 +161,7 @@ export default {
handlePlayMusic,
handlePlayMusicLater,
doubleClickPlay,
} = usePlay({ selectedList, props, removeAllSelect, setting, emit })
} = usePlay({ selectedList, props, removeAllSelect, emit })
const {
isShowListAdd,
@@ -143,10 +187,9 @@ export default {
menuLocation,
isShowItemMenu,
showMenu,
hideMenu,
menuClick,
} = useMenu({
listRef,
props,
assertApiSupport,
emit,
@@ -176,10 +219,10 @@ export default {
if (!event.target.classList.contains('select')) return
event.stopImmediatePropagation()
let classList = dom_listContent.value.classList
classList.add(styles.copying)
classList.add('copying')
window.requestAnimationFrame(() => {
let str = window.getSelection().toString()
classList.remove(styles.copying)
classList.remove('copying')
str = str.split(/\n\n/).map(s => s.replace(/\n/g, ' ')).join('\n').trim()
if (!str.length) return
clipboardWriteText(str)
@@ -220,7 +263,6 @@ export default {
isShowItemMenu,
menuLocation,
handleMenuClick,
hideMenu,
handleListRightClick,
assertApiSupport,
@@ -234,6 +276,7 @@ export default {
selectedDownloadMusicInfo,
scrollToTop,
actionButtonsVisible,
}
},
}
@@ -251,50 +294,23 @@ export default {
}
.list {
position: relative;
font-size: 14px;
overflow: hidden;
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
display: flex;
flex-flow: column nowrap;
height: 100%;
}
.thead {
flex: none;
}
.th:first-child {
color: @color-theme_2-font-label;
// padding-left: 10px;
font-size: 14px;
}
.content {
flex: auto;
min-height: 0;
position: relative;
height: 100%;
}
&.copying {
.no-select {
display: none;
}
}
}
:global(.list) {
height: 100%;
overflow-y: auto;
:global(.list-item-cell) {
font-size: 12px !important;
:global(.badge) {
margin-left: 3px;
}
&:first-child {
// padding-left: 10px;
font-size: 11px !important;
color: @color-theme_2-font-label !important;
}
}
:global(.badge) {
opacity: .85;
}
}
.pagination {
text-align: center;
padding: 15px 0;
@@ -311,30 +327,12 @@ export default {
flex-flow: column nowrap;
justify-content: center;
align-items: center;
// background-color: var(--color-000);
p {
font-size: 24px;
color: @color-theme_2-font-label;
color: var(--color-font-label);
}
}
each(@themes, {
:global(#root.@{value}) {
.th:first-child {
color: ~'@{color-@{value}-theme_2-font-label}';
}
:global(.list) {
:global(.list-item-cell) {
&:first-child {
color: ~'@{color-@{value}-theme_2-font-label}' !important;
}
}
}
.noitem {
p {
color: ~'@{color-@{value}-theme_2-font-label}';
}
}
}
})
</style>
@@ -0,0 +1,112 @@
import { computed, watch, ref, onBeforeUnmount } from '@common/utils/vueTools'
import { isFullscreen } from '@renderer/store'
import { appSetting } from '@renderer/store/setting'
import { getFontSizeWithScreen } from '@renderer/utils'
const useKeyEvent = ({ handleSelectAllData }: {
handleSelectAllData: () => void
}) => {
const keyEvent = {
isShiftDown: false,
isModDown: false,
}
const handle_key_shift_down = () => {
if (!keyEvent.isShiftDown) keyEvent.isShiftDown = true
}
const handle_key_shift_up = () => {
if (keyEvent.isShiftDown) keyEvent.isShiftDown = false
}
const handle_key_mod_down = () => {
if (!keyEvent.isModDown) keyEvent.isModDown = true
}
const handle_key_mod_up = () => {
if (keyEvent.isModDown) keyEvent.isModDown = false
}
const handle_key_mod_a_down = ({ event }: LX.KeyDownEevent) => {
if (!event || (event.target as HTMLElement).tagName == 'INPUT') return
event.preventDefault()
if (event.repeat) return
keyEvent.isModDown = false
handleSelectAllData()
}
onBeforeUnmount(() => {
window.key_event.off('key_shift_down', handle_key_shift_down)
window.key_event.off('key_shift_up', handle_key_shift_up)
window.key_event.off('key_mod_down', handle_key_mod_down)
window.key_event.off('key_mod_up', handle_key_mod_up)
window.key_event.off('key_mod+a_down', handle_key_mod_a_down)
})
window.key_event.on('key_shift_down', handle_key_shift_down)
window.key_event.on('key_shift_up', handle_key_shift_up)
window.key_event.on('key_mod_down', handle_key_mod_down)
window.key_event.on('key_mod_up', handle_key_mod_up)
window.key_event.on('key_mod+a_down', handle_key_mod_a_down)
return keyEvent
}
export default ({ props }: {
props: {
list: LX.Music.MusicInfoOnline[]
}
}) => {
const selectedList = ref<LX.Music.MusicInfoOnline[]>([])
let lastSelectIndex = -1
const listItemHeight = computed(() => {
return Math.ceil((isFullscreen.value ? getFontSizeWithScreen() : appSetting['common.fontSize']) * 2.3)
})
const removeAllSelect = () => {
selectedList.value = []
}
const handleSelectAllData = () => {
removeAllSelect()
selectedList.value = [...props.list]
}
const keyEvent = useKeyEvent({ handleSelectAllData })
const handleSelectData = (clickIndex: number) => {
if (keyEvent.isShiftDown) {
if (selectedList.value.length) {
removeAllSelect()
if (lastSelectIndex != clickIndex) {
let isNeedReverse = false
if (clickIndex < lastSelectIndex) {
let temp = lastSelectIndex
lastSelectIndex = clickIndex
clickIndex = temp
isNeedReverse = true
}
selectedList.value = props.list.slice(lastSelectIndex, clickIndex + 1)
if (isNeedReverse) selectedList.value.reverse()
}
} else {
selectedList.value.push(props.list[clickIndex])
lastSelectIndex = clickIndex
}
} else if (keyEvent.isModDown) {
lastSelectIndex = clickIndex
let item = props.list[clickIndex]
let index = selectedList.value.indexOf(item)
if (index < 0) {
selectedList.value.push(item)
} else {
selectedList.value.splice(index, 1)
}
} else if (selectedList.value.length) {
removeAllSelect()
}
}
watch(() => props.list, removeAllSelect)
return {
selectedList,
listItemHeight,
removeAllSelect,
handleSelectData,
}
}
@@ -1,8 +1,9 @@
import { computed, ref, reactive, useI18n, useCssModule, nextTick } from '@renderer/utils/vueTools'
import musicSdk from '@renderer/utils/music'
import { computed, ref, reactive, nextTick } from '@common/utils/vueTools'
import musicSdk from '@renderer/utils/musicSdk'
import { useI18n } from '@renderer/plugins/i18n'
export default ({
listRef,
props,
assertApiSupport,
emit,
@@ -21,9 +22,8 @@ export default ({
search: true,
sourceDetail: true,
})
const { t } = useI18n()
const styles = useCssModule()
const menuLocation = ref({ x: 0, y: 0 })
const t = useI18n()
const menuLocation = reactive({ x: 0, y: 0 })
const isShowItemMenu = ref(false)
const menus = computed(() => {
@@ -66,17 +66,17 @@ export default ({
// this.listMenu.itemMenuControl.play =
// this.listMenu.itemMenuControl.playLater =
itemMenuControl.download = assertApiSupport(musicInfo.source)
let dom_container = event.target.closest('.' + styles.songList)
const getOffsetValue = (target, x = 0, y = 0) => {
if (target === dom_container) return { x, y }
if (!target) return { x: 0, y: 0 }
x += target.offsetLeft
y += target.offsetTop
return getOffsetValue(target.offsetParent, x, y)
if (props.checkApiSource) {
itemMenuControl.playLater =
itemMenuControl.play =
itemMenuControl.download
}
let { x, y } = getOffsetValue(event.target)
menuLocation.value.x = x + event.offsetX
menuLocation.value.y = y + event.offsetY - listRef.value.getScrollTop()
menuLocation.x = event.pageX
menuLocation.y = event.pageY
if (isShowItemMenu.value) return
emit('show-menu')
nextTick(() => {
isShowItemMenu.value = true
@@ -119,6 +119,5 @@ export default ({
isShowItemMenu,
showMenu,
menuClick,
hideMenu,
}
}
@@ -1,6 +1,7 @@
import { useRouter } from '@renderer/utils/vueTools'
import musicSdk from '@renderer/utils/music'
import { openUrl } from '@renderer/utils'
import { useRouter } from '@common/utils/vueRouter'
import musicSdk from '@renderer/utils/musicSdk'
import { openUrl } from '@common/utils/electron'
import { toOldMusicInfo } from '@renderer/utils'
export default ({ props }) => {
@@ -9,7 +10,7 @@ export default ({ props }) => {
const handleSearch = index => {
const info = props.list[index]
router.push({
path: 'search',
path: '/search',
query: {
text: `${info.name} ${info.singer}`,
},
@@ -18,7 +19,7 @@ export default ({ props }) => {
const handleOpenMusicDetail = index => {
const minfo = props.list[index]
const url = musicSdk[minfo.source].getMusicDetailPageUrl(minfo)
const url = musicSdk[minfo.source].getMusicDetailPageUrl(toOldMusicInfo(minfo))
if (!url) return
openUrl(url)
}
@@ -1,4 +1,4 @@
import { ref, nextTick } from '@renderer/utils/vueTools'
import { ref, nextTick } from '@common/utils/vueTools'
export default ({ selectedList, props }) => {
const isShowListAdd = ref(false)
@@ -1,4 +1,4 @@
import { ref, nextTick } from '@renderer/utils/vueTools'
import { ref, nextTick } from '@common/utils/vueTools'
export default ({ selectedList, props }) => {
const isShowDownload = ref(false)
@@ -1,64 +0,0 @@
import { useCommit } from '@renderer/utils/vueTools'
import { defaultList } from '@renderer/core/share/list'
import { getList } from '@renderer/core/share/utils'
export default ({ selectedList, props, removeAllSelect, setting, emit }) => {
let clickTime = 0
let clickIndex = -1
const listAddMultiple = useCommit('list', 'listAddMultiple')
const listAdd = useCommit('list', 'listAdd')
const setList = useCommit('player', 'setList')
const setTempPlayList = useCommit('player', 'setTempPlayList')
const handlePlayMusic = (index, single) => {
let targetSong = props.list[index]
const defaultListMusics = getList(defaultList.id)
if (selectedList.value.length && !single) {
listAddMultiple({ id: defaultList.id, list: [...selectedList.value] })
removeAllSelect()
} else {
listAdd({ id: defaultList.id, musicInfo: targetSong })
}
let targetIndex = defaultListMusics.findIndex(s => s.songmid === targetSong.songmid)
if (targetIndex > -1) {
setList({
listId: defaultList.id,
index: targetIndex,
})
}
}
const handlePlayMusicLater = (index, single) => {
if (selectedList.value.length && !single) {
setTempPlayList(selectedList.value.map(s => ({ listId: '__temp__', musicInfo: s })))
removeAllSelect()
} else {
setTempPlayList([{ listId: '__temp__', musicInfo: props.list[index] }])
}
}
const doubleClickPlay = index => {
if (
window.performance.now() - clickTime > 400 ||
clickIndex !== index
) {
clickTime = window.performance.now()
clickIndex = index
return
}
if (setting.value.list.isClickPlayList) {
emit('play-list', index)
} else {
handlePlayMusic(index, true)
}
clickTime = 0
clickIndex = -1
}
return {
handlePlayMusic,
handlePlayMusicLater,
doubleClickPlay,
}
}
@@ -0,0 +1,67 @@
// import { useCommit } from '@common/utils/vueTools'
import { defaultList } from '@renderer/store/list/state'
import { getListMusics, addListMusics } from '@renderer/store/list/action'
import { addTempPlayList } from '@renderer/store/player/action'
import { appSetting } from '@renderer/store/setting'
import { Ref } from '@common/utils/vueTools'
import { playList } from '@renderer/core/player'
export default ({ selectedList, props, removeAllSelect, emit }: {
selectedList: Ref<LX.Music.MusicInfoOnline[]>
props: {
list: LX.Music.MusicInfoOnline[]
}
removeAllSelect: () => void
emit: (event: 'show-menu' | 'play-list' | 'togglePage', ...args: any[]) => void
}) => {
let clickTime = 0
let clickIndex = -1
const handlePlayMusic = async(index: number, single: boolean) => {
let targetSong = props.list[index]
const defaultListMusics = await getListMusics(defaultList.id)
if (selectedList.value.length && !single) {
await addListMusics(defaultList.id, [...selectedList.value])
removeAllSelect()
} else {
await addListMusics(defaultList.id, [targetSong])
}
let targetIndex = defaultListMusics.findIndex(s => s.id === targetSong.id)
if (targetIndex > -1) {
playList(defaultList.id, targetIndex)
}
}
const handlePlayMusicLater = (index: number, single: boolean) => {
if (selectedList.value.length && !single) {
addTempPlayList(selectedList.value.map(s => ({ listId: '__temp__', musicInfo: s })))
removeAllSelect()
} else {
addTempPlayList([{ listId: '__temp__', musicInfo: props.list[index] }])
}
}
const doubleClickPlay = (index: number) => {
if (
window.performance.now() - clickTime > 400 ||
clickIndex !== index
) {
clickTime = window.performance.now()
clickIndex = index
return
}
if (appSetting['list.isClickPlayList']) {
emit('play-list', index)
} else {
void handlePlayMusic(index, true)
}
clickTime = 0
clickIndex = -1
}
return {
handlePlayMusic,
handlePlayMusicLater,
doubleClickPlay,
}
}
+62 -119
View File
@@ -1,52 +1,56 @@
<template>
<div :class="$style.pagination" v-if="allPage &gt; 1">
<ul>
<li v-if="page===1" :class="$style.disabled"><span>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" height="100%" viewBox="0 0 451.846 451.847" space="preserve">
<use xlink:href="#icon-left"></use>
</svg></span></li>
<li v-else>
<button type="button" @click="handleClick(page - 1)" :aria-label="$t('pagination__prev')">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" height="100%" viewBox="0 0 451.846 451.847" space="preserve">
<use xlink:href="#icon-left"></use>
</svg>
</button>
</li>
<li v-if="allPage &gt; btnLength &amp;&amp; page &gt; pageEvg+1" :class="$style.first">
<button type="button" @click="handleClick(1)" :aria-label="$t('pagination__page', { num: 1 })">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" height="100%" viewBox="0 0 451.846 451.847" space="preserve">
<use xlink:href="#icon-first"></use>
</svg>
</button>
</li>
<li v-for="(p, index) in pages" :key="index" :class="{[$style.active] : p == page}"><span v-if="p === page" v-text="page"></span>
<button v-else type="button" @click="handleClick(p)" v-text="p" :aria-label="$t('pagination__page', { num: p })"></button>
</li>
<li v-if="allPage &gt; btnLength &amp;&amp; allPage - page &gt; pageEvg" :class="$style.last">
<button type="button" @click="handleClick(allPage)" :aria-label="$t('pagination__page', { num: allPage })">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" height="100%" viewBox="0 0 451.846 451.847" space="preserve">
<use xlink:href="#icon-last"></use>
</svg>
</button>
</li>
<li v-if="page===allPage" :class="$style.disabled"><span>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" height="100%" viewBox="0 0 451.846 451.847" space="preserve">
<use xlink:href="#icon-right"></use>
</svg></span></li>
<li v-else>
<button type="button" @click="handleClick(page + 1)" :aria-label="$t('pagination__next')">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" height="100%" viewBox="0 0 451.846 451.847" space="preserve">
<use xlink:href="#icon-right"></use>
</svg>
</button>
</li>
</ul>
</div>
<div v-if="maxPage > 1" :class="$style.pagination">
<ul>
<li v-if="page == 1" :class="$style.disabled">
<span>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" height="100%" viewBox="0 0 451.846 451.847" space="preserve">
<use xlink:href="#icon-left" />
</svg>
</span>
</li>
<li v-else>
<button type="button" :aria-label="$t('pagination__prev')" @click="handleClick(page - 1)">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" height="100%" viewBox="0 0 451.846 451.847" space="preserve">
<use xlink:href="#icon-left" />
</svg>
</button>
</li>
<li v-if="maxPage > btnLength && page > pageEvg+1" :class="$style.first">
<button type="button" :aria-label="$t('pagination__page', { num: 1 })" @click="handleClick(1)">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" height="100%" viewBox="0 0 451.846 451.847" space="preserve">
<use xlink:href="#icon-first" />
</svg>
</button>
</li>
<li v-for="p in pages" :key="p" :class="{[$style.active] : p == page}">
<span v-if="p === page" v-text="page" />
<button v-else type="button" :aria-label="$t('pagination__page', { num: p })" @click="handleClick(p)" v-text="p" />
</li>
<li v-if="maxPage > btnLength && maxPage - page > pageEvg" :class="$style.last">
<button type="button" :aria-label="$t('pagination__page', { num: maxPage })" @click="handleClick(maxPage)">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" height="100%" viewBox="0 0 451.846 451.847" space="preserve">
<use xlink:href="#icon-last" />
</svg>
</button>
</li>
<li v-if="page == maxPage" :class="$style.disabled">
<span>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" height="100%" viewBox="0 0 451.846 451.847" space="preserve">
<use xlink:href="#icon-right" />
</svg></span>
</li>
<li v-else>
<button type="button" :aria-label="$t('pagination__next')" @click="handleClick(page + 1)">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" height="100%" viewBox="0 0 451.846 451.847" space="preserve">
<use xlink:href="#icon-right" />
</svg>
</button>
</li>
</ul>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
props: {
@@ -66,61 +70,31 @@ export default {
type: Number,
default: 7,
},
maxPage: {
type: Number,
default: null,
},
},
data() {
return {
pageArr: [],
}
},
emits: ['btn-click'],
computed: {
...mapGetters(['userInfo']),
allPage() {
return this.maxPage == null ? Math.ceil(this.count / this.limit) || 1 : this.maxPage
maxPage() {
return Math.ceil(this.count / this.limit) || 1
},
pageEvg() {
return Math.floor(this.btnLength / 2)
},
pages() {
if (this.allPage <= this.btnLength) return this.pageArr
if (this.maxPage <= this.btnLength) return Array.from({ length: this.maxPage }, (_, i) => i + 1)
let start =
this.page - this.pageEvg > 1
? this.allPage - this.page < this.pageEvg + 1
? this.allPage - (this.btnLength - 1)
? this.maxPage - this.page < this.pageEvg + 1
? this.maxPage - (this.btnLength - 1)
: this.page - this.pageEvg
: 1
let end =
this.page + this.pageEvg <= this.btnLength
? this.btnLength
: this.page + this.pageEvg <= this.allPage
? this.page + this.pageEvg
: this.allPage
// console.log(start-1);
// console.log(end);
// console.log(this.pageArr.slice(start-1, end-1));
return this.pageArr.slice(start - 1, end)
},
},
watch: {
allPage() {
this.initPageArr()
return Array.from({ length: this.btnLength }, (_, i) => start + i)
},
},
methods: {
initPageArr() {
this.pageArr = []
for (let i = 1; i <= this.allPage; i++) this.pageArr.push(i)
},
handleClick(page) {
this.$emit('btn-click', page)
},
},
mounted() {
this.initPageArr()
},
}
</script>
@@ -130,7 +104,7 @@ export default {
.pagination {
display: inline-block;
background-color: @color-pagination-background;
background-color: var(--color-button-background);
// border-top-left-radius: 8px;
border-radius: @radius-border;
ul {
@@ -140,7 +114,7 @@ export default {
// border-radius: .3125rem;
li {
// margin-right: @padding;
color: @color-theme;
// color: var(--color-button-font);
// border: .0625rem solid @theme_line;
// border-radius: .3125rem;
transition: 0.4s ease;
@@ -156,12 +130,12 @@ export default {
display: block;
padding: 7px 12px;
line-height: 1;
color: @color-theme;
color: var(--color-button-font);
font-size: 13px;
}
&.active {
span {
background-color: @color-pagination-select;
background-color: var(--color-button-background-selected);
}
}
button {
@@ -171,10 +145,10 @@ export default {
outline: none;
transition: background-color .3s ease;
&:hover {
background-color: @color-pagination-hover;
background-color: var(--color-button-background-hover);
}
&:active {
background-color: @color-pagination-active;
background-color: var(--color-button-background-active);
}
}
&.disabled {
@@ -207,35 +181,4 @@ export default {
}
each(@themes, {
:global(#root.@{value}) {
.pagination {
background-color: ~'@{color-@{value}-pagination-background}';
ul {
li {
color: ~'@{color-@{value}-theme}';
span,
button {
color: ~'@{color-@{value}-theme}';
}
&.active {
span {
background-color: ~'@{color-@{value}-pagination-select}';
}
}
button {
&:hover {
background-color: ~'@{color-@{value}-pagination-hover}';
}
&:active {
background-color: ~'@{color-@{value}-pagination-active}';
}
}
}
}
}
}
})
</style>
@@ -1,50 +1,57 @@
<template>
<div :class="$style.container">
<div :class="[$style.search, {[$style.active]: focus}, {[$style.big]: big}, {[$style.small]: small}]">
<div :class="$style.form">
<input :placeholder="placeholder"
v-model.trim="text"
ref="dom_input"
@focus="handleFocus"
@blur="handleBlur"
@input="$emit('update:modelValue', text)"
@change="sendEvent('change')"
@keyup.enter="handleSearch"
@keyup.arrow-down.prevent="handleKeyDown"
@keyup.arrow-up.prevent="handleKeyUp"
@contextmenu="handleContextMenu" />
<transition enter-active-class="animated zoomIn" leave-active-class="animated zoomOut">
<button type="button" @click="handleClearList" v-show="text">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" height="100%" viewBox="0 0 24 24" space="preserve">
<use xlink:href="#icon-window-close"></use>
</svg>
<div :class="$style.container">
<div :class="[$style.search, {[$style.active]: focus}, {[$style.big]: big}, {[$style.small]: small}]">
<div :class="$style.form">
<input
ref="dom_input"
v-model.trim="text"
:placeholder="placeholder"
@focus="handleFocus"
@blur="handleBlur"
@input="$emit('update:modelValue', text)"
@change="sendEvent('change')"
@keyup.enter="handleSearch"
@keyup.arrow-down.prevent="handleKeyDown"
@keyup.arrow-up.prevent="handleKeyUp"
@contextmenu="handleContextMenu"
>
<transition enter-active-class="animated zoomIn" leave-active-class="animated zoomOut">
<button v-show="text" type="button" @click="handleClearList">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" height="100%" viewBox="0 0 24 24" space="preserve">
<use xlink:href="#icon-window-close" />
</svg>
</button>
</transition>
<button type="button" @click="handleSearch">
<slot>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" height="100%" viewBox="0 0 30.239 30.239" space="preserve">
<use xlink:href="#icon-search" />
</svg>
</slot>
</button>
</transition>
<button type="button" @click="handleSearch">
<slot>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" height="100%" viewBox="0 0 30.239 30.239" space="preserve">
<use xlink:href="#icon-search"></use>
</svg>
</slot>
</button>
</div>
<div v-if="list" :class="$style.list" :style="listStyle">
<ul ref="dom_list">
<li v-for="(item, index) in list"
:key="item"
:class="{[$style.select]: selectIndex === index }"
@mouseenter="selectIndex = index"
@click="handleTemplistClick(index)"
><span>{{item}}</span></li>
</ul>
</div>
<div v-if="list" :class="$style.list" :style="listStyle">
<ul ref="dom_list">
<li
v-for="(item, index) in list"
:key="item"
:class="{[$style.select]: selectIndex === index }"
@mouseenter="selectIndex = index"
@click="handleTemplistClick(index)"
>
<span>{{ item }}</span>
</li>
</ul>
</div>
</div>
</div>
</div>
</template>
<script>
import { clipboardReadText } from '@renderer/utils'
import { common as eventCommonNames } from '@common/hotKey'
import { clipboardReadText } from '@common/utils/electron'
import { HOTKEY_COMMON } from '@common/hotKey'
import { appSetting } from '@renderer/store/setting'
export default {
props: {
placeholder: {
@@ -53,6 +60,9 @@ export default {
},
list: {
type: Array,
default() {
return []
},
},
visibleList: {
type: Boolean,
@@ -99,7 +109,7 @@ export default {
},
},
mounted() {
if (this.$store.getters.setting.search.isFocusSearchBox) this.handleFocusInput()
if (appSetting['search.isFocusSearchBox']) this.handleFocusInput()
this.handleRegisterEvent('on')
},
beforeUnmount() {
@@ -107,9 +117,9 @@ export default {
},
methods: {
handleRegisterEvent(action) {
let eventHub = window.eventHub
let eventHub = window.key_event
let name = action == 'on' ? 'on' : 'off'
eventHub[name](eventCommonNames.focusSearchInput.action, this.handleFocusInput)
eventHub[name](HOTKEY_COMMON.focusSearchInput.action, this.handleFocusInput)
},
handleFocusInput() {
this.$refs.dom_input.focus()
@@ -162,7 +172,7 @@ export default {
str = str.replace(/\t|\r\n|\n|\r/g, ' ')
str = str.replace(/\s+/g, ' ')
let dom_input = this.$refs.dom_input
this.text = `${this.text.substring(0, dom_input.selectionStart)}${str}${this.text.substring(dom_input.selectionEnd, this.text.length)}`
this.text = this.text.substring(0, dom_input.selectionStart) + str + this.text.substring(dom_input.selectionEnd, this.text.length)
this.$emit('update:modelValue', this.text)
},
handleClearList() {
@@ -189,10 +199,10 @@ export default {
position: absolute;
width: 100%;
border-radius: @form-radius;
transition: box-shadow .4s ease, background-color @transition-theme;
transition: box-shadow .4s ease, background-color @transition-normal;
display: flex;
flex-flow: column nowrap;
background-color: @color-search-form-background;
background-color: var(--color-primary-light-600-alpha-100);
&.active {
box-shadow: 0 1px 5px 0 rgba(0,0,0,.2);
@@ -216,8 +226,8 @@ export default {
border-top-left-radius: 3px;
border-bottom-left-radius: 3px;
background-color: transparent;
// border-bottom: 2px solid @color-theme;
// border-color: @color-theme;
// border-bottom: 2px solid var(--color-primary);
// border-color: var(--color-primary);
border: none;
outline: none;
@@ -227,7 +237,7 @@ export default {
font-size: 13.5px;
line-height: @height-toolbar * 0.52 + 5px;
&::placeholder {
color: @color-btn;
color: var(--color-button-font);
}
}
button {
@@ -239,7 +249,7 @@ export default {
cursor: pointer;
height: 100%;
padding: 6px 7px;
color: @color-btn;
color: var(--color-button-font);
transition: background-color .2s ease;
&:last-child {
@@ -248,10 +258,10 @@ export default {
}
&:hover {
background-color: @color-theme-hover;
background-color: var(--color-button-background-hover);
}
&:active {
background-color: @color-theme-active;
background-color: var(--color-button-background-active);
}
}
}
@@ -272,7 +282,7 @@ export default {
}
&.select {
background-color: @color-search-list-hover;
background-color: var(--color-primary-dark-100-alpha-700);
}
&:last-child {
border-bottom-left-radius: 3px;
@@ -295,41 +305,5 @@ export default {
}
}
each(@themes, {
:global(#root.@{value}) {
.search {
background-color: ~'@{color-@{value}-search-form-background}';
&.active {
box-shadow: 0 1px 5px 0 rgba(0,0,0,.2);
}
.form {
input {
&::placeholder {
color: ~'@{color-@{value}-btn}';
}
}
button {
color: ~'@{color-@{value}-btn}';
&:hover {
background-color: ~'@{color-@{value}-theme-hover}';
}
&:active {
background-color: ~'@{color-@{value}-theme-active}';
}
}
}
.list {
li {
&.select {
background-color: ~'@{color-@{value}-search-list-hover}';
}
}
}
}
}
})
</style>
@@ -0,0 +1,18 @@
<template>
<div :class="$style.container" />
</template>
<script>
</script>
<style lang="less" module>
@import '@renderer/assets/styles/layout.less';
.container {
}
</style>
+31
View File
@@ -0,0 +1,31 @@
import { apiSource, qualityList, userApi } from '@renderer/store'
import { appSetting, setApiSource } from '@renderer/store/setting'
import { setUserApi as setUserApiAction } from '@renderer/utils/ipc'
import musicSdk from '@renderer/utils/musicSdk'
import apiSourceInfo from '@renderer/utils/musicSdk/api-source-info'
export const setUserApi = (apiId: string) => {
if (/^user_api/.test(apiId)) {
qualityList.value = {}
userApi.status = false
userApi.message = 'initing'
setUserApiAction(apiId).then(() => {
apiSource.value = apiId
}).catch(err => {
console.log(err)
let api = apiSourceInfo.find(api => !api.disabled)
if (!api) return
apiSource.value = api.id
if (api.id != appSetting['common.apiSource']) setApiSource(api.id)
})
} else {
// @ts-expect-error
qualityList.value = musicSdk.supportQuality[apiId] as LX.QualityList
apiSource.value = apiId
void setUserApiAction(apiId)
}
if (apiId != appSetting['common.apiSource']) setApiSource(apiId)
}
+32
View File
@@ -0,0 +1,32 @@
// import defaultSetting from '@common/defaultSetting'
import createWorkers from '@renderer/worker'
window.lx = {
// appSetting: defaultSetting,
isEditingHotKey: false,
isPlayedStop: false,
appHotKeyConfig: {
local: {
enable: false,
keys: {},
},
global: {
enable: false,
keys: {},
},
},
songListInfo: {
fromName: '',
searchKey: '',
searchPosition: 0,
songlistKey: '',
songlistPosition: 0,
},
restorePlayInfo: null,
worker: createWorkers(),
isDev: process.env.NODE_ENV == 'production',
}
window.lxData = {}
window.ELECTRON_DISABLE_SECURITY_WARNINGS = process.env.ELECTRON_DISABLE_SECURITY_WARNINGS
+214
View File
@@ -0,0 +1,214 @@
import Lyric from '@common/utils/lyric-font-player'
import { getAnalyser, getCurrentTime as getPlayerCurrentTime } from '@renderer/plugins/player'
import { lyric, setLines, setOffset, setTempOffset, setText } from '@renderer/store/player/lyric'
import { isPlay, musicInfo } from '@renderer/store/player/state'
import { setStatusText } from '@renderer/store/player/action'
import { markRawList } from '@common/utils/vueTools'
import { appSetting } from '@renderer/store/setting'
import { onNewDesktopLyricProcess } from '@renderer/utils/ipc'
const getCurrentTime = () => {
return getPlayerCurrentTime() * 1000
}
let lrc: Lyric
let desktopLyricPort: Electron.IpcRendererEvent['ports'][0] | null = null
const analyserTools: {
dataArray: Uint8Array
bufferLength: number
analyser: AnalyserNode | null
sendDataArray: () => void
} = {
dataArray: new Uint8Array(),
bufferLength: 0,
analyser: null,
sendDataArray() {
if (this.analyser == null) {
this.analyser = getAnalyser()
// console.log(this.analyser)
if (!this.analyser) return
this.bufferLength = this.analyser.frequencyBinCount
}
const dataArray = new Uint8Array(this.bufferLength)
this.analyser.getByteFrequencyData(dataArray)
sendDesktopLyricInfo({
action: 'send_analyser_data_array',
data: dataArray,
}, [dataArray.buffer])
},
}
export const sendDesktopLyricInfo = (info: LX.DesktopLyric.LyricActions, transferList?: Transferable[]) => {
if (desktopLyricPort == null) return
if (transferList) desktopLyricPort.postMessage(info, transferList)
else desktopLyricPort.postMessage(info)
}
const handleDesktopLyricMessage = (action: LX.DesktopLyric.WinMainActions) => {
switch (action) {
case 'get_info':
sendDesktopLyricInfo({
action: 'set_info',
data: {
id: musicInfo.id,
singer: musicInfo.singer,
name: musicInfo.name,
album: musicInfo.album,
lrc: musicInfo.lrc,
tlrc: musicInfo.tlrc,
rlrc: musicInfo.rlrc,
lxlrc: musicInfo.lxlrc,
// pic: musicInfo.pic,
isPlay: isPlay.value,
line: lyric.line,
played_time: getCurrentTime(),
},
})
break
case 'get_status':
sendDesktopLyricInfo({
action: 'set_status',
data: {
isPlay: isPlay.value,
line: lyric.line,
played_time: getCurrentTime(),
},
})
break
case 'get_analyser_data_array':
analyserTools.sendDataArray()
break
default:
break
}
}
export const init = () => {
lrc = new Lyric({
shadowContent: false,
onPlay(line, text) {
setText(text, line)
setStatusText(text)
// console.log(line, text)
},
onSetLyric(lines, offset) { // listening lyrics seting event
// console.log(lines) // lines is array of all lyric text
setLines(markRawList([...lines]))
setText(lines[0] ?? '', 0)
setOffset(offset) // 歌词延迟
setTempOffset(0) // 重置临时延迟
},
onUpdateLyric(lines) {
setLines(markRawList([...lines]))
setText(lines[0] ?? '', 0)
},
// offset: 80,
})
onNewDesktopLyricProcess(({ event }) => {
console.log('onNewDesktopLyricProcess')
const [port] = event.ports
desktopLyricPort = port
port.onmessage = ({ data }) => {
handleDesktopLyricMessage(data.action)
// The event data can be any serializable object (and the event could even
// carry other MessagePorts with it!)
// const result = doWork(event.data)
// port.postMessage(result)
}
port.onmessageerror = (event) => {
console.log('onmessageerror', event)
}
})
}
export const setLyricOffset = (offset: number) => {
const tempOffset = offset - lyric.offset
setTempOffset(tempOffset)
lrc.setOffset(tempOffset)
sendDesktopLyricInfo({
action: 'set_offset',
data: tempOffset,
})
if (isPlay.value) {
setTimeout(() => {
const time = getCurrentTime()
sendDesktopLyricInfo({
action: 'set_play',
data: time,
})
lrc.play(time)
})
}
}
export const setLyric = () => {
if (!musicInfo.id) return
if (musicInfo.lrc) {
const extendedLyrics = []
if (appSetting['player.isShowLyricTranslation'] && musicInfo.tlrc) extendedLyrics.push(musicInfo.tlrc)
if (appSetting['player.isShowLyricRoma'] && musicInfo.rlrc) extendedLyrics.push(musicInfo.rlrc)
lrc.setLyric(
appSetting['player.isPlayLxlrc'] && musicInfo.lxlrc ? musicInfo.lxlrc : musicInfo.lrc,
extendedLyrics,
)
sendDesktopLyricInfo({
action: 'set_lyric',
data: {
lrc: musicInfo.lrc,
tlrc: musicInfo.tlrc,
rlrc: musicInfo.rlrc,
lxlrc: musicInfo.lxlrc,
},
})
}
if (isPlay.value) {
setTimeout(() => {
const time = getCurrentTime()
sendDesktopLyricInfo({ action: 'set_play', data: time })
lrc.play(time)
})
}
}
export const play = () => {
// if (!musicInfo.lrc) return
const currentTime = getCurrentTime()
lrc.play(currentTime)
sendDesktopLyricInfo({ action: 'set_play', data: currentTime })
}
export const pause = () => {
lrc.pause()
sendDesktopLyricInfo({ action: 'set_pause' })
}
export const stop = () => {
lrc.setLyric('')
sendDesktopLyricInfo({ action: 'set_stop' })
// setLines([])
setText('', 0)
}
export const sendInfo = () => {
sendDesktopLyricInfo({
action: 'set_info',
data: {
id: musicInfo.id,
singer: musicInfo.singer,
name: musicInfo.name,
album: musicInfo.album,
lrc: musicInfo.lrc,
tlrc: musicInfo.tlrc,
rlrc: musicInfo.rlrc,
lxlrc: musicInfo.lxlrc,
// pic: musicInfo.pic,
isPlay: isPlay.value,
line: lyric.line,
played_time: getCurrentTime(),
},
})
}
+72
View File
@@ -0,0 +1,72 @@
import { appSetting } from '@renderer/store/setting'
import { getDownloadFilePath } from '@renderer/utils/music'
import {
getMusicUrl as getOnlineMusicUrl,
getPicUrl as getOnlinePicUrl,
getLyricInfo as getOnlineLyricInfo,
} from './online'
import { buildLyricInfo, getCachedLyricInfo } from './utils'
export const getMusicUrl = async({ musicInfo, isRefresh, onToggleSource = () => {} }: {
musicInfo: LX.Download.ListItem
isRefresh: boolean
onToggleSource?: (musicInfo?: LX.Music.MusicInfoOnline) => void
}): Promise<string> => {
if (!isRefresh) {
const path = await getDownloadFilePath(musicInfo, appSetting['download.savePath'])
if (path) return path
}
return await getOnlineMusicUrl({ musicInfo: musicInfo.metadata.musicInfo, isRefresh, onToggleSource })
}
export const getPicUrl = async({ musicInfo, isRefresh, listId, onToggleSource = () => {} }: {
musicInfo: LX.Download.ListItem
isRefresh: boolean
listId?: string | null
onToggleSource?: (musicInfo?: LX.Music.MusicInfoOnline) => void
}): Promise<string> => {
if (!isRefresh) {
const onlineMusicInfo = musicInfo.metadata.musicInfo
if (onlineMusicInfo.meta.picUrl && !isRefresh) return onlineMusicInfo.meta.picUrl
const path = await getDownloadFilePath(musicInfo, appSetting['download.savePath'])
if (path) {
const pic = await window.lx.worker.main.getMusicFilePic(path)
if (pic) return pic
}
}
return await getOnlinePicUrl({ musicInfo: musicInfo.metadata.musicInfo, isRefresh, onToggleSource }).then((url) => {
// TODO: when listId required save url (update downloadInfo)
return url
})
}
export const getLyricInfo = async({ musicInfo, isRefresh, onToggleSource = () => {} }: {
musicInfo: LX.Download.ListItem
isRefresh: boolean
onToggleSource?: (musicInfo?: LX.Music.MusicInfoOnline) => void
}): Promise<LX.Player.LyricInfo> => {
if (!isRefresh) {
const lyricInfo = await getCachedLyricInfo(musicInfo.metadata.musicInfo)
if (lyricInfo) return await buildLyricInfo(lyricInfo)
}
return getOnlineLyricInfo({
musicInfo: musicInfo.metadata.musicInfo,
isRefresh,
onToggleSource,
}).catch(async() => {
// 尝试读取文件内歌词
const path = await getDownloadFilePath(musicInfo, appSetting['download.savePath'])
if (path) {
const rawlrcInfo = await window.lx.worker.main.getMusicFileLyric(path)
if (rawlrcInfo) return buildLyricInfo(rawlrcInfo)
}
throw new Error('failed')
})
}
+78
View File
@@ -0,0 +1,78 @@
// if (targetSong.key) { // 如果是已下载的歌曲
// const filePath = path.join(appSetting['download.savePath'], targetSong.metadata.fileName)
// // console.log(filePath)
import {
getMusicUrl as getOnlineMusicUrl,
getPicUrl as getOnlinePicUrl,
getLyricInfo as getOnlineLyricInfo,
} from './online'
import {
getMusicUrl as getDownloadMusicUrl,
getPicUrl as getDownloadPicUrl,
getLyricInfo as getDownloadLyricInfo,
} from './download'
import {
getMusicUrl as getLocalMusicUrl,
getPicUrl as getLocalPicUrl,
getLyricInfo as getLocalLyricInfo,
} from './local'
export const getMusicUrl = async({
musicInfo,
quality,
isRefresh = false,
onToggleSource,
}: {
musicInfo: LX.Music.MusicInfo | LX.Download.ListItem
isRefresh?: boolean
quality?: LX.Quality
onToggleSource?: (musicInfo?: LX.Music.MusicInfoOnline) => void
}): Promise<string> => {
if ('progress' in musicInfo) {
return await getDownloadMusicUrl({ musicInfo, isRefresh, onToggleSource })
} else if (musicInfo.source == 'local') {
return await getLocalMusicUrl({ musicInfo, isRefresh, onToggleSource })
} else {
return await getOnlineMusicUrl({ musicInfo, isRefresh, quality, onToggleSource })
}
}
export const getPicPath = async({
musicInfo,
isRefresh = false,
listId,
onToggleSource,
}: {
musicInfo: LX.Music.MusicInfo | LX.Download.ListItem
listId?: string | null
isRefresh?: boolean
onToggleSource?: (musicInfo?: LX.Music.MusicInfoOnline) => void
}): Promise<string> => {
if ('progress' in musicInfo) {
return await getDownloadPicUrl({ musicInfo, isRefresh, listId, onToggleSource })
} else if (musicInfo.source == 'local') {
return await getLocalPicUrl({ musicInfo, isRefresh, listId, onToggleSource })
} else {
return await getOnlinePicUrl({ musicInfo, isRefresh, listId, onToggleSource })
}
}
export const getLyricInfo = async({
musicInfo,
isRefresh = false,
onToggleSource,
}: {
musicInfo: LX.Music.MusicInfo | LX.Download.ListItem
isRefresh?: boolean
onToggleSource?: (musicInfo?: LX.Music.MusicInfoOnline) => void
}): Promise<LX.Player.LyricInfo> => {
if ('progress' in musicInfo) {
return await getDownloadLyricInfo({ musicInfo, isRefresh, onToggleSource })
} else if (musicInfo.source == 'local') {
return await getLocalLyricInfo({ musicInfo, isRefresh, onToggleSource })
} else {
return await getOnlineLyricInfo({ musicInfo, isRefresh, onToggleSource })
}
}
+91
View File
@@ -0,0 +1,91 @@
import { updateListMusics } from '@renderer/store/list/action'
import { saveLyric, saveMusicUrl } from '@renderer/utils/ipc'
import { getLocalFilePath } from '@renderer/utils/music'
import {
buildLyricInfo,
getCachedLyricInfo,
getOnlineOtherSourceLyricInfo,
getOnlineOtherSourceMusicUrl,
getOnlineOtherSourcePicUrl,
getOtherSource,
} from './utils'
export const getMusicUrl = async({ musicInfo, isRefresh, onToggleSource = () => {} }: {
musicInfo: LX.Music.MusicInfoLocal
isRefresh: boolean
onToggleSource?: (musicInfo?: LX.Music.MusicInfoOnline) => void
}): Promise<string> => {
if (!isRefresh) {
const path = await getLocalFilePath(musicInfo)
if (path) return path
}
onToggleSource()
const otherSource = await getOtherSource(musicInfo)
if (!otherSource.length) throw new Error('source not found')
return await getOnlineOtherSourceMusicUrl({ musicInfos: [...otherSource], onToggleSource, isRefresh }).then(({ url, quality: targetQuality, musicInfo: targetMusicInfo, isFromCache }) => {
// saveLyric(musicInfo, data.lyricInfo)
if (!isFromCache) void saveMusicUrl(targetMusicInfo, targetQuality, url)
// TODO: save url ?
return url
})
}
export const getPicUrl = async({ musicInfo, listId, isRefresh, onToggleSource = () => {} }: {
musicInfo: LX.Music.MusicInfoLocal
listId?: string | null
isRefresh: boolean
onToggleSource?: (musicInfo?: LX.Music.MusicInfoOnline) => void
}): Promise<string> => {
if (!isRefresh) {
if (musicInfo.meta.picUrl) return musicInfo.meta.picUrl
const pic = await window.lx.worker.main.getMusicFilePic(musicInfo.meta.filePath)
if (pic) return pic
}
onToggleSource()
const otherSource = await getOtherSource(musicInfo)
if (!otherSource.length) throw new Error('source not found')
return await getOnlineOtherSourcePicUrl({ musicInfos: [...otherSource], onToggleSource, isRefresh }).then(({ url, musicInfo: targetMusicInfo, isFromCache }) => {
if (listId) {
musicInfo.meta.picUrl = url
void updateListMusics([{ id: listId, musicInfo }])
}
return url
})
}
export const getLyricInfo = async({ musicInfo, isRefresh, onToggleSource = () => {} }: {
musicInfo: LX.Music.MusicInfoLocal
isRefresh: boolean
onToggleSource?: (musicInfo?: LX.Music.MusicInfoOnline) => void
}): Promise<LX.Player.LyricInfo> => {
if (!isRefresh) {
const lyricInfo = await getCachedLyricInfo(musicInfo)
if (lyricInfo) {
// 存在已编辑、原始歌词
if (lyricInfo.rawlrcInfo.lyric) return await buildLyricInfo(lyricInfo)
}
// 尝试读取文件内歌词
const rawlrcInfo = await window.lx.worker.main.getMusicFileLyric(musicInfo.meta.filePath)
if (rawlrcInfo) return buildLyricInfo(lyricInfo ? { ...lyricInfo, rawlrcInfo } : rawlrcInfo)
}
onToggleSource()
const otherSource = await getOtherSource(musicInfo)
if (!otherSource.length) throw new Error('source not found')
// eslint-disable-next-line @typescript-eslint/promise-function-async
return await getOnlineOtherSourceLyricInfo({ musicInfos: [...otherSource], onToggleSource, isRefresh }).then(({ lyricInfo, musicInfo: targetMusicInfo, isFromCache }) => {
void saveLyric(musicInfo, lyricInfo)
if (isFromCache) return buildLyricInfo(lyricInfo)
void saveLyric(targetMusicInfo, lyricInfo)
return buildLyricInfo(lyricInfo)
})
}
+103
View File
@@ -0,0 +1,103 @@
import { updateListMusics } from '@renderer/store/list/action'
import { appSetting } from '@renderer/store/setting'
import {
saveLyric,
saveMusicUrl,
getMusicUrl as getStoreMusicUrl,
} from '@renderer/utils/ipc'
import {
buildLyricInfo,
getPlayQuality,
handleGetOnlineLyricInfo,
handleGetOnlineMusicUrl,
handleGetOnlinePicUrl,
getCachedLyricInfo,
} from './utils'
/* export const setMusicUrl = ({ musicInfo, type, url }: {
musicInfo: LX.Music.MusicInfo
type: LX.Quality
url: string
}) => {
saveMusicUrl(musicInfo, type, url)
}
export const setPic = (datas: {
listId: string
musicInfo: LX.Music.MusicInfo
url: string
}) => {
datas.musicInfo.img = datas.url
updateMusicInfo({
listId: datas.listId,
id: datas.musicInfo.songmid,
data: { img: datas.url },
musicInfo: datas.musicInfo,
})
}
*/
export const getMusicUrl = async({ musicInfo, quality, isRefresh, allowToggleSource = true, onToggleSource = () => {} }: {
musicInfo: LX.Music.MusicInfoOnline
quality?: LX.Quality
isRefresh: boolean
allowToggleSource?: boolean
onToggleSource?: (musicInfo?: LX.Music.MusicInfoOnline) => void
}): Promise<string> => {
// if (!musicInfo._types[type]) {
// // 兼容旧版酷我源搜索列表过滤128k音质的bug
// if (!(musicInfo.source == 'kw' && type == '128k')) throw new Error('该歌曲没有可播放的音频')
// // return Promise.reject(new Error('该歌曲没有可播放的音频'))
// }
const targetQuality = quality ?? getPlayQuality(appSetting['player.highQuality'], musicInfo)
const cachedUrl = await getStoreMusicUrl(musicInfo, targetQuality)
if (cachedUrl && !isRefresh) return cachedUrl
return await handleGetOnlineMusicUrl({ musicInfo, quality, onToggleSource, isRefresh, allowToggleSource }).then(({ url, quality: targetQuality, musicInfo: targetMusicInfo, isFromCache }) => {
if (targetMusicInfo.id != musicInfo.id && !isFromCache) void saveMusicUrl(targetMusicInfo, targetQuality, url)
void saveMusicUrl(musicInfo, targetQuality, url)
return url
})
}
export const getPicUrl = async({ musicInfo, listId, isRefresh, allowToggleSource = true, onToggleSource = () => {} }: {
musicInfo: LX.Music.MusicInfoOnline
listId?: string | null
isRefresh: boolean
allowToggleSource?: boolean
onToggleSource?: (musicInfo?: LX.Music.MusicInfoOnline) => void
}): Promise<string> => {
if (musicInfo.meta.picUrl && !isRefresh) return musicInfo.meta.picUrl
return await handleGetOnlinePicUrl({ musicInfo, onToggleSource, isRefresh, allowToggleSource }).then(({ url, musicInfo: targetMusicInfo, isFromCache }) => {
// picRequest = null
if (listId) {
musicInfo.meta.picUrl = url
void updateListMusics([{ id: listId, musicInfo }])
}
// savePic({ musicInfo, url, listId })
return url
})
}
export const getLyricInfo = async({ musicInfo, isRefresh, allowToggleSource = true, onToggleSource = () => {} }: {
musicInfo: LX.Music.MusicInfoOnline
isRefresh: boolean
allowToggleSource?: boolean
onToggleSource?: (musicInfo?: LX.Music.MusicInfoOnline) => void
}): Promise<LX.Player.LyricInfo> => {
if (!isRefresh) {
const lyricInfo = await getCachedLyricInfo(musicInfo)
if (lyricInfo) return await buildLyricInfo(lyricInfo)
}
// lrcRequest = music[musicInfo.source].getLyric(musicInfo)
return await handleGetOnlineLyricInfo({ musicInfo, onToggleSource, isRefresh, allowToggleSource }).then(async({ lyricInfo, musicInfo: targetMusicInfo, isFromCache }) => {
// lrcRequest = null
if (isFromCache) return await buildLyricInfo(lyricInfo)
if (targetMusicInfo.id == musicInfo.id) void saveLyric(musicInfo, lyricInfo)
else void saveLyric(targetMusicInfo, lyricInfo)
return await buildLyricInfo(lyricInfo)
})
}
+417
View File
@@ -0,0 +1,417 @@
import { qualityList } from '@renderer/store'
import { assertApiSupport } from '@renderer/store/utils'
import musicSdk from '@renderer/utils/musicSdk'
import {
getOtherSource as getOtherSourceFromStore,
saveOtherSource as saveOtherSourceFromStore,
getMusicUrl as getStoreMusicUrl,
getPlayerLyric as getStoreLyric,
} from '@renderer/utils/ipc'
import { appSetting } from '@renderer/store/setting'
import { langS2T, toNewMusicInfo, toOldMusicInfo } from '@renderer/utils'
const getOtherSourcePromises = new Map()
export const existTimeExp = /\[\d{1,2}:.*\d{1,4}\]/
export const getOtherSource = async(musicInfo: LX.Music.MusicInfo | LX.Download.ListItem, isRefresh = false): Promise<LX.Music.MusicInfoOnline[]> => {
if (!isRefresh) {
const cachedInfo = await getOtherSourceFromStore(musicInfo.id)
if (cachedInfo.length) return cachedInfo
}
let key: string
let searchMusicInfo: {
name: string
singer: string
source: string
albumName: string
interval: string
}
if ('progress' in musicInfo) {
key = `local_${musicInfo.id}`
searchMusicInfo = {
name: musicInfo.metadata.musicInfo.name,
singer: musicInfo.metadata.musicInfo.singer,
source: musicInfo.metadata.musicInfo.source,
albumName: musicInfo.metadata.musicInfo.meta.albumName,
interval: musicInfo.metadata.musicInfo.interval ?? '',
}
} else {
key = `${musicInfo.source}_${musicInfo.id}`
searchMusicInfo = {
name: musicInfo.name,
singer: musicInfo.singer,
source: musicInfo.source,
albumName: musicInfo.meta.albumName,
interval: musicInfo.interval ?? '',
}
}
if (getOtherSourcePromises.has(key)) return getOtherSourcePromises.get(key)
const promise = musicSdk.findMusic(searchMusicInfo).then((otherSource) => {
const sources: LX.Music.MusicInfoOnline[] = otherSource.map(toNewMusicInfo)
if (sources.length) void saveOtherSourceFromStore(musicInfo.id, sources)
return sources
}).finally(() => {
if (getOtherSourcePromises.has(key)) getOtherSourcePromises.delete(key)
})
getOtherSourcePromises.set(key, promise)
return promise
}
export const buildLyricInfo = async(lyricInfo: MakeOptional<LX.Player.LyricInfo, 'rawlrcInfo'>): Promise<LX.Player.LyricInfo> => {
if (!appSetting['player.isS2t']) {
// @ts-expect-error
if (lyricInfo.rawlrcInfo) return lyricInfo
return { ...lyricInfo, rawlrcInfo: { ...lyricInfo } }
}
if (appSetting['player.isS2t']) {
const tasks = [
lyricInfo.lyric ? window.lx.worker.main.langS2t(lyricInfo.lyric) : Promise.resolve(''),
lyricInfo.tlyric ? langS2T(lyricInfo.tlyric) : Promise.resolve(''),
lyricInfo.rlyric ? langS2T(lyricInfo.rlyric) : Promise.resolve(''),
lyricInfo.lxlyric ? langS2T(lyricInfo.lxlyric) : Promise.resolve(''),
]
if (lyricInfo.rawlrcInfo) {
tasks.push(lyricInfo.lyric ? window.lx.worker.main.langS2t(lyricInfo.lyric) : Promise.resolve(''))
tasks.push(lyricInfo.tlyric ? window.lx.worker.main.langS2t(lyricInfo.tlyric) : Promise.resolve(''))
tasks.push(lyricInfo.rlyric ? window.lx.worker.main.langS2t(lyricInfo.rlyric) : Promise.resolve(''))
tasks.push(lyricInfo.lxlyric ? window.lx.worker.main.langS2t(lyricInfo.lxlyric) : Promise.resolve(''))
}
return await Promise.all(tasks).then(([lyric, tlyric, rlyric, lxlyric, lyric_raw, tlyric_raw, rlyric_raw, lxlyric_raw]) => {
const rawlrcInfo = lyric_raw ? {
lyric: lyric_raw,
tlyric: tlyric_raw,
rlyric: rlyric_raw,
lxlyric: lxlyric_raw,
} : {
lyric,
tlyric,
rlyric,
lxlyric,
}
return {
lyric,
tlyric,
rlyric,
lxlyric,
rawlrcInfo,
}
})
}
// @ts-expect-error
return lyricInfo.rawlrcInfo ? lyricInfo : { ...lyricInfo, rawlrcInfo: { ...lyricInfo } }
}
export const getCachedLyricInfo = async(musicInfo: LX.Music.MusicInfo): Promise<LX.Player.LyricInfo | null> => {
let lrcInfo = await getStoreLyric(musicInfo)
// lrcInfo = {}
if (existTimeExp.test(lrcInfo.lyric) && lrcInfo.tlyric != null) {
// if (musicInfo.lrc.startsWith('\ufeff[id:$00000000]')) {
// let str = musicInfo.lrc.replace('\ufeff[id:$00000000]\n', '')
// commit('setLrc', { musicInfo, lyric: str, tlyric: musicInfo.tlrc, lxlyric: musicInfo.tlrc })
// } else if (musicInfo.lrc.startsWith('[id:$00000000]')) {
// let str = musicInfo.lrc.replace('[id:$00000000]\n', '')
// commit('setLrc', { musicInfo, lyric: str, tlyric: musicInfo.tlrc, lxlyric: musicInfo.tlrc })
// }
if (lrcInfo.lxlyric == null) {
switch (musicInfo.source) {
case 'kg':
case 'kw':
case 'mg':
break
default:
return lrcInfo
}
} else if (lrcInfo.rlyric == null) {
if (!['wy', 'kg'].includes(musicInfo.source)) return lrcInfo
} else return lrcInfo
}
return null
}
export const getPlayQuality = (highQuality: boolean, musicInfo: LX.Music.MusicInfoOnline): LX.Quality => {
let type: LX.Quality = '128k'
let list = qualityList.value[musicInfo.source]
if (highQuality && musicInfo.meta._qualitys['320k'] && list && list.includes('320k')) type = '320k'
return type
}
export const getOnlineOtherSourceMusicUrl = async({ musicInfos, quality, onToggleSource, isRefresh, retryedSource = [] }: {
musicInfos: LX.Music.MusicInfoOnline[]
quality?: LX.Quality
onToggleSource: (musicInfo?: LX.Music.MusicInfoOnline) => void
isRefresh: boolean
retryedSource?: LX.OnlineSource[]
}): Promise<{
url: string
musicInfo: LX.Music.MusicInfoOnline
quality: LX.Quality
isFromCache: boolean
}> => {
let musicInfo: LX.Music.MusicInfoOnline | null = null
let itemQuality: LX.Quality | null = null
// eslint-disable-next-line no-cond-assign
while (musicInfo = (musicInfos.shift() as LX.Music.MusicInfoOnline)) {
if (retryedSource.includes(musicInfo.source)) continue
retryedSource.push(musicInfo.source)
if (!assertApiSupport(musicInfo.source)) continue
itemQuality = quality ?? getPlayQuality(appSetting['player.highQuality'], musicInfo)
if (!musicInfo.meta._qualitys[itemQuality]) continue
console.log('try toggle to: ', musicInfo.source, musicInfo.name, musicInfo.singer, musicInfo.interval)
onToggleSource(musicInfo)
break
}
if (!musicInfo || !itemQuality) throw new Error(window.i18n.t('toggle_source_failed'))
const cachedUrl = await getStoreMusicUrl(musicInfo, itemQuality)
if (cachedUrl && !isRefresh) return { url: cachedUrl, musicInfo, quality: itemQuality, isFromCache: true }
let reqPromise
try {
reqPromise = musicSdk[musicInfo.source].getMusicUrl(toOldMusicInfo(musicInfo), itemQuality).promise
} catch (err: any) {
reqPromise = Promise.reject(err)
}
retryedSource.includes(musicInfo.source)
// eslint-disable-next-line @typescript-eslint/promise-function-async
return reqPromise.then(({ url, type }: { url: string, type: LX.Quality }) => {
return { musicInfo, url, quality: type, isFromCache: false }
// eslint-disable-next-line @typescript-eslint/promise-function-async
}).catch((err: any) => {
console.log(err)
return getOnlineOtherSourceMusicUrl({ musicInfos, quality, onToggleSource, isRefresh, retryedSource })
})
}
/**
* 获取在线音乐URL
*/
export const handleGetOnlineMusicUrl = async({ musicInfo, quality, onToggleSource, isRefresh, allowToggleSource }: {
musicInfo: LX.Music.MusicInfoOnline
quality?: LX.Quality
isRefresh: boolean
allowToggleSource: boolean
onToggleSource: (musicInfo?: LX.Music.MusicInfoOnline) => void
}): Promise<{
url: string
musicInfo: LX.Music.MusicInfoOnline
quality: LX.Quality
isFromCache: boolean
}> => {
// console.log(musicInfo.source)
const targetQuality = quality ?? getPlayQuality(appSetting['player.highQuality'], musicInfo)
let reqPromise
try {
reqPromise = musicSdk[musicInfo.source].getMusicUrl(toOldMusicInfo(musicInfo), targetQuality).promise
} catch (err: any) {
reqPromise = Promise.reject(err)
}
return reqPromise.then(({ url, type }: { url: string, type: LX.Quality }) => {
return { musicInfo, url, quality: type, isFromCache: false }
}).catch(async(err: any) => {
console.log(err)
if (!allowToggleSource) throw err
onToggleSource()
// eslint-disable-next-line @typescript-eslint/promise-function-async
return await getOtherSource(musicInfo).then(otherSource => {
console.log('find otherSource', otherSource)
if (otherSource.length) {
return getOnlineOtherSourceMusicUrl({
musicInfos: [...otherSource],
onToggleSource,
quality,
isRefresh,
retryedSource: [musicInfo.source],
})
}
throw err
})
})
}
export const getOnlineOtherSourcePicUrl = async({ musicInfos, onToggleSource, isRefresh, retryedSource = [] }: {
musicInfos: LX.Music.MusicInfoOnline[]
onToggleSource: (musicInfo?: LX.Music.MusicInfoOnline) => void
isRefresh: boolean
retryedSource?: LX.OnlineSource[]
}): Promise<{
url: string
musicInfo: LX.Music.MusicInfoOnline
isFromCache: boolean
}> => {
let musicInfo: LX.Music.MusicInfoOnline | null = null
// eslint-disable-next-line no-cond-assign
while (musicInfo = (musicInfos.shift() as LX.Music.MusicInfoOnline)) {
if (retryedSource.includes(musicInfo.source)) continue
retryedSource.push(musicInfo.source)
// if (!assertApiSupport(musicInfo.source)) continue
console.log('try toggle to: ', musicInfo.source, musicInfo.name, musicInfo.singer, musicInfo.interval)
onToggleSource(musicInfo)
break
}
if (!musicInfo) throw new Error(window.i18n.t('toggle_source_failed'))
if (musicInfo.meta.picUrl && !isRefresh) return { musicInfo, url: musicInfo.meta.picUrl, isFromCache: true }
let reqPromise
try {
reqPromise = musicSdk[musicInfo.source].getPic(toOldMusicInfo(musicInfo)).promise
} catch (err: any) {
reqPromise = Promise.reject(err)
}
retryedSource.includes(musicInfo.source)
return reqPromise.then((url: string) => {
return { musicInfo, url, isFromCache: false }
// eslint-disable-next-line @typescript-eslint/promise-function-async
}).catch((err: any) => {
console.log(err)
return getOnlineOtherSourcePicUrl({ musicInfos, onToggleSource, isRefresh, retryedSource })
})
}
/**
* 获取在线歌曲封面
*/
export const handleGetOnlinePicUrl = async({ musicInfo, isRefresh, onToggleSource, allowToggleSource }: {
musicInfo: LX.Music.MusicInfoOnline
onToggleSource: (musicInfo?: LX.Music.MusicInfoOnline) => void
isRefresh: boolean
allowToggleSource: boolean
}): Promise<{
url: string
musicInfo: LX.Music.MusicInfoOnline
isFromCache: boolean
}> => {
// console.log(musicInfo.source)
let reqPromise
try {
reqPromise = musicSdk[musicInfo.source].getPic(toOldMusicInfo(musicInfo)).promise
} catch (err) {
reqPromise = Promise.reject(err)
}
return reqPromise.then((url: string) => {
return { musicInfo, url, isFromCache: false }
}).catch(async(err: any) => {
console.log(err)
if (!allowToggleSource) throw err
onToggleSource()
// eslint-disable-next-line @typescript-eslint/promise-function-async
return await getOtherSource(musicInfo).then(otherSource => {
console.log('find otherSource', otherSource)
if (otherSource.length) {
return getOnlineOtherSourcePicUrl({
musicInfos: [...otherSource],
onToggleSource,
isRefresh,
retryedSource: [musicInfo.source],
})
}
throw err
})
})
}
export const getOnlineOtherSourceLyricInfo = async({ musicInfos, onToggleSource, isRefresh, retryedSource = [] }: {
musicInfos: LX.Music.MusicInfoOnline[]
onToggleSource: (musicInfo?: LX.Music.MusicInfoOnline) => void
isRefresh: boolean
retryedSource?: LX.OnlineSource[]
}): Promise<{
lyricInfo: LX.Music.LyricInfo | LX.Player.LyricInfo
musicInfo: LX.Music.MusicInfoOnline
isFromCache: boolean
}> => {
let musicInfo: LX.Music.MusicInfoOnline | null = null
// eslint-disable-next-line no-cond-assign
while (musicInfo = (musicInfos.shift() as LX.Music.MusicInfoOnline)) {
if (retryedSource.includes(musicInfo.source)) continue
retryedSource.push(musicInfo.source)
// if (!assertApiSupport(musicInfo.source)) continue
console.log('try toggle to: ', musicInfo.source, musicInfo.name, musicInfo.singer, musicInfo.interval)
onToggleSource(musicInfo)
break
}
if (!musicInfo) throw new Error(window.i18n.t('toggle_source_failed'))
if (!isRefresh) {
const lyricInfo = await getCachedLyricInfo(musicInfo)
if (lyricInfo) return { musicInfo, lyricInfo, isFromCache: true }
}
let reqPromise
try {
// TODO: remove any type
reqPromise = (musicSdk[musicInfo.source].getLyric(toOldMusicInfo(musicInfo)) as any).promise
} catch (err: any) {
reqPromise = Promise.reject(err)
}
retryedSource.includes(musicInfo.source)
return reqPromise.then((lyricInfo: LX.Music.LyricInfo) => {
return existTimeExp.test(lyricInfo.lyric) ? {
lyricInfo,
musicInfo,
isFromCache: false,
} : Promise.reject(new Error('failed'))
// eslint-disable-next-line @typescript-eslint/promise-function-async
}).catch((err: any) => {
console.log(err)
return getOnlineOtherSourceLyricInfo({ musicInfos, onToggleSource, isRefresh, retryedSource })
})
}
/**
* 获取在线歌词信息
*/
export const handleGetOnlineLyricInfo = async({ musicInfo, onToggleSource, isRefresh, allowToggleSource }: {
musicInfo: LX.Music.MusicInfoOnline
onToggleSource: (musicInfo?: LX.Music.MusicInfoOnline) => void
isRefresh: boolean
allowToggleSource: boolean
}): Promise<{
musicInfo: LX.Music.MusicInfoOnline
lyricInfo: LX.Music.LyricInfo | LX.Player.LyricInfo
isFromCache: boolean
}> => {
// console.log(musicInfo.source)
let reqPromise
try {
// TODO: remove any type
reqPromise = (musicSdk[musicInfo.source].getLyric(toOldMusicInfo(musicInfo)) as any).promise
} catch (err) {
reqPromise = Promise.reject(err)
}
return reqPromise.then((lyricInfo: LX.Music.LyricInfo) => {
return existTimeExp.test(lyricInfo.lyric) ? {
musicInfo,
lyricInfo,
isFromCache: false,
} : Promise.reject(new Error('failed'))
}).catch(async(err: any) => {
console.log(err)
if (!allowToggleSource) throw err
onToggleSource()
// eslint-disable-next-line @typescript-eslint/promise-function-async
return await getOtherSource(musicInfo).then(otherSource => {
console.log('find otherSource', otherSource)
if (otherSource.length) {
return getOnlineOtherSourceLyricInfo({
musicInfos: [...otherSource],
onToggleSource,
isRefresh,
retryedSource: [musicInfo.source],
})
}
throw err
})
})
}
+434
View File
@@ -0,0 +1,434 @@
import { isEmpty, setPause, setPlay, setResource, setStop } from '@renderer/plugins/player'
import { isPlay, playedList, playInfo, playMusicInfo, tempPlayList } from '@renderer/store/player/state'
import {
getList,
clearPlayedList,
clearTempPlayeList,
setPlayMusicInfo,
addPlayedList,
setMusicInfo,
setAllStatus,
removeTempPlayList,
setPlayListId,
removePlayedList,
} from '@renderer/store/player/action'
import { appSetting } from '@renderer/store/setting'
import { getMusicUrl, getPicPath, getLyricInfo } from '../music/index'
import { filterList } from './utils'
import { requestMsg } from '@renderer/utils/message'
import { getRandom } from '@renderer/utils/index'
// import { checkMusicFileAvailable } from '@renderer/utils/music'
let isGettingUrl = false
const createDelayNextTimeout = (delay: number) => {
let timeout: NodeJS.Timeout | null
const clearDelayNextTimeout = () => {
// console.log(this.timeout)
if (timeout) {
clearTimeout(timeout)
timeout = null
}
}
const addDelayNextTimeout = () => {
clearDelayNextTimeout()
timeout = setTimeout(() => {
timeout = null
if (window.lx.isPlayedStop) return
console.warn('delay next timeout timeout', delay)
void playNext(true)
}, delay)
}
return {
clearDelayNextTimeout,
addDelayNextTimeout,
}
}
const { addDelayNextTimeout, clearDelayNextTimeout } = createDelayNextTimeout(5000)
const { addDelayNextTimeout: addLoadTimeout, clearDelayNextTimeout: clearLoadTimeout } = createDelayNextTimeout(100000)
/**
* 检查音乐信息是否已更改
*/
const diffCurrentMusicInfo = (curMusicInfo: LX.Music.MusicInfo | LX.Download.ListItem): boolean => {
return curMusicInfo !== playMusicInfo.musicInfo || isPlay.value
}
const getMusicPlayUrl = async(musicInfo: LX.Music.MusicInfo | LX.Download.ListItem, isRefresh = false, isRetryed = false): Promise<string | null> => {
// this.musicInfo.url = await getMusicPlayUrl(targetSong, type)
setAllStatus(window.i18n.t('player__geting_url'))
// const type = getPlayType(appSetting['player.highQuality'], musicInfo)
return getMusicUrl({
musicInfo,
isRefresh,
onToggleSource(mInfo) {
if (diffCurrentMusicInfo(musicInfo)) return
setAllStatus(window.i18n.t('toggle_source_try'))
},
}).then(url => {
if (window.lx.isPlayedStop || diffCurrentMusicInfo(musicInfo)) return null
return url
}).catch(err => {
// console.log('err', err.message)
if (window.lx.isPlayedStop ||
diffCurrentMusicInfo(musicInfo) ||
err.message == requestMsg.cancelRequest) return null
if (!isRetryed) return getMusicPlayUrl(musicInfo, isRefresh, true)
throw err
})
}
export const setMusicUrl = (musicInfo: LX.Music.MusicInfo | LX.Download.ListItem, isRefresh?: boolean) => {
if (appSetting['player.autoSkipOnError']) addLoadTimeout()
isGettingUrl = true
void getMusicPlayUrl(musicInfo, isRefresh).then((url) => {
if (!url) return
setResource(url)
}).catch((err: any) => {
console.log(err)
setAllStatus(err.message)
window.app_event.error()
if (appSetting['player.autoSkipOnError']) addDelayNextTimeout()
}).finally(() => {
clearLoadTimeout()
if (musicInfo === playMusicInfo.musicInfo) isGettingUrl = false
})
}
// 恢复上次播放的状态
const handleRestorePlay = async(restorePlayInfo: LX.Player.SavedPlayInfo) => {
const musicInfo = playMusicInfo.musicInfo
if (!musicInfo) return
setImmediate(() => {
window.app_event.setProgress(appSetting['player.isSavePlayTime'] ? restorePlayInfo.time : 0, restorePlayInfo.maxTime)
})
void getPicPath({ musicInfo, listId: playMusicInfo.isTempPlay ? null : playMusicInfo.listId }).then((url: string) => {
if (musicInfo.id != playMusicInfo.musicInfo?.id) return
setMusicInfo({ pic: url })
window.app_event.picUpdated()
})
void getLyricInfo({ musicInfo }).then((lyricInfo) => {
if (musicInfo.id != playMusicInfo.musicInfo?.id) return
setMusicInfo({
lrc: lyricInfo.lyric,
tlrc: lyricInfo.tlyric,
lxlrc: lyricInfo.lxlyric,
rlrc: lyricInfo.rlyric,
rawlrc: lyricInfo.rawlrcInfo.lyric,
})
}).catch((err) => {
console.log(err)
if (musicInfo.id != playMusicInfo.musicInfo?.id) return
setAllStatus(window.i18n.t('lyric__load_error'))
}).finally(() => {
window.app_event.lyricUpdated()
})
if (appSetting['player.togglePlayMethod'] == 'random') addPlayedList({ ...playMusicInfo as LX.Player.PlayMusicInfo })
}
// 处理音乐播放
const handlePlay = () => {
if (window.lx.isPlayedStop) window.lx.isPlayedStop = false
if (isGettingUrl) isGettingUrl = false
if (window.lx.restorePlayInfo) {
void handleRestorePlay(window.lx.restorePlayInfo)
window.lx.restorePlayInfo = null
return
}
setStop()
window.app_event.pause()
clearDelayNextTimeout()
clearLoadTimeout()
const musicInfo = playMusicInfo.musicInfo
if (!musicInfo) return
if (appSetting['player.togglePlayMethod'] == 'random' && !playMusicInfo.isTempPlay) addPlayedList({ ...(playMusicInfo as LX.Player.PlayMusicInfo) })
void setMusicUrl(musicInfo)
void getPicPath({ musicInfo, listId: playMusicInfo.isTempPlay ? null : playMusicInfo.listId }).then((url: string) => {
if (musicInfo.id != playMusicInfo.musicInfo?.id) return
setMusicInfo({ pic: url })
window.app_event.picUpdated()
})
void getLyricInfo({ musicInfo }).then((lyricInfo) => {
if (musicInfo.id != playMusicInfo.musicInfo?.id) return
setMusicInfo({
lrc: lyricInfo.lyric,
tlrc: lyricInfo.tlyric,
lxlrc: lyricInfo.lxlyric,
rlrc: lyricInfo.rlyric,
rawlrc: lyricInfo.rawlrcInfo.lyric,
})
}).catch((err) => {
console.log(err)
if (musicInfo.id != playMusicInfo.musicInfo?.id) return
setAllStatus(window.i18n.t('lyric__load_error'))
}).finally(() => {
window.app_event.lyricUpdated()
})
}
/**
* 播放列表内歌曲
* @param listId 列表id
* @param index 播放的歌曲位置
*/
export const playList = (listId: string, index: number) => {
setPlayListId(listId)
setPlayMusicInfo(listId, getList(listId)[index])
clearPlayedList()
clearTempPlayeList()
handlePlay()
}
const handleToggleStop = () => {
stop()
setTimeout(() => {
setPlayMusicInfo(null, null)
})
}
/**
* 下一曲
* @param isAutoToggle 是否自动切换
* @returns
*/
export const playNext = async(isAutoToggle = false): Promise<void> => {
if (tempPlayList.length) { // 如果稍后播放列表存在歌曲则直接播放改列表的歌曲
const playMusicInfo = tempPlayList[0]
removeTempPlayList(0)
setPlayMusicInfo(playMusicInfo.listId, playMusicInfo.musicInfo, playMusicInfo.isTempPlay)
handlePlay()
return
}
if (playMusicInfo.musicInfo == null) return handleToggleStop()
// console.log(playInfo.playerListId)
const currentListId = playInfo.playerListId
if (!currentListId) return handleToggleStop()
const currentList = getList(currentListId)
if (playedList.length) { // 移除已播放列表内不存在原列表的歌曲
let currentId: string
if (playMusicInfo.isTempPlay) {
const musicInfo = currentList[playInfo.playerPlayIndex]
if (musicInfo) currentId = musicInfo.id
} else {
currentId = playMusicInfo.musicInfo.id
}
// 从已播放列表移除播放列表已删除的歌曲
let index
for (index = playedList.findIndex(m => m.musicInfo.id === currentId) + 1; index < playedList.length; index++) {
const playMusicInfo = playedList[index]
const currentId = playMusicInfo.musicInfo.id
if (playMusicInfo.listId == currentListId && !currentList.some(m => m.id === currentId)) {
removePlayedList(index)
continue
}
break
}
if (index < playedList.length) {
const playMusicInfo = playedList[index]
setPlayMusicInfo(playMusicInfo.listId, playMusicInfo.musicInfo, playMusicInfo.isTempPlay)
handlePlay()
return
}
}
// const isCheckFile = findNum > 2 // 针对下载列表,如果超过两次都碰到无效歌曲,则过滤整个列表内的无效歌曲
let { filteredList, playerIndex } = await filterList({ // 过滤已播放歌曲
listId: currentListId,
list: currentList,
playedList,
playerMusicInfo: currentList[playInfo.playerPlayIndex],
})
if (!filteredList.length) return handleToggleStop()
// let currentIndex: number = filteredList.indexOf(currentList[playInfo.playerPlayIndex])
if (playerIndex == -1 && filteredList.length) playerIndex = 0
let nextIndex = playerIndex
let togglePlayMethod = appSetting['player.togglePlayMethod']
if (!isAutoToggle) {
switch (togglePlayMethod) {
case 'list':
case 'singleLoop':
case 'none':
togglePlayMethod = 'listLoop'
}
}
switch (togglePlayMethod) {
case 'listLoop':
nextIndex = playerIndex === filteredList.length - 1 ? 0 : playerIndex + 1
break
case 'random':
nextIndex = getRandom(0, filteredList.length)
break
case 'list':
nextIndex = playerIndex === filteredList.length - 1 ? -1 : playerIndex + 1
break
case 'singleLoop':
break
default:
nextIndex = -1
return
}
if (nextIndex < 0) return
const nextPlayMusicInfo = {
musicInfo: filteredList[nextIndex],
listId: currentListId,
isTempPlay: false,
}
setPlayMusicInfo(nextPlayMusicInfo.listId, nextPlayMusicInfo.musicInfo)
handlePlay()
}
/**
* 上一曲
*/
export const playPrev = async(isAutoToggle = false): Promise<void> => {
if (playMusicInfo.musicInfo == null) return handleToggleStop()
const currentListId = playInfo.playerListId
if (!currentListId) return handleToggleStop()
const currentList = getList(currentListId)
if (playedList.length) {
let currentId: string
if (playMusicInfo.isTempPlay) {
const musicInfo = currentList[playInfo.playerPlayIndex]
if (musicInfo) currentId = musicInfo.id
} else {
currentId = playMusicInfo.musicInfo.id
}
// 从已播放列表移除播放列表已删除的歌曲
let index
for (index = playedList.findIndex(m => m.musicInfo.id === currentId) - 1; index > -1; index--) {
const playMusicInfo = playedList[index]
const currentId = playMusicInfo.musicInfo.id
if (playMusicInfo.listId == currentListId && !currentList.some(m => m.id === currentId)) {
removePlayedList(index)
continue
}
break
}
if (index > -1) {
const playMusicInfo = playedList[index]
setPlayMusicInfo(playMusicInfo.listId, playMusicInfo.musicInfo, playMusicInfo.isTempPlay)
handlePlay()
return
}
}
// const isCheckFile = findNum > 2
let { filteredList, playerIndex } = await filterList({ // 过滤已播放歌曲
listId: currentListId,
list: currentList,
playedList,
playerMusicInfo: currentList[playInfo.playerPlayIndex],
})
if (!filteredList.length) return handleToggleStop()
// let currentIndex = filteredList.indexOf(currentList[playInfo.playerPlayIndex])
if (playerIndex == -1 && filteredList.length) playerIndex = 0
let nextIndex = playerIndex
if (!playMusicInfo.isTempPlay) {
let togglePlayMethod = appSetting['player.togglePlayMethod']
if (!isAutoToggle) {
switch (togglePlayMethod) {
case 'list':
case 'singleLoop':
case 'none':
togglePlayMethod = 'listLoop'
}
}
switch (togglePlayMethod) {
case 'random':
nextIndex = getRandom(0, filteredList.length)
break
case 'listLoop':
case 'list':
nextIndex = playerIndex === 0 ? filteredList.length - 1 : playerIndex - 1
break
case 'singleLoop':
break
default:
nextIndex = -1
return
}
if (nextIndex < 0) return
}
const nextPlayMusicInfo = {
musicInfo: filteredList[nextIndex],
listId: currentListId,
isTempPlay: false,
}
setPlayMusicInfo(nextPlayMusicInfo.listId, nextPlayMusicInfo.musicInfo)
handlePlay()
}
/**
* 恢复播放
*/
export const play = () => {
if (playMusicInfo.musicInfo == null) return
if (isEmpty()) {
void setMusicUrl(playMusicInfo.musicInfo)
return
}
setPlay()
}
/**
* 暂停播放
*/
export const pause = () => {
setPause()
}
/**
* 停止播放
*/
export const stop = () => {
setStop()
setTimeout(() => {
window.app_event.stop()
})
}
/**
* 播放、暂停播放切换
*/
export const togglePlay = () => {
if (window.lx.isPlayedStop) window.lx.isPlayedStop = false
if (isPlay.value) {
pause()
} else {
play()
}
}
+2
View File
@@ -0,0 +1,2 @@
export * from './action'
export * from './timeoutStop'
+99
View File
@@ -0,0 +1,99 @@
import { ref, computed, ComputedRef } from '@common/utils/vueTools'
import { isPlay } from '@renderer/store/player/state'
import { appSetting } from '@renderer/store/setting'
// import { interval, intervalCancel } from '@renderer/utils/ipc'
import { stop } from './action'
const time = ref(-1)
const timeoutTools: {
isRunning: boolean
// time: number
interval: null | number
timeout: NodeJS.Timeout | null
endTime: number
exit: () => void
clearTimeout: () => void
start: (_time: number) => void
} = {
isRunning: false,
timeout: null,
// time: -1,
endTime: 0,
interval: null,
exit() {
window.lx.isPlayedStop = true
if (!appSetting['player.waitPlayEndStop'] && isPlay.value) {
stop()
}
},
clearTimeout() {
if (this.interval) {
window.clearInterval(this.interval)
this.interval = null
}
if (this.timeout) {
clearTimeout(this.timeout)
this.timeout = null
}
if (!this.isRunning) return
// this.time = -1
time.value = -1
this.isRunning = false
},
start(_time: number) {
this.clearTimeout()
// this.time = _time
time.value = _time
this.isRunning = true
this.endTime = performance.now() + _time * 1000
this.interval = window.setInterval(() => {
// this.endTime = performance.now()
// if (this.time > 0) {
// this.time--
// }
time.value = Math.max(0, Math.round((this.endTime - performance.now()) / 1000))
}, 1000)
this.timeout = setTimeout(() => {
this.timeout = null
time.value = -1
this.clearTimeout()
this.exit()
}, _time * 1000)
},
}
export const startTimeoutStop = (time: number) => {
if (window.lx.isPlayedStop) window.lx.isPlayedStop = false
timeoutTools.start(time)
}
export const stopTimeoutStop = () => {
console.warn('stopTimeoutStop')
if (window.lx.isPlayedStop) window.lx.isPlayedStop = false
timeoutTools.clearTimeout()
}
const formatTime = (time: number): string => {
// let d = parseInt(time / 86400)
// d = d ? d.toString() + ':' : ''
// time = time % 86400
let h: number | string = Math.trunc(time / 3600)
h = h ? h.toString() + ':' : ''
time = time % 3600
const m = Math.trunc(time / 60).toString().padStart(2, '0')
const s = Math.trunc(time % 60).toString().padStart(2, '0')
return `${h}${m}:${s}`
}
export const useTimeout = () => {
const timeLabel: ComputedRef<string> = computed(() => {
return time.value > 0 ? formatTime(time.value) : ''
})
return {
time,
timeLabel,
}
}
+39
View File
@@ -0,0 +1,39 @@
import { toRaw, markRawList } from '@common/utils/vueTools'
import { qualityList } from '@renderer/store'
import { clearPlayedList } from '@renderer/store/player/action'
import { appSetting } from '@renderer/store/setting'
export const getPlayType = (highQuality: boolean, musicInfo: LX.Music.MusicInfo | LX.Download.ListItem): LX.Quality | null => {
if ('progress' in musicInfo || musicInfo.source == 'local') return null
let type: LX.Quality = '128k'
let list = qualityList.value[musicInfo.source]
if (highQuality && musicInfo.meta._qualitys['320k'] && list && list.includes('320k')) type = '320k'
return type
}
/**
* 过滤列表中已播放的歌曲
*/
export const filterList = async({ playedList, listId, list, playerMusicInfo }: {
playedList: LX.Player.PlayMusicInfo[]
listId: string
list: Array<LX.Music.MusicInfo | LX.Download.ListItem>
playerMusicInfo?: LX.Music.MusicInfo | LX.Download.ListItem
}) => {
// if (this.list.listName === null) return
// console.log(isCheckFile)
let { filteredList, canPlayList, playerIndex } = await window.lx.worker.main.filterMusicList({
listId,
list: list.map(m => toRaw(m)),
playedList: toRaw(playedList),
savePath: appSetting['download.savePath'],
playerMusicInfo: toRaw(playerMusicInfo),
})
if (!filteredList.length && playedList.length) {
clearPlayedList()
return { filteredList: markRawList(canPlayList), playerIndex }
}
return { filteredList: markRawList(filteredList), playerIndex }
}
-26
View File
@@ -1,26 +0,0 @@
import { reactive, ref, markRaw } from '@renderer/utils/vueTools'
export const isInitedList = ref(false)
export const setInited = () => {
isInitedList.value = true
}
export const downloadList = reactive([])
export const downloadListMap = new Map()
export const initDownloadList = list => {
downloadList.splice(0, downloadList.length, ...list)
downloadListMap.clear()
for (const task of list) {
downloadListMap.set(task.key, task)
}
}
export const downloadStatus = markRaw({
RUN: 'run',
WAITING: 'waiting',
PAUSE: 'pause',
ERROR: 'error',
COMPLETED: 'completed',
})
-51
View File
@@ -1,51 +0,0 @@
import { ref, reactive, shallowRef, markRaw } from '@renderer/utils/vueTools'
import { windowSizeList as configWindowSizeList, themes as configThemes, themeLights as themeLightIds, themeDarks as themeDarkIds } from '@common/config'
import { version } from '../../../../package.json'
process.versions.app = version
export const apiSource = ref(null)
export const isShowPact = window.isShowPact = ref(false)
export const proxy = reactive({})
export const versionInfo = window.versionInfo = reactive({
version,
newVersion: null,
showModal: false,
isError: false,
isTimeOut: false,
isUnknow: false,
isDownloaded: false,
isDownloading: false,
isLatestVer: false,
downloadProgress: null,
})
export const userApi = reactive({
list: [],
status: false,
message: 'initing',
apis: {},
})
export const sync = window.sync = reactive({
enable: false,
isShowSyncMode: false,
deviceName: '',
status: {
status: false,
message: '',
address: [],
code: '',
devices: [],
},
})
export const isFullscreen = ref(false)
export const windowSizeList = markRaw(configWindowSizeList)
export const themes = markRaw(configThemes)
export const themeLights = markRaw(configThemes.filter(t => themeLightIds.includes(t.id)))
export const themeDarks = markRaw(configThemes.filter(t => themeDarkIds.includes(t.id)))
export const themeShouldUseDarkColors = ref(window.shouldUseDarkColors)
delete window.shouldUseDarkColors
export const qualityList = shallowRef({})
export const setQualityList = _qualityList => {
qualityList.value = _qualityList
}
-131
View File
@@ -1,131 +0,0 @@
import { reactive, ref, markRaw } from '@renderer/utils/vueTools'
// const TEMP_LIST = 'TEMP_LIST'
export const isInitedList = ref(false)
export const setInited = () => {
isInitedList.value = true
}
export const allList = window.allList = markRaw({})
export const allListInit = (newLists) => {
const newIds = [defaultList.id, loveList.id, tempList.id, ...newLists.userList.map(l => l.id)]
for (const id of Object.keys(allList)) {
if (newIds.includes(id)) continue
delete allList[id]
}
allListUpdate(defaultList.id, newLists.defaultList.list)
allListUpdate(loveList.id, newLists.loveList.list)
if (newLists.tempList) {
allListUpdate(tempList.id, newLists.tempList.list)
tempList.meta = newLists.tempList.meta ?? {}
}
userLists.splice(0, userLists.length)
for (const { list, ...listInfo } of newLists.userList) {
allListUpdate(listInfo.id, list)
userLists.push(listInfo)
}
}
export const allListUpdate = (id, list) => {
if (allList[id]) {
allList[id].splice(0, allList[id].length, ...list)
} else {
allList[id] = reactive(list)
}
}
export const allListRemove = id => {
delete allList[id]
}
export const defaultList = reactive({
id: 'default',
name: '试听列表',
})
export const loveList = reactive({
id: 'love',
name: '我的收藏',
})
export const tempList = reactive({
id: 'temp',
name: '临时列表',
meta: {},
})
export const userLists = window.userLists = reactive([])
export const addUserList = ({
name,
id,
list,
source,
sourceListId,
position,
locationUpdateTime,
}) => {
if (position == null) {
userLists.push({
name,
id,
source,
sourceListId,
locationUpdateTime,
})
} else {
userLists.splice(position + 1, 0, {
name,
id,
source,
sourceListId,
locationUpdateTime,
})
}
allListUpdate(id, list)
}
export const updateList = ({
name,
id,
list,
source,
sourceListId,
meta,
locationUpdateTime,
}) => {
let targetList
switch (id) {
case defaultList.id:
case loveList.id:
break
case tempList.id:
tempList.meta = meta || {}
break
default:
targetList = userLists.find(l => l.id == id)
targetList.name = name
targetList.source = source
targetList.sourceListId = sourceListId
if (locationUpdateTime) targetList.locationUpdateTime = locationUpdateTime
break
}
allListUpdate(id, list)
}
export const removeUserList = id => {
const index = userLists.findIndex(l => l.id == id)
if (index < 0) return
userLists.splice(index, 1)
allListRemove(id)
}
export const getList = id => {
return allList[id] ?? []
}
export const fetchingListStatus = reactive({})
export const setFetchingListStatus = (id, status) => {
fetchingListStatus[id] = status
}
-23
View File
@@ -1,23 +0,0 @@
import { reactive } from '@renderer/utils/vueTools'
export const lyric = reactive({
lines: [],
text: '',
line: 0,
offset: 0, // 歌词延迟
tempOffset: 0, // 歌词临时延迟
})
export const setLines = lines => {
lyric.lines = lines
}
export const setText = (text, line) => {
lyric.text = text
lyric.line = line
}
export const setOffset = offset => {
lyric.offset = offset
}
export const setTempOffset = offset => {
lyric.tempOffset = offset
}
-186
View File
@@ -1,186 +0,0 @@
import { reactive, ref, shallowRef } from '@renderer/utils/vueTools'
import { getList } from '@renderer/core/share/utils'
export const musicInfo = window.musicInfo = reactive({
songmid: null,
img: null,
lrc: null,
tlrc: null,
rlrc: null,
lxlrc: null,
rawlrc: null,
url: null,
name: '',
singer: '',
album: '',
})
const musicInfoKeys = Object.keys(musicInfo)
export const setMusicInfo = _musicInfo => {
for (const key of musicInfoKeys) {
if (_musicInfo[key] !== undefined) {
musicInfo[key] = _musicInfo[key]
}
}
}
export const musicInfoItem = shallowRef({})
export const setMusicInfoItem = musicInfo => {
musicInfoItem.value = musicInfo.key ? musicInfo.metadata.musicInfo : musicInfo
}
export const isPlay = ref(false)
export const setPlay = val => {
isPlay.value = val
}
export const status = ref('')
export const setStatus = val => {
status.value = val
}
export const statusText = ref('')
export const setStatusText = val => {
statusText.value = val
}
export const setAllStatus = val => {
status.value = val
statusText.value = val
}
export const isShowPlayerDetail = ref(false)
export const setShowPlayerDetail = val => {
isShowPlayerDetail.value = val
}
export const isShowPlayComment = ref(false)
export const setShowPlayComment = val => {
isShowPlayComment.value = val
}
export const isShowLrcSelectContent = ref(false)
export const setShowPlayLrcSelectContentLrc = val => {
isShowLrcSelectContent.value = val
}
export const playMusicInfo = reactive({
listId: null, // 当前播放歌曲的列表 id
musicInfo: null, // 当前播放歌曲的歌曲信息
isTempPlay: false, // 是否属于 “稍后播放”
})
export const playInfo = reactive({
playIndex: -1, // 当前正在播放歌曲 index
playListId: null, // 播放器的播放列表 id
listPlayIndex: -1, // 播放器播放歌曲 index
})
// 设置播放器的播放列表
export const setPlayList = listId => {
playInfo.playListId = listId
}
// 更新播放位置
export const updatePlayIndex = () => {
const indexInfo = getPlayIndex(playMusicInfo.listId, playMusicInfo.musicInfo, playMusicInfo.isTempPlay)
// console.log(indexInfo)
playInfo.playIndex = indexInfo.playIndex
playInfo.listPlayIndex = indexInfo.listPlayIndex
return indexInfo
}
export const getPlayIndex = (listId, musicInfo, isTempPlay) => {
const playerList = getList(playInfo.playListId)
// if (listIndex < 0) throw new Error('music info not found')
// playInfo.playIndex = listIndex
let playIndex = -1
let listPlayIndex = -1
if (playerList?.length) {
listPlayIndex = Math.min(playInfo.listPlayIndex, playerList.length - 1)
}
const list = getList(listId)
if (list?.length && musicInfo) {
if (musicInfo.key) { // 已下载的歌曲
const currentKey = musicInfo.key
playIndex = list.findIndex(m => m.key == currentKey)
} else {
const currentSongmid = musicInfo.songmid
playIndex = list.findIndex(m => m.songmid == currentSongmid)
}
if (!isTempPlay) {
if (playIndex < 0) {
listPlayIndex = listPlayIndex < 1 ? (list.length - 1) : (listPlayIndex - 1)
} else {
listPlayIndex = playIndex
}
}
}
return {
playIndex,
listPlayIndex,
}
}
// 设置当前播放歌曲的信息
export const setPlayMusicInfo = (listId, musicInfo, isTempPlay = false) => {
playMusicInfo.listId = listId
playMusicInfo.musicInfo = musicInfo
playMusicInfo.isTempPlay = isTempPlay
if (musicInfo == null) {
playInfo.playIndex = -1
playInfo.playListId = null
playInfo.listPlayIndex = -1
return
}
const { playIndex, listPlayIndex } = getPlayIndex(listId, musicInfo, isTempPlay)
playInfo.playIndex = playIndex
playInfo.listPlayIndex = listPlayIndex
setMusicInfoItem(musicInfo)
// console.log(playInfo)
}
export const playedList = window.playedList = reactive([])
export const addPlayedList = (item) => {
if (playedList.some(m => m.musicInfo === item.musicInfo)) return
playedList.push(item)
}
export const removePlayedList = (index) => {
playedList.splice(index, 1)
}
export const clearPlayedList = () => {
playedList.splice(0, playedList.length)
}
export const tempPlayList = reactive([])
export const addTempPlayList = (list) => {
const topList = []
const bottomList = list.filter(({ isTop, ...musicInfo }) => {
if (isTop) {
topList.push(musicInfo)
return false
}
return true
})
if (topList.length) tempPlayList.unshift(...topList.map(({ musicInfo, listId }) => ({ musicInfo, listId, isTempPlay: true })))
if (bottomList.length) tempPlayList.push(...bottomList.map(({ musicInfo, listId }) => ({ musicInfo, listId, isTempPlay: true })))
}
export const removeTempPlayList = (index) => {
tempPlayList.splice(index, 1)
}
export const clearTempPlayeList = () => {
tempPlayList.splice(0, tempPlayList.length)
}
window.playInfo = playInfo
window.playMusicInfo = playMusicInfo
-7
View File
@@ -1,7 +0,0 @@
import { getList as getMyList } from './list'
import { downloadList } from './download'
export const getList = listId => {
return listId == 'download' ? downloadList : getMyList(listId)
}
-13
View File
@@ -1,13 +0,0 @@
import { ref } from '@renderer/utils/vueTools'
export const volume = ref(0)
export const isMute = ref(false)
export const setVolume = num => {
volume.value = num
}
export const setMute = flag => {
isMute.value = flag
}
@@ -1,66 +0,0 @@
import { useAction, useCommit } from '@renderer/utils/vueTools'
import { tempList } from '@renderer/core/share/list'
const getListPlayIndex = (list, index) => {
if (index == null) {
index = 1
} else {
index = parseInt(index)
if (Number.isNaN(index)) {
index = 1
} else {
if (index < 1) index = 1
else if (index > list.length) index = list.length
}
}
return index - 1
}
export default () => {
const getListDetail = useAction('songList', 'getListDetail')
const getListDetailAll = useAction('songList', 'getListDetailAll')
const setTempList = useCommit('player', 'setTempList')
const updateTempList = useCommit('player', 'updateTempList')
const playSongListDetail = async(source, link, playIndex) => {
console.log(source, link, playIndex)
if (link == null) return
let isPlayingList = false
const id = decodeURIComponent(link)
const playListId = `${source}__${decodeURIComponent(link)}`
let list
try {
list = await getListDetail({ source, id, page: 1 })
} catch (err) {
console.log(err)
}
if (list.length > playIndex) {
isPlayingList = true
setTempList({
list,
index: getListPlayIndex(list, playIndex),
id: playListId,
})
}
getListDetailAll({ source, id }).then(list => {
if (isPlayingList) {
if (tempList.meta.id == id) {
updateTempList({
list,
id: playListId,
})
}
} else {
setTempList({
list,
index: getListPlayIndex(list, playIndex),
id: playListId,
})
}
})
}
return (source, link, playIndex) => {
playSongListDetail(source, link, playIndex)
}
}
@@ -0,0 +1,46 @@
import { playList } from '@renderer/core/player'
import { setTempList } from '@renderer/store/list/action'
import { tempList, tempListMeta } from '@renderer/store/list/state'
import { getListDetail, getListDetailAll } from '@renderer/store/songList/action'
const getListPlayIndex = (list: LX.Music.MusicInfoOnline[], index?: number) => {
if (index == null) {
index = 1
} else {
if (index < 1) index = 1
else if (index > list.length) index = list.length
}
return index - 1
}
export default () => {
const playSongListDetail = async(source: LX.OnlineSource, link: string, playIndex?: number) => {
// console.log(source, link, playIndex)
if (link == null) return
let isPlayingList = false
const id = decodeURIComponent(link)
const playListId = `${source}__${decodeURIComponent(link)}`
let list = (await getListDetail(id, source, 1)).list
if (playIndex != null && list.length > playIndex) {
isPlayingList = true
await setTempList(playListId, list)
playList(tempList.id, getListPlayIndex(list, playIndex))
}
list = await getListDetailAll(id, source)
if (isPlayingList) {
if (tempListMeta.id == id) await setTempList(playListId, list)
} else {
await setTempList(playListId, list)
playList(tempList.id, getListPlayIndex(list, playIndex))
}
}
return async(source: LX.OnlineSource, link: string, playIndex?: number) => {
try {
await playSongListDetail(source, link, playIndex)
} catch (err) {
console.error(err)
throw new Error('Get play list failed.')
}
}
}
-78
View File
@@ -1,78 +0,0 @@
import { isLinux } from '@common/utils'
import { getEnvParams, setIgnoreMouseEvents } from '@renderer/utils/tools'
import { useRefGetter } from '@renderer/utils/vueTools'
import { sync, apiSource, proxy, isFullscreen } from '@renderer/core/share'
import useSync from './useSync'
import useUpdate from './useUpdate'
import useDataInit from './useDataInit'
import useHandleEnvParams from './useHandleEnvParams'
import useEventListener from './useEventListener'
import useDeepLink from './useDeepLink'
import usePlayer from './usePlayer'
import useListAutoUpdate from './useListAutoUpdate'
export default () => {
const isProd = process.env.NODE_ENV === 'production'
const setting = useRefGetter('setting')
sync.enable = setting.value.sync.enable
apiSource.value = setting.value.apiSource
Object.assign(proxy, setting.value.network.proxy)
isFullscreen.value = setting.value.startInFullscreen
const dieableIgnoreMouseEvents = () => {
if (window.dt) return
setIgnoreMouseEvents(false)
}
const enableIgnoreMouseEvents = () => {
if (window.dt) return
setIgnoreMouseEvents(true)
}
useUpdate(setting)
useSync()
useEventListener({
dieableIgnoreMouseEvents,
enableIgnoreMouseEvents,
setting,
isProd,
isLinux,
})
const initPlayer = usePlayer({ setting })
const handleEnvParams = useHandleEnvParams()
const initData = useDataInit({
setting,
})
const initDeepLink = useDeepLink()
const handleListAutoUpdate = useListAutoUpdate()
getEnvParams().then(envParams => {
// 移除代理相关的环境变量设置,防止请求库自动应用它们
// eslint-disable-next-line no-undef
const processEnv = ENVIRONMENT
for (const key of Object.keys(processEnv)) {
if (/^(?:http_proxy|https_proxy|NO_PROXY)$/i.test(key)) delete processEnv[key]
}
const envProxy = envParams.cmdParams['proxy-server']
if (envProxy && typeof envProxy == 'string') {
const [host, port = ''] = envProxy.split(':')
proxy.envProxy = {
host,
port,
}
}
// 初始化我的列表、下载列表等数据
initData().then(() => {
initPlayer()
handleEnvParams(envParams) // 处理传入的启动参数
initDeepLink(envParams)
handleListAutoUpdate()
})
})
}
+73
View File
@@ -0,0 +1,73 @@
import { checkUpdate, getEnvParams, getViewPrevState, sendInited } from '@renderer/utils/ipc'
import { proxy, isFullscreen, themeId } from '@renderer/store'
import { appSetting } from '@renderer/store/setting'
import useSync from './useSync'
import useUpdate from './useUpdate'
import useDataInit from './useDataInit'
// import useHandleEnvParams from './useHandleEnvParams'
import useEventListener from './useEventListener'
import useDeeplink from './useDeeplink'
import usePlayer from './usePlayer'
import handleListAutoUpdate from './listAutoUpdate'
import { useRouter } from '@common/utils/vueRouter'
import useSettingSync from './useSettingSync'
export default () => {
// apiSource.value = appSetting['common.apiSource']
proxy.enable = appSetting['network.proxy.enable']
proxy.host = appSetting['network.proxy.host']
proxy.port = appSetting['network.proxy.port']
proxy.username = appSetting['network.proxy.username']
proxy.password = appSetting['network.proxy.password']
isFullscreen.value = appSetting['common.startInFullscreen']
themeId.value = appSetting['theme.id']
const router = useRouter()
const initSyncService = useSync()
useEventListener()
const initPlayer = usePlayer()
// const handleEnvParams = useHandleEnvParams()
const initData = useDataInit()
const initDeeplink = useDeeplink()
// const handleListAutoUpdate = useListAutoUpdate()
useUpdate()
useSettingSync()
void getEnvParams().then(envParams => {
// 移除代理相关的环境变量设置,防止请求库自动应用它们
// eslint-disable-next-line no-undef
// const processEnv = ENVIRONMENT
// for (const key of Object.keys(processEnv)) {
// // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
// if (/^(?:http_proxy|https_proxy|NO_PROXY)$/i.test(key)) delete processEnv[key]
// }
const envProxy = envParams.cmdParams['proxy-server']
if (envProxy && typeof envProxy == 'string') {
const [host, port = ''] = envProxy.split(':')
proxy.envProxy = {
host,
port,
}
}
void getViewPrevState().then(state => {
void router.push({ path: state.url, query: state.query })
})
// 初始化我的列表、下载列表等数据
void initData().then(() => {
initPlayer()
// handleEnvParams(envParams) // 处理传入的启动参数
void initDeeplink(envParams)
void initSyncService()
handleListAutoUpdate()
if (window.lx.isDev) checkUpdate()
sendInited()
})
})
}
@@ -0,0 +1,24 @@
import { getListUpdateInfo } from '@renderer/utils/data'
import { userLists } from '@renderer/store/list/state'
import syncSourceList from '@renderer/store/list/syncSourceList'
const handleSyncSourceList = async(waitUpdateLists: LX.List.UserListInfo[]) => {
if (!waitUpdateLists.length) return
const targetListInfo = waitUpdateLists.shift() as LX.List.UserListInfo
// console.log(targetListInfo)
try {
await syncSourceList(targetListInfo)
} catch {}
void handleSyncSourceList(waitUpdateLists)
}
export default () => {
void getListUpdateInfo().then(listUpdateInfo => {
const waitUpdateLists = Object.entries(listUpdateInfo)
.map(([id, info]) => info.isAutoUpdate && userLists.find(l => l.id == id))
.filter(_ => _) as LX.List.UserListInfo[]
for (let i = 2; i > 0; i--) {
void handleSyncSourceList(waitUpdateLists)
}
})
}
-163
View File
@@ -1,163 +0,0 @@
import { useCommit, useRefGetter } from '@renderer/utils/vueTools'
import { getPlayList } from '@renderer/utils'
import { getPlayInfo, getSearchHistoryList } from '@renderer/utils/tools'
import { initListPosition, initListPrevSelectId, initListUpdateInfo } from '@renderer/utils/data'
import music from '@renderer/utils/music'
import { log } from '@common/utils'
import {
defaultList as stateDefaultList,
loveList as stateloveList,
userLists as stateUserLists,
tempList as stateTempList,
} from '@renderer/core/share/list'
import { getList } from '@renderer/core/share/utils'
import { downloadStatus, downloadList } from '@renderer/core/share/download'
import useSaveData from './useSaveData'
import useInitUserApi from './useInitUserApi'
const useListInit = ({
saveMyListThrottle,
}) => {
const initList = useCommit('list', 'initList')
const updateDownloadList = useCommit('download', 'initDownloadList')
const initMyList = ({ defaultList, loveList, userList, tempList }) => {
if (!defaultList) defaultList = { ...stateDefaultList }
if (!loveList) loveList = { ...stateloveList }
if (!tempList) tempList = { ...stateTempList }
if (userList) {
let needSave = false
const getListId = id => id.includes('.') ? getListId(id.substring(0, id.lastIndexOf('_'))) : id
userList.forEach(l => {
if (!l.id.includes('__') || l.source) return
let [source, id] = l.id.split('__')
id = getListId(id)
l.source = source
l.sourceListId = id
if (!needSave) needSave = true
})
if (needSave) saveMyListThrottle({ userList })
} else {
userList = [...stateUserLists]
}
if (!defaultList.list) defaultList.list = []
if (!loveList.list) loveList.list = []
if (!tempList.list) tempList.list = []
initList({ defaultList, loveList, userList, tempList })
}
const initDownloadList = list => {
if (list) {
list = list.filter(item => item?.key)
for (const item of list) {
if (item.status == downloadStatus.RUN || item.status == downloadStatus.WAITING) {
item.status = downloadStatus.PAUSE
item.statusText = '暂停下载'
}
if (!item.metadata) { // 转换v1.15.3及以前的任务信息
if (item.name == null) {
item.name = `${item.musicInfo.name} - ${item.musicInfo.singer}`
item.songmid = item.musicInfo.songmid
}
item.metadata = {
musicInfo: item.musicInfo,
url: item.url,
type: item.type,
ext: item.ext,
fileName: item.fileName,
filePath: item.filePath,
}
delete item.musicInfo
delete item.url
delete item.type
delete item.ext
delete item.fileName
delete item.filePath
}
}
updateDownloadList(list)
}
}
return () => {
return getPlayList().then(({ defaultList, loveList, userList, tempList, downloadList }) => {
initMyList({ defaultList, loveList, userList, tempList })
initDownloadList(downloadList) // 初始化下载列表
})
}
}
const usePlayInfoInit = () => {
const setPlayList = useCommit('player', 'setList')
return () => {
return getPlayInfo().then(info => {
window.restorePlayInfo = null
if (!info?.listId || info.index < 0) return
const list = getList(info.listId)
if (!list || !list[info.index]) return
window.restorePlayInfo = info
setPlayList({
listId: info.listId,
index: info.index,
})
})
}
}
const useSearchHistoryInit = () => {
const setSearchHistoryList = useCommit('search', 'setHistory')
return saveSearchHistoryListThrottle => {
return getSearchHistoryList().then(historyList => {
if (historyList == null) {
historyList = []
saveSearchHistoryListThrottle(historyList)
} else {
setSearchHistoryList(historyList)
}
})
}
}
export default ({
setting,
}) => {
const searchHistoryList = useRefGetter('search', 'historyList')
// 数据保存初始化
const { saveMyListThrottle, saveSearchHistoryListThrottle } = useSaveData({
setting,
searchHistoryList,
})
const initUserApi = useInitUserApi({ setting })
// 列表初始化
const initList = useListInit({
saveMyListThrottle,
})
// 播放信息初始化
const initPlayInfo = usePlayInfoInit()
// 搜索历史初始化
const initSearchHistory = useSearchHistoryInit()
return async() => {
await Promise.all([
initListPosition(), // 列表位置记录
initListPrevSelectId(), // 上次选中的列表记录
initListUpdateInfo(), // 列表更新设置
initUserApi(), // 自定义API
]).catch(err => log.error(err))
music.init() // 初始化音乐sdk
await initList().catch(err => log.error(err)) // 初始化列表
await initPlayInfo(downloadList).catch(err => log.error(err)) // 初始化上次的歌曲播放信息
await initSearchHistory(saveSearchHistoryListThrottle).catch(err => log.error(err)) // 初始化搜索历史记录
}
}
+44
View File
@@ -0,0 +1,44 @@
import { getPlayInfo } from '@renderer/utils/ipc'
import music from '@renderer/utils/musicSdk'
import { log } from '@common/utils'
import { getListMusics, getUserLists, registerAction } from '@renderer/store/list/action'
import useInitUserApi from './useInitUserApi'
import { play, playList } from '@renderer/core/player'
import { onBeforeUnmount } from '@common/utils/vueTools'
import { appSetting } from '@renderer/store/setting'
const initPrevPlayInfo = async() => {
const info = await getPlayInfo()
window.lx.restorePlayInfo = null
if (!info?.listId || info.index < 0) return
const list = await getListMusics(info.listId)
if (!list || !list[info.index]) return
window.lx.restorePlayInfo = info
playList(info.listId, info.index)
if (appSetting['player.startupAutoPlay']) setTimeout(play)
}
export default () => {
const initUserApi = useInitUserApi()
let unregister: null | (() => void) = null
onBeforeUnmount(() => {
if (unregister) unregister()
})
return async() => {
await Promise.all([
initUserApi(), // 自定义API
]).catch(err => log.error(err))
void music.init() // 初始化音乐sdk
unregister = registerAction((ids) => {
window.app_event.myListUpdate(ids)
})
window.lxData.userLists = await getUserLists() // 获取用户列表
await initPrevPlayInfo().catch(err => log.error(err)) // 初始化上次的歌曲播放信息
}
}
@@ -1,6 +1,5 @@
import { onBeforeUnmount } from '@renderer/utils/vueTools'
import { base as eventBaseName } from '@renderer/event/names'
import { getEnvParams, clearEnvParamsDeeplink } from '@renderer/utils/tools'
import { onBeforeUnmount } from '@common/utils/vueTools'
import { clearEnvParamsDeeplink, focusWindow, onDeeplink } from '@renderer/utils/ipc'
import { useDialog } from './utils'
import useMusicAction from './useMusicAction'
@@ -15,11 +14,17 @@ export default () => {
const handleSonglistAction = useSonglistAction()
const handleLinkAction = link => {
const handleLinkAction = async(link: string) => {
// console.log(link)
const [url, search] = link.split('?')
const [type, action, ...paths] = url.replace('lxmusic://', '').split('/')
const params = {}
const params: {
paths: string[]
data?: any
[key: string]: any
} = {
paths: [],
}
if (search) {
for (const param of search.split('&')) {
const [key, value] = param.split('=')
@@ -34,39 +39,36 @@ export default () => {
handleMusicAction(action, params)
break
case 'songlist':
handleSonglistAction(action, params)
await handleSonglistAction(action, params)
break
default: throw new Error('Unknown type: ' + type)
}
}
const handleFocus = () => {
const rDeeplink = onDeeplink(async({ params: link }) => {
console.log(link)
if (!isInited) return
getEnvParams().then(envParams => {
if (!envParams.deeplink) return
clearEnvParamsDeeplink()
try {
handleLinkAction(envParams.deeplink)
} catch (err) {
showErrorDialog(err.message)
}
})
}
window.eventHub.on(eventBaseName.focus, handleFocus)
onBeforeUnmount(() => {
window.eventHub.off(eventBaseName.focus, handleFocus)
clearEnvParamsDeeplink()
try {
await handleLinkAction(link)
} catch (err: any) {
showErrorDialog(err.message)
focusWindow()
}
})
return envParams => {
onBeforeUnmount(() => {
rDeeplink()
})
return async(envParams: LX.EnvParams) => {
if (envParams.deeplink) {
clearEnvParamsDeeplink()
try {
handleLinkAction(envParams.deeplink)
} catch (err) {
await handleLinkAction(envParams.deeplink)
} catch (err: any) {
showErrorDialog(err.message)
focusWindow()
}
}
isInited = true

Some files were not shown because too many files have changed in this diff Show More