完成播放功能
|
@ -11,7 +11,7 @@
|
||||||
"name": "yutent",
|
"name": "yutent",
|
||||||
"email": "yutent.io@gmail.com"
|
"email": "yutent.io@gmail.com"
|
||||||
},
|
},
|
||||||
"homepage": "https://yutent.me",
|
"homepage": "https://yutent.top",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"crypto.js": "^2.0.2",
|
"crypto.js": "^2.0.2",
|
||||||
|
|
|
@ -39,41 +39,6 @@
|
||||||
z-index: 9;
|
z-index: 9;
|
||||||
height: 26px;
|
height: 26px;
|
||||||
background: rgba(32, 32, 32, 0.5);
|
background: rgba(32, 32, 32, 0.5);
|
||||||
|
|
||||||
.btn-box {
|
|
||||||
display: inline-flex;
|
|
||||||
width: auto;
|
|
||||||
height: 12px;
|
|
||||||
padding: 0 8px;
|
|
||||||
|
|
||||||
.item {
|
|
||||||
display: inline-flex;
|
|
||||||
width: 12px;
|
|
||||||
height: 12px;
|
|
||||||
margin: 0 3px;
|
|
||||||
background: url(/images/btn-grey.svg) no-repeat;
|
|
||||||
background-size: cover;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.focus {
|
|
||||||
.quit {
|
|
||||||
background-image: url(/images/btn-close.svg);
|
|
||||||
}
|
|
||||||
.min {
|
|
||||||
background-image: url(/images/btn-mini.svg);
|
|
||||||
}
|
|
||||||
// .max {background-image:url(/images/btn-maxi.svg);}
|
|
||||||
}
|
|
||||||
&:hover {
|
|
||||||
.quit {
|
|
||||||
background-image: url(/images/btn-close_a.svg);
|
|
||||||
}
|
|
||||||
.min {
|
|
||||||
background-image: url(/images/btn-mini_a.svg);
|
|
||||||
}
|
|
||||||
// .max {background-image:url(/images/btn-maxi_a.svg);}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-body {
|
.main-body {
|
||||||
|
@ -255,11 +220,22 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress {
|
.progress {
|
||||||
|
position: relative;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
height: 3px;
|
height: 3px;
|
||||||
background: #b2cfe3;
|
background: #b2cfe3;
|
||||||
|
|
||||||
|
input {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 3px;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.thumb {
|
.thumb {
|
||||||
|
@ -373,7 +349,46 @@
|
||||||
transition: background 0.1s ease-in-out;
|
transition: background 0.1s ease-in-out;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
&.volume {
|
&.volume,
|
||||||
|
&.mute {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
a {
|
||||||
|
display: block;
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: cover;
|
||||||
|
transition: background 0.1s ease-in-out;
|
||||||
|
}
|
||||||
|
.range {
|
||||||
|
visibility: hidden;
|
||||||
|
position: absolute;
|
||||||
|
right: -5px;
|
||||||
|
bottom: 20px;
|
||||||
|
width: 32px;
|
||||||
|
height: 128px;
|
||||||
|
padding: 14px;
|
||||||
|
border-radius: 3px;
|
||||||
|
background: rgba(128, 128, 128, 0.8);
|
||||||
|
|
||||||
|
input {
|
||||||
|
display: block;
|
||||||
|
width: 4px;
|
||||||
|
height: 100px;
|
||||||
|
border-radius: 3px;
|
||||||
|
appearance: slider-vertical;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.range {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.volume a {
|
||||||
background-image: url(/images/ctrl/volume.png);
|
background-image: url(/images/ctrl/volume.png);
|
||||||
&:hover,
|
&:hover,
|
||||||
&:active {
|
&:active {
|
||||||
|
@ -381,7 +396,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.mute {
|
&.mute a {
|
||||||
background-image: url(/images/ctrl/mute.png);
|
background-image: url(/images/ctrl/mute.png);
|
||||||
&:hover,
|
&:hover,
|
||||||
&:active {
|
&:active {
|
||||||
|
|
|
@ -1,19 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024" viewBox="0 0 1024 1024">
|
|
||||||
<defs>
|
|
||||||
<style>
|
|
||||||
.cls-1 {
|
|
||||||
fill: #f55449;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cls-2 {
|
|
||||||
fill: #333;
|
|
||||||
opacity: 0.1;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</defs>
|
|
||||||
<title>btn_close_hover</title>
|
|
||||||
<g id="red">
|
|
||||||
<circle class="cls-1" cx="512" cy="512" r="512"/>
|
|
||||||
<path class="cls-2" d="M512,1024A512.13,512.13,0,0,1,312.7,40.24,512.13,512.13,0,0,1,711.3,983.76,508.81,508.81,0,0,1,512,1024Zm0-984A472.13,472.13,0,0,0,328.28,946.92,472.13,472.13,0,0,0,695.72,77.08,469,469,0,0,0,512,40Z"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 594 B |
|
@ -1,25 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024" viewBox="0 0 1024 1024">
|
|
||||||
<defs>
|
|
||||||
<style>
|
|
||||||
.cls-1 {
|
|
||||||
fill: #f55449;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cls-2 {
|
|
||||||
fill: #333;
|
|
||||||
opacity: 0.1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cls-3 {
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</defs>
|
|
||||||
<title>btn_close_hover</title>
|
|
||||||
<g id="red">
|
|
||||||
<circle class="cls-1" cx="512" cy="512" r="512"/>
|
|
||||||
<path class="cls-2" d="M512,1024A512.13,512.13,0,0,1,312.7,40.24,512.13,512.13,0,0,1,711.3,983.76,508.81,508.81,0,0,1,512,1024Zm0-984A472.13,472.13,0,0,0,328.28,946.92,472.13,472.13,0,0,0,695.72,77.08,469,469,0,0,0,512,40Z"/>
|
|
||||||
<rect class="cls-3" x="192" y="476" width="640" height="72" rx="36" ry="36" transform="translate(512 1236.08) rotate(-135)"/>
|
|
||||||
<rect class="cls-3" x="191" y="475" width="640" height="72" rx="36" ry="36" transform="translate(1233.66 511) rotate(135)"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 899 B |
|
@ -1,19 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024" viewBox="0 0 1024 1024">
|
|
||||||
<defs>
|
|
||||||
<style>
|
|
||||||
.cls-1 {
|
|
||||||
fill: #dee1e3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cls-2 {
|
|
||||||
fill: #333;
|
|
||||||
opacity: 0.1;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</defs>
|
|
||||||
<title>btn_grey</title>
|
|
||||||
<g id="grey">
|
|
||||||
<circle class="cls-1" cx="512" cy="512" r="512"/>
|
|
||||||
<path class="cls-2" d="M512,1024A512.13,512.13,0,0,1,312.7,40.24,512.13,512.13,0,0,1,711.3,983.76,508.81,508.81,0,0,1,512,1024Zm0-984A472.13,472.13,0,0,0,328.28,946.92,472.13,472.13,0,0,0,695.72,77.08,469,469,0,0,0,512,40Z"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 588 B |
|
@ -1,19 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024" viewBox="0 0 1024 1024">
|
|
||||||
<defs>
|
|
||||||
<style>
|
|
||||||
.cls-1 {
|
|
||||||
fill: #39ea49;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cls-2 {
|
|
||||||
fill: #333;
|
|
||||||
opacity: 0.1;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</defs>
|
|
||||||
<title>btn_max_hover</title>
|
|
||||||
<g id="green">
|
|
||||||
<circle class="cls-1" cx="512" cy="512" r="512"/>
|
|
||||||
<path class="cls-2" d="M512,1024A512.13,512.13,0,0,1,312.7,40.24,512.13,512.13,0,0,1,711.3,983.76,508.81,508.81,0,0,1,512,1024Zm0-984A472.13,472.13,0,0,0,328.28,946.92,472.13,472.13,0,0,0,695.72,77.08,469,469,0,0,0,512,40Z"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 594 B |
|
@ -1,24 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024" viewBox="0 0 1024 1024">
|
|
||||||
<defs>
|
|
||||||
<style>
|
|
||||||
.cls-1 {
|
|
||||||
fill: #39ea49;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cls-2 {
|
|
||||||
fill: #333;
|
|
||||||
opacity: 0.1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cls-3 {
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</defs>
|
|
||||||
<title>btn_max_hover</title>
|
|
||||||
<g id="green">
|
|
||||||
<circle class="cls-1" cx="512" cy="512" r="512"/>
|
|
||||||
<path class="cls-2" d="M512,1024A512.13,512.13,0,0,1,312.7,40.24,512.13,512.13,0,0,1,711.3,983.76,508.81,508.81,0,0,1,512,1024Zm0-984A472.13,472.13,0,0,0,328.28,946.92,472.13,472.13,0,0,0,695.72,77.08,469,469,0,0,0,512,40Z"/>
|
|
||||||
<path class="cls-3" d="M395.93,267.8a31.32,31.32,0,0,1,22.86-9.48l319.33-2.55A31.25,31.25,0,0,1,770,287.64l-2.5,319.29A32,32,0,0,1,758,629.79a31.06,31.06,0,0,1-22.86,9.48,30.67,30.67,0,0,1-22.72-9.15L395.65,313.33c-6.22-6.13-9.24-13.67-9.15-22.72a31.28,31.28,0,0,1,9.43-22.82ZM629.84,758A31.32,31.32,0,0,1,607,767.45L287.64,770a31.25,31.25,0,0,1-31.87-31.87l2.5-319.29A32,32,0,0,1,267.75,396a31.06,31.06,0,0,1,22.86-9.48,30.67,30.67,0,0,1,22.72,9.15L630.12,712.44c6.22,6.13,9.24,13.67,9.15,22.72A31.28,31.28,0,0,1,629.84,758Z"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 1.1 KiB |
|
@ -1,19 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024" viewBox="0 0 1024 1024">
|
|
||||||
<defs>
|
|
||||||
<style>
|
|
||||||
.cls-1 {
|
|
||||||
fill: #fac536;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cls-2 {
|
|
||||||
fill: #333;
|
|
||||||
opacity: 0.1;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</defs>
|
|
||||||
<title>btn_min_hover</title>
|
|
||||||
<g id="yellow">
|
|
||||||
<circle class="cls-1" cx="512" cy="512" r="512"/>
|
|
||||||
<path class="cls-2" d="M512,1024A512.13,512.13,0,0,1,312.7,40.24,512.13,512.13,0,0,1,711.3,983.76,508.81,508.81,0,0,1,512,1024Zm0-984A472.13,472.13,0,0,0,328.28,946.92,472.13,472.13,0,0,0,695.72,77.08,469,469,0,0,0,512,40Z"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 595 B |
|
@ -1,24 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024" viewBox="0 0 1024 1024">
|
|
||||||
<defs>
|
|
||||||
<style>
|
|
||||||
.cls-1 {
|
|
||||||
fill: #fac536;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cls-2 {
|
|
||||||
fill: #333;
|
|
||||||
opacity: 0.1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cls-3 {
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</defs>
|
|
||||||
<title>btn_min_hover</title>
|
|
||||||
<g id="yellow">
|
|
||||||
<circle class="cls-1" cx="512" cy="512" r="512"/>
|
|
||||||
<path class="cls-2" d="M512,1024A512.13,512.13,0,0,1,312.7,40.24,512.13,512.13,0,0,1,711.3,983.76,508.81,508.81,0,0,1,512,1024Zm0-984A472.13,472.13,0,0,0,328.28,946.92,472.13,472.13,0,0,0,695.72,77.08,469,469,0,0,0,512,40Z"/>
|
|
||||||
<rect class="cls-3" x="192" y="476" width="640" height="72" rx="36" ry="36"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 723 B |
|
@ -16,11 +16,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="title-bar app-drag">
|
<div class="title-bar app-drag">
|
||||||
<div class="btn-box app-nodrag focus">
|
|
||||||
<a class="item quit"></a>
|
|
||||||
<a class="item min"></a>
|
|
||||||
<a class="item max"></a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<main class="main-body">
|
<main class="main-body">
|
||||||
|
@ -57,8 +53,8 @@
|
||||||
<wc-scroll class="list">
|
<wc-scroll class="list">
|
||||||
<section
|
<section
|
||||||
class="item"
|
class="item"
|
||||||
@click="prviewSong(it, i)"
|
@click="previewSong(it, i)"
|
||||||
@dblclick="playSong(it, i)"
|
@dblclick="playSong(i)"
|
||||||
:class="{on: curr === i}"
|
:class="{on: curr === i}"
|
||||||
:for="i it in list">
|
:for="i it in list">
|
||||||
<span class="idx" :text="i + 1"></span>
|
<span class="idx" :text="i + 1"></span>
|
||||||
|
@ -75,7 +71,8 @@
|
||||||
<section class="stat-bar">
|
<section class="stat-bar">
|
||||||
<span class="time" :text="song.time | time"></span>
|
<span class="time" :text="song.time | time"></span>
|
||||||
<div class="progress">
|
<div class="progress">
|
||||||
<span class="thumb" :css="{width: ~~((100 * song.time) / song.duration) + '%'}"></span>
|
<span class="thumb" :css="{width: ((100 * song.time) / song.duration).toFixed(1) + '%'}"></span>
|
||||||
|
<input type="range" max="712" min="0" step="1" :duplex="progress">
|
||||||
</div>
|
</div>
|
||||||
<span class="time" :text="song.duration | time"></span>
|
<span class="time" :text="song.duration | time"></span>
|
||||||
</section>
|
</section>
|
||||||
|
@ -87,22 +84,30 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="play-btn">
|
<div class="play-btn">
|
||||||
<a class="item prev"></a>
|
<a class="item prev" @click="play(-1)"></a>
|
||||||
<a class="item" :class="{on: isplaying, off: !isplaying}" @click="play"></a>
|
<a class="item"
|
||||||
<a class="item next"></a>
|
:class="{on: isplaying, off: !isplaying}"
|
||||||
|
@click="play(0)">
|
||||||
|
</a>
|
||||||
|
<a class="item next" @click="play(1)"></a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="holder"></div>
|
<div class="holder"></div>
|
||||||
|
|
||||||
<div class="play-action">
|
<div class="play-action">
|
||||||
<a class="item" :class="{
|
<item class="item" :class="{
|
||||||
all: playmode === 1,
|
all: playmode === 1,
|
||||||
single: playmode === 2,
|
single: playmode === 2,
|
||||||
rand: playmode === 3,
|
rand: playmode === 3,
|
||||||
}"
|
}"
|
||||||
@click="switchMode">
|
@click="switchMode">
|
||||||
</a>
|
</item>
|
||||||
<a class="item" :class="{mute: mute, volume: !mute}" @click="toggleMute"></a>
|
<item class="item" :class="{mute: mute || volume <= 0, volume: !mute && volume > 0}">
|
||||||
|
<div class="range">
|
||||||
|
<input type="range" max="100" min="0" step="1" :duplex="volume">
|
||||||
|
</div>
|
||||||
|
<a @click="toggleMute"></a>
|
||||||
|
</item>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
162
src/js/app.js
|
@ -15,6 +15,8 @@ import Player from '/js/lib/audio/index.js'
|
||||||
|
|
||||||
const id3 = require('music-metadata')
|
const id3 = require('music-metadata')
|
||||||
|
|
||||||
|
const MODE_DICT = { 1: 'all', 2: 'single', 3: 'random' }
|
||||||
|
|
||||||
var kb = new Keyboard()
|
var kb = new Keyboard()
|
||||||
|
|
||||||
var player = new Player()
|
var player = new Player()
|
||||||
|
@ -26,9 +28,10 @@ Anot({
|
||||||
$id: 'app',
|
$id: 'app',
|
||||||
state: {
|
state: {
|
||||||
defaultCover: '/images/disk.png',
|
defaultCover: '/images/disk.png',
|
||||||
isplaying: true,
|
isplaying: false,
|
||||||
playmode: 1,
|
playmode: +Anot.ls('app_mode') || 1,
|
||||||
mute: false,
|
mute: false,
|
||||||
|
volume: +Anot.ls('app_volume') || 50,
|
||||||
preview: {
|
preview: {
|
||||||
name: '',
|
name: '',
|
||||||
album: '',
|
album: '',
|
||||||
|
@ -42,24 +45,13 @@ Anot({
|
||||||
time: 0,
|
time: 0,
|
||||||
duration: 0
|
duration: 0
|
||||||
},
|
},
|
||||||
curr: 2,
|
progress: 0,
|
||||||
|
curr: -1,
|
||||||
list: []
|
list: []
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
var list = app.dispatch('scan-dir', { path: '/Volumes/extends/music' })
|
var list = app.dispatch('get-all-songs')
|
||||||
|
// var list = app.dispatch('scan-dir', { dir: '/Volumes/extends/music' })
|
||||||
this.list = list
|
|
||||||
|
|
||||||
for (let it of this.list) {
|
|
||||||
let { album, artist, title, duration } = await this.getID3(it.path)
|
|
||||||
|
|
||||||
it.name = title || it.name
|
|
||||||
it.artist = artist
|
|
||||||
it.album = album
|
|
||||||
it.duration = duration
|
|
||||||
}
|
|
||||||
|
|
||||||
player.load(list.map(it => `sonist://${it.path}`))
|
|
||||||
|
|
||||||
kb.on(['left'], ev => {
|
kb.on(['left'], ev => {
|
||||||
var time = this.song.time - 5
|
var time = this.song.time - 5
|
||||||
|
@ -75,17 +67,121 @@ Anot({
|
||||||
}
|
}
|
||||||
this.song.time = time
|
this.song.time = time
|
||||||
})
|
})
|
||||||
|
kb.on(['down'], ev => {
|
||||||
|
var vol = +this.volume - 5
|
||||||
|
if (vol < 0) {
|
||||||
|
vol = 0
|
||||||
|
}
|
||||||
|
this.volume = vol
|
||||||
|
})
|
||||||
|
kb.on(['up'], ev => {
|
||||||
|
var vol = +this.volume + 5
|
||||||
|
if (vol > 100) {
|
||||||
|
vol = 100
|
||||||
|
}
|
||||||
|
this.volume = vol
|
||||||
|
})
|
||||||
|
|
||||||
|
// for (let it of list) {
|
||||||
|
// let { album, artist, title, duration } = await this.getID3(it.file_path)
|
||||||
|
|
||||||
|
// it.name = title || it.name
|
||||||
|
// it.artist = artist
|
||||||
|
// it.album = album
|
||||||
|
// it.duration = duration
|
||||||
|
// app.dispatch('add-song', {
|
||||||
|
// name: it.name,
|
||||||
|
// artist,
|
||||||
|
// album,
|
||||||
|
// duration,
|
||||||
|
// file_path: it.path
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
this.list = list
|
||||||
|
|
||||||
|
player.volume = this.volume / 100
|
||||||
|
player.load(list.map(it => `sonist://${it.file_path}`))
|
||||||
|
|
||||||
|
player.on('play', time => {
|
||||||
|
this.song.time = time
|
||||||
|
})
|
||||||
|
player.on('stop', _ => {
|
||||||
|
var idx = this.curr
|
||||||
|
var repeat = false
|
||||||
|
switch (this.playmode) {
|
||||||
|
case 1: // all
|
||||||
|
idx++
|
||||||
|
if (idx >= this.list.length) {
|
||||||
|
idx = 0
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 2: // single
|
||||||
|
repeat = true
|
||||||
|
break
|
||||||
|
case 3: // random
|
||||||
|
idx = ~~(Math.random() * this.list.length)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
this.playSong(this.list[idx], idx, repeat)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
volume(v) {
|
||||||
|
Anot.ls('app_volume', v)
|
||||||
|
player.volume = v / 100
|
||||||
|
},
|
||||||
|
playmode(v) {
|
||||||
|
Anot.ls('app_mode', v)
|
||||||
|
},
|
||||||
|
progress(v) {
|
||||||
|
var t = +(this.song.duration * (v / 712)).toFixed(2)
|
||||||
|
player.seek(t)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
load() {
|
play(act) {
|
||||||
// window.player = new Howl({
|
var idx = this.curr
|
||||||
// src: [`sonist://${this.list[0].path}`]
|
var repeat = false
|
||||||
// })
|
|
||||||
// window.player = this.__PLAYER__
|
switch (act) {
|
||||||
},
|
case 0:
|
||||||
play() {
|
if (idx > -1) {
|
||||||
|
player.play(-1)
|
||||||
|
} else {
|
||||||
|
this.playSong(0)
|
||||||
|
}
|
||||||
this.isplaying = !this.isplaying
|
this.isplaying = !this.isplaying
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
switch (this.playmode) {
|
||||||
|
case 1: // all
|
||||||
|
idx += act
|
||||||
|
if (idx < 0) {
|
||||||
|
idx = this.list.length - 1
|
||||||
|
}
|
||||||
|
if (idx >= this.list.length) {
|
||||||
|
idx = 0
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 2: // single
|
||||||
|
if (idx === -1) {
|
||||||
|
idx = ~~(Math.random() * this.list.length)
|
||||||
|
}
|
||||||
|
repeat = true
|
||||||
|
break
|
||||||
|
case 3: // random
|
||||||
|
idx = ~~(Math.random() * this.list.length)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
this.playSong(idx, repeat)
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
switchMode() {
|
switchMode() {
|
||||||
var n = this.playmode + 1
|
var n = this.playmode + 1
|
||||||
if (n > 3) {
|
if (n > 3) {
|
||||||
|
@ -105,18 +201,30 @@ Anot({
|
||||||
return { album, artist, title, duration: ~~duration }
|
return { album, artist, title, duration: ~~duration }
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
prviewSong(it, i) {
|
previewSong(it) {
|
||||||
var { album, artist, name, cover } = it
|
var { album, artist, name, cover } = it
|
||||||
Object.assign(this.preview, { album, artist, name, cover })
|
Object.assign(this.preview, { album, artist, name, cover })
|
||||||
},
|
},
|
||||||
playSong(it, i) {
|
playSong(i, repeat) {
|
||||||
//
|
//
|
||||||
|
var it = this.list[i]
|
||||||
|
|
||||||
|
if (this.curr === i) {
|
||||||
|
if (repeat) {
|
||||||
|
this.song.time = 0
|
||||||
|
player.play(-1, repeat)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
this.curr = i
|
this.curr = i
|
||||||
this.song.name = it.name
|
this.song.name = it.name
|
||||||
this.song.artist = it.artist
|
this.song.artist = it.artist
|
||||||
this.song.duration = it.duration
|
this.song.duration = it.duration
|
||||||
this.song.src = `file://${it.path}`
|
this.song.src = `file://${it.file_path}`
|
||||||
this.song.time = 0
|
this.song.time = 0
|
||||||
|
this.isplaying = true
|
||||||
|
this.previewSong(it)
|
||||||
|
player.play(i)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -4,8 +4,12 @@
|
||||||
* @date 2020/11/19 17:32:19
|
* @date 2020/11/19 17:32:19
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import $ from '../utils.js'
|
||||||
import fetch from '../fetch/index.js'
|
import fetch from '../fetch/index.js'
|
||||||
|
|
||||||
|
const { EventEmitter } = require('events')
|
||||||
|
const util = require('util')
|
||||||
|
|
||||||
function hide(target, key, value) {
|
function hide(target, key, value) {
|
||||||
Object.defineProperty(target, key, {
|
Object.defineProperty(target, key, {
|
||||||
value,
|
value,
|
||||||
|
@ -19,16 +23,39 @@ export default class Player {
|
||||||
constructor() {
|
constructor() {
|
||||||
hide(this, '__LIST__', [])
|
hide(this, '__LIST__', [])
|
||||||
hide(this, '__AC__', new AudioContext())
|
hide(this, '__AC__', new AudioContext())
|
||||||
hide(this, '__AUDIO__', new Audio())
|
|
||||||
hide(this, 'props', {
|
hide(this, 'props', {
|
||||||
curr: '',
|
curr: '',
|
||||||
stat: 'ready',
|
stat: 'ready',
|
||||||
volume: 0.5,
|
volume: 0.5,
|
||||||
mode: 'all', // 循环模式, all, single, rand
|
|
||||||
time: 0,
|
|
||||||
duration: 0
|
duration: 0
|
||||||
})
|
})
|
||||||
hide(this, 'track', null)
|
hide(this, 'track', null)
|
||||||
|
hide(this, 'gain', null)
|
||||||
|
|
||||||
|
this.__main__()
|
||||||
|
}
|
||||||
|
|
||||||
|
__main__() {
|
||||||
|
hide(this, '__AUDIO__', new Audio())
|
||||||
|
|
||||||
|
this.__playFn = $.bind(this.__AUDIO__, 'timeupdate', _ => {
|
||||||
|
this.emit('play', this.__AUDIO__.currentTime)
|
||||||
|
})
|
||||||
|
this.__stopFn = $.bind(this.__AUDIO__, 'ended', _ => {
|
||||||
|
this.props.stat = 'paused'
|
||||||
|
this.emit('stop')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
__destroy__() {
|
||||||
|
$.unbind(this.__AUDIO__, 'timeupdate', this.__playFn)
|
||||||
|
$.unbind(this.__AUDIO__, 'ended', this.__stopFn)
|
||||||
|
|
||||||
|
this.__AUDIO__.pause()
|
||||||
|
this.__AUDIO__.currentTime = 0
|
||||||
|
|
||||||
|
delete this.__playFn
|
||||||
|
delete this.__stopFn
|
||||||
}
|
}
|
||||||
|
|
||||||
load(list) {
|
load(list) {
|
||||||
|
@ -37,11 +64,16 @@ export default class Player {
|
||||||
}
|
}
|
||||||
|
|
||||||
async _getTrack(file) {
|
async _getTrack(file) {
|
||||||
|
this.__main__()
|
||||||
this.__AUDIO__.src = URL.createObjectURL(
|
this.__AUDIO__.src = URL.createObjectURL(
|
||||||
await fetch(file).then(r => r.blob())
|
await fetch(file).then(r => r.blob())
|
||||||
)
|
)
|
||||||
console.log(this.__AUDIO__)
|
|
||||||
return this.__AC__.createMediaElementSource(this.__AUDIO__)
|
this.gain = this.__AC__.createGain()
|
||||||
|
this.gain.gain.value = this.volume
|
||||||
|
|
||||||
|
this.track = this.__AC__.createMediaElementSource(this.__AUDIO__)
|
||||||
|
this.track.connect(this.gain).connect(this.__AC__.destination)
|
||||||
}
|
}
|
||||||
|
|
||||||
get volume() {
|
get volume() {
|
||||||
|
@ -49,7 +81,7 @@ export default class Player {
|
||||||
}
|
}
|
||||||
|
|
||||||
set volume(val) {
|
set volume(val) {
|
||||||
val = +val || 0.5
|
val = +val
|
||||||
if (val < 0) {
|
if (val < 0) {
|
||||||
val = 0
|
val = 0
|
||||||
}
|
}
|
||||||
|
@ -57,31 +89,31 @@ export default class Player {
|
||||||
val = 1
|
val = 1
|
||||||
}
|
}
|
||||||
this.props.volume = val
|
this.props.volume = val
|
||||||
|
if (this.gain) {
|
||||||
|
this.gain.gain.value = val
|
||||||
}
|
}
|
||||||
|
|
||||||
get mode() {
|
|
||||||
return this.props.mode
|
|
||||||
}
|
|
||||||
|
|
||||||
set mode(val) {
|
|
||||||
this.props.mode = val
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get time() {
|
get time() {
|
||||||
return this.props.time
|
return this.__AUDIO__.currentTime
|
||||||
}
|
}
|
||||||
|
|
||||||
get stat() {
|
get stat() {
|
||||||
return this.props.stat
|
return this.props.stat
|
||||||
}
|
}
|
||||||
|
|
||||||
async play(id) {
|
/**
|
||||||
var url, gain
|
* id: 歌曲序号
|
||||||
|
* force: 强制重新播放
|
||||||
if (id === undefined) {
|
*/
|
||||||
|
play(id, force = false) {
|
||||||
|
if (id === -1) {
|
||||||
if (this.track) {
|
if (this.track) {
|
||||||
|
if (force) {
|
||||||
|
this.seek(0)
|
||||||
|
this.props.stat = 'paused'
|
||||||
|
}
|
||||||
if (this.stat === 'playing') {
|
if (this.stat === 'playing') {
|
||||||
this.props.time = this.track.context.currentTime
|
|
||||||
this.__AUDIO__.pause()
|
this.__AUDIO__.pause()
|
||||||
this.props.stat = 'paused'
|
this.props.stat = 'paused'
|
||||||
} else if (this.stat === 'paused') {
|
} else if (this.stat === 'paused') {
|
||||||
|
@ -90,21 +122,17 @@ export default class Player {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
url = this.__LIST__[id]
|
var url = this.__LIST__[id]
|
||||||
if (!url || this.props.curr === url) {
|
if (!url || this.props.curr === url) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
gain = this.__AC__.createGain()
|
|
||||||
|
|
||||||
if (this.track) {
|
if (this.track) {
|
||||||
this.stop()
|
this.stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
gain.gain.value = this.volume
|
this.props.curr = url
|
||||||
|
this._getTrack(url)
|
||||||
this.track = await this._getTrack(url)
|
|
||||||
this.track.connect(gain).connect(this.__AC__.destination)
|
|
||||||
|
|
||||||
this.__AUDIO__.play()
|
this.__AUDIO__.play()
|
||||||
this.props.stat = 'playing'
|
this.props.stat = 'playing'
|
||||||
|
@ -112,15 +140,19 @@ export default class Player {
|
||||||
}
|
}
|
||||||
|
|
||||||
seek(time) {
|
seek(time) {
|
||||||
this.props.time = time
|
if (this.track) {
|
||||||
|
this.__AUDIO__.currentTime = time
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
stop() {
|
stop() {
|
||||||
if (this.track) {
|
if (this.track) {
|
||||||
this.__AUDIO__.pause()
|
|
||||||
this.track = null
|
this.track = null
|
||||||
this.props.time = 0
|
this.gain = null
|
||||||
|
this.__destroy__()
|
||||||
this.props.stat = 'stoped'
|
this.props.stat = 'stoped'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
util.inherits(Player, EventEmitter)
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
const { ipcRenderer } = require('electron')
|
const { ipcRenderer } = require('electron')
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
dispatch(type = '', params = {}) {
|
dispatch(type = '', data = {}) {
|
||||||
return ipcRenderer.sendSync('app', Object.assign(params, { type }))
|
return ipcRenderer.sendSync('app', { data, type })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
23
src/main.js
|
@ -7,12 +7,12 @@
|
||||||
const { app, session, protocol, globalShortcut } = require('electron')
|
const { app, session, protocol, globalShortcut } = require('electron')
|
||||||
const path = require('path')
|
const path = require('path')
|
||||||
const fs = require('iofs')
|
const fs = require('iofs')
|
||||||
// const {exec} = require('child_process')
|
|
||||||
|
|
||||||
require('./tools/init.js')
|
require('./tools/init.js')
|
||||||
|
|
||||||
|
const { createMainWindow, createMiniWindow } = require('./tools/windows.js')
|
||||||
const { createAppTray, createLrcTray } = require('./tools/tray.js')
|
const { createAppTray, createLrcTray } = require('./tools/tray.js')
|
||||||
const createMenu = require('./tools/menu.js')
|
const createMenu = require('./tools/menu.js')
|
||||||
const { createMainWindow, createMiniWindow } = require('./tools/windows.js')
|
|
||||||
|
|
||||||
const MIME_TYPES = {
|
const MIME_TYPES = {
|
||||||
'.js': 'text/javascript',
|
'.js': 'text/javascript',
|
||||||
|
@ -47,18 +47,17 @@ protocol.registerSchemesAsPrivileged([
|
||||||
// 初始化应用
|
// 初始化应用
|
||||||
app.once('ready', () => {
|
app.once('ready', () => {
|
||||||
// 注册协议
|
// 注册协议
|
||||||
protocol.registerBufferProtocol('app', (req, cb) => {
|
protocol.registerBufferProtocol('app', function(req, cb) {
|
||||||
let file = decodeURIComponent(req.url.replace(/^app:\/\/local\//, ''))
|
var file = decodeURIComponent(req.url.replace(/^app:\/\/local\//, ''))
|
||||||
let ext = path.extname(req.url)
|
var ext = path.extname(req.url)
|
||||||
let buff = fs.cat(path.resolve(__dirname, file))
|
var buff = fs.cat(path.resolve(__dirname, file))
|
||||||
cb({ data: buff, mimeType: MIME_TYPES[ext] })
|
cb({ data: buff, mimeType: MIME_TYPES[ext] })
|
||||||
})
|
})
|
||||||
|
|
||||||
protocol.registerBufferProtocol('sonist', (req, cb) => {
|
protocol.registerBufferProtocol('sonist', function(req, cb) {
|
||||||
let file = decodeURIComponent(req.url.replace(/^sonist:[\/]+/, '/'))
|
var file = decodeURIComponent(req.url.replace(/^sonist:[\/]+/, '/'))
|
||||||
let ext = path.extname(req.url)
|
var ext = path.extname(req.url)
|
||||||
let buff = fs.cat(file)
|
cb({ data: fs.cat(file), mimeType: MIME_TYPES[ext] || MIME_TYPES.all })
|
||||||
cb({ data: buff, mimeType: MIME_TYPES[ext] || MIME_TYPES.all })
|
|
||||||
})
|
})
|
||||||
// 修改app的UA
|
// 修改app的UA
|
||||||
session.defaultSession.setUserAgent(
|
session.defaultSession.setUserAgent(
|
||||||
|
@ -74,7 +73,7 @@ app.once('ready', () => {
|
||||||
// mac专属事件,点击dock栏图标,可激活窗口
|
// mac专属事件,点击dock栏图标,可激活窗口
|
||||||
app.on('activate', _ => {
|
app.on('activate', _ => {
|
||||||
if (win) {
|
if (win) {
|
||||||
win.webContents.send('dock-click')
|
win.restore()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
* @date 2020/07/14 18:17:59
|
* @date 2020/07/14 18:17:59
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// 歌单
|
||||||
const TABLE_PLAYLIST = `
|
const TABLE_PLAYLIST = `
|
||||||
CREATE TABLE IF NOT EXISTS "playlist" (
|
CREATE TABLE IF NOT EXISTS "playlist" (
|
||||||
"id" integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
"id" integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
@ -11,15 +12,16 @@ CREATE TABLE IF NOT EXISTS "playlist" (
|
||||||
)
|
)
|
||||||
`
|
`
|
||||||
|
|
||||||
|
// aid: 歌手ID
|
||||||
const TABLE_SONGS = `
|
const TABLE_SONGS = `
|
||||||
CREATE TABLE IF NOT EXISTS "songs" (
|
CREATE TABLE IF NOT EXISTS "songs" (
|
||||||
"id" integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
"id" integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
"pid" integer NOT NULL,
|
|
||||||
"aid" integer NOT NULL,
|
|
||||||
"name" char(128) NOT NULL,
|
"name" char(128) NOT NULL,
|
||||||
|
"artist" char(128) NOT NULL,
|
||||||
"album" char(128) NOT NULL,
|
"album" char(128) NOT NULL,
|
||||||
|
"duration" integer NOT NULL,
|
||||||
"cover" char(256) NOT NULL,
|
"cover" char(256) NOT NULL,
|
||||||
"path" char(256) NOT NULL,
|
"file_path" char(256) NOT NULL,
|
||||||
"lrc" text NOT NULL
|
"lrc" text NOT NULL
|
||||||
)
|
)
|
||||||
`
|
`
|
||||||
|
@ -28,15 +30,7 @@ const TABLE_RELATIONS = `
|
||||||
CREATE TABLE IF NOT EXISTS "relations" (
|
CREATE TABLE IF NOT EXISTS "relations" (
|
||||||
"id" integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
"id" integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
"sid" integer NOT NULL,
|
"sid" integer NOT NULL,
|
||||||
"pid" integer NOT NULL,
|
"pid" integer NOT NULL
|
||||||
)
|
|
||||||
`
|
|
||||||
|
|
||||||
const TABLE_ARTISTS = `
|
|
||||||
CREATE TABLE IF NOT EXISTS "artists" (
|
|
||||||
"id" integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
|
||||||
"name" integer NOT NULL,
|
|
||||||
"avatar" char(256) NOT NULL
|
|
||||||
)
|
)
|
||||||
`
|
`
|
||||||
|
|
||||||
|
@ -50,5 +44,4 @@ module.exports = function(db) {
|
||||||
db.query(TABLE_PLAYLIST).catch(error)
|
db.query(TABLE_PLAYLIST).catch(error)
|
||||||
db.query(TABLE_SONGS).catch(error)
|
db.query(TABLE_SONGS).catch(error)
|
||||||
db.query(TABLE_RELATIONS).catch(error)
|
db.query(TABLE_RELATIONS).catch(error)
|
||||||
db.query(TABLE_ARTISTS).catch(error)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -74,11 +74,63 @@ ipcMain.on('app', (ev, conn) => {
|
||||||
fs.echo(JSON.stringify(conn.data, null, 2), INIT_FILE)
|
fs.echo(JSON.stringify(conn.data, null, 2), INIT_FILE)
|
||||||
break
|
break
|
||||||
|
|
||||||
|
case 'add-song':
|
||||||
|
var {
|
||||||
|
name,
|
||||||
|
artist = '',
|
||||||
|
album = '',
|
||||||
|
duration,
|
||||||
|
cover = '',
|
||||||
|
file_path,
|
||||||
|
lrc = ''
|
||||||
|
} = conn.data
|
||||||
|
db.query(
|
||||||
|
'INSERT INTO `songs` (name, artist, album, duration, cover, file_path, lrc) VALUES ($name, $artist, $album, $duration, $cover, $file_path, $lrc)',
|
||||||
|
{
|
||||||
|
$name: name,
|
||||||
|
$artist: artist,
|
||||||
|
$album: album,
|
||||||
|
$duration: duration,
|
||||||
|
$cover: cover,
|
||||||
|
$file_path: file_path,
|
||||||
|
$lrc: lrc
|
||||||
|
}
|
||||||
|
)
|
||||||
|
ev.returnValue = true
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'get-all-songs':
|
||||||
|
db.getAll('SELECT id, name, duration, artist, file_path FROM songs')
|
||||||
|
.then(res => {
|
||||||
|
ev.returnValue = res
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
ev.returnValue = err
|
||||||
|
})
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'get-songs':
|
||||||
|
db.getAll(
|
||||||
|
'SELECT id, name, duration, artist, file_path ' +
|
||||||
|
'FROM songs ' +
|
||||||
|
'WHERE id IN ' +
|
||||||
|
'(SELECT sid FROM relations WHERE pid = $pid)',
|
||||||
|
{ $pid: conn.pid }
|
||||||
|
)
|
||||||
|
.then(res => {
|
||||||
|
ev.returnValue = res
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
ev.returnValue = err
|
||||||
|
})
|
||||||
|
break
|
||||||
|
|
||||||
// 扫描目录
|
// 扫描目录
|
||||||
case 'scan-dir':
|
case 'scan-dir':
|
||||||
if (fs.isdir(conn.path)) {
|
var { dir } = conn.data
|
||||||
|
if (fs.isdir(dir)) {
|
||||||
let list = fs
|
let list = fs
|
||||||
.ls(conn.path, true)
|
.ls(dir, true)
|
||||||
.filter(it => {
|
.filter(it => {
|
||||||
if (fs.isdir(it)) {
|
if (fs.isdir(it)) {
|
||||||
return false
|
return false
|
||||||
|
@ -97,7 +149,13 @@ ipcMain.on('app', (ev, conn) => {
|
||||||
end: 256,
|
end: 256,
|
||||||
encoding: 'base64'
|
encoding: 'base64'
|
||||||
})
|
})
|
||||||
return { name, path: it, artist: '', album: '', duration: '00:00' }
|
return {
|
||||||
|
name,
|
||||||
|
file_path: it,
|
||||||
|
artist: '',
|
||||||
|
album: '',
|
||||||
|
duration: '00:00'
|
||||||
|
}
|
||||||
})
|
})
|
||||||
ev.returnValue = list
|
ev.returnValue = list
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -15,7 +15,13 @@ module.exports = function(win) {
|
||||||
submenu: [
|
submenu: [
|
||||||
{ role: 'about', label: '关于 Sonist' },
|
{ role: 'about', label: '关于 Sonist' },
|
||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
{ role: 'quit', label: '退出' }
|
{
|
||||||
|
label: '退出 Sonist',
|
||||||
|
accelerator: 'Command+Q',
|
||||||
|
click(a, b, ev) {
|
||||||
|
win.destroy()
|
||||||
|
}
|
||||||
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -16,6 +16,7 @@ exports.createMainWindow = function(icon) {
|
||||||
width: 820,
|
width: 820,
|
||||||
height: 460,
|
height: 460,
|
||||||
frame: false,
|
frame: false,
|
||||||
|
titleBarStyle: 'hiddenInset',
|
||||||
resizable: false,
|
resizable: false,
|
||||||
maximizable: false,
|
maximizable: false,
|
||||||
icon,
|
icon,
|
||||||
|
@ -40,6 +41,11 @@ exports.createMainWindow = function(icon) {
|
||||||
win.openDevTools()
|
win.openDevTools()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
win.on('close', ev => {
|
||||||
|
ev.preventDefault()
|
||||||
|
win.hide()
|
||||||
|
})
|
||||||
|
|
||||||
return win
|
return win
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|