😂 安装依赖, 移植 demo 代码

This commit is contained in:
hunlongyu
2020-04-15 18:12:58 +08:00
parent d0024458ea
commit 8f10dcfb20
23 changed files with 4243 additions and 87 deletions

View File

@@ -13,13 +13,21 @@
},
"main": "background.js",
"dependencies": {
"axios": "^0.19.2",
"core-js": "^3.6.4",
"dexie": "^2.0.4",
"element-ui": "^2.13.1",
"html2canvas": "^1.0.0-rc.5",
"leancloud-storage": "^4.5.3",
"macaddress": "^0.2.9",
"modern-normalize": "^0.6.0",
"qrcode.vue": "^1.7.0",
"vue": "^2.6.11",
"vue-i18n": "^8.17.0",
"vue-router": "^3.1.6",
"vuex": "^3.1.3"
"vuex": "^3.1.3",
"xgplayer": "^2.6.14",
"xgplayer-hls.js": "^2.1.6"
},
"devDependencies": {
"@vue/cli-plugin-babel": "~4.3.0",

View File

@@ -1,5 +1,5 @@
<template>
<div id="app">
<div id="app" class="theme-light">
<router-view/>
</div>
</template>

View File

@@ -0,0 +1,118 @@
<template>
<div class="aside">
<span :class="[view === 'Film' ? 'active ': ''] + 'film'" @click="changeView('Film')">
<svg role="img" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" aria-labelledby="apertureIconTitle">
<title id="apertureIconTitle">{{$t('view')}}</title>
<path d="M12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22Z"></path>
<g stroke-linecap="round">
<path d="M3 16H14.3164"></path>
<path d="M4.03589 6.20575L9.68257 15.9861"></path>
<path d="M13.0359 2.20575L7.37891 12.004"></path>
<path d="M10.9641 21.7942L16.6146 12.0074"></path>
<path d="M19.9641 17.7942L14.3086 7.99866"></path>
<path d="M21 7.98721H9.71844"></path>
</g>
</svg>
</span>
<span :class="[view === 'Play' ? 'active ': ''] + 'play'" @click="changeView('Play')">
<svg role="img" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" aria-labelledby="playIconTitle">
<title id="playIconTitle">播放</title>
<path d="M20 12L5 21V3z"></path>
</svg>
</span>
<span :class="[view === 'Star' ? 'active ': ''] + 'star'" @click="changeView('Star')">
<svg role="img" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" aria-labelledby="favouriteIconTitle">
<title id="favouriteIconTitle">收藏</title>
<path d="M12,21 L10.55,19.7051771 C5.4,15.1242507 2,12.1029973 2,8.39509537 C2,5.37384196 4.42,3 7.5,3 C9.24,3 10.91,3.79455041 12,5.05013624 C13.09,3.79455041 14.76,3 16.5,3 C19.58,3 22,5.37384196 22,8.39509537 C22,12.1029973 18.6,15.1242507 13.45,19.7149864 L12,21 Z"></path>
</svg>
</span>
<span :class="[view === 'Setting' ? 'active ': ''] + 'setting'" @click="changeView('Setting')">
<svg role="img" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" aria-labelledby="settingsIconTitle">
<title id="settingsIconTitle">设置</title>
<path d="M5.03506429,12.7050339 C5.01187484,12.4731696 5,12.2379716 5,12 C5,11.7620284 5.01187484,11.5268304 5.03506429,11.2949661 L3.20577137,9.23205081 L5.20577137,5.76794919 L7.9069713,6.32070904 C8.28729123,6.0461342 8.69629298,5.80882212 9.12862533,5.61412402 L10,3 L14,3 L14.8713747,5.61412402 C15.303707,5.80882212 15.7127088,6.0461342 16.0930287,6.32070904 L18.7942286,5.76794919 L20.7942286,9.23205081 L18.9649357,11.2949661 C18.9881252,11.5268304 19,11.7620284 19,12 C19,12.2379716 18.9881252,12.4731696 18.9649357,12.7050339 L20.7942286,14.7679492 L18.7942286,18.2320508 L16.0930287,17.679291 C15.7127088,17.9538658 15.303707,18.1911779 14.8713747,18.385876 L14,21 L10,21 L9.12862533,18.385876 C8.69629298,18.1911779 8.28729123,17.9538658 7.9069713,17.679291 L5.20577137,18.2320508 L3.20577137,14.7679492 L5.03506429,12.7050339 Z"></path>
<circle cx="12" cy="12" r="1"></circle>
</svg>
</span>
</div>
</template>
<script>
import { mapMutations } from 'vuex'
export default {
name: 'Aside',
computed: {
view: {
get () {
return this.$store.getters.getView
},
set (val) {
this.SET_VIEW(val)
}
}
},
methods: {
...mapMutations(['SET_VIEW']),
changeView (e) {
this.view = e
}
}
}
</script>
<style lang="scss" scoped>
svg{
width: 24px;
height: 24px;
stroke: #823aa099;
stroke-width: 1;
stroke-linecap: round;
stroke-linejoin: round;
fill: none;
}
.aside{
width: 60px;
height: 100%;
user-select: none;
-webkit-app-region: drag;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
border-right: 1px solid #00000010;
span{
-webkit-app-region: no-drag;
display: flex;
justify-content: center;
align-items: center;
writing-mode: tb-rl;
color: #823aa055;
width: 60px;
height: 60px;
cursor: pointer;
overflow: hidden;
&:hover{
animation: SHAni 0.8s ease both;
@keyframes SHAni {
to{
background-color: #823aa011;
}
}
svg{
animation: SHSAni 0.8s ease both;
@keyframes SHSAni {
to{
stroke: #823aa0ee;
stroke-width: 1.5;
fill: #823aa022;
}
}
}
}
&.active{
svg{
stroke: #823aa0;
stroke-width: 2;
fill: #823aa033;
}
}
}
}
</style>

View File

@@ -0,0 +1,383 @@
<template>
<div class="detail">
<div class="detail-content">
<div class="detail-header">
<span class="detail-title">详情</span>
<span class="detail-close" @click="closeDetail">
<svg role="img" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" aria-labelledby="closeIconTitle">
<title id="closeIconTitle">关闭</title>
<path d="M6.34314575 6.34314575L17.6568542 17.6568542M6.34314575 17.6568542L17.6568542 6.34314575"></path>
</svg>
</span>
</div>
<div class="detail-body" v-show="!loading" :style="{overflowY:scroll? 'auto' : 'hidden',paddingRight: scroll ? '0': '5px' }" @mouseenter="scroll = true" @mouseleave="scroll = false">
<div class="info" v-html="vDetail.info"></div>
<div class="desc" v-html="vDetail.desc" v-if="show.desc"></div>
<div class="m3u8_urls">
<div class="title">播放:</div>
<div class="box">
<span v-for="(i, j) in vDetail.m3u8_urls" :key="j" @click="playEvent(i)">{{i | ftName}}</span>
</div>
</div>
<div class="mp4_urls" v-if="show.download">
<div class="title">下载链接:</div>
<div class="box">
<span v-for="(i, j) in vDetail.mp4_urls" :key="j" @click="download(i)">{{i | ftName}}</span>
<span @click="allDownload" v-show="vDetail.mp4_urls.length > 1">全集下载</span>
</div>
</div>
</div>
<div class="detail-mask" v-show="loading">
<div class="loader"></div>
</div>
</div>
</div>
</template>
<script>
import { mapMutations } from 'vuex'
import tools from '../lib/site/tools'
// const { clipboard } = require('electron')
export default {
name: 'detail',
data () {
return {
scroll: false,
loading: true,
vDetail: {},
show: {
desc: false,
download: false
}
}
},
filters: {
ftName (e) {
const name = e.split('$')[0]
return name
}
},
computed: {
view: {
get () {
return this.$store.getters.getView
},
set (val) {
this.SET_VIEW(val)
}
},
video: {
get () {
return this.$store.getters.getVideo
},
set (val) {
this.SET_VIDEO(val)
}
},
detail: {
get () {
return this.$store.getters.getDetail
},
set (val) {
this.SET_DETAIL(val)
}
}
},
methods: {
...mapMutations(['SET_VIEW', 'SET_VIDEO', 'SET_DETAIL']),
closeDetail () {
this.detail.show = false
},
getDetail () {
tools.detail_get(this.detail.v.site, this.detail.v.detail).then(res => {
this.vDetail = res
if (res.desc.length > 0) {
this.show.desc = true
}
if (res.mp4_urls.length > 0) {
this.show.download = true
}
this.$nextTick(() => {
this.loading = false
})
})
},
playEvent (e) {
this.video = this.detail.v
this.detail.show = false
this.view = 'Play'
}
// download (e) {
// const name = e.split('$')[0]
// const txt = encodeURI(e.split('$')[1])
// clipboard.writeText(txt)
// this.$message.success(`已复制 ${name} 下载链接, 快去下载吧!`)
// },
// allDownload () {
// const urls = [...this.vDetail.mp4_urls]
// let txt = ''
// for (const i of urls) {
// const url = encodeURI(i.split('$')[1])
// txt += (url + '\n')
// }
// clipboard.writeText(txt)
// this.$message.success('已复制全集下载链接, 快去下载吧!')
// }
},
created () {
this.getDetail()
}
}
</script>
<style lang="scss">
.detail{
position: absolute;
left: 0;
bottom: 0;
width: 100%;
height: 680px;
background-color: #fff;
box-shadow:0 -4px 12px 0 #8e8da21f;
padding: 10px;
color: #808695;
z-index: 999;
.detail-content{
height: 660px;
padding: 10px 40px;
position: relative;
.detail-header{
width: 100%;
height: 40px;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 -40px;
.detail-title{
font-size: 16px;
}
.detail-close{
margin-right: 5px;
svg{
width: 24px;
height: 24px;
stroke: #823aa099;
stroke-width: 1;
stroke-linecap: round;
stroke-linejoin: round;
fill: none;
&:hover{
stroke-width: 2px;
stroke: #823aa0aa;
}
}
cursor: pointer;
}
}
.detail-body{
height: 600px;
overflow-y: auto;
&::-webkit-scrollbar{
width: 5px;
height: 1px;
}
&::-webkit-scrollbar-thumb {
border-radius: 10px;
box-shadow: inset 0 0 5px #823aa005;
background: #823aa055;
position: absolute;
}
&::-webkit-scrollbar-track {
box-shadow: inset 0 0 5px #823aa005;
border-radius: 10px;
background: #EDEDED;
position: absolute;
}
.info{
display: flex;
justify-content: flex-start;
align-items: flex-start;
flex-wrap: wrap;
width: 970px;
padding: 10px;
border: 1px solid #823aa022;
border-radius: 2px;
margin-bottom: 10px;
.vodImg{
width: 200px;
img{
width: 100%;
height: auto;
}
}
.vodAd{
display: none;
}
.vodInfo{
flex: 1;
margin-left: 20px;
overflow: hidden;
.vodh{
margin-bottom: 6px;
h2{
display: inline-block;
margin: 0;
}
span{
font-size: 12px;
margin-left: 10px;
}
label{
font-size: 20px;
font-weight: bold;
margin-left: 20px;
}
}
.cont, .tags{
display: none;
}
ul{
padding: 0;
margin: 0;
}
a{
display: none;
pointer-events: none;
}
li{
list-style: none;
font-size: 14px;
line-height: 18px;
height: 18px;
overflow: hidden;
span{
word-wrap: nowrap;
}
}
}
.whitetitle{
width: 100%;
font-size: 22px;
font-weight: bold;
margin: 4px 0;
}
.people{
display: flex;
justify-content: flex-start;
align-items: flex-start;
flex-wrap: wrap;
.left{
width: 200px;
img{
width: 100%;
height: auto;
}
}
.right{
flex: 1;
margin-left: 20px;
overflow: hidden;
p{
font-size: 14px;
}
a{
pointer-events: none;
color: #808695;
text-decoration: none;
}
}
}
}
.desc{
border: 1px solid #823aa033;
padding: 10px;
width: 970px;
margin-bottom: 10px;
border-radius: 2px;
font-size: 14px;
line-height: 20px;
}
.m3u8_urls, .mp4_urls{
border: 1px solid #823aa033;
padding: 10px;
width: 970px;
margin-bottom: 10px;
border-radius: 2px;
.title{
font-size: 16px;
}
.box{
width: 100%;
display: flex;
justify-content: space-between;
flex-wrap: wrap;
span{
font-size: 12px;
border: 1px solid #823aa055;
border-radius: 2px;
cursor: pointer;
margin: 6px 10px 0px 0px;
padding: 8px 22px;
&:hover{
color: #6e7380;
background-color: #823aa011;
}
}
&::after {
content: '';
flex: 1;
}
}
}
.mp4_urls{
margin-bottom: 10px;
}
}
.detail-mask{
width: 980px;
height: 600px;
position: absolute;
top: 50px;
display: flex;
justify-content: center;
align-items: center;
.loader {
color: #823aa055;
font-size: 8px;
width: 1em;
height: 1em;
border-radius: 50%;
position: relative;
text-indent: -9999em;
animation: load4 1.3s infinite linear;
transform: translateZ(0);
}
@keyframes load4 {
0%,
100% {
box-shadow: 0 -3em 0 0.2em, 2em -2em 0 0em, 3em 0 0 -1em, 2em 2em 0 -1em, 0 3em 0 -1em, -2em 2em 0 -1em, -3em 0 0 -1em, -2em -2em 0 0;
}
12.5% {
box-shadow: 0 -3em 0 0, 2em -2em 0 0.2em, 3em 0 0 0, 2em 2em 0 -1em, 0 3em 0 -1em, -2em 2em 0 -1em, -3em 0 0 -1em, -2em -2em 0 -1em;
}
25% {
box-shadow: 0 -3em 0 -0.5em, 2em -2em 0 0, 3em 0 0 0.2em, 2em 2em 0 0, 0 3em 0 -1em, -2em 2em 0 -1em, -3em 0 0 -1em, -2em -2em 0 -1em;
}
37.5% {
box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em, 3em 0em 0 0, 2em 2em 0 0.2em, 0 3em 0 0em, -2em 2em 0 -1em, -3em 0em 0 -1em, -2em -2em 0 -1em;
}
50% {
box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em, 3em 0 0 -1em, 2em 2em 0 0em, 0 3em 0 0.2em, -2em 2em 0 0, -3em 0em 0 -1em, -2em -2em 0 -1em;
}
62.5% {
box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em, 3em 0 0 -1em, 2em 2em 0 -1em, 0 3em 0 0, -2em 2em 0 0.2em, -3em 0 0 0, -2em -2em 0 -1em;
}
75% {
box-shadow: 0em -3em 0 -1em, 2em -2em 0 -1em, 3em 0em 0 -1em, 2em 2em 0 -1em, 0 3em 0 -1em, -2em 2em 0 0, -3em 0em 0 0.2em, -2em -2em 0 0;
}
87.5% {
box-shadow: 0em -3em 0 0, 2em -2em 0 -1em, 3em 0 0 -1em, 2em 2em 0 -1em, 0 3em 0 -1em, -2em 2em 0 0, -3em 0em 0 0, -2em -2em 0 0.2em;
}
}
}
}
}
</style>

View File

@@ -0,0 +1,344 @@
<template>
<div class="film">
<div class="top" v-if="top">
<!-- site -->
<div class="vue-select" @mouseleave="show.site = false">
<div class="vs-placeholder" @click="show.site = true">{{site.name}}</div>
<div class="vs-options" v-show="show.site">
<ul>
<li :class="site === j ? 'active' : ''" v-for="(i, j) in sites" :key="j" @click="siteClick(i)">{{ i.name }}</li>
</ul>
</div>
</div>
<!-- tags -->
<div class="vue-select" @mouseleave="show.tags = false" v-if="site.tags.length > 0">
<div class="vs-placeholder" @click="show.tags = true">{{site.tags[tag].title}}</div>
<div class="vs-options" v-show="show.tags">
<ul>
<li :class="tag === j ? 'active' : ''" v-for="(i, j) in site.tags" :key="j" @click="tagClick(i, j)">{{ i.title }}</li>
</ul>
</div>
</div>
<!-- type -->
<div class="vue-select" @mouseleave="show.type = false" v-if="site.tags[tag].children.length > 0">
<div class="vs-placeholder" @click="show.type = true">{{site.tags[tag].children[type].title}}</div>
<div class="vs-options" v-show="show.type">
<ul>
<li :class="type === j ? 'active' : ''" v-for="(i, j) in site.tags[tag].children" :key="j" @click="typeClick(i, j)">{{ i.title }}</li>
</ul>
</div>
</div>
<div :class="[inputFocus ? 'active ': ''] + 'search'" @mouseover="inputFocus = true" @mouseleave="inputFocus = false">
<div class="search-icon">
<svg role="img" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" aria-labelledby="searchIconTitle">
<title id="searchIconTitle">Search</title>
<path d="M14.4121122,14.4121122 L20,20"></path>
<circle cx="10" cy="10" r="6"></circle>
</svg>
</div>
<input type="text" class="search-box" v-model="keywords" @keypress.enter="searchEvent">
</div>
</div>
<div class="middle">
<div class="vue-table">
<div class="tHead">
<span class="name">影片名称</span>
<span class="type">类型</span>
<span class="time">时间</span>
<span class="operate">操作</span>
</div>
<div class="tBody">
<ul v-show="!tb.loading">
<li v-for="(i, j) in tb.list" :key="j" @click="detailEvent(i)">
<span class="name">{{i.name}}</span>
<span class="type">{{i.type}}</span>
<span class="time">{{i.time}}</span>
<span class="operate">
<span class="btn" @click.stop="playEvent(i)">播放</span>
<span class="btn" @click.stop="starEvent(i)">收藏</span>
<span class="btn" @click.stop="shareEvent(i)">分享</span>
</span>
</li>
</ul>
<div class="tBody-mask" v-show="tb.loading">
<div class="loader"></div>
</div>
</div>
<div class="tFooter">
<span class="tFooter-span">今日更新: {{ tb.update }} </span>
<el-pagination small :page-size="tb.size" :total="tb.total" :current-page="tb.page" @current-change="tbPageChange" layout="total, prev, pager, next, jumper"></el-pagination>
</div>
</div>
</div>
</div>
</template>
<script>
import { mapMutations } from 'vuex'
import { sites, getSite } from '../lib/site/sites'
import tools from '../lib/site/tools'
import video from '../lib/dexie/video'
import setting from '../lib/dexie/setting'
export default {
name: 'film',
data () {
return {
sites: sites,
site: {},
top: false,
tag: 0,
type: 0,
keywords: '',
id: '',
show: {
site: false,
tags: false,
type: false
},
inputFocus: false,
tb: {
list: [],
page: 1,
size: 50,
total: 0,
update: 0,
loading: true
}
}
},
computed: {
view: {
get () {
return this.$store.getters.getView
},
set (val) {
this.SET_VIEW(val)
}
},
gSite: {
get () {
return this.$store.getters.getSite
},
set (val) {
this.SET_SITE(val)
}
},
detail: {
get () {
return this.$store.getters.getDetail
},
set (val) {
this.SET_DETAIL(val)
}
},
video: {
get () {
return this.$store.getters.getVideo
},
set (val) {
this.SET_VIDEO(val)
}
},
share: {
get () {
return this.$store.getters.getShare
},
set (val) {
this.SET_SHARE(val)
}
}
},
watch: {
gSite (n, o) {
const s = getSite(n)
this.siteClick(s)
}
},
methods: {
...mapMutations(['SET_VIEW', 'SET_SITE', 'SET_DETAIL', 'SET_VIDEO', 'SET_SHARE']),
init () {
setting.find().then(res => {
this.site = getSite(res.site)
this.top = true
tools.film_get(res.site).then(tRes => {
this.tb.list = tRes.list
this.tb.total = tRes.total
this.tb.update = tRes.update
this.tb.loading = false
})
})
},
siteClick (e) {
this.site = e
this.tb.update = 0
this.tb.total = 0
this.tag = 0
this.id = e.tags[0].id
this.tb.loading = true
this.show.site = false
tools.film_get(e.key, this.id).then(res => {
this.tb.list = res.list
this.tb.total = res.total
this.tb.update = res.update
this.tb.loading = false
})
},
tagClick (e, n) {
this.tb.update = 0
this.tb.total = 0
this.tag = n
this.type = 0
if (e.children.length === 0) {
this.id = e.id
} else {
this.id = e.children[this.type].id
}
this.tb.loading = true
this.show.tags = false
tools.film_get(this.site.key, this.id).then(res => {
this.tb.list = res.list
this.tb.total = res.total
this.tb.update = res.update
this.tb.loading = false
})
},
typeClick (e, n) {
this.tb.update = 0
this.tb.total = 0
this.type = n
this.id = e.id
this.tb.loading = true
this.show.type = false
tools.film_get(this.site.key, this.id).then(res => {
this.tb.list = res.list
this.tb.total = res.total
this.tb.update = res.update
this.tb.loading = false
})
},
searchEvent () {
const flag = this.site.search
if (flag === '') {
this.$message.warning('该视频源不支持搜索')
return false
}
this.tb.loading = true
this.tb.update = 0
this.tb.total = 0
tools.search_get(this.site.key, this.keywords).then(res => {
this.tb.list = res.list
this.tb.total = res.total
this.tb.loading = false
})
},
detailEvent (e) {
this.detail = {
show: true,
v: e
}
},
playEvent (e) {
this.video = e
this.view = 'Play'
},
starEvent (e) {
video.find({ detail: e.detail }).then(res => {
if (res) {
this.$message.warning('已存在')
} else {
video.add(e).then(res => {
this.$message.success('收藏成功')
})
}
})
},
shareEvent (e) {
this.share = {
show: true,
v: e
}
},
tbPageChange (e) {
this.tb.loading = true
this.tb.page = e
tools.film_get(this.site.key, this.id, this.tb.page).then(res => {
this.tb.list = res.list
this.tb.loading = false
})
}
},
created () {
this.init()
}
}
</script>
<style lang="scss" scoped>
.film{
height: 670px;
width: 100%;
display: flex;
flex-direction: column;
animation: viewFadeIn 1s ease-in both;
.top{
width: 100%;
height: 30px;
display: flex;
justify-content: space-between;
align-items: center;
.search{
width: 200px;
height: 30px;
display: flex;
background-color: #fff;
justify-content: center;
align-items: center;
border-radius: 15px;
box-shadow: 0 3px 1px -2px #8e8da233, 0 2px 2px 0 #8e8da224, 0 1px 5px 0 #8e8da21f;
svg{
width: 20px;
height: 20px;
stroke: #823aa099;
stroke-width: 1;
stroke-linecap: round;
stroke-linejoin: round;
fill: none;
}
.search-icon{
width: 40px;
height: 30px;
display: flex;
justify-content: center;
align-items: center;
}
.search-box{
width: 160px;
height: 30px;
border-radius: 20px;
border: none;
background-color: #00000000;
text-indent: 2px;
font-size: 14px;
&:focus{
outline: none;
border: none;
}
}
&.active{
box-shadow: 0 14px 26px -12px #8e8da26b, 0 4px 23px 0 #8e8da21f, 0 8px 10px -5px #8e8da233;
svg{
stroke-width: 1.5;
fill: #823aa022;
}
}
}
}
.middle{
height: 620px;
width: 100%;
margin-top: 10px;
padding-bottom:0px;
background-color: #fff;
border-radius: 5px;
box-shadow: 0 3px 1px -2px #8e8da233, 0 2px 2px 0 #8e8da224, 0 1px 5px 0 #8e8da21f;
}
}
</style>

View File

@@ -0,0 +1,71 @@
<template>
<div class="frame">
<span class="min" @click="frameClickEvent('min')"></span>
<span class="close" @click="frameClickEvent('close')"></span>
</div>
</template>
<script>
// const ipc = require('electron').ipcRenderer
export default {
name: 'frame',
methods: {
// frameClickEvent (e) {
// ipc.send(e)
// }
}
}
</script>
<style lang="scss" scoped>
.frame{
width: 100%;
height: 40px;
display: flex;
justify-content: flex-end;
align-items: center;
user-select: none;
-webkit-app-region: drag;
span{
-webkit-app-region: no-drag;
display: inline-block;
width: 16px;
height: 16px;
border-radius: 50%;
margin-left: 10px;
cursor: pointer;
opacity: 0.5;
&.min{
background-color: #ffbe2a;
}
&.close{
background-color: #ff5f56;
}
&:hover{
animation: heartbeat 3s ease-in-out infinite both;
}
@keyframes heartbeat {
from {
transform: scale(1);
transform-origin: center center;
animation-timing-function: ease-out;
}
10% {
opacity: 1;
transform: scale(0.91);
animation-timing-function: ease-in;
}
17% {
transform: scale(0.98);
animation-timing-function: ease-out;
}
33% {
transform: scale(0.87);
animation-timing-function: ease-in;
}
45% {
transform: scale(1);
animation-timing-function: ease-out;
}
}
}
}
</style>

View File

@@ -1,60 +0,0 @@
<template>
<div class="hello">
<h1>{{ msg }}</h1>
<p>
For a guide and recipes on how to configure / customize this project,<br>
check out the
<a href="https://cli.vuejs.org" target="_blank" rel="noopener">vue-cli documentation</a>.
</p>
<h3>Installed CLI Plugins</h3>
<ul>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel" target="_blank" rel="noopener">babel</a></li>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-router" target="_blank" rel="noopener">router</a></li>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-vuex" target="_blank" rel="noopener">vuex</a></li>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-eslint" target="_blank" rel="noopener">eslint</a></li>
</ul>
<h3>Essential Links</h3>
<ul>
<li><a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a></li>
<li><a href="https://forum.vuejs.org" target="_blank" rel="noopener">Forum</a></li>
<li><a href="https://chat.vuejs.org" target="_blank" rel="noopener">Community Chat</a></li>
<li><a href="https://twitter.com/vuejs" target="_blank" rel="noopener">Twitter</a></li>
<li><a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a></li>
</ul>
<h3>Ecosystem</h3>
<ul>
<li><a href="https://router.vuejs.org" target="_blank" rel="noopener">vue-router</a></li>
<li><a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a></li>
<li><a href="https://github.com/vuejs/vue-devtools#vue-devtools" target="_blank" rel="noopener">vue-devtools</a></li>
<li><a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener">vue-loader</a></li>
<li><a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">awesome-vue</a></li>
</ul>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
props: {
msg: String
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped lang="scss">
h3 {
margin: 40px 0 0;
}
ul {
list-style-type: none;
padding: 0;
}
li {
display: inline-block;
margin: 0 10px;
}
a {
color: #42b983;
}
</style>

View File

@@ -0,0 +1,548 @@
<template>
<div class="play">
<div class="box">
<div class="title">{{name}}</div>
<div id="xg"></div>
<div class="more" v-show="more">
<span @click="nextEvent" v-show="showNext">
<svg role="img" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" aria-labelledby="forwardIconTitle">
<title id="forwardIconTitle">下一集</title>
<path d="M10 14.74L3 19V5l7 4.26V5l12 7-12 7v-4.26z"></path>
</svg>
</span>
<span @click="listEvent" :class="right.type === 'list' ? 'active' : ''" v-show="right.listData.length > 0">
<svg role="img" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" aria-labelledby="dashboardIconTitle">
<title id="dashboardIconTitle">播放列表</title>
<rect width="20" height="20" x="2" y="2"></rect>
<path d="M11 7L17 7M11 12L17 12M11 17L17 17"></path>
<line x1="7" y1="7" x2="7" y2="7"></line>
<line x1="7" y1="12" x2="7" y2="12"></line>
<line x1="7" y1="17" x2="7" y2="17"></line>
</svg>
</span>
<span @click="historyEvent" :class="right.type === 'history' ? 'active' : ''">
<svg role="img" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" aria-labelledby="timeIconTitle">
<title id="timeIconTitle">历史记录</title>
<circle cx="12" cy="12" r="10"></circle>
<polyline points="12 5 12 12 16 16"></polyline>
</svg>
</span>
<span @click="starEvent" :class="isStar ? 'active' : ''" v-show="right.listData.length > 0">
<svg role="img" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" aria-labelledby="favouriteIconTitle">
<title id="favouriteIconTitle">收藏</title>
<path d="M12,21 L10.55,19.7051771 C5.4,15.1242507 2,12.1029973 2,8.39509537 C2,5.37384196 4.42,3 7.5,3 C9.24,3 10.91,3.79455041 12,5.05013624 C13.09,3.79455041 14.76,3 16.5,3 C19.58,3 22,5.37384196 22,8.39509537 C22,12.1029973 18.6,15.1242507 13.45,19.7149864 L12,21 Z"></path>
</svg>
</span>
<span @click="topEvent" :class="isTop ? 'active' : ''" v-show="right.listData.length > 0">
<svg role="img" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" aria-labelledby="arrowUpIconTitle">
<title id="arrowUpIconTitle">置顶</title>
<path d="M9 10.5l3-3 3 3"></path>
<path d="M12 16.5V9"></path>
<path stroke-linecap="round" d="M12 7.5V9"></path>
<circle cx="12" cy="12" r="10"></circle>
</svg>
</span>
<span @click="detailEvent" v-show="right.listData.length > 0">
<svg role="img" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" aria-labelledby="feedIconTitle">
<title id="feedIconTitle">详情</title>
<circle cx="7.5" cy="7.5" r="2.5"></circle>
<path d="M22 13H2"></path>
<path d="M18 6h-5m5 3h-5"></path>
<path d="M5 2h14a3 3 0 0 1 3 3v17H2V5a3 3 0 0 1 3-3z"></path>
</svg>
</span>
<!-- <span @click="smallEvent" v-show="right.listData.length > 0">
<svg role="img" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" aria-labelledby="tvIconTitle">
<title id="tvIconTitle">精简模式</title>
<polygon points="20 8 20 20 4 20 4 8"></polygon>
<polyline stroke-linejoin="round" points="8 4 12 7.917 16 4"></polyline>
</svg>
</span> -->
<span @click="shareEvent" v-show="right.listData.length > 0">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-labelledby="qrIconTitle">
<title id="qrIconTitle">分享</title>
<rect x="10" y="3" width="7" height="7" transform="rotate(90 10 3)"></rect>
<rect width="1" height="1" transform="matrix(-1 0 0 1 7 6)"></rect>
<rect x="10" y="14" width="7" height="7" transform="rotate(90 10 14)"></rect>
<rect x="6" y="17" width="1" height="1"></rect>
<rect x="14" y="20" width="1" height="1"></rect>
<rect x="17" y="17" width="1" height="1"></rect>
<rect x="14" y="14" width="1" height="1"></rect>
<rect x="20" y="17" width="1" height="1"></rect>
<rect x="20" y="14" width="1" height="1"></rect>
<rect x="20" y="20" width="1" height="1"></rect>
<rect x="21" y="3" width="7" height="7" transform="rotate(90 21 3)"></rect>
<rect x="17" y="6" width="1" height="1"></rect>
</svg>
</span>
</div>
</div>
<transition name="slideX">
<div v-if="right.show" class="list">
<div class="list-top">
<span class="list-top-title">{{ right.type === 'list' ? '播放列表' : '历史记录' }}</span>
<span class="list-top-close" @click="closeEvent">
<svg role="img" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" aria-labelledby="closeIconTitle">
<title id="closeIconTitle">关闭</title>
<path d="M6.34314575 6.34314575L17.6568542 17.6568542M6.34314575 17.6568542L17.6568542 6.34314575"></path>
</svg>
</span>
</div>
<div class="list-body" :style="{overflowY:scroll? 'auto' : 'hidden',paddingRight: scroll ? '0': '5px' }" @mouseenter="scroll = true" @mouseleave="scroll = false">
<ul v-show="right.type === 'list'">
<li v-show="right.listData.length === 0">无数据</li>
<li @click="listItemEvent(j)" :class="video.index === j ? 'active' : ''" v-for="(i, j) in right.listData" :key="j">{{i | ftName}}</li>
</ul>
<ul v-show="right.type === 'history'">
<li v-show="right.historyData.length > 1" @click="clearAll">清空数据</li>
<li v-show="right.historyData.length === 0">无数据</li>
<li @click="historyItemEvent(m)" v-for="(m, n) in right.historyData" :key="n"><span class="title">{{m.name}}</span><span @click.stop="removeItem(m)" class="delete">删除</span></li>
</ul>
</div>
</div>
</transition>
</div>
</template>
<script>
import { mapMutations } from 'vuex'
import tools from '../lib/site/tools'
import history from '../lib/dexie/history'
import video from '../lib/dexie/video'
import 'xgplayer'
import Hls from 'xgplayer-hls.js'
// const { ipcRenderer: ipc } = require('electron')
export default {
name: 'play',
data () {
return {
xg: null,
right: {
show: false,
type: '',
listData: [],
historyData: []
},
config: {
id: 'xg',
lang: 'zh-cn',
url: '',
fluid: true,
autoplay: false,
videoInit: true,
screenShot: true,
keyShortcut: 'on',
crossOrigin: true,
defaultPlaybackRate: 1,
playbackRate: [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2, 3, 4, 5]
},
name: '',
timer: null,
scroll: false,
more: true,
showNext: false,
isStar: false,
isTop: false
}
},
computed: {
view: {
get () {
return this.$store.getters.getView
},
set (val) {
this.SET_VIEW(val)
}
},
video: {
get () {
return this.$store.getters.getVideo
},
set (val) {
this.SET_VIDEO(val)
}
},
detail: {
get () {
return this.$store.getters.getDetail
},
set (val) {
this.SET_DETAIL(val)
}
},
share: {
get () {
return this.$store.getters.getShare
},
set (val) {
this.SET_SHARE(val)
}
}
},
filters: {
ftName (e) {
return e.split('$')[0]
}
},
watch: {
view () {
this.right.show = false
this.right.type = ''
},
video: {
handler () {
this.getUrls()
},
deep: true
}
},
methods: {
...mapMutations(['SET_VIEW', 'SET_DETAIL', 'SET_VIDEO', 'SET_SHARE']),
getUrls () {
if (this.timer !== null) {
clearInterval(this.timer)
this.timer = null
}
if (this.xg) {
this.xg.pause()
this.xg.off('play', () => {
console.log('play off')
})
}
this.changeVideo()
tools.detail_get(this.video.site, this.video.detail).then(res => {
this.name = this.video.name
this.right.listData = res.m3u8_urls
if (res.m3u8_urls.length > 1) {
const m3 = res.m3u8_urls
const arr = []
for (const i of m3) {
arr.push(i.split('$')[1])
}
this.xg.src = arr[this.video.index]
this.showNext = true
} else {
const link = res.m3u8_urls[this.video.index]
const src = link.split('$')[1]
this.xg.src = src
this.showNext = false
}
const currentTime = this.video.currentTime
if (currentTime !== '') {
this.xg.play()
this.xg.once('playing', () => {
this.xg.currentTime = currentTime
})
} else {
this.xg.play()
}
this.onPlayVideo()
})
},
changeVideo () {
this.checkStar()
this.checkTop()
this.name = ''
},
checkStar () {
video.find({ detail: this.video.detail }).then(res => {
if (res) {
this.isStar = true
} else {
this.isStar = false
}
})
},
// checkTop () {
// ipc.send('checkTop')
// ipc.on('isTop', (e, flag) => {
// this.isTop = flag
// })
// },
onPlayVideo () {
this.more = true
const h = { ...this.video }
history.find({ detail: h.detail }).then(res => {
if (res) {
history.update(res.id, h)
} else {
h.currentTime = ''
delete h.id
history.add(h)
}
})
this.timerEvent(h.detail)
},
timerEvent (d) {
this.timer = setInterval(() => {
history.find({ detail: d }).then(res => {
if (res) {
const h = { ...this.video }
h.currentTime = this.xg.currentTime
delete h.id
history.update(res.id, h)
}
})
}, 10000)
},
closeEvent () {
this.right.show = false
this.right.type = ''
},
nextEvent () {
const v = { ...this.video }
const i = v.index + 1
if (i < this.right.listData.length) {
this.video.index++
} else {
this.$message.warning('这是最后一集了.')
}
},
listEvent () {
if (this.right.type === 'list') {
this.right.show = false
this.right.type = ''
} else {
this.right.show = true
this.right.type = 'list'
}
},
historyEvent () {
if (this.right.type === 'history') {
this.right.show = false
this.right.type = ''
} else {
this.right.show = true
this.right.type = 'history'
}
history.all().then(res => {
this.right.historyData = res.reverse()
})
},
starEvent () {
video.find({ detail: this.video.detail }).then(res => {
if (res) {
video.remove(this.video.id).then(r => {
this.$message.info('删除成功')
this.isStar = false
})
} else {
const v = { ...this.video }
if (v.id) {
delete v.id
}
video.add(v).then(r => {
this.$message.success('收藏成功')
this.isStar = true
})
}
})
},
topEvent () {
ipc.send('top')
this.checkTop()
},
detailEvent () {
this.detail = {
show: true,
v: this.video
}
},
smallEvent () {}, // TODO 小窗口模式
shareEvent () {
this.share = {
show: true,
v: this.video
}
},
clearAll () {
history.clear().then(res => {
this.right.historyData = []
})
},
listItemEvent (n) {
this.video.index = n
},
historyItemEvent (e) {
this.video = e
},
removeItem (e) {
history.remove(e.id).then(res => {
history.all().then(e => {
this.right.historyData = e.reverse()
})
})
}
},
mounted () {
this.xg = new Hls(this.config)
}
}
</script>
<style lang="scss" scoped>
.play{
position: relative;
height: 660px;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
background-color: #ffffff;
border-radius: 5px;
box-shadow: 0 3px 1px -2px #8e8da233, 0 2px 2px 0 #8e8da224, 0 1px 5px 0 #8e8da21f;
.box{
width: 92%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
.title{
width: 100%;
height: 40px;
display: flex;
justify-content: flex-start;
align-items: center;
}
.more{
width: 100%;
height: 60px;
display: flex;
justify-content: flex-start;
align-items: center;
span{
display: flex;
margin-right: 10px;
cursor: pointer;
&:hover{
svg{
stroke: #823aa0ee;
stroke-width: 1.5;
fill: #823aa022;
}
}
&.active{
svg{
stroke: #823aa0;
stroke-width: 2;
fill: #823aa033;
}
}
}
svg{
width: 24px;
height: 24px;
stroke: #823aa099;
stroke-width: 1;
stroke-linecap: round;
stroke-linejoin: round;
fill: none;
}
}
}
.list{
position: absolute;
top: 0;
right: 0;
width: 300px;
height: 100%;
border: 1px solid #00000022;
background-color: #fff;
z-index: 555;
border-radius: 3px;
padding: 6px;
display: flex;
flex-direction: column;
svg{
width: 24px;
height: 24px;
stroke: #823aa099;
stroke-width: 1;
stroke-linecap: round;
stroke-linejoin: round;
fill: none;
}
.list-top{
display: flex;
justify-content: space-between;
align-items: center;
height: 30px;
.list-top-title{
font-size: 16px;
}
.list-top-close{
display: inline-block;
cursor: pointer;
}
}
.list-body{
flex: 1;
overflow-y: auto;
&::-webkit-scrollbar{
width: 5px;
height: 1px;
}
&::-webkit-scrollbar-thumb {
border-radius: 10px;
box-shadow: inset 0 0 5px #823aa005;
background: #823aa055;
position: absolute;
}
&::-webkit-scrollbar-track {
box-shadow: inset 0 0 5px #823aa005;
border-radius: 10px;
background: #EDEDED;
position: absolute;
}
ul{
margin: 0;
padding: 0;
list-style: none;
li{
position: relative;
height: 28px;
width: 100%;
line-height: 28px;
padding-left: 10px;
font-size: 14px;
cursor: pointer;
color: #808695;
&.active{
background-color: #823aa011;
}
&:hover{
background-color: #823aa011;
.delete{
display: inline-block;
}
}
.title{
display: inline-block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: 231px;
}
.delete{
display: none;
position: absolute;
right: 0;
height: 28px;
width: 50px;
text-align: center;
&:hover{
background-color: #823aa022;
}
}
}
}
}
}
.slideX-enter-active, .slideX-leave-active{
transition: all .5s ease-in-out;
}
.slideX-enter, .slideX-leave-to{
transform: translateX(100%);
opacity: 0;
}
}
</style>

View File

@@ -0,0 +1,152 @@
<template>
<div class="setting" v-if="show.setting">
<div class="logo"><img src="@/assets/image/logo.png"></div>
<div class="info"><a href="https://github.com/Hunlongyu/ZY-Player">官网</a><a href="https://github.com/Hunlongyu/ZY-Player/issues">反馈</a></div>
<div class="change">
<div class="vue-select" @mouseleave="show.language = false">
<div class="vs-placeholder" @click="show.language = true">{{$t('language')}}</div>
<div class="vs-options" v-show="show.language">
<ul>
<li :class="s.language === i.key ? 'active' : ''" v-for="(i, j) in languages" :key="j" @click="languageClick(i.key)">{{ i.name }}</li>
</ul>
</div>
</div>
<div class="vue-select" @mouseleave="show.site = false">
<div class="vs-placeholder" @click="show.site = true">{{$t('default_site')}}</div>
<div class="vs-options" v-show="show.site">
<ul>
<li :class="s.site === i.key ? 'active' : ''" v-for="(i, j) in sites" :key="j" @click="siteClick(i.key)">{{ i.name }}</li>
</ul>
</div>
</div>
</div>
<div class="theme">主题</div>
<div class="qrcode">喝咖啡</div>
</div>
</template>
<script>
import { mapMutations } from 'vuex'
import setting from '../lib/dexie/setting'
import { sites } from '../lib/site/sites'
export default {
name: 'setting',
data () {
return {
s: {},
languages: [
{
key: 'zhCn',
name: '中文'
},
{
key: 'en',
name: 'English'
}
],
sites: sites,
show: {
setting: false,
language: false,
site: false
}
}
},
computed: {
theme: {
get () {
return this.$store.getters.getTheme
},
set (val) {
this.SET_THEME(val)
}
},
language: {
get () {
return this.$store.getters.getLanguage
},
set (val) {
this.SET_LANGUAGE(val)
}
},
site: {
get () {
return this.$store.getters.getSite
},
set (val) {
this.SET_SITE(val)
}
}
},
methods: {
...mapMutations(['SET_THEME', 'SET_LANGUAGE', 'SET_SITE']),
languageClick (e) {
this.language = e
this.show.language = false
this.$i18n.locale = e
this.s.language = e
setting.update(this.s).then(res => {
this.$message.success('设置成功')
})
},
siteClick (e) {
this.site = e
this.show.site = false
this.s.site = e
setting.update(this.s).then(res => {
this.$message.success('设置成功')
})
}
},
created () {
setting.find().then(res => {
this.s = res
this.$i18n.locale = this.s.language
this.show.setting = true
})
}
}
</script>
<style lang="scss" scoped>
.setting{
height: 660px;
width: 100%;
display: flex;
flex-direction: column;
background-color: #ffffff;
border-radius: 5px;
box-shadow: 0 3px 1px -2px #8e8da233, 0 2px 2px 0 #8e8da224, 0 1px 5px 0 #8e8da21f;
.logo{
margin-top: 40px;
width: 100%;
text-align: center;
img{
width: 120px;
height: auto;
}
}
.info{
width: 100%;
margin-top: 20px;
text-align: center;
a{
text-decoration: none;
margin: 0 10px;
font-size: 14px;
color: #808695;
&:hover{
color: #4c4f57;
}
}
}
.change{
width: 100%;
display: flex;
justify-content: flex-start;
padding-left: 20px;
margin-top: 40px;
.vue-select{
margin-right: 20px;
}
}
}
</style>

View File

@@ -0,0 +1,199 @@
<template>
<div class="share" id="share" @click="shareClickEvent">
<div class="left">
<img :src="this.card.img" alt="">
</div>
<div class="right">
<div class="title">{{ card.name }}</div>
<qrcode-vue id="qr" :value="value" :size="160" level="L" />
<div class="tips">
<p>长按识别二维码, 即可播放.</p>
<p><img src="@/assets/image/logo.png"></p>
<p class="zy">ZY Player提供技术支持.严禁传播违法资源</p>
</div>
</div>
<div class="share-mask" v-show="loading">
<div class="loader"></div>
</div>
</div>
</template>
<script>
import { mapMutations } from 'vuex'
import tools from '../lib/site/tools'
import QrcodeVue from 'qrcode.vue'
import html2canvas from 'html2canvas'
// const { clipboard, nativeImage } = require('electron')
export default {
name: 'share',
data () {
return {
card: {
img: '',
name: '',
png: ''
},
value: 'https://www.baidu.com',
loading: true
}
},
components: {
QrcodeVue
},
computed: {
share: {
get () {
return this.$store.getters.getShare
},
set (val) {
this.SET_SHARE(val)
}
}
},
watch: {
share: {
handler () {
this.getDetail()
},
deep: true
}
},
methods: {
...mapMutations(['SET_SHARE']),
// getDetail () {
// this.loading = true
// tools.detail_get(this.share.v.site, this.share.v.detail).then(res => {
// const info = res.info
// const parser = new DOMParser()
// const html = parser.parseFromString(info, 'text/html')
// const img = html.querySelector('img').src
// this.card.img = img
// this.card.name = this.share.v.name
// this.loading = false
// this.$nextTick(() => {
// const dom = document.getElementById('share')
// html2canvas(dom, { allowTaint: true, useCORS: true }).then(res => {
// const png = res.toDataURL('image/png')
// const p = nativeImage.createFromDataURL(png)
// clipboard.writeImage(p)
// this.$message.success('已复制到剪贴板中, 快去分享吧~')
// this.share.show = true
// })
// })
// })
// },
shareClickEvent () {
this.share = {
show: false,
v: {}
}
}
},
created () {
this.getDetail()
}
}
</script>
<style lang="scss" scoped>
.share{
position: absolute;
bottom: 20px;
right: 20px;
width: 540px;
height: 360px;
background-color: #fff;
border-radius: 2px;
display: flex;
justify-content: space-between;
align-items: center;
border: 1px solid #cfcfcf;
padding: 20px;
z-index: 888;
.left, .right{
width: 50%;
height: 100%;
}
.left{
display: flex;
justify-content: center;
align-items: center;
img{
height: 320px;
width: auto;
max-width: 240px;
}
}
.right{
.title{
font-size: 18px;
color: #666;
margin-bottom: 10px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
#qr{
text-align: center;
}
.tips{
font-size: 14px;
text-align: center;
color: #808695;
img{
width: 50px;
}
.zy{
font-size: 12px;
}
}
}
.share-mask{
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
background-color: #fff;
.loader {
color: #823aa055;
font-size: 8px;
width: 1em;
height: 1em;
border-radius: 50%;
position: relative;
text-indent: -9999em;
animation: load4 1.3s infinite linear;
transform: translateZ(0);
}
@keyframes load4 {
0%,
100% {
box-shadow: 0 -3em 0 0.2em, 2em -2em 0 0em, 3em 0 0 -1em, 2em 2em 0 -1em, 0 3em 0 -1em, -2em 2em 0 -1em, -3em 0 0 -1em, -2em -2em 0 0;
}
12.5% {
box-shadow: 0 -3em 0 0, 2em -2em 0 0.2em, 3em 0 0 0, 2em 2em 0 -1em, 0 3em 0 -1em, -2em 2em 0 -1em, -3em 0 0 -1em, -2em -2em 0 -1em;
}
25% {
box-shadow: 0 -3em 0 -0.5em, 2em -2em 0 0, 3em 0 0 0.2em, 2em 2em 0 0, 0 3em 0 -1em, -2em 2em 0 -1em, -3em 0 0 -1em, -2em -2em 0 -1em;
}
37.5% {
box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em, 3em 0em 0 0, 2em 2em 0 0.2em, 0 3em 0 0em, -2em 2em 0 -1em, -3em 0em 0 -1em, -2em -2em 0 -1em;
}
50% {
box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em, 3em 0 0 -1em, 2em 2em 0 0em, 0 3em 0 0.2em, -2em 2em 0 0, -3em 0em 0 -1em, -2em -2em 0 -1em;
}
62.5% {
box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em, 3em 0 0 -1em, 2em 2em 0 -1em, 0 3em 0 0, -2em 2em 0 0.2em, -3em 0 0 0, -2em -2em 0 -1em;
}
75% {
box-shadow: 0em -3em 0 -1em, 2em -2em 0 -1em, 3em 0em 0 -1em, 2em 2em 0 -1em, 0 3em 0 -1em, -2em 2em 0 0, -3em 0em 0 0.2em, -2em -2em 0 0;
}
87.5% {
box-shadow: 0em -3em 0 0, 2em -2em 0 -1em, 3em 0 0 -1em, 2em 2em 0 -1em, 0 3em 0 -1em, -2em 2em 0 0, -3em 0em 0 0, -2em -2em 0 0.2em;
}
}
}
}
</style>

View File

@@ -0,0 +1,160 @@
<template>
<div class="star">
<div class="vue-table">
<div class="tHead">
<span class="name">影片名称</span>
<span class="type">类型</span>
<span class="time">时间</span>
<span class="from">来源</span>
<span class="operate" style="width: 160px">操作</span>
</div>
<div class="tBody">
<ul v-show="!loading">
<li v-for="(i, j) in data" :key="j" @click="detailEvent(i)">
<span class="name">{{i.name}}</span>
<span class="type">{{i.type}}</span>
<span class="time">{{i.time}}</span>
<span class="from">{{i.site | ftSite}}</span>
<span class="operate" style="width: 160px">
<span class="btn" @click.stop="playEvent(i)">播放</span>
<span class="btn" @click.stop="deleteEvent(i)">删除</span>
<span class="btn" @click.stop="shareEvent(i)">分享</span>
<span class="btn" @click.stop="updateEvent(i)">同步</span>
</span>
</li>
</ul>
<div class="tBody-mask" v-show="loading">
<div class="loader"></div>
</div>
</div>
<div class="tFooter">
<span class="tFooter-span"> {{data.length}} 条数据</span>
</div>
</div>
</div>
</template>
<script>
import { mapMutations } from 'vuex'
import tools from '../lib/site/tools'
import video from '../lib/dexie/video'
import { sites, getSite } from '../lib/site/sites'
export default {
name: 'star',
data () {
return {
sites: sites,
data: [],
loading: true
}
},
computed: {
view: {
get () {
return this.$store.getters.getView
},
set (val) {
this.SET_VIEW(val)
}
},
detail: {
get () {
return this.$store.getters.getDetail
},
set (val) {
this.SET_DETAIL(val)
}
},
video: {
get () {
return this.$store.getters.getVideo
},
set (val) {
this.SET_VIDEO(val)
}
},
share: {
get () {
return this.$store.getters.getShare
},
set (val) {
this.SET_SHARE(val)
}
}
},
filters: {
ftSite (e) {
const name = getSite(e).name
return name
}
},
watch: {
view () {
this.getAllStar()
}
},
methods: {
...mapMutations(['SET_VIEW', 'SET_DETAIL', 'SET_VIDEO', 'SET_SHARE']),
detailEvent (e) {
this.detail = {
show: true,
v: e
}
},
playEvent (e) {
this.video = e
this.view = 'Play'
},
deleteEvent (e) {
video.remove(e.id).then(res => {
if (res) {
this.$message.warning('删除失败')
} else {
this.$message.success('删除成功')
}
this.getAllStar()
})
},
shareEvent (e) {
this.share = {
show: true,
v: e
}
},
updateEvent (e) {
tools.detail_get(e.site, e.detail).then(res => {
const nameOne = e.name.replace(/\s*/g, '')
const nameTwo = res.name.replace(/\s*/g, '')
if (nameOne === nameTwo) {
this.$message.info('同步成功, 未查询到更新.')
} else {
const h = e
h.name = res.name
video.update(h.id, h).then(res => {
this.$message.success('同步成功, 查询到更新.')
})
}
})
},
getAllStar () {
video.all().then(res => {
this.data = res.reverse()
this.loading = false
})
}
},
created () {
this.getAllStar()
}
}
</script>
<style lang="scss" scoped>
.star{
height: 660px;
width: 100%;
display: flex;
flex-direction: column;
background-color: #ffffff;
border-radius: 5px;
box-shadow: 0 3px 1px -2px #8e8da233, 0 2px 2px 0 #8e8da224, 0 1px 5px 0 #8e8da21f;
}
</style>

View File

@@ -0,0 +1,22 @@
import Vue from 'vue'
import Aside from './Aside'
import Detail from './Detail'
import Film from './Film'
import Frame from './Frame'
import Play from './Play'
import Setting from './Setting'
import Share from './Share'
import Star from './Star'
export default {
registerComponents () {
Vue.component('Aside', Aside)
Vue.component('Detail', Detail)
Vue.component('Film', Film)
Vue.component('Frame', Frame)
Vue.component('Play', Play)
Vue.component('Setting', Setting)
Vue.component('Share', Share)
Vue.component('Star', Star)
}
}

View File

@@ -0,0 +1,32 @@
import setting from '../dexie/setting'
const os = require('os')
const macadress = require('macaddress')
const AV = require('leancloud-storage')
setting.find().then(res => {
const cloud = res.cloud
if (!cloud) {
macadress.one((err, mac) => {
if (err) {
console.log(err)
}
const system = os.hostname() + ' ' + os.type() + ' ' + os.arch()
AV.init({
appId: 'X6TRIcMjgOG7EJ0t1l5r9In1-gzGzoHsz',
appKey: 'JmkGF9UqkWGQNYDcJ2g1QV1b',
serverURL: 'https://x6tricmj.lc-cn-n1-shared.com'
})
const ZYPlayer = AV.Object.extend('ZYPlayer')
const zyPlayer = new ZYPlayer()
zyPlayer.set('os', system)
zyPlayer.set('mac', mac)
zyPlayer.save().then(e => {
const id = e.id
res.cloud = true
res.cloudKey = id
setting.update(res)
})
})
}
})

22
src/lib/dexie/history.js Normal file
View File

@@ -0,0 +1,22 @@
import db from './index'
const { history } = db
export default {
async add (doc) {
return await history.add(doc)
},
async find (doc) {
return await history.get(doc)
},
async update (id, docs) {
return await history.update(id, docs)
},
async all () {
return await history.toArray()
},
async remove (id) {
return await history.delete(id)
},
async clear () {
return await history.clear()
}
}

View File

@@ -0,0 +1,32 @@
import Dexie from 'dexie'
const db = new Dexie('ZYDB')
db.version(1).stores({
theme: '++id, theme',
site: '++id, site',
video: '++id, name, type, time, detail, urls, index'
})
db.version(2).stores({
setting: 'id, theme, site, language, cloud, cloudKey',
video: '++id, site, name, type, time, detail, index',
history: '++id, site, name, type, time, detail, index, currentTime'
})
const initData = [{
id: 0,
theme: 'light',
site: 'zuidazy',
language: 'zhCn',
cloud: false,
cloudKey: ''
}]
db.on('populate', () => {
db.setting.bulkAdd(initData)
})
db.open()
export default db

18
src/lib/dexie/setting.js Normal file
View File

@@ -0,0 +1,18 @@
import db from './index'
const { setting } = db
export default {
async find () {
return await setting.get({ id: 0 })
},
async update (docs) {
return await setting.update(0, docs)
}
}
/*
setting.find().then(res => {
console.log(res, 'find')
})
setting.update({ theme: 'yellow' }).then(res => {
console.log(res, 'update')
})
*/

19
src/lib/dexie/video.js Normal file
View File

@@ -0,0 +1,19 @@
import db from './index'
const { video } = db
export default {
async add (doc) {
return await video.add(doc)
},
async find (doc) {
return await video.get(doc)
},
async update (id, docs) {
return await video.update(id, docs)
},
async all () {
return await video.toArray()
},
async remove (id) {
return await video.delete(id)
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,364 @@
import axios from 'axios'
import { getSite } from './sites'
const zy = {
key: 'zuidazy', // sites[n] 视频源
id: 0, // 视频类型
page: 1, // 第几页
keywords: '', // 搜索关键字
// 获取浏览列表
film_get (key, id = 1, page = 1) {
return new Promise((resolve, reject) => {
const site = getSite(key)
let url = ''
if (id === 0) {
url = site.new.replace(/{page}/, page)
} else {
url = site.view.replace(/{id}/, id).replace(/{page}/, page)
}
const type = site.type
axios.get(url).then(async res => {
const data = res.data
if (type === 0) {
const zeroData = await this.film_get_type_zero(data, key)
resolve(zeroData)
}
if (type === 1) {
const oneData = await this.film_get_type_one(data, key)
resolve(oneData)
}
if (type === 2) {
const twoData = await this.film_get_type_two(data, key)
resolve(twoData)
}
}).catch(err => {
reject(err)
})
})
},
film_get_type_zero (txt, key) {
return new Promise((resolve, reject) => {
try {
const parser = new DOMParser()
const html = parser.parseFromString(txt, 'text/html')
const list = html.querySelectorAll('.xing_vb li')
const d = { list: [], total: 0, update: 0 }
const url = getSite(key).url
for (let i = 1; i < list.length - 1; i++) {
const info = {
site: key,
name: list[i].childNodes[1].innerText,
type: list[i].childNodes[3].innerText,
time: list[i].childNodes[5].innerText,
detail: url + list[i].childNodes[1].querySelector('a').getAttribute('href'),
index: 0
}
d.list.push(info)
}
d.update = parseInt(html.querySelectorAll('.xing_top_right li strong')[0].innerText)
let t = html.querySelector('.pages').innerText
t = t.split('条')[0]
t = t.split('共')[1]
d.total = parseInt(t)
resolve(d)
} catch (err) {
reject(err)
}
})
},
film_get_type_one (txt, key) {
return new Promise((resolve, reject) => {
try {
const parser = new DOMParser()
const html = parser.parseFromString(txt, 'text/html')
const list = html.querySelectorAll('.videoContent li')
const d = { list: [], total: 0, update: 0 }
const url = getSite(key).url
for (let i = 0; i < list.length; i++) {
const info = {
site: key,
name: list[i].querySelector('.videoName').innerText,
type: list[i].querySelector('.category').innerText,
time: list[i].querySelector('.time').innerText,
detail: url + list[i].querySelector('.address').getAttribute('href'),
index: 0
}
d.list.push(info)
}
d.update = parseInt(html.querySelectorAll('.header_list li span')[0].innerText)
let t = html.querySelectorAll('.pagination li')
t = t[t.length - 2].innerText
d.total = parseInt(t) * 50
resolve(d)
} catch (err) {
reject(err)
}
})
},
film_get_type_two (txt, key) {
return new Promise((resolve, reject) => {
try {
const parser = new DOMParser()
const html = parser.parseFromString(txt, 'text/html')
const list = html.querySelectorAll('.nr')
const d = { list: [], total: 0, update: 0 }
const url = getSite(key).url
for (let i = 0; i < list.length; i++) {
const info = {
site: key,
name: '',
type: list[i].querySelector('.btn_span').innerText,
time: list[i].querySelector('.hours').innerText,
detail: url + list[i].querySelector('.name').getAttribute('href'),
index: 0
}
let name = list[i].querySelector('.name').innerText
name = name.replace(/^\s*|\s*$/g, '')
info.name = name
d.list.push(info)
}
d.update = parseInt(html.querySelector('.kfs em').innerText)
d.total = parseInt(html.querySelector('.date span').innerText)
let t = html.querySelector('.pag2').innerText
t = t.split('条')[0]
t = t.split('共')[1]
d.total = parseInt(t)
resolve(d)
} catch (err) {
reject(err)
}
})
},
// 获取详情
detail_get (key, url) {
return new Promise((resolve, reject) => {
const type = getSite(key).type
axios.get(url).then(async res => {
if (type === 0) {
const zeroData = await this.detail_get_type_zero(res.data, key)
resolve(zeroData)
}
if (type === 1) {
const oneData = await this.detail_get_type_one(res.data, key)
resolve(oneData)
}
if (type === 2) {
const twoData = await this.detail_get_type_two(res.data, key)
resolve(twoData)
}
}).catch(err => {
reject(err)
})
})
},
detail_get_type_zero (txt, key) {
return new Promise((resolve, reject) => {
try {
const parser = new DOMParser()
const html = parser.parseFromString(txt, 'text/html')
const data = {
site: key,
name: '',
info: '',
desc: '',
m3u8_urls: [],
mp4_urls: []
}
const vodBox = html.querySelector('.vodBox')
data.info = vodBox.innerHTML
const title = html.querySelector('.vodh h2').innerText
const index = html.querySelector('.vodh span').innerText
data.name = title + index
const vodInfo = html.querySelectorAll('.playBox')
for (let i = 0; i < vodInfo.length; i++) {
const k = vodInfo[i].innerText
if (k.indexOf('剧情介绍') >= 0) {
data.desc = vodInfo[i].querySelector('.vodplayinfo').innerHTML
}
}
const vodLi = html.querySelectorAll('.ibox .vodplayinfo li')
const m3u8UrlArr = []
const mp4UrlArr = []
for (let i = 0; i < vodLi.length; i++) {
const j = vodLi[i].innerText
if (j.indexOf('.m3u8') >= 0) {
m3u8UrlArr.push(j)
}
if (j.indexOf('.mp4') >= 0) {
mp4UrlArr.push(j)
}
}
data.m3u8_urls = m3u8UrlArr
data.mp4_urls = mp4UrlArr
resolve(data)
} catch (err) {
reject(err)
}
})
},
detail_get_type_one (txt, key) {
return new Promise((resolve, reject) => {
try {
const parser = new DOMParser()
const html = parser.parseFromString(txt, 'text/html')
const data = {
site: key,
name: '',
info: '',
desc: '',
m3u8_urls: [],
mp4_urls: []
}
let name = html.querySelector('.whitetitle').innerText
name = name.split('')[1].replace(/^\s*|\s*$/g, '')
data.name = name
const vodBox = html.querySelector('.white').innerHTML
data.info = vodBox
const vodInfo = html.querySelectorAll('.white')
for (let i = 0; i < vodInfo.length; i++) {
const k = vodInfo[i].innerText
if (k.indexOf('剧情介绍') >= 0) {
data.desc = vodInfo[i].querySelector('div').innerText
}
}
const vodLi = html.querySelectorAll('.playlist li #m3u8')
const m3u8UrlArr = []
const mp4UrlArr = []
for (let i = 0; i < vodLi.length; i++) {
const j = vodLi[i].value
if (j.indexOf('.m3u8') >= 0) {
m3u8UrlArr.push(j)
}
if (j.indexOf('.mp4') >= 0) {
mp4UrlArr.push(j)
}
}
data.m3u8_urls = m3u8UrlArr
data.mp4_urls = mp4UrlArr
resolve(data)
} catch (err) {
reject(err)
}
})
},
detail_get_type_two (txt, key) {
return new Promise((resolve, reject) => {
try {
const parser = new DOMParser()
const html = parser.parseFromString(txt, 'text/html')
const data = {
site: key,
name: '',
info: '',
desc: '',
m3u8_urls: [],
mp4_urls: []
}
const title = html.querySelector('.vodh h2').innerText
const index = html.querySelector('.vodh span').innerText
data.name = title + index
const vodBox = html.querySelector('.vodBox').innerHTML
data.info = vodBox
data.desc = html.querySelector('.vodplayinfo').innerText
const vodLi = html.querySelectorAll('.vodplayinfo li')
const m3u8UrlArr = []
const mp4UrlArr = []
for (let i = 0; i < vodLi.length; i++) {
const j = vodLi[i].innerText
if (j.indexOf('.m3u8') >= 0) {
m3u8UrlArr.push(j)
}
if (j.indexOf('.mp4') >= 0) {
mp4UrlArr.push(j)
}
}
data.m3u8_urls = m3u8UrlArr
data.mp4_urls = mp4UrlArr
resolve(data)
} catch (err) {
reject(err)
}
})
},
// 搜索列表
search_get (key, keywords = '', page = 1) {
return new Promise((resolve, reject) => {
const site = getSite(key)
const type = site.type
let url = null
if (type === 0) {
url = site.search.replace(/{page}/, page).replace(/{keywords}/, keywords)
}
if (type === 1) {
url = site.search.replace(/{keywords}/, keywords)
}
axios.get(url).then(async res => {
const data = res.data
if (type === 0) {
const zeroData = await this.search_get_type_zero(data, key)
resolve(zeroData)
}
if (type === 1) {
const oneData = await this.search_get_type_one(data, key)
resolve(oneData)
}
}).catch(err => {
reject(err)
})
})
},
search_get_type_zero (txt, key) {
return new Promise((resolve, reject) => {
try {
const parser = new DOMParser()
const html = parser.parseFromString(txt, 'text/html')
const list = html.querySelectorAll('.xing_vb li')
const d = { list: [], total: 0 }
const url = getSite(key).url
for (let i = 1; i < list.length - 1; i++) {
const info = {
site: key,
name: list[i].childNodes[1].innerText,
type: list[i].childNodes[3].innerText,
time: list[i].childNodes[5].innerText,
detail: url + list[i].childNodes[1].querySelector('a').getAttribute('href'),
index: 0
}
d.list.push(info)
}
const t = html.querySelector('.nvc dd').innerText.replace(/[^\d]/g, '')
d.total = parseInt(t)
resolve(d)
} catch (err) {
reject(err)
}
})
},
search_get_type_one (txt, key) {
return new Promise((resolve, reject) => {
try {
const parser = new DOMParser()
const html = parser.parseFromString(txt, 'text/html')
const list = html.querySelectorAll('.videoContent li')
const d = { list: [], total: 0 }
const url = getSite(key).url
for (let i = 0; i < list.length; i++) {
const info = {
site: key,
name: list[i].querySelector('.videoName').innerText,
type: list[i].querySelector('.category').innerText,
time: list[i].querySelector('.time').innerText,
detail: url + list[i].querySelector('.address').getAttribute('href'),
index: 0
}
d.list.push(info)
}
d.total = list.length
resolve(d)
} catch (err) {
reject(err)
}
})
}
}
export default zy

View File

@@ -3,12 +3,15 @@ import App from './App.vue'
import router from './router'
import store from './store'
import 'modern-normalize'
import Register from './components/register'
import VueI18n from 'vue-i18n'
import { languages, defaultLocal } from './locales/index'
import './lib/element/index'
Vue.config.productionTip = false
Register.registerComponents()
Vue.use(VueI18n)
const messages = Object.assign(languages)
const i18n = new VueI18n({

View File

@@ -5,11 +5,64 @@ Vue.use(Vuex)
export default new Vuex.Store({
state: {
view: 'Setting',
theme: 'light',
site: 'zuidazy',
language: 'zhCn',
detail: {
show: false,
v: {}
},
share: {
show: false,
v: {}
},
video: {}
},
getters: {
getView: state => {
return state.view
},
getTheme: state => {
return state.theme
},
getSite: state => {
return state.site
},
getLanguage: state => {
return state.language
},
getDetail: state => {
return state.detail
},
getVideo: state => {
return state.video
},
getShare: state => {
return state.share
}
},
mutations: {
},
actions: {
},
modules: {
SET_VIEW: (state, payload) => {
state.view = payload
},
SET_THEME: (state, payload) => {
state.theme = payload
},
SET_SITE: (state, payload) => {
state.site = payload
},
SET_LANGUAGE: (state, payload) => {
state.language = payload
},
SET_DETAIL: (state, payload) => {
state.detail = payload
},
SET_VIDEO: (state, payload) => {
state.video = payload
},
SET_SHARE: (state, payload) => {
state.share = payload
}
}
})

View File

@@ -1,19 +1,44 @@
<template>
<div class="home">
<img alt="Vue logo" src="../assets/image/logo.png">
<HelloWorld msg="Welcome to Your Vue.js App"/>
<span>{{ $t('language') }}</span>
<Aside />
<div class="body">
<Frame />
<Film v-show="view === 'Film'" />
<Play v-show="view === 'Play'" />
<Star v-show="view === 'Star'" />
<Setting v-show="view === 'Setting'" />
</div>
<transition name="slide">
<Detail v-if="detail.show"/>
</transition>
<transition name="slide">
<Share v-if="share.show"/>
</transition>
</div>
</template>
<script>
// @ is an alias to /src
import HelloWorld from '@/components/HelloWorld.vue'
export default {
name: 'Home',
components: {
HelloWorld
data () {
return {
view: 'Film',
detail: {
show: false
},
share: {
show: false
}
}
}
}
</script>
<style lang="scss" scoped>
.home{
width: 100%;
height: 100%;
overflow: hidden;
}
</style>

574
yarn.lock

File diff suppressed because it is too large Load Diff