This repository has been archived on 2023-08-30. You can view files and clone it, but cannot push or open issues/pull-requests.
appcat
/
sonist
Archived
1
0
Fork 0

初始化项目

2.x
宇天 2018-12-26 23:58:24 +08:00
commit 59e01abe22
52 changed files with 1775 additions and 0 deletions

14
.gitignore vendored Normal file
View File

@ -0,0 +1,14 @@
.DS_Store
.AppleDouble
.LSOverride
.idea
.vscode
._*
.Spotlight-V100
.Trashes
node_modules
node_modules/**
package-lock.json

39
Readme.md Normal file
View File

@ -0,0 +1,39 @@
# Sonist 音乐播放器
> 一个音乐播放器, 主打本地音乐播放。支持 自动歌词/自动封面/均衡器等常见功能。
>> 同时利用酷狗音乐的API(**来源于网络,仅供学习使用**), 获取实时的云音乐(**仅免费的那部分,付费部分无法提供**)。
界面预览
![demo](./demo1.jpg)
![demo](./demo2.jpg)
## 开发计划
- [x] 主界面框架
- [ ] 酷狗音乐电台
- [ ] 酷狗音乐排行榜
- [ ] 酷狗歌手列表(完成20%)
- [ ] 酷狗音乐MV
- [ ] 试听列表
- [ ] 本地音乐(50%)
- [ ] 设置界面
- [ ] 均衡器
- [ ] 桌面歌词
- [ ] 迷你模式
- [ ] KTV模式
- [ ] 多媒体快捷键
- [ ] 酷狗账号直接登录(犹豫中)
- [ ] 铃声制作(犹豫中)
- [ ] 歌曲ID3信息修改(技术攻坚中)
- [ ] 用户评论/点赞(取决于登陆功能是否开发)
- [ ] 试听下载
- [ ] 歌曲质量选择
- [ ] 等你来建议
## 捐助
> 开发app其实挺辛苦的。 喜欢我的作品的童鞋, 可以给我打赏个几块钱茶水费, 感激不尽。
>> 没钱的, 可以扫支付宝领红包, 也算支持我了。

1
css/app.css Normal file

File diff suppressed because one or more lines are too long

188
css/app.scss Normal file
View File

@ -0,0 +1,188 @@
@charset "UTF-8";
/**
* {sonist app style}
* @authors yutent<yutent@doui.cc>
* @date 2018/12/16 17:15:07
*/
@import "./var.scss";
@font-face {font-family: "sonist font";
src: url('data:application/x-font-woff;charset=utf-8;base64,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') format('woff')
}
[class^="s-icon-"], [class*=" s-icon-"] {display:inline-block;font-family:"sonist font" !important;font-style:normal;-webkit-font-smoothing: antialiased;-moz-osx-font-smoothing: grayscale;}
.s-icon-all:before { content: "\e714"; }
.s-icon-eq:before { content: "\e715"; }
.s-icon-heart:before { content: "\e716"; }
.s-icon-music:before { content: "\e717"; }
.s-icon-prev:before { content: "\e718"; }
.s-icon-play-list:before { content: "\e719"; }
.s-icon-pause:before { content: "\e71a"; }
.s-icon-play:before { content: "\e71b"; }
.s-icon-next:before { content: "\e71c"; }
.s-icon-mv:before { content: "\e71d"; }
.s-icon-rank:before { content: "\e71e"; }
.s-icon-singer:before { content: "\e71f"; }
.s-icon-random:before { content: "\e720"; }
.s-icon-radio:before { content: "\e721"; }
.s-icon-single:before { content: "\e722"; }
.do-fn-drag {-webkit-app-region:drag;user-select: none;}
.do-fn-nodrag {-webkit-app-region:no-drag;}
html {font-size:62.5%}
body {line-height:1.5;background:#fff;font-size:1.4rem;color:nth($cd, 1)}
table {overflow:auto;display:table;width:100%;line-height:2.5rem;
thead tr {height:4.5rem;border-bottom:.1rem solid nth($cp, 1)}
thead th {padding:1rem .8rem;border:0;}
tbody tr {height:auto;@include ts(background, .3s);
&:hover {background:#f7f8fb;}
}
tbody td {padding:.9rem .8rem}
}
::-webkit-scrollbar {width:8px;height:8px;background:#f7f8fb;}
::-webkit-scrollbar:hover {background:#f3f5fb;}
::-webkit-scrollbar-button {display:none;}
::-webkit-scrollbar-thumb {background:nth($cp, 3);}
::-webkit-scrollbar-thumb:hover {background:nth($ct, 1);}
.do-mod-app {display:flex;position:fixed;left:0;top:0;width:100%;height:100%;
.menubar {position:absolute;left:1.2rem;top:0;z-index:99;width:auto;height:3rem;padding:.9rem 0;
.item {display:inline-block;width:1.2rem;height:1.2rem;margin:0 .2rem;background:url(/images/btn_gray@2x.png) no-repeat;background-size:cover;}
&.focus {
.quit {background-image:url(/images/btn_close_focus@2x.png);}
.min {background-image:url(/images/btn_min_focus@2x.png);}
.max {background-image:url(/images/btn_max_focus@2x.png);}
}
&:hover {
.quit {background-image:url(/images/btn_close_hover@2x.png);}
.min {background-image:url(/images/btn_min_hover@2x.png);}
.max {background-image:url(/images/btn_max_hover@2x.png);}
}
}
.menubar-win {position:absolute;right:1.2rem;top:0;z-index:99;width:auto;height:4rem;padding:.9rem 0;line-height:1.8rem;
.item {display:inline-block;width:2.2rem;height:2.2rem;margin:0 .2rem;padding:.2rem;font-size:1.6rem;
&:hover {transform:scale(1.1)}
&.opt {font-size:1.8rem;}
}
}
.sidebar {flex:0 1 22rem;position:relative;height:100%;background:nth($cp, 1);
//
.user-box {width:18rem;height:16.5rem;margin:4rem 2rem 0;text-align:center;
.avatar {overflow:hidden;width:12rem;height:12rem;margin:0 3rem;border:.6rem solid #fff;border-radius:50%;box-shadow:0 .5rem 1.5rem rgba(0, 0, 0, .15);}
img {width:100%;height:100%;}
.uname {line-height:2;font-weight:normal;}
}
//
.music-box {width:100%;height:auto;padding:0 1.5rem;
dt.title {line-height:4rem;color:nth($cgr, 1)}
dd.item {height:3rem;margin:.3rem 0;padding:0 .8rem;line-height:3rem;color:nth($cgr, 3);
.icon {float:left;width:3rem;height:3rem;padding:0 .5rem;font-size:2.4rem;}
&.active {border-radius:.3rem;background:nth($ct, 1);color:#fff;}
&.disabled {opacity:.25}
}
}
//
.play-contrl {position:absolute;left:0;bottom:0;width:100%;height:8rem;background:rgba(255,255,255,.3);
.item {position:absolute;top:2rem;width:4rem;height:4rem;line-height:4rem;font-size:3.5rem;text-align:center;color:nth($ct, 2);@include ts();
&:hover {color:nth($ct, 1)}
&:active {color:nth($ct, 3)}
}
.prev {left:2.5rem;}
.play {left:50%;top:1.5rem;width:5rem;height:5rem;margin-left:-2.5rem;line-height:5rem;font-size:4.5rem;}
.next {right:2.5rem;}
}
}
//
.main {flex:1;display:flex;flex-flow:column wrap;
//
.tool-bar {flex:0 1 5rem;padding:1rem;
.search {position:relative;display:inline-block;line-height:3rem;}
.icon {position:absolute;right:0;top:0;width:2.6rem;height:3rem;}
input {width:20rem;padding:0 1.3rem;border-radius:1.5rem;}
}
//
.module {flex:1;display:flex;flex-flow:column wrap;}
//
.play-bar {position:relative;flex:0 1 8rem;display:flex;justify-content:center;align-items:center;background:#f5f6fc;
.song-stat {flex:1;height:8rem;margin:0 2rem 0 0;
canvas {display:flex;width:100%;height:100%;}
}
.ctrl {position:relative;flex:0 1 3.5rem;height:3rem;line-height:3rem;text-align:center;color:nth($ct, 2);font-size:2rem;
&:hover {color:nth($ct, 1)}
&:active {color:nth($ct, 3)}
&.lrc {margin-right:2rem;font-size:1.6rem;}
}
}
}
}
@keyframes play {
from {transform:rotate(0deg)}
to {transform:rotate(360deg)}
}

1
css/modules.css Normal file
View File

@ -0,0 +1 @@
.do-mod-artist{position:relative;display:flex;width:100%;height:100%}.do-mod-artist .filter-box{flex:0 1 12rem;border-right:0.1rem solid #f3f5fb;text-align:right}.do-mod-artist .filter-box .item{width:100%;height:2.4rem;padding:0 1.2rem;line-height:2.4rem;color:#98acae}.do-mod-artist .filter-box .item.active{color:#3fc2a7;font-weight:bold}.do-mod-artist .filter-box .item:hover{padding-right:1.3rem;color:#3fc2a7}.do-mod-artist .filter-box .pipe{display:block;width:100%;height:.7rem}.do-mod-artist .list-box{overflow-y:auto;display:flex;flex-flow:row wrap;flex:1;padding:0 1rem}.do-mod-artist .list-box .item{display:flex;justify-content:center;align-items:center;flex:45%;height:7rem;margin:1rem 2.5%;padding:.5rem;background:#f3f5fb}.do-mod-artist .list-box .item img{flex:0 1 6rem;height:6rem}.do-mod-artist .list-box .item summary{flex:2;padding:0 1rem}.do-mod-artist .list-box .item strong{font-size:1.6rem}.do-mod-artist .list-box .item p{font-size:1.2rem;color:#98acae}.do-mod-artist .artist-box{position:absolute;left:0;top:0;z-index:9;width:100%;height:100%;background-size:cover;background-repeat:no-repeat;background-position:center center}.do-mod-artist .artist-box .content{display:flex;flex-flow:column wrap;width:100%;height:100%;padding:1.5rem 2.5rem;background:linear-gradient(to bottom, #fff 2%, rgba(255,255,255,0.75), #fff 98%);-webkit-backdrop-filter:blur(1rem);backdrop-filter:blur(1rem)}.do-mod-artist .artist-box .content .name{flex:0 1 3.6rem;font-size:1.4rem;font-style:italic;font-weight:normal}.do-mod-artist .artist-box .content .name a{text-decoration:underline;color:#3fc2a7}.do-mod-artist .artist-box .content .name i{color:#98acae}.do-mod-artist .artist-box .content .desc{flex:0 1 3rem;font-size:1.2rem;color:#98acae}.do-mod-artist .artist-box .content .desc span{padding:0 .5rem;text-decoration:underline;color:#3fc2a7}.do-mod-artist .artist-box .content .song-album{flex:1;display:flex;flex-flow:column wrap}.do-mod-artist .artist-box .content .tab{flex:0 1 3rem;display:flex;padding:0 .5rem;line-height:2.9rem;border-bottom:0.1rem solid #e8ebf4;text-align:center}.do-mod-artist .artist-box .content .tab .item{flex:0 0 7.5rem;height:3rem;margin:0 .3rem}.do-mod-artist .artist-box .content .tab .item.active{border-bottom:0.2rem solid #3fc2a7;color:#3fc2a7}.do-mod-artist .artist-box .content .tab .item.disabled{opacity:.25}.do-mod-local{flex:1;display:flex;flex-flow:column wrap}.do-mod-local .toolbar{flex:0 1 3rem;padding:0 1rem;line-height:2.9rem;border-bottom:0.1rem solid #e8ebf4}.do-mod-local .toolbar .refresh{margin-left:1rem;color:#3fc2a7;text-decoration:underline}.do-mod-local .table{overflow:auto;flex:1}.do-mod-local .table .stat{width:2.6rem;height:2.6rem;line-height:2.6rem}.do-mod-local .table .ac{text-align:center}.do-mod-local .table .active{color:#3fc2a7}.do-mod-local .table .active i{-webkit-animation:play 2s infinite linear;animation:play 2s infinite linear}.do-mod-search{flex:1;display:flex;flex-flow:column wrap}.do-mod-search .tabbar{flex:0 1 3rem;display:flex;padding:0 .5rem;line-height:2.9rem;border-bottom:0.1rem solid #e8ebf4;text-align:center}.do-mod-search .tabbar .item{flex:0 0 7.5rem;height:3rem;margin:0 .3rem;border:0.1rem solid #e8ebf4;background:#fff;color:#dae1e9}.do-mod-search .tabbar .item.active{border-bottom-color:transparent;color:#62778d}.do-mod-search .tabbar .item i{color:#ff5061}.do-mod-search .table{overflow:auto;flex:1}.do-mod-search .table .active{color:#3fc2a7}.do-mod-search .table .ac{text-align:center}.artist-desc-layer{width:60rem;height:30rem}.artist-desc-layer .layer-content{overflow-y:auto;height:85% !important;padding:1rem;line-height:2;text-indent:2em}

150
css/modules.scss Normal file
View File

@ -0,0 +1,150 @@
@charset "UTF-8";
/**
*
* @authors yutent<yutent@doui.cc>
* @date 2018/12/24 17:11:35
*/
@import "./var.scss";
//
.do-mod-artist {position:relative;display:flex;width:100%;height:100%;
.filter-box {flex:0 1 12rem;border-right:.1rem solid nth($cp, 1);text-align:right;
.item {width:100%;height:2.4rem;padding:0 1.2rem;line-height:2.4rem;color:nth($cgr, 1);
&.active {color:nth($ct,1);font-weight:bold}
&:hover {padding-right:1.3rem;color:nth($ct,1);}
}
.pipe {display:block;width:100%;height:.7rem;}
}
.list-box {overflow-y:auto;display:flex;flex-flow:row wrap;flex:1;padding:0 1rem;
.item {display:flex;justify-content:center;align-items:center;flex:45%;height:7rem;margin:1rem 2.5%;padding:.5rem;background:nth($cp, 1);
img {flex:0 1 6rem;height:6rem}
summary {flex:2;padding:0 1rem;}
strong {font-size:1.6rem}
p {font-size:1.2rem;color:nth($cgr, 1)}
}
}
/* ------------------------------------------------------------ */
/* -------------------- 歌手&专辑样式 -------------------- */
/* ------------------------------------------------------------ */
.artist-box {
position:absolute;left:0;top:0;z-index:9;width:100%;height:100%;
background-size:cover;background-repeat:no-repeat;background-position:center center;
.content {display:flex;flex-flow:column wrap;width:100%;height:100%;padding:1.5rem 2.5rem;background:linear-gradient(to bottom,#fff 2%, rgba(255, 255, 255, .75), #fff 98%);backdrop-filter:blur(1rem);
.name {flex:0 1 3.6rem;font-size:1.4rem;font-style:italic;font-weight:normal;
a {text-decoration:underline;color:nth($ct, 1)}
i {color:nth($cgr, 1)}
}
.desc {flex:0 1 3rem; font-size:1.2rem;color:nth($cgr, 1);
span {padding:0 .5rem;text-decoration:underline;color:nth($ct, 1)}
}
.song-album {flex:1;display:flex;flex-flow:column wrap;}
.tab {flex:0 1 3rem;display:flex;padding:0 .5rem;line-height:2.9rem;border-bottom:.1rem solid nth($cp, 2);text-align:center;
.item {flex:0 0 7.5rem;height:3rem;margin:0 .3rem;
&.active {border-bottom:.2rem solid nth($ct, 1);color:nth($ct, 1);}
&.disabled {opacity:.25}
}
}
}
}
}
//
.do-mod-local {flex:1;display:flex;flex-flow:column wrap;
.toolbar {flex:0 1 3rem;padding:0 1rem;line-height:2.9rem;border-bottom:.1rem solid nth($cp, 2);
.refresh {margin-left:1rem;color:nth($ct, 1);text-decoration:underline;}
}
.table {overflow:auto;flex:1;
.stat {width:2.6rem;height:2.6rem;line-height:2.6rem;}
.ac {text-align:center}
.active {color:nth($ct, 1);
i {animation: play 2s infinite linear;}
}
}
}
// &
.do-mod-search {flex:1;display:flex;flex-flow:column wrap;
.tabbar {flex:0 1 3rem;display:flex;padding:0 .5rem;line-height:2.9rem;border-bottom:.1rem solid nth($cp, 2);text-align:center;
.item {flex:0 0 7.5rem;height:3rem;margin:0 .3rem;border:.1rem solid nth($cp, 2);background:#fff;color:nth($cp, 3);
&.active {border-bottom-color:transparent;color:nth($cd, 1);}
i {color:nth($cr, 1)}
}
}
.table {overflow:auto;flex:1;
.active {color:nth($ct, 1)}
.ac {text-align:center}
}
}
.artist-desc-layer {width:60rem;height:30rem;
.layer-content {overflow-y:auto;height:85%!important;padding:1rem;line-height:2;text-indent:2em;}
}

14
css/var.scss Normal file
View File

@ -0,0 +1,14 @@
$ct: #3fc2a7 #19b491 #16967a;
$cg: #58d68d #2ecc71 #27ae60;
$cpp: #ac61ce #9b59b6 #8e44ad;
$cb: #52a3de #2d8dd6 #2776b1;
$cr: #ff5061 #eb3b48 #ce3742;
$co: #ffb618 #f39c12 #e67e22;
$cp: #f3f5fb #e8ebf4 #dae1e9;
$cgr: #98acae #8a9b9c #748182;
$cd: #62778d #526273 #425064;
@mixin ts($c: all, $t: .2s, $m: ease-in-out){
transition:$c $t $m;
}

BIN
demo1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 KiB

BIN
demo2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 261 KiB

8
dist/anot.next.js vendored Normal file

File diff suppressed because one or more lines are too long

204
dist/audio/index.js vendored Normal file
View File

@ -0,0 +1,204 @@
/**
* 播放器
* @author yutent<yutent@doui.cc>
* @date 2018/12/23 23:14:40
*/
'use strict'
const { exec } = require('child_process')
const { EventEmitter } = require('events')
const util = require('util')
class AudioPlayer {
constructor() {
this.__PLAYER__ = new Audio()
this.__IS_PLAYED__ = false
this.__LIST__ = [] // 播放列表
this.__CURR__ = -1 // 当前播放的歌曲的id
this.__PLAY_MODE__ = 'all' // all | single | random
this.__PLAYER__.valume = 0.7
this.__init__()
}
__init__() {
this.__PLAYER__.addEventListener(
'timeupdate',
_ => {
this.emit('play', this.__PLAYER__.currentTime)
},
false
)
this.__PLAYER__.addEventListener(
'ended',
_ => {
this.emit('end')
},
false
)
}
static ID3(song) {
let cmd = `ffprobe -v quiet -print_format json -show_entries format "${song}"`
let pc = exec(cmd)
let buf = []
return new Promise((resolve, reject) => {
pc.stdout.on('data', _ => {
buf.push(_)
})
pc.stderr.on('data', reject)
pc.stdout.on('close', _ => {
let { format } = Buffer.from(buf)
try {
res = JSON.parse(res)
resolve({
title: format.tags.TITLE || format.tags.title,
album: format.tags.ALBUM || format.tags.album,
artist: format.tags.ARTIST || format.tags.artist,
duration: +format.duration,
size: +(format.size / 1024 / 1024).toFixed(2)
})
} catch (err) {
reject(err)
}
})
})
}
get stat() {
return this.__LIST__.length ? 'ready' : 'stop'
}
get IS_MUTED() {
return this.__PLAYER__.muted
}
set valume(val) {
this.__PLAYER__.valume = val / 100
}
set mode(val = 'all') {
this.__PLAY_MODE__ = val
}
clear() {
this.__LIST__ = []
}
push(songs) {
this.__LIST__.push.apply(this.__LIST__, songs)
}
// 上一首
prev() {
let id = this.__CURR__
switch (this.__PLAY_MODE__) {
case 'all':
id--
if (id < 0) {
id = this.__LIST__.length - 1
}
break
case 'random':
id = (Math.random() * this.__LIST__.length) >>> 0
break
// single
default:
break
}
this.play(id)
return Promise.resolve(this.__LIST__[id])
}
// 下一首
next() {
let id = this.__CURR__
switch (this.__PLAY_MODE__) {
case 'all':
id++
if (id >= this.__LIST__.length) {
id = 0
}
break
case 'random':
id = (Math.random() * this.__LIST__.length) >>> 0
break
// single
default:
break
}
this.play(id)
return Promise.resolve(this.__LIST__[id])
}
// 播放
play(id) {
// 播放列表里没有数据的话, 不作任何处理
if (!this.__LIST__.length) {
return
}
// 有ID的话,不管之前是否在播放,都切换歌曲
if (id !== undefined) {
let song = this.__LIST__[id]
if (song) {
this.__CURR__ = id
this.__IS_PLAYED__ = true
this.__PLAYER__.pause()
this.__PLAYER__.currentTime = 0
this.__PLAYER__.src = song.path
this.__PLAYER__.play()
return Promise.resolve(song)
}
return Promise.reject('song not found')
} else {
if (!this.__IS_PLAYED__) {
this.__IS_PLAYED__ = true
this.__PLAYER__.play()
}
return Promise.resolve(true)
}
}
// 暂停
pause() {
if (!this.__IS_PLAYED__) {
return
}
this.__IS_PLAYED__ = false
this.__PLAYER__.pause()
}
// 切换静音
mute() {
if (this.__CURR__ < 0) {
return
}
this.__PLAYER__.muted = !this.__PLAYER__.muted
}
// 跳到指定位置播放
seek(time) {
if (this.__CURR__ < 0) {
return
}
this.__PLAYER__.pause()
this.__PLAYER__.currentTime = time
this.__PLAYER__.play()
}
}
util.inherits(AudioPlayer, EventEmitter)
export default AudioPlayer

BIN
dist/avatar/def.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

1
dist/avatar/index.js vendored Normal file
View File

@ -0,0 +1 @@
"use strict";function arraySum(t){var e=0;return t.forEach(function(t){e+=+t}),e}export const create=(t,e)=>{if(!t)return this.defafultImg;(!e||e<100)&&(e=100);var l,a=document.createElement("canvas"),r=a.getContext("2d"),c=t.slice(-3),i=t.slice(-9,-6),f=t.slice(0,8).match(/([\w]{1})/g),n=t.slice(8,16).match(/([\w]{1})/g),s=t.slice(16,24).match(/([\w]{1})/g),m=e/10;a.width=e,a.height=e,f=f.map(t=>(t=parseInt(t,16))%8),n=n.map(t=>(t=parseInt(t,16))%4),s=s.map(t=>(t=parseInt(t,16))%4),l=arraySum(f)>32?c:i,r.fillStyle="#"+c,r.fillRect(0,0,e,e);for(var u=1;u<9;u++){var o=f[u-1],h=n[u-1],p=s[u-1];o+h>8&&(o=8-h),r.fillStyle="#"+i,r.fillRect((h+1)*m,u*m,o*m,m),r.fillStyle="#"+i,r.fillRect((9-h-o)*m,u*m,o*m,m),r.fillStyle="#"+l,r.fillRect((p+1)*m,u*m,m,m),r.fillStyle="#"+l,r.fillRect((8-p)*m,u*m,m,m)}return a.toDataURL()};

1
dist/css/elem-ui.css vendored Normal file

File diff suppressed because one or more lines are too long

1
dist/css/form.css vendored Normal file

File diff suppressed because one or more lines are too long

1
dist/css/layer-normal.css vendored Normal file

File diff suppressed because one or more lines are too long

1
dist/css/pager.css vendored Normal file
View File

@ -0,0 +1 @@
.do-pager{display:block;height:auto;text-align:center;font-size:14px;color:#8a9b9c}.do-pager.mini{line-height:30px}.do-pager.mini .button,.do-pager.mini .page{min-width:30px;height:30px}.do-pager.medium{line-height:35px}.do-pager.medium .button,.do-pager.medium .page{min-width:35px;height:35px}.do-pager.large{line-height:40px}.do-pager.large .button,.do-pager.large .page{min-width:40px;height:40px}.do-pager .button,.do-pager .page{display:inline-block;border:0;color:#8a9b9c;text-decoration:none;cursor:pointer;vertical-align:top;font-size:14px;font-weight:100;-webkit-appearance:none;-moz-appearance:none;appearance:none}.do-pager .button{font-size:18px}.do-pager .curr,.do-pager .disabled{cursor:default}.do-pager.skin-1{width:100%}.do-pager.skin-1 .page,.do-pager.skin-1 .button,.do-pager.skin-1 .disabled,.do-pager.skin-1 .curr{padding:0 8px;margin:0 3px}.do-pager.skin-1 .curr{font-weight:bold;font-size:15px}.do-pager.skin-1 .page.disabled{min-width:0;padding:0;background:none;color:#8a9b9c}.do-pager.skin-1 .page.disabled:hover,.do-pager.skin-1 .page.disabled:active{background:none}.do-pager.skin-1 .page.curr{background:none;color:#8a9b9c}.do-pager.skin-1 .page.curr:hover,.do-pager.skin-1 .page.curr:active{background:none}.do-pager.skin-1 .button[disabled]{cursor:not-allowed}.do-pager.skin-1 .total-box,.do-pager.skin-1 .input-box{display:inline-block;padding:0 8px}.do-pager.skin-1 .input-box input{display:inline-block;width:40px;height:30px;padding:0 3px;font-size:14px;background:#fff;border:1px solid #ddd;text-align:center}.do-pager.skin-2{float:right;width:auto}.do-pager.skin-2 .page,.do-pager.skin-2 .button,.do-pager.skin-2 .disabled,.do-pager.skin-2 .curr{float:left;margin:0;padding:0 5px;color:#fff}.do-pager.skin-2 .page.disabled{display:none}.do-pager.skin-2 .button[disabled]{cursor:not-allowed}.do-pager.skin-2 .input-box{display:none}.do-pager.skin-2 .total-box{float:left;display:inline-block;padding:0 8px}.do-pager.plain .page,.do-pager.plain .button{background:#e8ebf4;color:#8a9b9c}.do-pager.plain .page:hover,.do-pager.plain .button:hover{background:#f3f5fb}.do-pager.plain .page:active,.do-pager.plain .button:active{background:#dae1e9}.do-pager.plain .button[disabled]{background:#e8ebf4}.do-pager.plain.skin-2 .curr{background:#dae1e9}.do-pager.grey .page,.do-pager.grey .button{background:#8a9b9c;color:#fff}.do-pager.grey .page:hover,.do-pager.grey .button:hover{background:#98acae}.do-pager.grey .page:active,.do-pager.grey .button:active{background:#748182}.do-pager.grey .button[disabled]{background:#8a9b9c}.do-pager.grey.skin-2 .curr{background:#748182}.do-pager.dark .page,.do-pager.dark .button{background:#526273;color:#fff}.do-pager.dark .page:hover,.do-pager.dark .button:hover{background:#526273}.do-pager.dark .page:active,.do-pager.dark .button:active{background:#425064}.do-pager.dark .button[disabled]{background:#526273}.do-pager.dark.skin-2 .curr{background:#425064}.do-pager.red .page,.do-pager.red .button{background:#eb3b48;color:#fff}.do-pager.red .page:hover,.do-pager.red .button:hover{background:#ff5061}.do-pager.red .page:active,.do-pager.red .button:active{background:#ce3742}.do-pager.red .button[disabled]{background:#eb3b48}.do-pager.red.skin-2 .curr{background:#ce3742}.do-pager.orange .page,.do-pager.orange .button{background:#f39c12;color:#fff}.do-pager.orange .page:hover,.do-pager.orange .button:hover{background:#ffb618}.do-pager.orange .page:active,.do-pager.orange .button:active{background:#e67e22}.do-pager.orange .button[disabled]{background:#f39c12}.do-pager.orange.skin-2 .curr{background:#e67e22}.do-pager.green .page,.do-pager.green .button{background:#2ecc71;color:#fff}.do-pager.green .page:hover,.do-pager.green .button:hover{background:#58d68d}.do-pager.green .page:active,.do-pager.green .button:active{background:#27ae60}.do-pager.green .button[disabled]{background:#2ecc71}.do-pager.green.skin-2 .curr{background:#27ae60}.do-pager.teal .page,.do-pager.teal .button{background:#19b491;color:#fff}.do-pager.teal .page:hover,.do-pager.teal .button:hover{background:#3fc2a7}.do-pager.teal .page:active,.do-pager.teal .button:active{background:#16967a}.do-pager.teal .button[disabled]{background:#19b491}.do-pager.teal.skin-2 .curr{background:#16967a}.do-pager.blue .page,.do-pager.blue .button{background:#2d8dd6;color:#fff}.do-pager.blue .page:hover,.do-pager.blue .button:hover{background:#52a3de}.do-pager.blue .page:active,.do-pager.blue .button:active{background:#2776b1}.do-pager.blue .button[disabled]{background:#2d8dd6}.do-pager.blue.skin-2 .curr{background:#2776b1}.do-pager.purple .page,.do-pager .button .page{background:#9b59b6;color:#fff}.do-pager.purple .page:hover,.do-pager .button .page:hover{background:#ac61ce}.do-pager.purple .page:active,.do-pager .button .page:active{background:#8e44ad}.do-pager.purple .button[disabled],.do-pager .button .button[disabled]{background:#9b59b6}.do-pager.purple.skin-2 .curr,.do-pager .button.skin-2 .curr{background:#8e44ad}

1
dist/css/reset-basic.css vendored Normal file

File diff suppressed because one or more lines are too long

85
dist/drag/doc.md vendored Normal file
View File

@ -0,0 +1,85 @@
# 拖拽插件
> 该插件可以让任意一个元素可以被拖拽,而不需要该元素是否具有定位属性。
> 使用时,在目标元素上添加`:drag`属性即可以实现拖拽功能。
## 依赖
> 依赖`Anot`框架
## 浏览器兼容性
+ chrome
+ firefox
+ safari
+ IE10+
## 用法
> 只需要在要拖拽的元素上添加`:drag`即可;
> 如果要拖拽的元素不是当前元素,只需要给该属性增加一个值为想要拖拽元素的类名或ID。
> 具体请看示例:
> **注意:** `拖拽的元素不是本身时,只会往父级一级一级找相匹配的`
```html
<!DOCTYPE html>
<html>
<head>
<style>
* {margin:0;padding:0}
.box {width:200px;height:100px;background:#aaa;}
.box .handle {width:200px;height:30px;background:#f30;}
</style>
</head>
<body :controller="test">
<div class="box" :drag></div>
<div class="box">
<div class="handle" :drag="box"></div>
</div>
<script>
import Anot from 'lib/drag/index.js'
Anot({
$id: 'test'
})
</script>
</body>
</html>
```
## 额外参数
### `data-limit`
> 用于限制元素的拖动范围,默认没有限制。 可选值为 "window"和"parent", 分别为 "限制在可视区"和"限制在父级元素的范围"
### `data-axis`
> 用于限制拖动的方向, 默认值为 "xy",即不限制方向。可选值为 "x"和"y", 即只能在"x轴"或"y轴"方向拖动。
### `data-beforedrag`
> 拖动前的回调,如果有设置回调方法, 则该回调的返回值,可决定该元素是否能被拖拽, 可用于在特殊场景下,临时禁用拖拽。
> `注:`
> 1. 该回调方法,会传入3个参数, 第1个为被拖拽的元素(dom对象), 第2个参数为 该元素的x轴绝对坐标, 第3个元素为y轴绝对坐标;
> 2. 该回调方法, 返回false时, 本次拖拽将临时失效, 返回其他值,或没有返回值,则忽略。
### `data-dragging`
> 元素被拖动时的回调。
> `注:`
> 1.该回调方法,会传入3个参数, 第1个为被拖拽的元素(dom对象), 第2个参数为 该元素的x轴绝对坐标, 第3个元素为y轴绝对坐标;
### `data-dragged`
> 元素被拖动结束后的回调。
> `注:`
> 1. 该回调方法,会传入3个参数, 第1个为被拖拽的元素(dom对象), 第2个参数为 该元素的x轴绝对坐标, 第3个元素为y轴绝对坐标;

1
dist/drag/index.js vendored Normal file
View File

@ -0,0 +1 @@
"use strict";function getBindingCallback(e,t,i){var n=e.getAttribute(t);if(n)for(var a,o=0;a=i[o++];)if(a.hasOwnProperty(n)&&"function"==typeof a[n])return a[n]}Anot.ui.drag="1.0.0",Anot.directive("drag",{priority:1500,init:function(e){e.expr='"'+e.expr+'"';let t=document.documentMode?"move":"grab";window.sidebar?t="-moz-"+t:window.chrome&&(t="-webkit-"+t),Anot(e.element).css("cursor",t),e.beforedrag=getBindingCallback(e.element,"data-beforedrag",e.vmodels),e.dragging=getBindingCallback(e.element,"data-dragging",e.vmodels),e.dragged=getBindingCallback(e.element,"data-dragged",e.vmodels),e.overflow=!0,e.axis="xy",e.element.dataset.axis&&(e.axis=e.element.dataset.axis,delete e.element.dataset.axis),e.limit=!1,e.element.dataset.limit&&(e.limit=e.element.dataset.limit,e.overflow=!1,delete e.element.dataset.limit),delete e.element.dataset.beforedrag,delete e.element.dataset.dragging,delete e.element.dataset.dragged},update:function(e){let t,i,n,a,o,l,r,d,s,g,m,u,c,f,p,b=this,x=e?this.element.parentNode:this.element,v=Anot(this.element),h=Anot(document),w=null,y=null;for(;e&&x&&(x.classList||Anot.error(`${this.name}=${this.expr}, 解析异常[元素不存在]`),!x.classList.contains(e)&&x.id!==e);)x=x.parentNode;w=Anot(x),"parent"===this.limit&&(y=x.parentNode),v.bind("mousedown",function(e){let v=getComputedStyle(x),A=v.transform.replace(/matrix\((.*)\)/,"$1"),C=w.offset();if("0s"!==v.transitionDuration&&(p=v.transitionDuration,x.style.transitionDuration="0s"),(A="none"!==A?A.split(", "):[1,0,0,1,0,0])[4]-=0,A[5]-=0,t=A[4],i=A[5],c=h.scrollTop(),f=h.scrollLeft(),o=C.left-t-f,l=C.top-i-c,n=e.pageX,a=e.pageY,m=window.innerWidth,u=window.innerHeight,s=x.clientWidth,g=x.clientHeight,b.beforedrag){if(!1===b.beforedrag.call(b.vmodels[0],x,o+t,l+i))return}let k=[0,u-g,0,m-s];if("parent"===b.limit){let e=getComputedStyle(y).transform.replace(/matrix\((.*)\)/,"$1"),t=Anot(y).offset();e="none"!==e?e.split(", "):[1,0,0,1,0,0];let i=t.left-e[4]-f,n=t.top-e[5]-c;k=[n,n+y.clientHeight-g,i,i+y.clientWidth-s]}let D=h.bind("mousemove",function(e){e.preventDefault(),"y"!==b.axis&&(A[4]=e.pageX-n+t),"x"!==b.axis&&(A[5]=e.pageY-a+i),r=o+A[4],d=l+A[5],b.overflow||("y"!==b.axis&&(r<=k[2]&&(r=k[2],A[4]=r-o),r>=k[3]&&(r=k[3],A[4]=r-o)),"x"!==b.axis&&(d<=k[0]&&(d=k[0],A[5]=d-l),d>=k[1]&&(d=k[1],A[5]=d-l))),w.css({transform:"matrix("+A.join(", ")+")"}),b.dragging&&b.dragging.call(b.vmodels[0],x,r,d)}),B=h.bind("mouseup",function(e){h.unbind("mousemove",D),h.unbind("mouseup",B),x.style.transitionDuration=p,b.dragged&&b.dragged.call(b.vmodels[0],x,r,d,A[4],A[5])})})}});

1
dist/form/index.js vendored Normal file
View File

@ -0,0 +1 @@
importCss("/css/form.css");const log=console.log;Anot.ui.form="0.1.0",Anot.component("button",{__init__(e,s,t){s.text=this.text(),s.style={"border-radius":e.radius},this.classList.add("do-fn-noselect"),this.classList.add("do-button"),this.classList.add(e.color||"grey"),this.setAttribute(":click","onClick"),this.setAttribute(":class","{disabled: disabled}"),this.setAttribute(":css","style"),e.size&&this.classList.add(e.size),e.hasOwnProperty("disabled")&&(s.disabled=!0),delete e.disabled,delete e.color,delete e.size,t()},render(e){let s="";return this.props.icon&&(s=`<i class="do-button__icon do-icon-${this.props.icon}"></i>`),`${s}<span class="do-button__text" :text="text"></span>`},state:{text:"",disabled:!1,style:{}},props:{click:Anot.PropsTypes.isFunction()},skip:["style"],watch:{},methods:{onClick(){this.disabled||"function"==typeof this.props.click&&this.props.click(this.props.prop)}}}),Anot.component("radio",{__init__(e,s,t){e.hasOwnProperty("disabled")&&(s.disabled=!0),e.hasOwnProperty("checked")&&null===s.value&&(s.value=e.label),s.text=this.text(),s.checked=s.value===e.label,this.classList.add("do-radio"),this.classList.add("do-fn-noselect"),this.classList.add(e.color||"grey"),this.setAttribute(":class","{disabled: disabled, checked: checked}"),this.setAttribute(":click","onClick"),delete e.disabled,delete e.color,t()},render:()=>'\n <span class="do-radio__box"></span>\n <span class="do-radio__text" :text="text"></span>\n ',state:{value:null,text:"",checked:!1,disabled:!1},props:{label:""},watch:{value(e){this.checked=this.props.label===e}},methods:{onClick(){this.disabled||this.checked||(this.checked=!0,this.value=this.props.label)}}}),Anot.component("switch",{__init__(e,s,t){e.hasOwnProperty("disabled")&&(s.disabled=!0),e.hasOwnProperty("checked")&&null===s.value&&(s.value=!0),s.value=!!s.value,this.classList.add("do-switch"),this.classList.add("do-fn-noselect"),this.classList.add(e.color||"grey"),this.setAttribute(":class","{disabled: disabled, checked: value}"),this.setAttribute(":click","onClick"),delete e.disabled,delete e.color,t()},render:()=>'\n <span class="do-switch__label"><i class="do-switch__dot"></i></span>\n ',state:{value:null,disabled:!1},methods:{onClick(){this.disabled||(this.value=!this.value)}}}),Anot.component("checkbox",{__init__(e,s,t){Array.isArray(s.value)||(this.parentNode.removeChild(this),Anot.error("多选框的传入值必须一个数组",TypeError)),e.hasOwnProperty("disabled")&&(s.disabled=!0),e.hasOwnProperty("checked")&&Anot.Array.ensure(s.value,e.label),s.text=this.text(),s.checked=s.value.indexOf(e.label)>-1,this.classList.add("do-checkbox"),this.classList.add("do-fn-noselect"),this.classList.add(e.color||"grey"),this.setAttribute(":class","{disabled: disabled, checked: checked}"),this.setAttribute(":click","onClick"),delete e.disabled,delete e.color,t()},render:()=>'\n <span class="do-checkbox__box">\n <i class="do-icon-get" :visible="checked"></i>\n </span>\n <span class="do-checkbox__text" :text="text"></span>\n ',state:{value:[],text:"",checked:!1,disabled:!1},props:{label:""},watch:{"value.*"(e,s,t,i){this.checked=this.value.indexOf(this.props.label)>-1},"value.length"(e,s,t,i){this.checked=this.value.indexOf(this.props.label)>-1},value(e,s,t,i){this.checked=this.value.indexOf(this.props.label)>-1}},methods:{onClick(){if(this.disabled)return;let{label:e}=this.props,s=this.value.$model;for(let t in s)if(s[t]===e)return this.checked=!1,void this.value.removeAt.call(this.value,t);this.checked=!0,this.value.push(e)}}}),Anot.component("input",{__init__(e,s,t){e.hasOwnProperty("disabled")&&(s.disabled=!0),e.iconR&&(s.pos="right",e.icon=e.iconR,delete e.iconR),this.classList.add("do-input"),this.classList.add("do-fn-noselect"),this.classList.add(e.color||"grey"),e.icon&&this.classList.add("icon-"+s.pos),this.setAttribute(":class","{disabled: disabled, active: active}"),this.setAttribute(":css","{width: props.width}"),delete e.disabled,delete e.color,t()},render(){let{icon:e,placeholder:s}=this.props;return'\n <span \n class="do-input__holder"\n :class="{visible: !value || active}"\n :text="props.placeholder"></span>\n <input \n class="do-input__input"\n :attr="{disabled: disabled, type: props.type }"\n :duplex="value" \n :keyup="onKeyup"\n :blur="onBlur"\n :focus="onFocus" />'+(e?`<i class="do-input__icon do-icon-${e}"></i>`:"")},state:{pos:"left",value:"",disabled:!1,active:!1},skip:["pos"],props:{type:"text",width:180,placeholder:"",default:"",submit:Anot.PropsTypes.isFunction()},methods:{onFocus(){this.active=!0},onBlur(){this.active=!1},onKeyup(e){this.disabled||13===e.keyCode&&"function"==typeof this.props.submit&&this.props.submit()}}});export default Anot;

30
dist/layer/Release.md vendored Normal file
View File

@ -0,0 +1,30 @@
v1.0.0-base / 2017-09-20
==================
+ 统一字体图标
+ 精简动画类型
+ 优化样式
v0.0.4-base / 2017-04-20
==================
+ 优化offset的处理
+ 优化样式
v0.0.3-base / 2017-04-15
==================
+ 重构wrap方式创建弹窗实例的实现
v0.0.2-base / 2017-04-13
==================
+ 修复:layer方式创建实例时,漏掉自身的bug;
+ 修复layer.open()方法打开已有实例时不返回id的bug;
+ 修复layer.close()方法关闭实例时,未修改实例状态的bug;
+ 修改特殊模式下的实例的最小宽度为10px;
+ 优化:layer方式创建实例的逻辑处理;
+ 优化layer.alert()方法参数的处理;
v0.0.1-base / 2017-04-06
==================
+ 完成layer base版移植

1
dist/layer/index.js vendored Normal file

File diff suppressed because one or more lines are too long

1
dist/pager/index.js vendored Normal file
View File

@ -0,0 +1 @@
"use strict";importCss("/css/pager.css");function calculate({currPage:t,maxPageShow:e,totalPage:a}){let s=[],r=0,o=t<e/2?e-t:Math.floor(e/2);if(a<2)return s.push(1),s;t-o>1&&s.push("..."),a-t<o&&(r=o-a+t);for(let e=t-o-r;e<t+o+1&&e<=a;e++)e>0&&s.push(e);return t+o<a&&s.push("..."),s}function update(t,e){const{totalPage:a,props:{maxPageShow:s}}=e;e.currPage!==t&&(e.currPage=e.inputPage=t,"function"==typeof e.props.pageChanged&&e.props.pageChanged(t)),e.pageList.clear(),a>1?e.pageList.pushArray(calculate({currPage:t,totalPage:a,maxPageShow:s})):e.pageList.pushArray([1])}Anot.ui.pager="1.0.0";const tmpls={home:'<button class="do-icon-dbl-left button"\n :css="{\'border-radius\': props.radius}"\n :attr="{disabled: currPage === 1}"\n :data="{to: parseUrl(1)}"\n :click="go(1, $event)"></button>',end:'<button class="do-icon-dbl-right button"\n :css="{\'border-radius\': props.radius}"\n :attr="{disabled: currPage === totalPage}"\n :data="{to: parseUrl(totalPage)}"\n :click="go(totalPage, $event)"></button>',prev:'<button class="do-icon-left button"\n :css="{\'border-radius\': props.radius}"\n :attr="{disabled: currPage < 2}"\n :data="{to: parseUrl(currPage - 1)}"\n :click="go(currPage - 1, $event)"></button>',next:'<button class="do-icon-right button"\n :css="{\'border-radius\': props.radius}"\n :attr="{disabled: currPage >= totalPage}"\n :data="{to: parseUrl(currPage + 1)}"\n :click="go(currPage + 1, $event)"></button>',pager:'<button class="page"\n :for="pageList"\n :css="{\'border-radius\': props.radius}"\n :attr="{disabled: \'...\' === el || currPage === el}"\n :data="{to: parseUrl(el)}"\n :class="{disabled: \'...\' === el, curr: currPage === el}"\n :text="el"\n :click="go(el, $event)"></button>',curr:'<button class="page curr" :text="currPage"></button>',total:'<span class="total-box">共 {{totalPage}} 页 {{totalItem}} 条</span>',jumper:'<div class="input-box">前往\n <input type="text" :duplex="inputPage" :keyup="go(null, $event)"> 页\n </div>',slot:""};export default Anot.component("pager",{__init__:function(t,e,a){this.classList.add("do-pager"),this.classList.add("do-fn-noselect"),this.setAttribute(":class","{{classList.join(' ')}}"),t.theme=+t.theme||1,t.simpleMode&&(t.theme=1),e.classList=e.classList.concat("skin-"+t.theme,t.color||"plain",t.size||"mini"),t.total&&(e.totalItem=+t.total),t.pageSize&&(e.pageSize=+t.pageSize),t.layout||(t.layout="total,home,prev,pager,next,end,jumper"),2===t.theme&&(t.radius=null),delete t.total,delete t.pageSize,delete t.color,delete t.size,a()},render:function(t){let{layout:e,theme:a,simpleMode:s}=this.props;return s?e=["prev","curr","next"]:(e=e.replace(/\s/g,""),2===a&&(e=e.replace(/total|jumper/g,"")),e=e.split(",")),(e=e.map(e=>"slot"!==e?tmpls[e]||"":t&&t.extra?t.extra.join(""):void 0)).join("\n")},componentWillMount:function(){const{currPage:t,totalPage:e,props:a}=this;this.pageList.clear(),this.pageList.pushArray(calculate({currPage:t,totalPage:e,maxPageShow:a.maxPageShow}))},componentDidMount:function(){"function"==typeof this.props.created&&this.props.created(this)},state:{classList:[],currPage:1,totalItem:1,pageSize:20,inputPage:1,pageList:[]},computed:{totalPage:function(){return Math.ceil(this.totalItem/this.pageSize)}},props:{url:null,maxPageShow:5,simpleMode:!1,radius:3,pageChanged:Anot.PropsTypes.isFunction(),created:Anot.PropsTypes.isFunction()},skip:["classList"],methods:{parseUrl(t){return(t>>>=0)<1||!this.props.url||this.currPage===t?"":this.props.url.replace("{id}",t)},go(t,e){let{inputPage:a,totalPage:s,currPage:r}=this,o=e&&e.target||null;if(!(o&&o.disabled||r===t))if(t&&o){if("..."!==t){let e=o.dataset.to;e?location.href=e:t>>>=0,update(t,this)}}else if(null===t){if(a>>>=0,e&&13===e.keyCode){if(a<1||r===a)return this.inputPage=r;a>s&&(a=s),this.inputPage=a,update(a,this)}}else update(t>>>=0,this)},setSize(t){t=+t,this.pageSize!==t&&(this.pageSize=+t,update(1,this))},setTotal(t){t=+t,this.totalItem!==t&&(this.totalItem=+t,update(1,this))}}});

1
dist/request/index.js vendored Normal file

File diff suppressed because one or more lines are too long

1
dist/request/lib/format.js vendored Normal file
View File

@ -0,0 +1 @@
"use strict";function serialize(e,t,r){var o;if(Array.isArray(t))t.forEach(function(t,a){o=e?e+"["+(Array.isArray(t)?a:"")+"]":a,"object"==typeof t?serialize(o,t,r):r(o,t)});else for(var a in t)o=e?e+"["+a+"]":a,"object"==typeof t[a]?serialize(o,t[a],r):r(o,t[a])}var toS=Object.prototype.toString,doc=window.document,encode=encodeURIComponent,decode=decodeURIComponent,TagHooks=function(){this.option=doc.createElement("select"),this.thead=doc.createElement("table"),this.td=doc.createElement("tr"),this.area=doc.createElement("map"),this.tr=doc.createElement("tbody"),this.col=doc.createElement("colgroup"),this.legend=doc.createElement("fieldset"),this._default=doc.createElement("div"),this.g=doc.createElementNS("http://www.w3.org/2000/svg","svg"),this.optgroup=this.option,this.tbody=this.tfoot=this.colgroup=this.caption=this.thead,this.th=this.td},Format=function(){var e=this;this.tagHooks=new TagHooks,"circle,defs,ellipse,image,line,path,polygon,polyline,rect,symbol,text,use".replace(/,/g,function(t){e.tagHooks[t]=e.tagHooks.g}),this.rtagName=/<([\w:]+)/,this.rxhtml=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,this.scriptTypes={"text/javascript":1,"text/ecmascript":1,"application/ecmascript":1,"application/javascript":1},this.rhtml=/<|&#?\w+;/};Format.prototype={parseJS:function(code){if(code=(code+"").trim(),code)if(1===code.indexOf("use strict")){var script=doc.createElement("script");script.text=code,doc.head.appendChild(script).parentNode.removeChild(script)}else eval(code)},parseXML:function(e,t,r){try{t=(new DOMParser).parseFromString(e,"text/xml")}catch(e){t=void 0}return t&&t.documentElement&&!t.getElementsByTagName("parsererror").length||console.error("Invalid XML: "+e),t},parseHTML:function(e){var t=doc.createDocumentFragment().cloneNode(!1);if("string"!=typeof e)return t;if(!this.rhtml.test(e))return t.appendChild(document.createTextNode(e)),t;e=e.replace(this.rxhtml,"<$1></$2>").trim();var r=(this.rtagName.exec(e)||["",""])[1].toLowerCase(),o=this.tagHooks[r]||this.tagHooks._default,a=null;o.innerHTML=e;var i=o.getElementsByTagName("script");if(i.length)for(var c,n=0;c=i[n++];)if(this.scriptTypes[c.type]){var s=doc.createElement("script").cloneNode(!1);c.attributes.forEach(function(e){s.setAttribute(e.name,e.value)}),s.text=c.text,c.parentNode.replaceChild(s,c)}for(;a=o.firstChild;)t.appendChild(a);return t},param:function(e){if(!e||"string"==typeof e||"number"==typeof e)return e;var t=[];return"object"==typeof e&&serialize("",e,function(e,r){/native code/.test(r)||(r="function"==typeof r?r():r,r="[object File]"!==toS.call(r)?encode(r):r,t.push(encode(e)+"="+r))}),t.join("&")},parseForm:function(e){for(var t,r={},o=0;t=e.elements[o++];)switch(t.type){case"select-one":case"select-multiple":if(t.name.length&&!t.disabled)for(var a,i=0;a=t.options[i++];)a.selected&&(r[t.name]=a.value||a.text);break;case"file":t.name.length&&!t.disabled&&(r[t.name]=t.files[0]);break;case void 0:case"submit":case"reset":case"button":break;case"radio":case"checkbox":if(!t.checked)break;default:t.name.length&&!t.disabled&&(r[t.name]=t.value)}return r},merge:function(e,t){if("object"!=typeof e||"object"!=typeof t)throw new TypeError("argument must be an object");if(Object.assign)return Object.assign(e,t);for(var r in t)e[r]=t[r];return e}};export default new Format;

0
dist/request/light.js vendored Normal file
View File

1
dist/store/index.js vendored Normal file
View File

@ -0,0 +1 @@
const __STORE__={};function parse$And(_){let t="";for(let e in _){let i=_[e];switch(Anot.type(i)){case"object":if(i.$has){t+=`it.${e}.indexOf(${JSON.stringify(i.$has)}) > -1`;break}if(i.$in){t+=`${JSON.stringify(i.$in)}.indexOf(it.${e}) > -1`;break}if(i.$regex){t+=`${i.$regex}.test(it.${e})`;break}if(i.$lt||i.$lte){t+=`it.${e} <${i.$lte?"=":""} ${i.$lt||i.$lte}`,(i.$gt||i.$gte)&&(t+=` && it.${e} >${i.$gte?"=":""} ${i.$gt||i.$gte}`);break}if(i.$gt||i.$gte){t+=`it.${e} >${i.$gte?"=":""} ${i.$gt||i.$gte}`;break}if(i.$eq){t+=`it.${e} === ${i.$eq}`;break}default:t+=`it.${e} === ${JSON.stringify(_[e])}`}t+=" && "}return(t=t.slice(0,-4))||(t="true"),t}function parse$Or(_){let t="";return _.forEach(_=>{t+="(",t+=parse$And(_),t+=") || "}),t.slice(0,-4)}class AnotStore{constructor(_){Anot.hideProperty(this,"__name__",_),Anot.hideProperty(this,"__LAST_QUERY__",""),Anot.hideProperty(this,"__QUERY_HISTORY__",[]),__STORE__[_]||(__STORE__[_]=[],__STORE__[`${_}Dict`]={})}static collection(_){return new this(_)}__MAKE_FN__(_){let t="\n let result = [];\n let num = 0;\n for (let it of arr) {\n if(";return _.$or?t+=parse$Or(_.$or):t+=parse$And(_),t+="){\n result.push(it)\n num++\n if(limit > 0 && num >= limit){\n break\n }\n }\n }\n return result;",Function("arr","limit",t)}clear(_){this.__QUERY_HISTORY__=[],this.__LAST_QUERY__="",_&&(__STORE__[this.__name__]=[],__STORE__[`${this.__name__}Dict`]={})}getAll({filter:_,limit:t=[]}={}){const e=__STORE__[this.__name__];let i=[],r=!1;if(!e||!e.length)return i;if(t.length<1&&(t=[0]),t.length<2&&_&&(r=!0,t[0]>0&&t.unshift(0)),_){let n=JSON.stringify(_);if(this.__LAST_QUERY__===n)i=this.__QUERY_HISTORY__.slice.apply(this.__QUERY_HISTORY__,t);else{i=this.__MAKE_FN__(_)(e,r&&t[1]||0),r||(this.__LAST_QUERY__=n,this.__QUERY_HISTORY__=i,i=this.__QUERY_HISTORY__.slice.apply(this.__QUERY_HISTORY__,t))}}else i=e.slice.apply(e,t);return Anot.deepCopy(i)}get(_){const t=__STORE__[`${this.__name__}Dict`];return Anot.deepCopy(t[_])||null}count({filter:_}={}){return _?this.__LAST_QUERY__===JSON.stringify(_)?this.__QUERY_HISTORY__.length:this.getAll({filter:_,limit:[0]}).length:__STORE__[this.__name__].length}__INSERT__(_,t){let e=__STORE__[this.__name__],i=__STORE__[`${this.__name__}Dict`],r=_[t||"id"];i[r]?this.update(r,_):(e.push(_),i[r]=_)}insert(_,t){Array.isArray(_)||(_=[_]),_.forEach(_=>{this.__INSERT__(_,t)}),this.clear()}sort(_,t,e){let i="";t&&window.Intl&&(i+="\n let col = new Intl.Collator('zh')\n "),i+=e?"return arr.sort((b, a) => {":"return arr.sort((a, b) => {",i+=`\n let filter = function(val) {\n try {\n return val.${_} || ''\n } catch (err) {\n return ''\n }\n }\n `,t?window.Intl?i+="return col.compare(filter(a), filter(b))":i+="return (filter(a) + '').localeCompare(filter(b), 'zh')":i+="return filter(a) - filter(b)",i+="\n})",Function("arr",i).call(this,__STORE__[this.__name__]),this.clear()}update(_,t){let e=__STORE__[this.__name__],i=__STORE__[`${this.__name__}Dict`],r=i[_],n=e.indexOf(r);Object.assign(r,t),e.splice(n,1,r),i[_]=r}remove(_){let t=__STORE__[this.__name__],e=__STORE__[`${this.__name__}Dict`],i=e[_id],r=t.indexOf(i);t.splice(r,1),delete e[_id]}}Anot.store=window.store=AnotStore;export default AnotStore;

BIN
images/album.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

BIN
images/album.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
images/avatar.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
images/btn_close_hover@2x.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
images/btn_gray@2x.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
images/btn_max_focus@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 910 B

BIN
images/btn_max_hover@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
images/btn_min_focus@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 916 B

BIN
images/btn_min_hover@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 998 B

BIN
images/disk.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

BIN
images/tray_16x16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 B

BIN
images/tray_16x16@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

132
index.html Normal file
View File

@ -0,0 +1,132 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no">
<link href="dist/css/reset-basic.css" rel="stylesheet">
<link href="dist/css/elem-ui.css" rel="stylesheet">
<link href="css/app.css" rel="stylesheet">
<link href="css/modules.css" rel="stylesheet">
<script>window.LIBS_BASE_URL = location.origin + '/dist'</script>
<script type="module" src="js/app.js"></script>
</head>
<body class="do-fn-noselect">
<div class="do-mod-app" anot="app">
<nav class="menubar do-fn-nodrag" :if="theme === 1" :class="{focus: winFocus}">
<i class="item quit" :click="quit(false)"></i>
<i class="item min" :click="minimize"></i>
<i class="item max" :click="maximize"></i>
</nav>
<nav class="menubar-win do-fn-nodrag">
<i class="item opt do-icon-menu-right"></i>
<span :if="theme === 2">
<i class="item do-icon-minimize" :click="minimize"></i>
<i class="item do-icon-maximize" :click="maximize"></i>
<i class="item do-icon-close" :click="quit(false)"></i>
</span>
</nav>
<aside class="sidebar do-fn-drag">
<div class="user-box">
<div class="avatar">
<img src="/images/avatar.jpg" alt="yutent">
</div>
<h2 class="uname">yutent</h2>
</div>
<dl class="music-box">
<dt class="title">酷狗在线</dt>
<dd class="item disabled"
:click="toggleModule('radio')"
:class="{active: mod === 'radio'}">
<i class="s-icon-radio"></i> 音乐电台
</dd>
<dd class="item"
:click="toggleModule('rank')"
:class="{active: mod === 'rank'}">
<i class="s-icon-rank"></i> 排行榜
</dd>
<dd class="item"
:click="toggleModule('artist')"
:class="{active: mod === 'artist'}">
<i class="s-icon-singer"></i> 歌手
</dd>
<dd class="item disabled"
:click="toggleModule('mv')"
:class="{active: mod === 'mv'}">
<i class="s-icon-mv"></i> MV
</dd>
<dt class="title">我的音乐</dt>
<dd class="item"
:click="toggleModule('search')"
:class="{active: mod === 'search'}">
<i class="s-icon-heart"></i> 试听列表
</dd>
<dd class="item"
:click="toggleModule('local')"
:class="{active: mod === 'local'}">
<i class="s-icon-play-list"></i> 本地音乐
</dd>
</dl>
<div class="play-contrl">
<span class="item prev s-icon-prev" :click="nextSong(-1)"></span>
<span
class="item play"
:class="{'s-icon-play': !isPlaying, 's-icon-pause': isPlaying}"
:click="play(null)">
</span>
<span class="item next s-icon-next" :click="nextSong(1)"></span>
</div>
</aside>
<div class="main">
<div class="tool-bar do-fn-drag">
<div class="search do-fn-nodrag">
<input class="do-ui-input" value="">
<i class="icon do-icon-search"></i>
</div>
</div>
<content class="module" :include="views" data-cache="true"></content>
<div class="play-bar">
<div class="song-stat">
<canvas ref="player"></canvas>
</div>
<span
class="ctrl"
:class="{
's-icon-all': playMode === 0,
's-icon-single': playMode === 1,
's-icon-random': playMode === 2
}"
:click="togglePlayMode">
</span>
<span class="ctrl do-icon-unmute"></span>
<span class="ctrl s-icon-eq"></span>
<span class="ctrl lrc"></span>
</div>
</div>
</div>
</body>
</html>

108
js/api.js Normal file
View File

@ -0,0 +1,108 @@
/**
* 音乐APP接口
* @author yutent<yutent@doui.cc>
* @date 2018/12/24 16:02:00
*/
'use strict'
import request from '/dist/request/index.js'
const log = console.log
const BASE_API_URI = 'http://mobilecdnbj.kugou.com'
const get = uri => {
return request.get(BASE_API_URI + uri)
}
const post = uri => {
return request.post(BASE_API_URI + uri)
}
export default {
getLastHot100Artists() {
return get('/api/v5/singer/list')
.send({
sort: 1,
showtype: 1,
sextype: 0,
musician: 0,
pagesize: 100,
plat: 2,
type: 0,
page: 1
})
.then(res => {
if (res.status === 200) {
return JSON.parse(res.text)
}
})
},
getArtistList(sextype = 1, type = 1) {
return get('/api/v5/singer/list')
.send({
showtype: 2,
musician: 0,
type,
sextype
})
.then(res => {
if (res.status === 200) {
return JSON.parse(res.text)
}
})
},
getArtistInfo(singerid) {
return get('/api/v3/singer/info')
.send({ singerid })
.then(res => {
if (res.status === 200) {
return JSON.parse(res.text)
}
})
},
getArtistInfo(singerid) {
return get('/api/v3/singer/info')
.send({ singerid })
.then(res => {
if (res.status === 200) {
return JSON.parse(res.text)
}
})
},
getArtistSongs(singerid, page = 1) {
return get('/api/v3/singer/song')
.send({
sorttype: 2,
pagesize: 50,
singerid,
area_code: 1,
page
})
.then(res => {
if (res.status === 200) {
return JSON.parse(res.text)
}
})
},
getArtistAlbums(singerid, page = 1) {
return get('/api/v3/singer/album')
.send({
pagesize: 50,
singerid,
area_code: 1,
page
})
.then(res => {
if (res.status === 200) {
return JSON.parse(res.text)
}
})
}
}

339
js/app.js Normal file
View File

@ -0,0 +1,339 @@
/**
* {sonist app}
* @author yutent<yutent@doui.cc>
* @date 2018/12/16 17:15:57
*/
import '/dist/anot.next.js'
import layer from '/dist/layer/index.js'
import store from '/dist/store/index.js'
import AudioPlayer from '/dist/audio/index.js'
import Api from '/js/api.js'
import Artist from '/js/modules/artist.js'
import Local from '/js/modules/local.js'
const log = console.log
const fs = require('iofs')
const path = require('path')
const crypto = require('crypto.js')
const { exec } = require('child_process')
const {
remote: { app }
} = require('electron')
const HOME_PATH = app.getPath('appData')
const APP_INI_PATH = path.join(HOME_PATH, 'app.ini')
const MUSIC_DB_PATH = path.join(HOME_PATH, 'music.db')
const PLAY_MODE = {
0: 'all',
1: 'single',
2: 'random'
}
const FONTS_NAME =
' Helvetica, Arial,"WenQuanYi Micro Hei","PingFang SC","Hiragino Sans GB","Segoe UI", "Microsoft Yahei", sans-serif'
// 本地音乐和试用音乐列表
window.LS = store.collection('local')
window.TS = store.collection('temp')
// 音乐播放器
window.SONIST = new AudioPlayer()
let appInit = fs.cat(APP_INI_PATH)
let dbCache = fs.cat(MUSIC_DB_PATH)
dbCache = JSON.parse(dbCache)
appInit = JSON.parse(appInit)
LS.insert(dbCache)
dbCache = null
let list = fs.ls('/Volumes/extends/music')
let hasNew = false
// list.forEach(it => {
// let name = path.basename(it)
// if (name.startsWith('.')) {
// return
// }
// let hash = crypto.md5Sign(it)
// if (LS.get(hash)) {
// return
// }
// hasNew = true
// AudioPlayer.ID3(it).then(tag => {
// LS.insert({
// id: hash,
// title: tag.title,
// album: tag.album,
// artist: tag.artist,
// path: `file://${it}`,
// duration: tag.duration
// })
// })
// })
if (hasNew) {
setTimeout(() => {
dbCache = JSON.stringify(LS.getAll(), '', 2)
log(dbCache, MUSIC_DB_PATH)
fs.echo(dbCache, MUSIC_DB_PATH)
}, 500)
}
Anot({
$id: 'app',
state: {
theme: 1, // 1:macos, 2: deepin
winFocus: true,
mod: 'local',
playMode: Anot.ls('play-mode') >>> 0, // 0:all | 1:single | 2:random
isPlaying: false,
curr: {
id: '',
index: 0,
title: '',
artist: '',
album: '',
time: 0,
duration: 0
},
currTimeBar: '',
currTimeBarPercent: 0,
__DEG__: 0.01
},
skip: [],
computed: {
views() {
if (!this.mod) {
return
}
return '/views/' + this.mod + '.htm'
}
},
watch: {
'curr.*'() {
let { time, duration } = this.curr
let x = time / duration
this.currTimeBar = `matrix(1, 0, 0, 1, ${x * this.__TB_WIDTH__}, 0)`
this.currTimeBarPercent = 100 * x + '%'
},
mod(val) {
this.activeModule(val)
}
},
mounted() {
let canvas = this.$refs.player
// 画布放大4倍, 以解决模糊的问题
this.__WIDTH__ = canvas.clientWidth * 4
this.__HEIGHT__ = canvas.clientHeight * 4
canvas.width = this.__WIDTH__
canvas.height = this.__HEIGHT__
this.__CTX__ = canvas.getContext('2d')
// 修改歌曲进度
canvas.addEventListener(
'click',
ev => {
let rect = canvas.getBoundingClientRect()
let aw = rect.width
let ax = ev.pageX - rect.left
let ay = ev.pageY - rect.top
log(aw, ax, ay)
if (ax > 124 && ay > 55 && ay < 64) {
let pp = (ax - 124) / (aw - 124)
this.curr.time = pp * this.curr.duration
log(pp, this.curr.time)
SONIST.seek(this.curr.time)
}
},
true
)
// 设置循环模式
SONIST.mode = PLAY_MODE[this.playMode]
SONIST.on('play', time => {
this.curr.time = time
})
SONIST.on('end', time => {
this.nextSong(1)
})
this.activeModule(this.mod)
},
methods: {
quit() {},
minimize() {},
maximize() {},
activeModule(mod) {
switch (mod) {
case 'artist':
Artist.__init__()
break
case 'local':
Local.__init__()
break
default:
break
}
},
toggleModule(mod) {
if (['radio', 'mv'].includes(mod)) {
return
}
this.mod = mod
},
togglePlayMode() {
let mod = this.playMode
mod++
if (mod > 2) {
mod = 0
}
this.playMode = mod
SONIST.mode = PLAY_MODE[mod]
Anot.ls('play-mode', mod)
},
draw() {
let img1 = new Image()
let img2 = new Image()
let p1 = Promise.defer()
let p2 = Promise.defer()
let { title, artist, cover } = this.curr
let play = this.isPlaying
img1.onload = p1.resolve
img2.onload = p2.resolve
img1.src = '/images/disk.png'
img2.src = cover || '/images/album.png'
let rx = (play ? 112 : 40) + this.__HEIGHT__ / 2 // 旋转唱片的圆心坐标X
let ry = this.__HEIGHT__ / 2 // 旋转唱片的圆心坐标Y
let pw = this.__WIDTH__ - this.__HEIGHT__ - 180 // 进度条总长度
let wl = this.__HEIGHT__ + 180 // 文字的坐标X
const draw = () => {
let { time, duration } = this.curr
let pp = time / duration // 进度百分比
time = Anot.filters.time(time)
duration = Anot.filters.time(duration)
this.__CTX__.clearRect(0, 0, this.__WIDTH__, this.__HEIGHT__)
this.__CTX__.save()
// 将原点移到唱片圆心, 旋转完再回到初始值
this.__CTX__.translate(rx, ry)
this.__CTX__.rotate(this.__DEG__ * Math.PI)
this.__CTX__.translate(-rx, -ry)
this.__CTX__.drawImage(
img1,
play ? 112 : 40,
0,
this.__HEIGHT__,
this.__HEIGHT__
)
this.__CTX__.restore()
this.__CTX__.drawImage(img2, 0, 0, this.__HEIGHT__, this.__HEIGHT__)
// 歌曲标题和歌手
this.__CTX__.fillStyle = '#62778d'
this.__CTX__.font = '56px' + FONTS_NAME
this.__CTX__.fillText(`${title} - ${artist}`, wl, 100)
// 时间
this.__CTX__.fillStyle = '#98acae'
this.__CTX__.font = '48px' + FONTS_NAME
this.__CTX__.fillText(
`${time} / ${duration}`,
this.__WIDTH__ - 280,
100
)
// 歌词
this.__CTX__.fillStyle = '#98acae'
this.__CTX__.font = '48px' + FONTS_NAME
this.__CTX__.fillText(`暂无歌词...`, wl, 180)
// 进度条
this.__CTX__.fillStyle = '#dae1e9'
this.__CTX__.fillRect(wl, 230, pw, 16)
this.__CTX__.fillStyle = '#3fc2a7'
this.__CTX__.fillRect(wl, 230, pw * pp, 16)
this.__DEG__ += 0.01
}
Promise.all([p1.promise, p2.promise]).then(_ => {
clearInterval(this.timer)
if (play) {
this.timer = setInterval(() => {
draw(img1, img2, play, rx, ry)
}, 20)
} else {
draw(img1, img2, play, rx, ry)
}
})
},
nextSong(step) {
let _p = null
if (step > 0) {
_p = SONIST.next()
} else {
_p = SONIST.prev()
}
this.isPlaying = false
_p.then(it => {
this.curr = {
...it,
time: 0,
cover:
'http://imge.kugou.com/stdmusic/480/20170906/20170906161516611883.jpg'
}
// 通知子模块歌曲已经改变
this.$fire('child!curr', it.id)
this.play()
})
},
pause() {
this.isPlaying = false
},
play(song) {
// 有参数的,说明是播放回调通知
// 此时仅更新播放控制条的信息即可
if (song) {
this.curr = {
...song,
time: 0,
cover:
'http://imge.kugou.com/stdmusic/480/20170906/20170906161516611883.jpg'
}
this.isPlaying = true
} else {
if (SONIST.stat === 'ready') {
if (this.isPlaying) {
SONIST.pause()
} else {
SONIST.play()
}
this.isPlaying = !this.isPlaying
}
}
this.draw()
}
}
})

139
js/modules/artist.js Normal file
View File

@ -0,0 +1,139 @@
/**
* 歌手模块
* @author yutent<yutent@doui.cc>
* @date 2018/12/24 17:00:48
*/
'use strict'
import Api from '/js/api.js'
const log = console.log
export default Anot({
$id: 'artist',
state: {
filter: 'hot',
list: [], //歌手列表
display: 'artist', // list | artist | album
artist: {
avatar:
'http://singerimg.kugou.com/uploadpic/softhead/240/20181023/20181023141706176.jpg',
id: 3060,
name: '薛之谦',
info: '',
songCount: 0,
mvCount: 0,
albumCount: 0
},
songList: [], //单曲列表
albumList: [] //专辑列表
},
methods: {
__init__() {
// Api.getArtistList().then(json => {
// log(json)
// })
// this.getHotArtist()
this.getArtistInfo()
},
search(ev) {
let target = ev.target
if (target.tagName !== 'SECTION') {
return
}
let key = target.dataset.key
this.filter = key
switch (key) {
case 'hot':
this.getHotArtist()
break
default:
key = key.split(',')
this.getArtistList.apply(this, key)
}
},
pickArtist(ev) {
if (ev.target === ev.currentTarget) {
return
}
let target = ev.target
while (target.tagName !== 'LI') {
target = target.parentNode
}
let { index } = target.dataset
let artist = this.list[index]
this.artist.id = artist.id
this.artist.name = artist.name
this.artist.avatar = artist.avatar
this.display = 'artist'
this.getArtistInfo()
},
showArtistInfo() {
layer.open({
type: 7,
title: '歌手详细介绍',
content: this.artist.info,
fixed: true,
maskClose: true,
extraClass: 'artist-desc-layer'
})
},
toArtistListPage() {
this.display = 'list'
},
getArtistInfo() {
Api.getArtistInfo(this.artist.id).then(json => {
log(json)
this.artist.info = json.data.intro.replace(/\n/g, '<br>')
this.artist.songCount = json.data.songcount
this.artist.mvCount = json.data.mvcount
this.artist.albumCount = json.data.albumcount
})
},
getHotArtist() {
let cache = Anot.ss('hot-artist')
if (cache) {
cache = JSON.parse(cache)
this.list.clear()
this.list.pushArray(cache)
} else {
Api.getLastHot100Artists().then(json => {
log(json)
let list = json.data.info.map(it => {
return {
id: it.singerid,
name: it.singername,
avatar: it.imgurl.replace('{size}', '240'),
fans: it.fanscount
}
})
Anot.ss('hot-artist', JSON.stringify(list))
this.list.clear()
this.list.pushArray(list)
})
}
},
getArtistList(type, sextype) {
// Api.getArtistList().then(json => {
// log(json)
// })
}
}
})

47
js/modules/local.js Normal file
View File

@ -0,0 +1,47 @@
/**
* 本地音乐模块
* @author yutent<yutent@doui.cc>
* @date 2018/12/24 17:00:48
*/
'use strict'
import Api from '/js/api.js'
const log = console.log
export default Anot({
$id: 'local',
state: {
list: [],
curr: ''
},
__APP__: null,
mounted() {
this.__APP__ = Anot.vmodels.app
this.list = LS.getAll()
let lastPlay = Anot.ls('last-play') || 0
SONIST.clear()
SONIST.push(LS.getAll())
SONIST.play(lastPlay).then(it => {
this.__APP__.play(it)
this.curr = it.id
})
},
watch: {
'props.curr'(v) {
this.curr = v
}
},
methods: {
__init__() {},
play(idx) {
SONIST.play(idx).then(it => {
this.__APP__.play(it)
this.curr = it.id
})
}
}
})

33
js/modules/search.js Normal file
View File

@ -0,0 +1,33 @@
/**
* 本地音乐模块
* @author yutent<yutent@doui.cc>
* @date 2018/12/24 17:00:48
*/
'use strict'
import Api from '/js/api.js'
const log = console.log
export default Anot({
$id: 'search',
state: {
filter: 'hot',
list: [], //歌手列表
display: 'artist', // list | artist | album
artist: {
avatar:
'http://singerimg.kugou.com/uploadpic/softhead/240/20181023/20181023141706176.jpg',
id: 3060,
name: '薛之谦',
info: '',
songCount: 0,
mvCount: 0,
albumCount: 0
},
songList: [], //单曲列表
albumList: [] //专辑列表
},
methods: {}
})

58
main.js Normal file
View File

@ -0,0 +1,58 @@
const { app, BrowserWindow, protocol } = require('electron')
const path = require('path')
const fs = require('iofs')
const log = console.log
const ROOT = __dirname
const HOME = app.getPath('home')
const MIME_TYPES = {
js: 'application/javascript',
html: 'text/html',
htm: 'text/html',
css: 'text/css',
jpg: 'image/jpg',
png: 'image/png',
gif: 'image/gif'
}
let win = null
function createWindow() {
// 创建浏览器窗口
win = new BrowserWindow({
title: 'sonist',
width: 1024,
height: 600,
frame: false,
resizable: false,
webPreferences: {
webSecurity: false,
experimentalFeatures: true
}
})
// 然后加载应用的 index.html。
win.loadURL('app://sonist/index.html')
}
app.commandLine.appendSwitch('--autoplay-policy', 'no-user-gesture-required')
app.setPath('appData', path.resolve(HOME, '.sonist/'))
protocol.registerStandardSchemes(['app'], { secure: true })
let appPath = app.getPath('appData')
if (!fs.exists(appPath)) {
fs.mkdir(appPath)
fs.echo('{}', path.join(appPath, 'app.ini'))
fs.echo('[]', path.join(appPath, 'music.db'))
}
// 创建窗口
app.on('ready', () => {
protocol.registerBufferProtocol('app', (req, cb) => {
let file = req.url.replace(/^app:\/\/sonist\//, '')
let ext = path.extname(req.url).slice(1)
let buf = fs.cat(path.resolve(ROOT, file))
cb({ data: buf, mimeType: MIME_TYPES[ext] })
})
createWindow()
win.webContents.openDevTools()
})

18
package.json Normal file
View File

@ -0,0 +1,18 @@
{
"name": "sonist",
"version": "1.0.0",
"description": "Music Player",
"main": "main.js",
"scripts": {
"start": "electron ."
},
"author": "yutent",
"license": "ISC",
"dependencies": {
"crypto.js": "^1.1.6",
"iofs": "^1.1.0"
},
"devDependencies": {
"electron": "^4.0.0"
}
}

91
views/artist.htm Normal file
View File

@ -0,0 +1,91 @@
<div class="do-mod-artist" anot="artist">
<div class="filter-box" :click="search">
<section class="item" data-key="hot" :class="{active: filter === 'hot'}">热门歌手</section>
<section class="item" data-key="0,0" :class="{active: filter === '0,0'}">入驻音乐人</section>
<span class="pipe"></span>
<section class="item" data-key="1,1" :class="{active: filter === '1,1'}">华语男歌手</section>
<section class="item" data-key="1,2" :class="{active: filter === '1,2'}">华语女歌手</section>
<section class="item" data-key="1,3" :class="{active: filter === '1.3'}">华语组合</section>
<span class="pipe"></span>
<section class="item" data-key="6,1" :class="{active: filter === '6,1'}">韩国男歌手</section>
<section class="item" data-key="6,2" :class="{active: filter === '6,2'}">韩国女歌手</section>
<section class="item" data-key="6,3" :class="{active: filter === '6,3'}">韩国组合</section>
<span class="pipe"></span>
<section class="item" data-key="5,1" :class="{active: filter === '5,1'}">日本男歌手</section>
<section class="item" data-key="5,2" :class="{active: filter === '5,2'}">日本女歌手</section>
<section class="item" data-key="5,3" :class="{active: filter === '5,3'}">日本组合</section>
<span class="pipe"></span>
<section class="item" data-key="2,1" :class="{active: filter === '2,1'}">欧美男歌手</section>
<section class="item" data-key="2,2" :class="{active: filter === '2,2'}">欧美女歌手</section>
<section class="item" data-key="2,3" :class="{active: filter === '2,3'}">欧美组合</section>
<span class="pipe"></span>
<section class="item" data-key="4,0" :class="{active: filter === '4,0'}">其他歌手</section>
</div>
<ul class="list-box" :click="pickArtist">
<li class="item" :for="it in list" :data="{artist: it.id, index: $index}">
<img :attr="{src: it.avatar, alt: it.name}" />
<summary>
<strong :text="it.name"></strong>
<p>粉丝数: {{it.fans}}</p>
</summary>
</li>
</ul>
<div
class="artist-box"
:if="display === 'artist'"
:css="{'background-image': 'url(' + artist.avatar + ')'}">
<div class="content">
<h3 class="name">
<i class="s-icon-singer"></i>
<a :click="toArtistListPage">歌手列表</a>
<i class="do-icon-right"></i>
<span :text="artist.name"></span>
</h3>
<cite class="desc">
介绍: {{artist.info | truncate(50)}} <span :click="showArtistInfo">详细</span>
</cite>
<dl class="song-album">
<dt class="tab">
<span class="item active">单曲({{artist.songCount}})</span>
<span class="item">专辑({{artist.albumCount}})</span>
<span class="item disabled">MV({{artist.mvCount}})</span>
</dt>
<dd class="list">
<table>
<tbody>
<tr :for="it in songList">
<td :text="it.title"></td>
<td :text="it.album"></td>
<td class="ac" :text="it.duration | time"></td>
</tr>
</tbody>
</table>
</dd>
</dl>
</div>
</div>
<div class="album-box" :if="display === 'album'">
</div>
</div>

31
views/local.htm Normal file
View File

@ -0,0 +1,31 @@
<div class="do-mod-local" anot="local">
<div class="toolbar">
本地音乐({{list.length}}首)<span class="refresh">重新扫描</span>
</div>
<div class="table">
<table>
<thead>
<tr>
<th>&nbsp;</th>
<th>歌名</th>
<th>歌手</th>
<th>专辑</th>
<th>时长</th>
</tr>
</thead>
<tbody>
<tr
:class="{active: it.id === curr}"
:for="it in list"
:dblclick="play($index)">
<td class="ac"><i class="stat s-icon-music"></i></td>
<td :text="it.title"></td>
<td class="ac" :text="it.artist"></td>
<td class="ac" :text="it.album"></td>
<td class="ac" :text="it.duration | time"></td>
</tr>
</tbody>
</table>
</div>
</div>

32
views/search.htm Normal file
View File

@ -0,0 +1,32 @@
<div class="do-mod-search">
<div class="tabbar">
<span class="item active">我的音乐</span>
<span class="item">汪苏泷 <i class="do-icon-close"></i></span>
</div>
<div class="table">
<table>
<thead>
<tr>
<th>&nbsp;</th>
<th>歌名</th>
<th>歌手</th>
<th>专辑</th>
<th>时长</th>
</tr>
</thead>
<tbody>
<tr
:class="{active: it.id === curr.id}"
:for="it in list"
:dblclick="play(it, $index)">
<td><i class="s-icon-music"></i></td>
<td :text="it.title"></td>
<td class="ac" :text="it.artist"></td>
<td class="ac" :text="it.album"></td>
<td class="ac" :text="it.duration | time"></td>
</tr>
</tbody>
</table>
</div>
</div>