完成播放功能
|
@ -11,7 +11,7 @@
|
|||
"name": "yutent",
|
||||
"email": "yutent.io@gmail.com"
|
||||
},
|
||||
"homepage": "https://yutent.me",
|
||||
"homepage": "https://yutent.top",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"crypto.js": "^2.0.2",
|
||||
|
|
|
@ -39,41 +39,6 @@
|
|||
z-index: 9;
|
||||
height: 26px;
|
||||
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 {
|
||||
|
@ -255,11 +220,22 @@
|
|||
}
|
||||
|
||||
.progress {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
height: 3px;
|
||||
background: #b2cfe3;
|
||||
|
||||
input {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 3px;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.thumb {
|
||||
|
@ -373,7 +349,46 @@
|
|||
transition: background 0.1s ease-in-out;
|
||||
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);
|
||||
&:hover,
|
||||
&:active {
|
||||
|
@ -381,7 +396,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
&.mute {
|
||||
&.mute a {
|
||||
background-image: url(/images/ctrl/mute.png);
|
||||
&:hover,
|
||||
&: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 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>
|
||||
|
||||
<main class="main-body">
|
||||
|
@ -57,8 +53,8 @@
|
|||
<wc-scroll class="list">
|
||||
<section
|
||||
class="item"
|
||||
@click="prviewSong(it, i)"
|
||||
@dblclick="playSong(it, i)"
|
||||
@click="previewSong(it, i)"
|
||||
@dblclick="playSong(i)"
|
||||
:class="{on: curr === i}"
|
||||
:for="i it in list">
|
||||
<span class="idx" :text="i + 1"></span>
|
||||
|
@ -75,7 +71,8 @@
|
|||
<section class="stat-bar">
|
||||
<span class="time" :text="song.time | time"></span>
|
||||
<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>
|
||||
<span class="time" :text="song.duration | time"></span>
|
||||
</section>
|
||||
|
@ -87,22 +84,30 @@
|
|||
</div>
|
||||
|
||||
<div class="play-btn">
|
||||
<a class="item prev"></a>
|
||||
<a class="item" :class="{on: isplaying, off: !isplaying}" @click="play"></a>
|
||||
<a class="item next"></a>
|
||||
<a class="item prev" @click="play(-1)"></a>
|
||||
<a class="item"
|
||||
:class="{on: isplaying, off: !isplaying}"
|
||||
@click="play(0)">
|
||||
</a>
|
||||
<a class="item next" @click="play(1)"></a>
|
||||
</div>
|
||||
|
||||
<div class="holder"></div>
|
||||
|
||||
<div class="play-action">
|
||||
<a class="item" :class="{
|
||||
<item class="item" :class="{
|
||||
all: playmode === 1,
|
||||
single: playmode === 2,
|
||||
rand: playmode === 3,
|
||||
}"
|
||||
@click="switchMode">
|
||||
</a>
|
||||
<a class="item" :class="{mute: mute, volume: !mute}" @click="toggleMute"></a>
|
||||
</item>
|
||||
<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>
|
||||
</section>
|
||||
</div>
|
||||
|
|
164
src/js/app.js
|
@ -15,6 +15,8 @@ import Player from '/js/lib/audio/index.js'
|
|||
|
||||
const id3 = require('music-metadata')
|
||||
|
||||
const MODE_DICT = { 1: 'all', 2: 'single', 3: 'random' }
|
||||
|
||||
var kb = new Keyboard()
|
||||
|
||||
var player = new Player()
|
||||
|
@ -26,9 +28,10 @@ Anot({
|
|||
$id: 'app',
|
||||
state: {
|
||||
defaultCover: '/images/disk.png',
|
||||
isplaying: true,
|
||||
playmode: 1,
|
||||
isplaying: false,
|
||||
playmode: +Anot.ls('app_mode') || 1,
|
||||
mute: false,
|
||||
volume: +Anot.ls('app_volume') || 50,
|
||||
preview: {
|
||||
name: '',
|
||||
album: '',
|
||||
|
@ -42,24 +45,13 @@ Anot({
|
|||
time: 0,
|
||||
duration: 0
|
||||
},
|
||||
curr: 2,
|
||||
progress: 0,
|
||||
curr: -1,
|
||||
list: []
|
||||
},
|
||||
async mounted() {
|
||||
var list = app.dispatch('scan-dir', { path: '/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}`))
|
||||
var list = app.dispatch('get-all-songs')
|
||||
// var list = app.dispatch('scan-dir', { dir: '/Volumes/extends/music' })
|
||||
|
||||
kb.on(['left'], ev => {
|
||||
var time = this.song.time - 5
|
||||
|
@ -75,17 +67,121 @@ Anot({
|
|||
}
|
||||
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: {
|
||||
load() {
|
||||
// window.player = new Howl({
|
||||
// src: [`sonist://${this.list[0].path}`]
|
||||
// })
|
||||
// window.player = this.__PLAYER__
|
||||
},
|
||||
play() {
|
||||
this.isplaying = !this.isplaying
|
||||
play(act) {
|
||||
var idx = this.curr
|
||||
var repeat = false
|
||||
|
||||
switch (act) {
|
||||
case 0:
|
||||
if (idx > -1) {
|
||||
player.play(-1)
|
||||
} else {
|
||||
this.playSong(0)
|
||||
}
|
||||
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() {
|
||||
var n = this.playmode + 1
|
||||
if (n > 3) {
|
||||
|
@ -105,18 +201,30 @@ Anot({
|
|||
return { album, artist, title, duration: ~~duration }
|
||||
})
|
||||
},
|
||||
prviewSong(it, i) {
|
||||
previewSong(it) {
|
||||
var { album, artist, name, cover } = it
|
||||
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.song.name = it.name
|
||||
this.song.artist = it.artist
|
||||
this.song.duration = it.duration
|
||||
this.song.src = `file://${it.path}`
|
||||
this.song.src = `file://${it.file_path}`
|
||||
this.song.time = 0
|
||||
this.isplaying = true
|
||||
this.previewSong(it)
|
||||
player.play(i)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
@ -4,8 +4,12 @@
|
|||
* @date 2020/11/19 17:32:19
|
||||
*/
|
||||
|
||||
import $ from '../utils.js'
|
||||
import fetch from '../fetch/index.js'
|
||||
|
||||
const { EventEmitter } = require('events')
|
||||
const util = require('util')
|
||||
|
||||
function hide(target, key, value) {
|
||||
Object.defineProperty(target, key, {
|
||||
value,
|
||||
|
@ -19,16 +23,39 @@ export default class Player {
|
|||
constructor() {
|
||||
hide(this, '__LIST__', [])
|
||||
hide(this, '__AC__', new AudioContext())
|
||||
hide(this, '__AUDIO__', new Audio())
|
||||
hide(this, 'props', {
|
||||
curr: '',
|
||||
stat: 'ready',
|
||||
volume: 0.5,
|
||||
mode: 'all', // 循环模式, all, single, rand
|
||||
time: 0,
|
||||
duration: 0
|
||||
})
|
||||
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) {
|
||||
|
@ -37,11 +64,16 @@ export default class Player {
|
|||
}
|
||||
|
||||
async _getTrack(file) {
|
||||
this.__main__()
|
||||
this.__AUDIO__.src = URL.createObjectURL(
|
||||
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() {
|
||||
|
@ -49,7 +81,7 @@ export default class Player {
|
|||
}
|
||||
|
||||
set volume(val) {
|
||||
val = +val || 0.5
|
||||
val = +val
|
||||
if (val < 0) {
|
||||
val = 0
|
||||
}
|
||||
|
@ -57,31 +89,31 @@ export default class Player {
|
|||
val = 1
|
||||
}
|
||||
this.props.volume = val
|
||||
}
|
||||
|
||||
get mode() {
|
||||
return this.props.mode
|
||||
}
|
||||
|
||||
set mode(val) {
|
||||
this.props.mode = val
|
||||
if (this.gain) {
|
||||
this.gain.gain.value = val
|
||||
}
|
||||
}
|
||||
|
||||
get time() {
|
||||
return this.props.time
|
||||
return this.__AUDIO__.currentTime
|
||||
}
|
||||
|
||||
get stat() {
|
||||
return this.props.stat
|
||||
}
|
||||
|
||||
async play(id) {
|
||||
var url, gain
|
||||
|
||||
if (id === undefined) {
|
||||
/**
|
||||
* id: 歌曲序号
|
||||
* force: 强制重新播放
|
||||
*/
|
||||
play(id, force = false) {
|
||||
if (id === -1) {
|
||||
if (this.track) {
|
||||
if (force) {
|
||||
this.seek(0)
|
||||
this.props.stat = 'paused'
|
||||
}
|
||||
if (this.stat === 'playing') {
|
||||
this.props.time = this.track.context.currentTime
|
||||
this.__AUDIO__.pause()
|
||||
this.props.stat = 'paused'
|
||||
} else if (this.stat === 'paused') {
|
||||
|
@ -90,21 +122,17 @@ export default class Player {
|
|||
}
|
||||
}
|
||||
} else {
|
||||
url = this.__LIST__[id]
|
||||
var url = this.__LIST__[id]
|
||||
if (!url || this.props.curr === url) {
|
||||
return
|
||||
}
|
||||
|
||||
gain = this.__AC__.createGain()
|
||||
|
||||
if (this.track) {
|
||||
this.stop()
|
||||
}
|
||||
|
||||
gain.gain.value = this.volume
|
||||
|
||||
this.track = await this._getTrack(url)
|
||||
this.track.connect(gain).connect(this.__AC__.destination)
|
||||
this.props.curr = url
|
||||
this._getTrack(url)
|
||||
|
||||
this.__AUDIO__.play()
|
||||
this.props.stat = 'playing'
|
||||
|
@ -112,15 +140,19 @@ export default class Player {
|
|||
}
|
||||
|
||||
seek(time) {
|
||||
this.props.time = time
|
||||
if (this.track) {
|
||||
this.__AUDIO__.currentTime = time
|
||||
}
|
||||
}
|
||||
|
||||
stop() {
|
||||
if (this.track) {
|
||||
this.__AUDIO__.pause()
|
||||
this.track = null
|
||||
this.props.time = 0
|
||||
this.gain = null
|
||||
this.__destroy__()
|
||||
this.props.stat = 'stoped'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
util.inherits(Player, EventEmitter)
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
const { ipcRenderer } = require('electron')
|
||||
|
||||
export default {
|
||||
dispatch(type = '', params = {}) {
|
||||
return ipcRenderer.sendSync('app', Object.assign(params, { type }))
|
||||
dispatch(type = '', data = {}) {
|
||||
return ipcRenderer.sendSync('app', { data, type })
|
||||
}
|
||||
}
|
||||
|
|
23
src/main.js
|
@ -7,12 +7,12 @@
|
|||
const { app, session, protocol, globalShortcut } = require('electron')
|
||||
const path = require('path')
|
||||
const fs = require('iofs')
|
||||
// const {exec} = require('child_process')
|
||||
|
||||
require('./tools/init.js')
|
||||
|
||||
const { createMainWindow, createMiniWindow } = require('./tools/windows.js')
|
||||
const { createAppTray, createLrcTray } = require('./tools/tray.js')
|
||||
const createMenu = require('./tools/menu.js')
|
||||
const { createMainWindow, createMiniWindow } = require('./tools/windows.js')
|
||||
|
||||
const MIME_TYPES = {
|
||||
'.js': 'text/javascript',
|
||||
|
@ -47,18 +47,17 @@ protocol.registerSchemesAsPrivileged([
|
|||
// 初始化应用
|
||||
app.once('ready', () => {
|
||||
// 注册协议
|
||||
protocol.registerBufferProtocol('app', (req, cb) => {
|
||||
let file = decodeURIComponent(req.url.replace(/^app:\/\/local\//, ''))
|
||||
let ext = path.extname(req.url)
|
||||
let buff = fs.cat(path.resolve(__dirname, file))
|
||||
protocol.registerBufferProtocol('app', function(req, cb) {
|
||||
var file = decodeURIComponent(req.url.replace(/^app:\/\/local\//, ''))
|
||||
var ext = path.extname(req.url)
|
||||
var buff = fs.cat(path.resolve(__dirname, file))
|
||||
cb({ data: buff, mimeType: MIME_TYPES[ext] })
|
||||
})
|
||||
|
||||
protocol.registerBufferProtocol('sonist', (req, cb) => {
|
||||
let file = decodeURIComponent(req.url.replace(/^sonist:[\/]+/, '/'))
|
||||
let ext = path.extname(req.url)
|
||||
let buff = fs.cat(file)
|
||||
cb({ data: buff, mimeType: MIME_TYPES[ext] || MIME_TYPES.all })
|
||||
protocol.registerBufferProtocol('sonist', function(req, cb) {
|
||||
var file = decodeURIComponent(req.url.replace(/^sonist:[\/]+/, '/'))
|
||||
var ext = path.extname(req.url)
|
||||
cb({ data: fs.cat(file), mimeType: MIME_TYPES[ext] || MIME_TYPES.all })
|
||||
})
|
||||
// 修改app的UA
|
||||
session.defaultSession.setUserAgent(
|
||||
|
@ -74,7 +73,7 @@ app.once('ready', () => {
|
|||
// mac专属事件,点击dock栏图标,可激活窗口
|
||||
app.on('activate', _ => {
|
||||
if (win) {
|
||||
win.webContents.send('dock-click')
|
||||
win.restore()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
* @date 2020/07/14 18:17:59
|
||||
*/
|
||||
|
||||
// 歌单
|
||||
const TABLE_PLAYLIST = `
|
||||
CREATE TABLE IF NOT EXISTS "playlist" (
|
||||
"id" integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
|
@ -11,15 +12,16 @@ CREATE TABLE IF NOT EXISTS "playlist" (
|
|||
)
|
||||
`
|
||||
|
||||
// aid: 歌手ID
|
||||
const TABLE_SONGS = `
|
||||
CREATE TABLE IF NOT EXISTS "songs" (
|
||||
"id" integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
"pid" integer NOT NULL,
|
||||
"aid" integer NOT NULL,
|
||||
"name" char(128) NOT NULL,
|
||||
"artist" char(128) NOT NULL,
|
||||
"album" char(128) NOT NULL,
|
||||
"duration" integer NOT NULL,
|
||||
"cover" char(256) NOT NULL,
|
||||
"path" char(256) NOT NULL,
|
||||
"file_path" char(256) NOT NULL,
|
||||
"lrc" text NOT NULL
|
||||
)
|
||||
`
|
||||
|
@ -28,15 +30,7 @@ const TABLE_RELATIONS = `
|
|||
CREATE TABLE IF NOT EXISTS "relations" (
|
||||
"id" integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
"sid" 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
|
||||
"pid" integer NOT NULL
|
||||
)
|
||||
`
|
||||
|
||||
|
@ -50,5 +44,4 @@ module.exports = function(db) {
|
|||
db.query(TABLE_PLAYLIST).catch(error)
|
||||
db.query(TABLE_SONGS).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)
|
||||
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':
|
||||
if (fs.isdir(conn.path)) {
|
||||
var { dir } = conn.data
|
||||
if (fs.isdir(dir)) {
|
||||
let list = fs
|
||||
.ls(conn.path, true)
|
||||
.ls(dir, true)
|
||||
.filter(it => {
|
||||
if (fs.isdir(it)) {
|
||||
return false
|
||||
|
@ -97,7 +149,13 @@ ipcMain.on('app', (ev, conn) => {
|
|||
end: 256,
|
||||
encoding: 'base64'
|
||||
})
|
||||
return { name, path: it, artist: '', album: '', duration: '00:00' }
|
||||
return {
|
||||
name,
|
||||
file_path: it,
|
||||
artist: '',
|
||||
album: '',
|
||||
duration: '00:00'
|
||||
}
|
||||
})
|
||||
ev.returnValue = list
|
||||
} else {
|
||||
|
|
|
@ -15,7 +15,13 @@ module.exports = function(win) {
|
|||
submenu: [
|
||||
{ role: 'about', label: '关于 Sonist' },
|
||||
{ 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,
|
||||
height: 460,
|
||||
frame: false,
|
||||
titleBarStyle: 'hiddenInset',
|
||||
resizable: false,
|
||||
maximizable: false,
|
||||
icon,
|
||||
|
@ -40,6 +41,11 @@ exports.createMainWindow = function(icon) {
|
|||
win.openDevTools()
|
||||
})
|
||||
|
||||
win.on('close', ev => {
|
||||
ev.preventDefault()
|
||||
win.hide()
|
||||
})
|
||||
|
||||
return win
|
||||
}
|
||||
|
||||
|
|