Merge pull request #353 from Hunlongyu/mergeChannelDev

频道合并及播放页面的频道列表及精简模式
This commit is contained in:
buvta
2020-11-19 21:31:35 +08:00
committed by GitHub
14 changed files with 537 additions and 420 deletions

View File

@@ -111,6 +111,16 @@
}
}
.play{
.popper {
font-size: 1rem;;
border: none;
li {
font-size: 1rem;
border: none;
}
}
}
// Page of list using el-table
.listpage{
position: absolute;
@@ -222,6 +232,9 @@
position: absolute;
}
}
.disableExpand div.el-table__expand-icon{
display: none;
}
.el-input{
width: 200px;
}

View File

@@ -301,6 +301,30 @@
box-shadow: var(--d-bsc);
}
.play{
.el-input{
input{
background-color: var(--d-bgc-1);
border: 1px solid var(--d-bgc-1);
color: var(--d-fc-2);
}
}
.popper {
color: var(--d-fc-1);
background-color: var(--d-bgc-1);
box-shadow: var(--d-bsc);
li {
color: var(--d-fc-1);
background-color: var(--d-bgc-1);
&:hover{
background-color: var(--d-bgc-2);
}
}
.popper__arrow, .popper__arrow::after{
border-bottom-color: var(--d-bgc-1);
}
}
}
// Page of list using table and picture
.listpage{
color: var(--d-fc-2);

View File

@@ -301,6 +301,30 @@
box-shadow: var(--g-bsc);
}
.play{
.el-input{
input{
background-color: var(--g-bgc-1);
border: 1px solid var(--g-bgc-1);
color: var(--g-fc-2);
}
}
.popper {
color: var(--g-fc-1);
background-color: var(--g-bgc-1);
box-shadow: var(--g-bsc);
li {
color: var(--g-fc-1);
background-color: var(--g-bgc-1);
&:hover{
background-color: var(--g-bgc-2);
}
}
.popper__arrow, .popper__arrow::after{
border-bottom-color: var(--g-bgc-1);
}
}
}
// Page of list using table and picture
.listpage{
color: var(--g-fc-2);

View File

@@ -301,6 +301,30 @@
box-shadow: var(--l-bsc);
}
.play{
.el-input{
input{
background-color: var(--l-bgc-1);
border: 1px solid var(--l-bgc-1);
color: var(--l-fc-2);
}
}
.popper {
color: var(--l-fc-1);
background-color: var(--l-bgc-1);
box-shadow: var(--l-bsc);
li {
color: var(--l-fc-1);
background-color: var(--l-bgc-1);
&:hover{
background-color: var(--l-bgc-2);
}
}
.popper__arrow, .popper__arrow::after{
border-bottom-color: var(--l-bgc-1);
}
}
}
// Page of list using table and picture
.listpage{
color: var(--l-fc-2);

View File

@@ -301,6 +301,30 @@
box-shadow: var(--p-bsc);
}
.play{
.el-input{
input{
background-color: var(--p-bgc-1);
border: 1px solid var(--p-bgc-1);
color: var(--p-fc-2);
}
}
.popper {
color: var(--p-fc-1);
background-color: var(--p-bgc-1);
box-shadow: var(--p-bsc);
li {
color: var(--p-fc-1);
background-color: var(--p-bgc-1);
&:hover{
background-color: var(--p-bgc-2);
}
}
.popper__arrow, .popper__arrow::after{
border-bottom-color: var(--p-bgc-1);
}
}
}
// Page of list using table and picture
.listpage{
color: var(--p-fc-2);

View File

@@ -199,9 +199,9 @@ export default {
selectionCellClick (selection, row) {
if (this.shiftDown && this.selectionBegin !== '' && selection.includes(row)) {
this.selectionEnd = row.id
const start = Math.min(this.selectionBegin, this.selectionEnd) - 1
const end = Math.max(this.selectionBegin, this.selectionEnd)
const selections = this.sites.slice(start, end)
const start = this.sites.findIndex(e => e.id === Math.min(this.selectionBegin, this.selectionEnd))
const end = this.sites.findIndex(e => e.id === Math.max(this.selectionBegin, this.selectionEnd))
const selections = this.sites.slice(start, end + 1)
this.$nextTick(() => {
selections.forEach(e => this.$refs.editSitesTable.toggleRowSelection(e, true))
})
@@ -314,7 +314,7 @@ export default {
var randomstring = require('randomstring')
var doc = {
key: this.dialogType === 'edit' ? this.siteInfo.key : this.siteInfo.key ? this.siteInfo.key : randomstring.generate(6),
id: this.dialogType === 'edit' ? this.siteInfo.id : this.sites[this.sites.length - 1].id + 1,
id: this.dialogType === 'edit' ? this.siteInfo.id : this.sites.length ? this.sites[this.sites.length - 1].id + 1 : 1,
name: this.siteInfo.name,
api: this.siteInfo.api,
download: this.siteInfo.download,
@@ -412,7 +412,7 @@ export default {
this.updateDatabase()
},
syncTableData () {
if (this.$refs.editSitesTable.tableData) {
if (this.$refs.editSitesTable.tableData && this.$refs.editSitesTable.tableData.length === this.sites.length) {
this.sites = this.$refs.editSitesTable.tableData
}
},

View File

@@ -619,7 +619,6 @@ export default {
}
},
querySearch (queryString, cb) {
if (this.searchList.length === 0) return
var searchList = this.searchList.slice(0, -1)
var results = queryString ? searchList.filter(this.createFilter(queryString)) : this.searchList
// 调用 callback 返回建议列表的数据
@@ -748,7 +747,7 @@ export default {
if (this.searchGroup === undefined) setting.find().then(res => { this.searchGroup = res.searchGroup })
})
},
getSearchViewMode() {
getSearchViewMode () {
setting.find().then(res => {
this.searchViewMode = res.searchViewMode === undefined ? 'picture' : res.searchViewMode
})

View File

@@ -9,9 +9,10 @@
</div>
<div class="listpage-header" id="iptv-header" v-show="enableBatchEdit">
<el-switch v-model="enableBatchEdit" active-text="批处理分组"></el-switch>
<el-input placeholder="新组名" v-model="batchGroupName"></el-input>
<el-input placeholder="新组名/新频道名" v-model="inputContent"></el-input>
<el-switch v-model="batchIsActive" active-text="启用"></el-switch>
<el-button type="primary" icon="el-icon-edit" @click.stop="saveBatchEdit">保存</el-button>
<el-button type="primary" icon="el-icon-edit" @click.stop="saveBatchEdit">保存分组</el-button>
<el-button type="primary" icon="el-icon-film" @click.stop="mergeChannel">频道合并</el-button>
<el-button @click.stop="removeSelectedChannels" icon="el-icon-delete-solid">删除</el-button>
</div>
<div class="listpage-body" id="iptv-table">
@@ -20,10 +21,14 @@
ref="iptvTable"
size="mini" fit height="100%" row-key="id"
:data="filteredTableData"
lazy
:load="(row, treeNode, resolve) => resolve(row.channels)"
:tree-props="{hasChildren: 'hasChildren'}"
@expand-change="expandChange"
@row-click="playEvent"
@select="selectionCellClick"
@selection-change="handleSelectionChange"
@sort-change="handleSortChange">>
@sort-change="handleSortChange">
<el-table-column
type="selection"
v-if="enableBatchEdit">
@@ -31,6 +36,7 @@
<el-table-column
default-sort="ascending"
prop="name"
:class-name="enableBatchEdit ? 'disableExpand' : ''"
label="频道名">
<template #header>
<el-input
@@ -88,9 +94,9 @@
<span>总频道数:{{ iptvList.length }}</span>
</template>
<template slot-scope="scope">
<el-button @click.stop="moveToTopEvent(scope.row)" type="text">置顶</el-button>
<el-button @click.stop="moveToTopEvent(scope.row)" type="text" v-if="scope.row.channels">置顶</el-button>
<!-- 检测时先强制批量检测一遍,如果不强制直接单个检测时第一次不会显示“检测中”-->
<el-button size="mini" v-if="iptvList.every(channel => channel.status)" v-show="!checkAllChannelsLoading" @click.stop="checkSingleChannel(scope.row)" type="text">检测</el-button>
<el-button size="mini" v-if="iptvList.every(channel => channel.status)" v-show="!checkAllChannelsLoading" @click.stop="checkChannel(scope.row)" type="text">检测</el-button>
<el-button @click.stop="removeEvent(scope.row)" type="text">删除</el-button>
</template>
</el-table-column>
@@ -101,7 +107,7 @@
</template>
<script>
import { mapMutations } from 'vuex'
import { iptv, iptvSearch } from '../lib/dexie'
import { iptv, iptvSearch, channelList } from '../lib/dexie'
import { iptv as defaultChannels } from '../lib/dexie/initData'
import zy from '../lib/site/tools'
import { remote } from 'electron'
@@ -112,18 +118,21 @@ export default {
data () {
return {
iptvList: [],
channelList: [],
searchTxt: '',
searchRecordList: [],
enableBatchEdit: false,
batchGroupName: '',
inputContent: '',
batchIsActive: true,
shiftDown: false,
selectionBegin: '',
selectionEnd: '',
multipleSelection: [],
expandedRows: [],
checkAllChannelsLoading: false,
checkProgress: 0,
stopFlag: false,
sortableTable: '',
show: {
search: false
}
@@ -151,13 +160,13 @@ export default {
},
filteredTableData () {
if (this.searchTxt) {
return this.iptvList.filter(x => x.name.toLowerCase().includes(this.searchTxt.toLowerCase()))
return this.channelList.filter(x => x.name.toLowerCase().includes(this.searchTxt.toLowerCase()))
} else {
return this.iptvList
return this.channelList
}
},
getFilters () {
const groups = [...new Set(this.iptvList.map(iptv => iptv.group))]
const groups = [...new Set(this.channelList.map(iptv => iptv.group))]
var filters = []
groups.forEach(g => {
var doc = {
@@ -172,13 +181,22 @@ export default {
watch: {
view () {
if (this.view === 'IPTV') {
this.getChannels()
this.getChannelList()
}
},
enableBatchEdit () {
if (this.checkAllChannelsLoading) {
this.$message.info('正在检测, 请勿操作.')
this.enableBatchEdit = false
return
}
if (this.enableBatchEdit) {
this.$nextTick(() => {
this.expandedRows.forEach(e => this.$refs.iptvTable.toggleRowExpansion(e, false))
})
this.rowDrop()
} else {
this.sortableTable.destroy()
}
}
},
@@ -190,9 +208,9 @@ export default {
selectionCellClick (selection, row) {
if (this.shiftDown && this.selectionBegin !== '' && selection.includes(row)) {
this.selectionEnd = row.id
const start = Math.min(this.selectionBegin, this.selectionEnd) - 1
const end = Math.max(this.selectionBegin, this.selectionEnd)
const selections = this.iptvList.slice(start, end)
const start = this.channelList.findIndex(e => e.id === Math.min(this.selectionBegin, this.selectionEnd))
const end = this.channelList.findIndex(e => e.id === Math.max(this.selectionBegin, this.selectionEnd))
const selections = this.channelList.slice(start, end + 1) // 多选时强制不让展开
this.$nextTick(() => {
selections.forEach(e => this.$refs.iptvTable.toggleRowSelection(e, true))
})
@@ -205,27 +223,56 @@ export default {
this.selectionBegin = ''
}
},
expandChange (row, expanded) {
const index = this.expandedRows.indexOf(row)
if (expanded && index === -1) {
this.expandedRows.push(row)
} else if (!expanded && index !== -1) {
this.expandedRows.splice(index, 1)
}
},
handleSelectionChange (rows) {
this.multipleSelection = rows
},
handleSortChange (column, prop, order) {
if (this.checkAllChannelsLoading) {
this.$message.info('正在检测, 请勿操作.')
this.enableBatchEdit = false
return
}
this.updateDatabase()
},
saveBatchEdit () {
this.multipleSelection.forEach(ele => {
if (this.batchGroupName) {
ele.group = this.batchGroupName
if (this.inputContent) {
ele.group = this.inputContent
}
ele.isActive = this.batchIsActive
})
this.updateDatabase()
},
playEvent (e) {
this.video = { iptv: { name: e.name, url: e.url, id: e.id } }
mergeChannel () {
if (this.inputContent && this.multipleSelection.length) {
var channels = []
const id = this.multipleSelection[0].id
this.multipleSelection.forEach(ele => {
channels = channels.concat(ele.channels)
channels.forEach(e => { e.channelID = id })
channelList.remove(ele.id)
})
const mergeChannel = { id: id, name: this.inputContent, isActive: channels.some(c => c.isActive), group: this.determineGroup(this.inputContent), hasChildren: channels.length > 1, channels: channels }
channelList.add(mergeChannel)
this.getChannelList()
this.updateDatabase()
}
},
playEvent (e) { // 下一步与Play联动prefer
if (e.url) {
this.video = { iptv: e }
} else {
const prefer = e.prefer ? e.channels.find(c => c.id === e.prefer) : e.channels.filter(c => c.isActive)[0]
if (!prefer) return
this.video = { iptv: prefer }
}
this.view = 'Play'
},
containsearchTxt (i) {
@@ -235,18 +282,31 @@ export default {
return true
}
},
removeEvent (e) {
removeEvent (row) {
if (this.checkAllChannelsLoading) {
this.$message.info('正在检测, 请勿操作.')
return false
}
iptv.remove(e.id).then(res => {
this.getChannels()
}).catch(err => {
try {
if (row.url) { // tree树形控件节点一旦展开就不再重新加载节点数据
iptv.remove(row.id)
const parent = this.channelList.find(e => e.id === row.channelID)
parent.channels.splice(parent.channels.findIndex(e => e.id === row.id), 1)
channelList.remove(row.channelID)
if (parent.channels.length) channelList.add(parent)
this.$set(this.$refs.iptvTable.store.states.lazyTreeNodeMap, parent.id, parent.channels)
} else {
channelList.remove(row.id)
row.channels.forEach(e => {
iptv.remove(e.id)
})
}
this.getChannelList()
} catch (err) {
this.$message.warning('删除频道失败, 错误信息: ' + err)
})
}
},
exportChannels () {
exportChannels () { // 导出导入应为iptvList然后自动转换为channelList
const options = {
filters: [
{ name: 'm3u file', extensions: ['m3u'] },
@@ -287,6 +347,8 @@ export default {
}
remote.dialog.showOpenDialog(options).then(result => {
if (!result.canceled) {
var _id = 1
this.iptvList.forEach(e => { e.id = _id++ }) // 导入时需重置iptvList的id
var docs = this.iptvList
var id = docs.length + 1
result.filePaths.forEach(file => {
@@ -300,8 +362,7 @@ export default {
id: id,
name: ele.name,
url: ele.url,
isActive: true,
group: this.determineGroup(ele.name)
isActive: true
}
id += 1
docs.push(doc)
@@ -317,8 +378,7 @@ export default {
id: id,
name: ele.name,
url: ele.url,
isActive: ele.isActive === undefined ? true : ele.isActive,
group: this.determineGroup(ele.name)
isActive: ele.isActive === undefined ? true : ele.isActive
}
id += 1
docs.push(doc)
@@ -330,14 +390,14 @@ export default {
// const uniqueList = [...new Map(docs.map(item => [item.name, item])).values()]
iptv.clear().then(res => {
iptv.bulkAdd(docs).then(e => { // 支持导入同名频道,群里反馈
this.getChannels()
this.updateChannelList()
this.$message.success('导入成功')
})
})
}
})
},
determineGroup (name) {
determineGroup (name) { // iptvList不再需要group而是通过channelID来追踪归属
if (name.toLowerCase().includes('cctv') && (name.includes('蓝光') || name.includes('高清'))) {
return '央视高清'
} else if (name.toLowerCase().includes('cctv')) {
@@ -358,25 +418,59 @@ export default {
this.$message.info('部分检测还未完全终止, 请稍等...')
return
}
iptv.clear().then(iptv.bulkAdd(defaultChannels).then(this.getChannels()))
iptv.clear().then(iptv.bulkAdd(defaultChannels).then(this.updateChannelList()))
},
removeSelectedChannels () {
this.multipleSelection.forEach(e => iptv.remove(e.id))
this.multipleSelection.forEach(e => { channelList.remove(e.id); e.channels.forEach(c => iptv.remove(c.id)) })
this.$refs.iptvTable.clearFilter()
this.getChannels()
this.getChannelList()
this.updateDatabase()
this.enableBatchEdit = false
},
getChannels () {
updateChannelList () {
iptv.all().then(res => {
const resClone = JSON.parse(JSON.stringify(res))
const uniqueChannelName = {}
for (var i = 0; i < resClone.length; i++) {
var channelName = resClone[i].name.trim().replace(/[- ]?(1080p|蓝光|超清|高清|标清|hd|cq|4k)(\d{1,2})?$/i, '')
if (channelName.match(/cctv/i)) channelName = channelName.replace('-', '')
if (Object.keys(uniqueChannelName).some(name => channelName.match(new RegExp(`${name}(1080p|4k|(?!\\d))`, 'i')))) continue // 避免重复
const matchRule = new RegExp(`${channelName}(1080p|4k|(?!\\d))`, 'i')
for (var j = i; j < resClone.length; j++) {
if (resClone[j].name.match(/cctv/i)) {
resClone[j].name = resClone[j].name.replace('-', '')
}
if (matchRule.test(resClone[j].name)) {
if (uniqueChannelName[channelName]) {
!uniqueChannelName[channelName].includes(res[j]) && uniqueChannelName[channelName].push(res[j])
} else {
uniqueChannelName[channelName] = [res[j]]
}
}
}
}
res.forEach(ele => {
if (ele.isActive === undefined) {
ele.isActive = true
}
})
this.iptvList = res
let id = res.length // channelList全部放在末尾
this.channelList = Object.keys(uniqueChannelName).map(e => { return { id: ++id, name: e, isActive: uniqueChannelName[e].some(c => c.isActive), group: this.determineGroup(e), hasChildren: uniqueChannelName[e].length > 1, channels: uniqueChannelName[e] } })
this.getIptvList()
Object.values(this.$refs.iptvTable.store.states.treeData).forEach(e => { e.loaded = false })
channelList.clear().then(channelList.bulkAdd(this.channelList))
iptv.clear().then(iptv.bulkAdd(this.iptvList))
// 用于验证
// console.log(res.length, Array.from(new Set(this.iptvList)))
// res.forEach(e => { if (!this.iptvList.includes(e)) console.log(e) })
})
},
getChannelList () {
channelList.all().then(res => { if (res && res.length === 0) this.updateChannelList(); this.channelList = res; this.getIptvList() })
},
getIptvList () {
this.iptvList = this.channelList.reduce((result, item) => { item.channels.forEach(e => { e.channelID = item.id }); return result.concat(item.channels) }, [])
},
getSearchRecordList () {
iptvSearch.all().then(res => {
this.searchRecordList = res.reverse()
@@ -399,31 +493,40 @@ export default {
})
}
},
moveToTopEvent (i) {
moveToTopEvent (row) {
if (this.checkAllChannelsLoading) {
this.$message.info('正在检测, 请勿操作.')
return false
}
this.iptvList.sort(function (x, y) { return (x.name === i.name && x.url === i.url) ? -1 : (y.name === i.name && y.url === i.url) ? 1 : 0 })
this.updateDatabase()
// this.channelList.sort(function (x, y) { return (x.name === i.name && x.url === i.url) ? -1 : (y.name === i.name && y.url === i.url) ? 1 : 0 })
if (row.channels) {
this.channelList.splice(this.channelList.findIndex(e => e.id === row.id), 1)
this.channelList.unshift(row)
this.updateDatabase()
}
},
syncTableData () {
if (this.$refs.iptvTable.tableData) {
this.iptvList = this.$refs.iptvTable.tableData
if (this.$refs.iptvTable.tableData && this.$refs.iptvTable.tableData.length === this.channelList.length) {
this.channelList = this.$refs.iptvTable.tableData
}
},
updateDatabase () {
this.syncTableData()
iptv.clear().then(res => {
this.resetId(this.iptvList)
iptv.bulkAdd(this.iptvList)
Object.values(this.$refs.iptvTable.store.states.treeData).forEach(e => { e.loaded = false })
channelList.clear().then(res => {
this.resetId(this.channelList)
channelList.bulkAdd(this.channelList)
iptv.clear().then(iptv.bulkAdd(this.iptvList))
this.getChannelList()
})
},
resetId (inArray) {
var id = 1
inArray.forEach(ele => {
resetId (channelList) {
this.getIptvList()
var id = this.iptvList.length + 1
channelList.forEach(ele => {
ele.id = id
id += 1
ele.channels.forEach(e => { e.channelID = ele.id })
})
},
rowDrop () {
@@ -433,33 +536,49 @@ export default {
}
const tbody = document.getElementById('iptv-table').querySelector('.el-table__body-wrapper tbody')
const _this = this
Sortable.create(tbody, {
this.sortableTable = new Sortable(tbody, {
filter: '.el-table__row--level-1', // 禁止children拖动
onEnd ({ newIndex, oldIndex }) {
const currRow = _this.iptvList.splice(oldIndex, 1)[0]
_this.iptvList.splice(newIndex, 0, currRow)
const currRow = _this.channelList.splice(oldIndex, 1)[0]
_this.channelList.splice(newIndex, 0, currRow)
_this.updateDatabase()
}
})
},
isActiveChangeEvent (row) {
iptv.remove(row.id)
iptv.add(row)
if (row.url) {
iptv.remove(row.id)
iptv.add(row)
const parent = this.channelList.find(e => e.id === row.channelID)
parent.isActive = parent.channels.some(e => e.isActive)
channelList.remove(row.channelID)
channelList.add(parent)
} else {
channelList.remove(row.id)
channelList.add(row)
row.channels.forEach(e => {
e.isActive = row.isActive
iptv.remove(e.id)
iptv.add(e)
})
}
},
async checkAllChannels () {
this.checkAllChannelsLoading = true
this.stopFlag = false
this.checkProgress = 0
this.channelList.forEach(e => { e.status = ' '; e.hasCheckedNum = 0 })
const uncheckedList = this.iptvList.filter(e => e.status === undefined || e.status === ' ') // 未检测过的优先
const other = this.iptvList.filter(e => !uncheckedList.includes(e))
await this.checkChannelList(uncheckedList)
await this.checkChannelList(other).then(res => {
await this.checkChannelsBySite(uncheckedList)
await this.checkChannelsBySite(other).then(res => {
this.checkAllChannelsLoading = false
this.getChannels()
this.updateChannelList()
})
},
async checkChannelList (channelList) {
async checkChannelsBySite (channels) {
var siteList = {}
channelList.forEach(channel => {
channels.forEach(channel => {
const site = channel.url.split('/')[2]
if (siteList[site]) {
siteList[site].push(channel)
@@ -475,32 +594,48 @@ export default {
await this.checkSingleChannel(c)
}
},
async checkSingleChannel (row) {
row.status = ' '
async checkSingleChannel (channel) {
channel.status = ' '
const parent = this.channelList.find(e => e.id === channel.channelID)
if (this.stopFlag) {
this.checkProgress += 1
return row.status
return channel.status
}
const flag = await zy.checkChannel(row.url)
const flag = await zy.checkChannel(channel.url)
this.checkProgress += 1
parent.hasCheckedNum++
if (flag) {
row.status = '可用'
channel.status = '可用'
} else {
row.status = '失效'
row.isActive = false
channel.status = '失效'
channel.isActive = false
}
if (parent.hasCheckedNum === parent.channels.length) {
parent.status = parent.channels.some(channel => channel.status === '可用') ? '可用' : '失效'
if (parent.status === '失效') parent.isActive = false
channelList.remove(channel.channelID)
channelList.add(parent)
}
iptv.remove(channel.id)
iptv.add(channel)
return channel.status
},
async checkChannel (row) {
if (row.channels) {
row.status = ' '
row.hasCheckedNum = 0
row.channels.forEach(e => this.checkSingleChannel(e))
} else {
this.checkSingleChannel(row)
}
iptv.remove(row.id)
iptv.add(row)
return row.status
}
},
mounted () {
this.rowDrop()
addEventListener('keydown', code => { if (code.keyCode === 16) this.shiftDown = true })
addEventListener('keyup', code => { if (code.keyCode === 16) this.shiftDown = false })
},
created () {
this.getChannels()
this.getChannelList()
this.getSearchRecordList()
}
}

View File

@@ -101,7 +101,17 @@
<span class="last-tip" v-if="!video.key && right.history.length > 0" @click="historyItemEvent(right.history[0])">上次播放到{{right.history[0].site}}{{right.history[0].name}} {{right.history[0].index+1}}</span>
</div>
<div class="more" v-if="video.iptv" :key="Boolean(video.iptv)">
<span class="zy-svg" @click="otherEvent" v-if="right.otherChannels.length">
<span class="zy-svg" @click="channelListShow = !channelListShow">
<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 class="zy-svg" @click="otherEvent" v-if="right.sources.length > 1">
<svg role="img" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" aria-labelledby="coloursIconTitle">
<title id="coloursIconTitle">换源</title>
<circle cx="12" cy="9" r="5"></circle>
@@ -137,7 +147,7 @@
<span class="list-top-title" v-if="right.type === 'history'">历史记录</span>
<span class="list-top-title" v-if="right.type === 'shortcut'">快捷键指南{{ this.video.iptv ? '(直播时部分功能不可用)' : '' }}</span>
<span class="list-top-title" v-if="right.type === 'other'">同组其他源的视频</span>
<span class="list-top-title" v-if="right.type === 'otherChannels'">该频道其它</span>
<span class="list-top-title" v-if="right.type === 'sources'">该频道可用</span>
<span class="list-top-close zy-svg" @click="closeListEvent">
<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>
@@ -163,18 +173,52 @@
<li v-if="right.other.length === 0">无数据</li>
<li @click="otherItemEvent(m)" v-for="(m, n) in right.other" :key="n"><span class="title">{{m.name}} - [{{m.site.name}}]</span></li>
</ul>
<ul v-if="right.type === 'otherChannels'" class="list-other" v-on-clickaway="closeListEvent">
<li v-if="right.otherChannels.length === 0">无数据</li>
<li @click="playChannel(channel)" v-for="(channel, index) in right.otherChannels" :key="index"><span class="title">{{channel.name}}</span></li>
<ul v-if="right.type === 'sources'" class="list-other" v-on-clickaway="closeListEvent">
<li v-if="right.sources.length === 0">无数据</li>
<li @click="playChannel(channel)" v-for="(channel, index) in right.sources" :key="index">
<span class="title">{{ channel.id === video.iptv.id ? channel.name + '[当前]' : channel.name }}</span>
</li>
</ul>
</div>
</div>
<div v-if="channelListShow" class="list">
<div class="list-top">
<span class="list-top-title">频道列表</span>
<span class="list-top-close zy-svg" @click="channelListShow = false">
<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 zy-scroll" :style="{overflowY:scroll? 'auto' : 'hidden',paddingRight: scroll ? '0': '5px' }" @mouseenter="scroll = true" @mouseleave="scroll = false">
<el-autocomplete
clearable
size="small"
v-model.trim="searchTxt"
value-key="keywords"
:fetch-suggestions="querySearch"
:popper-append-to-body="false"
popper-class="popper"
placeholder="搜索"
@keyup.enter.native="searchAndRecord">
<i slot="prefix" class="el-input__icon el-icon-search"></i>
</el-autocomplete>
<el-tree ref="channelTree"
:data="channelListForShow"
:props="defaultProps"
accordion
:filter-node-method="filterNode"
@node-click="handleNodeClick">
</el-tree>
</div>
</div>
</transition>
</div>
</template>
<script>
import { mapMutations } from 'vuex'
import { star, history, setting, shortcut, mini, iptv, sites } from '../lib/dexie'
import { star, history, setting, shortcut, mini, channelList, iptvSearch, sites } from '../lib/dexie'
import zy from '../lib/site/tools'
import Player from 'xgplayer'
import HlsJsPlayer from 'xgplayer-hls.js'
@@ -238,7 +282,7 @@ export default {
history: [],
shortcut: [],
other: [],
otherChannels: [],
sources: [],
currentTime: 0
},
config: {
@@ -262,7 +306,8 @@ export default {
showHistory: true,
videoTitle: true,
airplay: true,
closeVideoTouch: true
closeVideoTouch: true,
ignores: ['cssFullscreen']
},
state: {
showList: false,
@@ -274,8 +319,17 @@ export default {
scroll: false,
isStar: false,
isTop: false,
mini: {},
iptvList: []
miniMode: false,
mainWindowBounds: {},
searchTxt: '',
searchRecordList: [],
channelList: [],
channelListForShow: [],
channelListShow: false,
defaultProps: {
label: 'label',
children: 'children'
}
}
},
filters: {
@@ -332,6 +386,7 @@ export default {
view () {
this.right.show = false
this.right.type = ''
this.getChannelList()
},
video: {
handler () {
@@ -355,10 +410,67 @@ export default {
} else {
span.innerText = `${this.name}`
}
},
searchTxt () {
if (this.searchTxt === '清除历史记录...') {
this.clearSearchRecords()
this.searchTxt = ''
}
this.searchEvent()
}
},
methods: {
...mapMutations(['SET_VIEW', 'SET_DETAIL', 'SET_VIDEO', 'SET_SHARE']),
handleNodeClick (node) {
if (node.channel) {
this.playChannel(node.channel)
}
},
filterNode (value, data) {
if (!value) return true
return data.label.toLowerCase().includes(value.toLowerCase())
},
querySearch (queryString, cb) {
var searchRecordList = this.searchRecordList.slice(0, -1)
var results = queryString ? searchRecordList.filter(this.createFilter(queryString)) : this.searchRecordList
// 调用 callback 返回建议列表的数据
console.log(results)
cb(results)
},
createFilter (queryString) {
return (item) => {
return (item.keywords.toLowerCase().indexOf(queryString.toLowerCase()) === 0)
}
},
getSearchRecordList () {
iptvSearch.all().then(res => {
this.searchRecordList = res.reverse()
this.searchRecordList.push({ id: this.searchRecordList.length + 1, keywords: '清除历史记录...' })
})
},
addSearchRecord () {
const wd = this.searchTxt
if (wd) {
iptvSearch.find({ keywords: wd }).then(res => {
if (!res) {
iptvSearch.add({ keywords: wd })
}
this.getSearchRecordList()
})
}
},
clearSearchRecords () {
iptvSearch.clear().then(res => {
this.getSearchRecordList()
})
},
searchEvent () {
this.$refs.channelTree.filter(this.searchTxt)
},
searchAndRecord () {
this.addSearchRecord()
this.searchEvent()
},
async getUrls () {
if (this.video.key === '') {
return false
@@ -376,6 +488,7 @@ export default {
// 是直播源,直接播放
this.playChannel(this.video.iptv)
} else {
this.iptvMode = false
const index = this.video.info.index | 0
var time = this.video.info.time
if (!time) {
@@ -391,7 +504,17 @@ export default {
}
},
playChannel (channel) {
this.getIptvList()
if (channel.channels) {
this.right.sources = channel.channels.filter(e => e.isActive)
channel = channel.channels.find(e => e.id === channel.prefer) || channel.channels.filter(e => e.isActive)[0]
} else {
const parent = this.channelList.find(e => e.id === channel.channelID)
parent.prefer = channel.id
channelList.remove(parent.id)
channelList.add(parent)
this.getChannelList()
this.right.sources = parent.channels.filter(e => e.isActive)
}
this.video.iptv = channel
this.name = channel.name
this.xg.src = channel.url
@@ -521,9 +644,9 @@ export default {
},
prevEvent () {
if (this.video.iptv) {
var index = this.iptvList.findIndex(obj => obj.name === this.video.iptv.name && obj.url === this.video.iptv.url)
var index = this.channelList.findIndex(obj => obj.prefer === this.video.iptv.id)
if (index >= 1) {
var channel = this.iptvList[index - 1]
var channel = this.channelList[index - 1]
this.playChannel(channel)
} else {
this.$message.warning('这已经是第一个频道了。')
@@ -539,9 +662,9 @@ export default {
},
nextEvent () {
if (this.video.iptv) {
var index = this.iptvList.findIndex(obj => obj.name === this.video.iptv.name && obj.url === this.video.iptv.url)
if (index < (this.iptvList.length - 1)) {
var channel = this.iptvList[index + 1]
var index = this.channelList.findIndex(obj => obj.prefer === this.video.iptv.id)
if (index < (this.channelList.length - 1)) {
var channel = this.channelList[index + 1]
this.playChannel(channel)
} else {
this.$message.warning('这已经是最后一个频道了。')
@@ -622,40 +745,33 @@ export default {
info: this.video.info
}
},
miniEvent () {
if (this.xg) {
this.xg.pause()
}
mini.find().then(res => {
var doc = {}
if (!this.video.iptv) {
doc = {
id: 0,
mode: 'video',
site: this.video.key,
ids: this.video.info.id,
name: this.video.info.name,
index: this.video.info.index,
time: this.xg.currentTime
}
} else {
doc = {
id: 0,
mode: 'iptv',
url: this.video.iptv.url
}
async miniEvent () {
const win = remote.getCurrentWindow()
this.mainWindowBounds = JSON.parse(JSON.stringify(win.getBounds()))
let miniWindowBounds
await mini.find().then(res => { if (res) miniWindowBounds = res.bounds })
if (!miniWindowBounds) miniWindowBounds = { x: win.getPosition()[0], y: win.getPosition()[1], width: 550, height: 340 }
win.setBounds(miniWindowBounds)
this.xg.getCssFullscreen()
this.miniMode = true
},
async exitMiniEvent () {
const win = remote.getCurrentWindow()
await mini.find().then(res => {
let doc = {}
doc = {
id: 0,
bounds: win.getBounds()
}
if (res) {
mini.update(doc)
} else {
mini.add(doc)
}
this.mini = doc
clearInterval(this.timer)
const win = remote.getCurrentWindow()
win.hide()
ipcRenderer.send('mini')
})
win.setBounds(this.mainWindowBounds)
this.xg.exitCssFullscreen()
this.miniMode = false
},
shareEvent () {
this.share = {
@@ -785,7 +901,7 @@ export default {
},
listItemEvent (n) {
if (this.video.iptv) {
var channel = this.iptvList[n]
var channel = this.channelList[n]
// 是直播源,直接播放
this.playChannel(channel)
} else {
@@ -849,7 +965,8 @@ export default {
this.getOtherSites()
this.right.currentTime = this.xg.currentTime
} else {
this.right.type = 'otherChannels'
this.right.type = 'sources'
console.log(this.right.sources)
}
this.right.show = true
},
@@ -939,6 +1056,9 @@ export default {
return false
}
if (e === 'escape') {
if (this.miniMode) {
this.exitMiniEvent()
}
if (this.xg.fullscreen) {
this.xg.exitFullscreen()
this.xg.exitCssFullscreen()
@@ -1035,10 +1155,9 @@ export default {
let li = ''
if (this.video.iptv) {
// 直播频道列表
this.getIptvList()
let index = 0
this.iptvList.forEach(e => {
if (e.name === this.video.iptv.name && e.url === this.video.iptv.url) {
this.channelList.forEach(e => {
if (e.prefer === this.video.iptv.id) {
li += `<li class="selected" data-index="${index}" title="${e.name}">${e.name}</li>`
} else {
li += `<li data-index="${index}" title="${e.name}">${e.name}</li>`
@@ -1110,25 +1229,25 @@ export default {
}
ul.innerHTML = li
},
getIptvList () {
iptv.all().then(res => {
this.iptvList = res.filter(e => e.isActive)
this.right.otherChannels = []
const iptvList = JSON.parse(JSON.stringify(this.iptvList))
var currentChannelName = this.video.iptv.name.trim().replace(/[- ]?(1080p|蓝光|超清|高清|标清|hd|cq|4k)(\d{1,2})?/i, '')
if (currentChannelName.match(/cctv/i)) currentChannelName = currentChannelName.replace('-', '')
const matchRule = new RegExp(`${currentChannelName}(1080p|4k|(?!\\d))`, 'i')
for (var i = 0; i < iptvList.length; i++) {
if (iptvList[i].name.match(/cctv/i)) {
iptvList[i].name = iptvList[i].name.replace('-', '')
getChannelList () {
channelList.all().then(res => {
this.channelList = res.filter(e => e.isActive)
this.channelListForShow = []
const groups = [...new Set(this.channelList.map(iptv => iptv.group))]
groups.forEach(g => {
var doc = {
label: g,
children: this.channelList.filter(x => x.group === g).map(i => { return { label: i.name, channel: i } })
}
if (matchRule.test(iptvList[i].name) && iptvList[i].id !== this.video.iptv.id) {
this.right.otherChannels.push(this.iptvList[i])
}
}
this.channelListForShow.push(doc)
})
})
},
bindEvent () {
this.xg.on('exitFullscreen', () => {
if (this.miniMode) this.xg.getCssFullscreen()
})
this.xg.on('playNextOne', () => {
this.nextEvent()
})
@@ -1447,6 +1566,9 @@ export default {
padding: 6px;
display: flex;
flex-direction: column;
.el-tree{
background-color: inherit;
}
.list-top{
display: flex;
justify-content: space-between;

View File

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

View File

@@ -12,8 +12,10 @@ db.version(4).stores({
recommendation: '++id, [key+ids], site, name, detail, index, rate, hasUpdate',
sites: '++id, key, name, api, download, isActive, group',
history: '++id, [site+ids], name, type, year, index, time, duration, detail',
mini: 'id, mode, site, ids, name, index, time, url',
iptv: '++id, name, url, group, isActive'
// mini: 'id, mode, site, ids, name, index, time, url',
mini: 'id, bounds',
iptv: '++id, name, url, channelID, isActive',
channelList: '++id, name, prefer, channels, group, isActive'
})
db.on('populate', () => {

View File

@@ -7,6 +7,7 @@ import sites from './sites'
import search from './search'
import iptvSearch from './iptvSearch'
import iptv from './iptv'
import channelList from './channelList'
import recommendation from './recommendation'
export {
@@ -17,6 +18,7 @@ export {
star,
sites,
iptv,
channelList,
search,
iptvSearch,
recommendation

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
import Vue from 'vue'
import { Message, Button, Table, TableColumn, Tag, Input, Dialog, Form, FormItem, Switch, Select, Option, Checkbox, Autocomplete, Col } from 'element-ui'
import { Message, Button, Table, TableColumn, Tag, Input, Dialog, Form, FormItem, Switch, Select, Option, Checkbox, Autocomplete, Col, Tree } from 'element-ui'
import Plugin from 'v-fit-columns'
Vue.use(Button)
Vue.use(Col)
@@ -16,4 +16,5 @@ Vue.use(Select)
Vue.use(Option)
Vue.use(Checkbox)
Vue.use(Autocomplete)
Vue.use(Tree)
Vue.prototype.$message = Message