forked from platypush/platypush
parent
1774e464cc
commit
3dc1ff3c6e
10 changed files with 879 additions and 108 deletions
|
@ -46,7 +46,7 @@
|
|||
@download="download"
|
||||
v-if="selectedView === 'search'" />
|
||||
|
||||
<TorrentView :plugin-name="torrentPlugin"
|
||||
<Transfers :plugin-name="torrentPlugin"
|
||||
:is-media="true"
|
||||
@play="play"
|
||||
v-else-if="selectedView === 'torrents'" />
|
||||
|
@ -92,7 +92,7 @@ import MediaView from "@/components/Media/View";
|
|||
import Nav from "@/components/panels/Media/Nav";
|
||||
import Results from "@/components/panels/Media/Results";
|
||||
import Subtitles from "@/components/panels/Media/Subtitles";
|
||||
import TorrentView from "@/components/panels/Torrent/View";
|
||||
import Transfers from "@/components/panels/Torrent/Transfers";
|
||||
import UrlPlayer from "@/components/panels/Media/UrlPlayer";
|
||||
|
||||
export default {
|
||||
|
@ -107,7 +107,7 @@ export default {
|
|||
Nav,
|
||||
Results,
|
||||
Subtitles,
|
||||
TorrentView,
|
||||
Transfers,
|
||||
UrlPlayer,
|
||||
},
|
||||
|
||||
|
|
|
@ -1,27 +1,119 @@
|
|||
<template>
|
||||
<div class="header" :class="{'with-filter': filterVisible}">
|
||||
<div class="header" :class="{'with-nav': withNav}">
|
||||
<div class="row">
|
||||
<div class="col-s-12 col-m-9 col-l-7 left side">
|
||||
<form @submit.prevent="$emit('torrent-add', torrentURL)">
|
||||
<div class="left side" :class="leftSideClasses">
|
||||
<form @submit.prevent="submit">
|
||||
<label class="search-box">
|
||||
<input type="search" placeholder="Add torrent URL" v-model="torrentURL">
|
||||
<input
|
||||
type="search"
|
||||
:disabled="loading"
|
||||
:placeholder="placeholder"
|
||||
v-model="torrentURL"
|
||||
v-if="selectedView === 'transfers'"
|
||||
>
|
||||
|
||||
<input
|
||||
type="search"
|
||||
:placeholder="placeholder"
|
||||
:value="query"
|
||||
ref="search"
|
||||
v-else-if="selectedView === 'search'"
|
||||
>
|
||||
</label>
|
||||
|
||||
<span class="button-container">
|
||||
<button type="submit" title="Loading" disabled v-if="loading">
|
||||
<Loading />
|
||||
</button>
|
||||
|
||||
<button type="submit" title="Add torrent URL" v-else-if="selectedView === 'transfers'">
|
||||
<i class="fa fa-download" />
|
||||
</button>
|
||||
|
||||
<button type="submit" title="Search" v-else-if="selectedView === 'search'">
|
||||
<i class="fa fa-search" />
|
||||
</button>
|
||||
</span>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="right side col-1" v-if="!withNav">
|
||||
<button @click="$emit('toggle')" title="Toggle navigation">
|
||||
<i class="fa fa-bars" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Loading from "@/components/Loading";
|
||||
|
||||
export default {
|
||||
name: "Header",
|
||||
emits: ['torrent-add'],
|
||||
emits: ['torrent-add', 'search', 'toggle'],
|
||||
components: {Loading},
|
||||
|
||||
props: {
|
||||
query: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
|
||||
withNav: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
|
||||
selectedView: {
|
||||
type: String,
|
||||
default: 'transfers',
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
torrentURL: '',
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
placeholder() {
|
||||
if (this.selectedView === 'transfers') {
|
||||
return 'Add torrent URL'
|
||||
}
|
||||
|
||||
return 'Search torrents'
|
||||
},
|
||||
|
||||
leftSideClasses() {
|
||||
if (!this.withNav) {
|
||||
return {
|
||||
'col-12': true,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
'col-11': true,
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
submit() {
|
||||
const query = this.$refs?.search?.value?.trim()
|
||||
if (this.selectedView === 'transfers' && this.torrentURL?.length) {
|
||||
this.$emit('torrent-add', this.torrentURL)
|
||||
} else if (this.selectedView === 'search' && query?.length) {
|
||||
this.$emit('search', query)
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -46,6 +138,9 @@ export default {
|
|||
align-items: center;
|
||||
|
||||
&.right {
|
||||
position: absolute;
|
||||
font-size: 1.1em;
|
||||
right: calc(#{$torrent-nav-width} / 4);
|
||||
justify-content: right;
|
||||
}
|
||||
}
|
||||
|
@ -54,7 +149,6 @@ export default {
|
|||
background: none;
|
||||
padding: 0 .25em;
|
||||
border: 0;
|
||||
margin-right: .25em;
|
||||
|
||||
&:hover {
|
||||
color: $default-hover-fg-2;
|
||||
|
@ -63,6 +157,7 @@ export default {
|
|||
|
||||
form {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
|
@ -70,8 +165,36 @@ export default {
|
|||
background: initial;
|
||||
}
|
||||
|
||||
.button-container {
|
||||
width: 3em;
|
||||
position: relative;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
:deep(.loading) {
|
||||
width: 5em;
|
||||
font-size: 1em;
|
||||
left: -0.5em;
|
||||
border-radius: 0 1em 1em 0;
|
||||
}
|
||||
|
||||
[type=submit] {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: $default-bg-4;
|
||||
border: $default-border-2;
|
||||
border-radius: 0 1em 1em 0;
|
||||
margin-left: -.5em;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: $hover-bg-3;
|
||||
}
|
||||
}
|
||||
|
||||
.search-box {
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
margin-left: .5em;
|
||||
|
||||
input[type=search] {
|
||||
|
|
|
@ -0,0 +1,120 @@
|
|||
<template>
|
||||
<div class="info">
|
||||
<div class="row">
|
||||
<div class="label">Title</div>
|
||||
<div class="value">{{ torrent.title }}</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="label">URL</div>
|
||||
<div class="value">
|
||||
<button title="Open" @click="openInNewTab(torrent.url)">
|
||||
<i class="fas fa-up-right-from-square" />
|
||||
</button>
|
||||
|
||||
<button title="Copy" @click="copyToClipboard(torrent.url)">
|
||||
<i class="fas fa-clipboard" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="label">Size</div>
|
||||
<div class="value">{{ convertSize(torrent.size) }}</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="label">Seeders</div>
|
||||
<div class="value">{{ torrent.seeds }}</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="label">Leechers</div>
|
||||
<div class="value">{{ torrent.peers }}</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="label">Uploaded</div>
|
||||
<div class="value">{{ formatDate(torrent.created_at, true) }}</div>
|
||||
</div>
|
||||
|
||||
<div class="row" v-if="torrent.description">
|
||||
<div class="label">Description</div>
|
||||
<div class="value">{{ torrent.description }}</div>
|
||||
</div>
|
||||
|
||||
<div class="row" v-if="torrent.year">
|
||||
<div class="label">Year</div>
|
||||
<div class="value">{{ torrent.year }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Utils from "@/Utils";
|
||||
|
||||
export default {
|
||||
mixins: [Utils],
|
||||
|
||||
props: {
|
||||
torrent: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
openInNewTab(url) {
|
||||
window.open(url, "_blank");
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.info {
|
||||
min-width: 50vw;
|
||||
max-width: 800px;
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 5px;
|
||||
border-bottom: $default-border;
|
||||
|
||||
@include until($desktop) {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
@include from($desktop) {
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
|
||||
.label {
|
||||
width: 10em;
|
||||
font-weight: bold;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.value {
|
||||
width: calc(100% - 10em);
|
||||
flex: 1;
|
||||
text-align: right;
|
||||
|
||||
button {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
|
||||
i {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: $default-hover-fg;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,109 @@
|
|||
<template>
|
||||
<nav>
|
||||
<button class="menu-button" @click="$emit('toggle')">
|
||||
<i class="fa fa-bars" />
|
||||
</button>
|
||||
|
||||
<li v-for="(view, name) in views" :key="name" :title="view.displayName"
|
||||
:class="{selected: name === selectedView}" @click="$emit('input', name)">
|
||||
<i :class="view.iconClass" />
|
||||
</li>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
emits: ['input', 'toggle'],
|
||||
props: {
|
||||
selectedView: {
|
||||
type: String,
|
||||
},
|
||||
|
||||
collapsed: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
|
||||
views: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {
|
||||
search: {
|
||||
displayName: 'Search',
|
||||
iconClass: 'fa fa-search',
|
||||
},
|
||||
|
||||
transfers: {
|
||||
displayName: 'Transfers',
|
||||
iconClass: 'fa fa-download',
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "vars";
|
||||
|
||||
nav {
|
||||
width: $torrent-nav-width;
|
||||
height: calc(100% + #{$torrent-header-height});
|
||||
margin-top: -$torrent-header-height;
|
||||
background: $nav-collapsed-bg;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
box-shadow: 2.5px 0 4.5px 2px $nav-collapsed-fg;
|
||||
margin-left: 2.5px;
|
||||
overflow: hidden;
|
||||
|
||||
.menu-button {
|
||||
position: absolute;
|
||||
top: 0.75em;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-size: 1.2em;
|
||||
border: 0;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
z-index: 1;
|
||||
|
||||
&:hover {
|
||||
color: $default-hover-fg;
|
||||
}
|
||||
}
|
||||
|
||||
li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 1.2em;
|
||||
cursor: pointer;
|
||||
list-style: none;
|
||||
padding: .6em;
|
||||
opacity: 0.7;
|
||||
|
||||
&.selected,
|
||||
&:hover {
|
||||
border-radius: 1.2em;
|
||||
margin: 0 0.2em;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: $nav-entry-collapsed-hover-bg;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background: $nav-entry-collapsed-selected-bg;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,24 +1,78 @@
|
|||
<template>
|
||||
<div class="torrent-container">
|
||||
<div class="header-container">
|
||||
<Header @torrent-add="download($event)" />
|
||||
<Modal
|
||||
title="Torrent info"
|
||||
:visible="infoItem !== null"
|
||||
@close="infoIndex = null"
|
||||
v-if="infoItem"
|
||||
>
|
||||
<Info :torrent="infoItem" />
|
||||
</Modal>
|
||||
|
||||
<div class="header-container" :class="{'with-nav': !navCollapsed}">
|
||||
<Header
|
||||
:with-nav="!navCollapsed"
|
||||
:selected-view="selectedView"
|
||||
:loading="loading"
|
||||
@search="search($event)"
|
||||
@torrent-add="download($event)"
|
||||
@toggle="navCollapsed = !navCollapsed"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="view-container">
|
||||
<TorrentView :plugin-name="pluginName" />
|
||||
<main>
|
||||
<div class="view-container" :class="{'with-nav': !navCollapsed}">
|
||||
<Transfers
|
||||
:transfers="transfers"
|
||||
@pause="pause($event)"
|
||||
@resume="resume($event)"
|
||||
@remove="remove($event)"
|
||||
v-if="selectedView === 'transfers'"
|
||||
/>
|
||||
|
||||
<Results
|
||||
:results="results"
|
||||
@download="download($event)"
|
||||
@info="infoIndex = $event"
|
||||
@next-page="search(query, page + 1)"
|
||||
v-else-if="selectedView === 'search'"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="nav-container">
|
||||
<Nav
|
||||
:selected-view="selectedView"
|
||||
@toggle="navCollapsed = !navCollapsed"
|
||||
@input="selectedView = $event"
|
||||
v-if="!navCollapsed"
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Info from "./Info";
|
||||
import Header from "@/components/panels/Torrent/Header";
|
||||
import TorrentView from "@/components/panels/Torrent/View";
|
||||
import Modal from "@/components/Modal";
|
||||
import Nav from "@/components/panels/Torrent/Nav";
|
||||
import Results from "@/components/panels/Torrent/Results";
|
||||
import Transfers from "@/components/panels/Torrent/Transfers";
|
||||
import Utils from "@/Utils";
|
||||
|
||||
export default {
|
||||
name: "Panel",
|
||||
components: {TorrentView, Header},
|
||||
mixins: [Utils],
|
||||
|
||||
components: {
|
||||
Info,
|
||||
Header,
|
||||
Modal,
|
||||
Nav,
|
||||
Results,
|
||||
Transfers,
|
||||
},
|
||||
|
||||
props: {
|
||||
pluginName: {
|
||||
type: String,
|
||||
|
@ -26,25 +80,250 @@ export default {
|
|||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
transfers: {},
|
||||
results: [],
|
||||
selectedView: "transfers",
|
||||
navCollapsed: false,
|
||||
query: "",
|
||||
page: 1,
|
||||
infoIndex: null,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
infoItem() {
|
||||
if (this.infoIndex === null) {
|
||||
return null
|
||||
}
|
||||
|
||||
return this.results[this.infoIndex]
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
torrentId(torrent) {
|
||||
if (torrent?.hash && torrent.hash.length)
|
||||
return torrent.hash
|
||||
|
||||
return torrent.url
|
||||
},
|
||||
|
||||
onTorrentUpdate(torrent) {
|
||||
this.transfers[this.torrentId(torrent)] = torrent
|
||||
},
|
||||
|
||||
onTorrentQueued(torrent) {
|
||||
this.onTorrentUpdate(torrent)
|
||||
this.notify({
|
||||
text: 'Torrent queued for download',
|
||||
image: {
|
||||
icon: 'hourglass-start',
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
onTorrentStart(torrent) {
|
||||
this.onTorrentUpdate(torrent)
|
||||
this.notify({
|
||||
html: `Torrent download started: <b>${torrent.name}</b>`,
|
||||
image: {
|
||||
icon: 'play',
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
onTorrentResume(torrent) {
|
||||
this.onTorrentUpdate(torrent)
|
||||
this.notify({
|
||||
html: `Torrent download resumed: <b>${torrent.name}</b>`,
|
||||
image: {
|
||||
icon: 'play',
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
onTorrentPause(torrent) {
|
||||
this.onTorrentUpdate(torrent)
|
||||
this.notify({
|
||||
html: `Torrent download paused: <b>${torrent.name}</b>`,
|
||||
image: {
|
||||
icon: 'pause',
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
onTorrentCompleted(torrent) {
|
||||
this.onTorrentUpdate(torrent)
|
||||
this.transfers[this.torrentId(torrent)].finish_date = new Date().toISOString()
|
||||
this.transfers[this.torrentId(torrent)].progress = 100
|
||||
this.notify({
|
||||
html: `Torrent download completed: <b>${torrent.name}</b>`,
|
||||
image: {
|
||||
icon: 'check',
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
onTorrentRemove(torrent) {
|
||||
const torrentId = this.torrentId(torrent)
|
||||
if (torrentId in this.transfers)
|
||||
delete this.transfers[torrentId]
|
||||
},
|
||||
|
||||
async search(query, page=1) {
|
||||
this.loading = true
|
||||
this.query = query
|
||||
let results = []
|
||||
|
||||
try {
|
||||
results = await this.request(
|
||||
`${this.pluginName}.search`,
|
||||
{query: query, page: page}
|
||||
)
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
|
||||
this.results = page === 1 ? results : this.results.concat(results)
|
||||
if (results.length > 0) {
|
||||
this.page = page
|
||||
}
|
||||
},
|
||||
|
||||
async download(torrent) {
|
||||
await this.request(`${this.pluginName}.download`, {torrent: torrent})
|
||||
},
|
||||
|
||||
async pause(torrent) {
|
||||
await this.request(`${this.pluginName}.pause`, {torrent: torrent.url})
|
||||
await this.refresh()
|
||||
},
|
||||
|
||||
async resume(torrent) {
|
||||
await this.request(`${this.pluginName}.resume`, {torrent: torrent.url})
|
||||
await this.refresh()
|
||||
},
|
||||
|
||||
async remove(torrent) {
|
||||
await this.request(`${this.pluginName}.remove`, {torrent: torrent.url})
|
||||
await this.refresh()
|
||||
},
|
||||
|
||||
async refresh() {
|
||||
this.loading = true
|
||||
|
||||
try {
|
||||
this.transfers = Object.values(await this.request(`${this.pluginName}.status`) || {})
|
||||
.reduce((obj, torrent) => {
|
||||
obj[this.torrentId(torrent)] = torrent
|
||||
return obj
|
||||
}, {})
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.refresh()
|
||||
this.selectedView = this.transfers.length ? 'transfers' : 'search'
|
||||
|
||||
this.subscribe(
|
||||
this.onTorrentUpdate,
|
||||
'on-torrent-update',
|
||||
'platypush.message.event.torrent.TorrentDownloadStartEvent',
|
||||
'platypush.message.event.torrent.TorrentDownloadProgressEvent',
|
||||
'platypush.message.event.torrent.TorrentSeedingStartEvent',
|
||||
'platypush.message.event.torrent.TorrentStateChangeEvent',
|
||||
)
|
||||
|
||||
this.subscribe(
|
||||
this.onTorrentQueued,
|
||||
'on-torrent-queued',
|
||||
'platypush.message.event.torrent.TorrentQueuedEvent',
|
||||
)
|
||||
|
||||
this.subscribe(
|
||||
this.onTorrentStart,
|
||||
'on-torrent-queued',
|
||||
'platypush.message.event.torrent.TorrentDownloadedMetadataEvent',
|
||||
)
|
||||
|
||||
this.subscribe(
|
||||
this.onTorrentResume,
|
||||
'on-torrent-resume',
|
||||
'platypush.message.event.torrent.TorrentResumedEvent',
|
||||
)
|
||||
|
||||
this.subscribe(
|
||||
this.onTorrentPause,
|
||||
'on-torrent-pause',
|
||||
'platypush.message.event.torrent.TorrentPausedEvent',
|
||||
)
|
||||
|
||||
this.subscribe(
|
||||
this.onTorrentStop,
|
||||
'on-torrent-stop',
|
||||
'platypush.message.event.torrent.TorrentDownloadStopEvent',
|
||||
)
|
||||
|
||||
this.subscribe(
|
||||
this.onTorrentCompleted,
|
||||
'on-torrent-completed',
|
||||
'platypush.message.event.torrent.TorrentDownloadCompletedEvent'
|
||||
)
|
||||
|
||||
this.subscribe(
|
||||
this.onTorrentRemove,
|
||||
'on-torrent-remove',
|
||||
'platypush.message.event.torrent.TorrentRemovedEvent'
|
||||
)
|
||||
|
||||
const searchBox = document.querySelector('.search-box input[type="search"]')
|
||||
if (searchBox) {
|
||||
this.$nextTick(() => searchBox.focus())
|
||||
}
|
||||
},
|
||||
|
||||
destroy() {
|
||||
this.unsubscribe('on-torrent-update')
|
||||
this.unsubscribe('on-torrent-remove')
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "vars";
|
||||
|
||||
.header-container {
|
||||
&.with-nav {
|
||||
width: calc(100% - #{$torrent-nav-width});
|
||||
}
|
||||
}
|
||||
|
||||
.torrent-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
|
||||
main {
|
||||
width: 100%;
|
||||
height: calc(100% - #{$torrent-header-height});
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.view-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
padding-top: .2em;
|
||||
|
||||
&.with-nav {
|
||||
width: calc(100% - #{$torrent-nav-width});
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -0,0 +1,188 @@
|
|||
<template>
|
||||
<div class="results-container">
|
||||
<div class="no-content" v-if="!results?.length">No results</div>
|
||||
<div class="results" ref="body" @scroll="onScroll" v-else>
|
||||
<div class="result" v-for="(result, i) in results" :key="i">
|
||||
<div class="info">
|
||||
<div class="title">{{ result.title }}</div>
|
||||
<div class="additional-info">
|
||||
<span class="info-pill size">
|
||||
<span class="label">
|
||||
<i class="fa fa-hdd" />
|
||||
</span>
|
||||
<span class="separator" />
|
||||
<span class="value">{{ convertSize(result.size) }}</span>
|
||||
</span>
|
||||
<span class="separator"> | </span>
|
||||
|
||||
<span class="info-pill seeds">
|
||||
<span class="label">
|
||||
<i class="fa fa-users" />
|
||||
</span>
|
||||
<span class="separator" />
|
||||
<span class="value">{{ result.seeds }}</span>
|
||||
</span>
|
||||
<span class="separator"> | </span>
|
||||
|
||||
<span class="info-pill created-at">
|
||||
<span class="label">
|
||||
<i class="fa fa-calendar" />
|
||||
</span>
|
||||
<span class="separator" />
|
||||
<span class="value">{{ formatDate(result.created_at, true) }}</span>
|
||||
</span>
|
||||
<span class="separator"> | </span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<button title="Torrent info" @click="$emit('info', i)">
|
||||
<i class="fa fa-info-circle" />
|
||||
</button>
|
||||
|
||||
<button title="Download" @click="$emit('download', result.url)">
|
||||
<i class="fa fa-download" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Utils from "@/Utils";
|
||||
|
||||
export default {
|
||||
emits: ['download', 'info', 'next-page'],
|
||||
mixins: [Utils],
|
||||
|
||||
props: {
|
||||
results: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
|
||||
page: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
scrollTimeout: null,
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
onScroll() {
|
||||
const offset = this.$refs.body.scrollTop
|
||||
const bodyHeight = parseFloat(getComputedStyle(this.$refs.body).height)
|
||||
const scrollHeight = this.$refs.body.scrollHeight
|
||||
|
||||
if (offset >= (scrollHeight - bodyHeight - 5)) {
|
||||
if (this.scrollTimeout || !this.results.length)
|
||||
return
|
||||
|
||||
this.scrollTimeout = setTimeout(() => {
|
||||
this.scrollTimeout = null
|
||||
}, 250)
|
||||
|
||||
this.$emit('next-page', this.page + 1)
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.results-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: $background-color;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.no-content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.results {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.result {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
border-bottom: $default-border;
|
||||
gap: 1em;
|
||||
padding: .5em 0;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: $hover-bg;
|
||||
}
|
||||
|
||||
.info {
|
||||
width: calc(100% - 5em);
|
||||
padding: .5em;
|
||||
}
|
||||
|
||||
.additional-info {
|
||||
font-size: .8em;
|
||||
opacity: .7;
|
||||
display: flex;
|
||||
|
||||
.separator {
|
||||
font-size: 1.5em;
|
||||
margin-right: .5em;
|
||||
}
|
||||
|
||||
.info-pill {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: .5em;
|
||||
|
||||
.label {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.separator {
|
||||
margin: -.2em .25em 0 .25em;
|
||||
font-size: 2em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 5em;
|
||||
|
||||
button {
|
||||
background: none;
|
||||
opacity: .8;
|
||||
border: none;
|
||||
padding: .25em;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: $hover-fg;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
margin-right: 1em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -9,8 +9,8 @@
|
|||
<div class="col-8 left side">
|
||||
<i class="icon fa" :class="{
|
||||
'fa-check': torrent.finish_date != null,
|
||||
'fa-play': !torrent.finish_date && torrent.state === 'downloading',
|
||||
'fa-pause': !torrent.finish_date && torrent.state === 'paused',
|
||||
'fa-play': !torrent.finish_date && torrent.state === 'downloading' && !torrent.paused,
|
||||
'fa-pause': !torrent.finish_date && torrent.state === 'downloading' && torrent.paused,
|
||||
'fa-stop': !torrent.finish_date && torrent.state === 'stopped',
|
||||
}" />
|
||||
<div class="title" v-text="torrent.name || torrent.hash || torrent.url" />
|
||||
|
@ -22,11 +22,11 @@
|
|||
|
||||
<div class="col-2 right side">
|
||||
<Dropdown title="Actions" icon-class="fa fa-ellipsis-h" @click="selectedItem = i">
|
||||
<DropdownItem icon-class="fa fa-pause" text="Pause transfer" @click="pause(torrentId(torrent))"
|
||||
v-if="torrent.state === 'downloading'" />
|
||||
<DropdownItem icon-class="fa fa-play" text="Resume transfer" @click="resume(torrentId(torrent))"
|
||||
v-if="torrent.state === 'paused'" />
|
||||
<DropdownItem icon-class="fa fa-trash" text="Remove transfer" @click="remove(torrentId(torrent))" />
|
||||
<DropdownItem icon-class="fa fa-pause" text="Pause transfer" @click="$emit('pause', torrent)"
|
||||
v-if="torrent.state === 'downloading' && !torrent.paused" />
|
||||
<DropdownItem icon-class="fa fa-play" text="Resume transfer" @click="$emit('resume', torrent)"
|
||||
v-if="torrent.state === 'downloading' && torrent.paused" />
|
||||
<DropdownItem icon-class="fa fa-trash" text="Remove transfer" @click="$emit('remove', torrent)" />
|
||||
<DropdownItem icon-class="fa fa-folder" text="View files" @click="$refs.torrentFiles.isVisible = true" />
|
||||
<DropdownItem icon-class="fa fa-info" text="Torrent info" @click="$refs.torrentInfo.isVisible = true" />
|
||||
</Dropdown>
|
||||
|
@ -96,6 +96,15 @@
|
|||
<div class="attr">Save path</div>
|
||||
<div class="value" v-text="transfers[selectedItem].save_path" />
|
||||
</div>
|
||||
|
||||
<div class="row" v-if="transfers[selectedItem].files">
|
||||
<div class="attr">Files</div>
|
||||
<div class="value">
|
||||
<div class="file" v-for="(file, i) in transfers[selectedItem].files" :key="i">
|
||||
<a :href="`/file?path=${encodeURIComponent(file)}`" target="_blank" v-text="file" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
|
@ -126,26 +135,31 @@ import Dropdown from "@/components/elements/Dropdown";
|
|||
import DropdownItem from "@/components/elements/DropdownItem";
|
||||
|
||||
export default {
|
||||
name: "View",
|
||||
emits: ['play', 'play-with-captions'],
|
||||
emits: [
|
||||
'pause',
|
||||
'play',
|
||||
'play-with-captions',
|
||||
'refresh',
|
||||
'remove',
|
||||
'resume',
|
||||
],
|
||||
components: {Dropdown, DropdownItem, Loading, Modal},
|
||||
mixins: [Utils, MediaUtils],
|
||||
props: {
|
||||
pluginName: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
|
||||
isMedia: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
|
||||
transfers: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
transfers: {},
|
||||
selectedItem: null,
|
||||
}
|
||||
},
|
||||
|
@ -158,79 +172,6 @@ export default {
|
|||
return this.transfers[this.selectedItem].files.map((file) => file.split('/').pop())
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
torrentId(torrent) {
|
||||
if (torrent?.hash && torrent.hash.length)
|
||||
return torrent.hash
|
||||
|
||||
return torrent.url
|
||||
},
|
||||
|
||||
async refresh() {
|
||||
this.loading = true
|
||||
|
||||
try {
|
||||
this.transfers = Object.values(await this.request(`${this.pluginName}.status`) || {})
|
||||
.reduce((obj, torrent) => {
|
||||
obj[this.torrentId(torrent)] = torrent
|
||||
return obj
|
||||
}, {})
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
async pause(torrent) {
|
||||
await this.request(`${this.pluginName}.pause`, {torrent: torrent})
|
||||
await this.refresh()
|
||||
},
|
||||
|
||||
async resume(torrent) {
|
||||
await this.request(`${this.pluginName}.resume`, {torrent: torrent})
|
||||
await this.refresh()
|
||||
},
|
||||
|
||||
async remove(torrent) {
|
||||
await this.request(`${this.pluginName}.remove`, {torrent: torrent})
|
||||
await this.refresh()
|
||||
},
|
||||
|
||||
onTorrentUpdate(torrent) {
|
||||
this.transfers[this.torrentId(torrent)] = torrent
|
||||
},
|
||||
|
||||
onTorrentRemove(torrent) {
|
||||
const torrentId = this.torrentId(torrent)
|
||||
if (torrentId in this.transfers)
|
||||
delete this.transfers[torrentId]
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.refresh()
|
||||
|
||||
this.subscribe(
|
||||
this.onTorrentUpdate,'on-torrent-update',
|
||||
'platypush.message.event.torrent.TorrentQueuedEvent',
|
||||
'platypush.message.event.torrent.TorrentDownloadedMetadataEvent',
|
||||
'platypush.message.event.torrent.TorrentDownloadStartEvent',
|
||||
'platypush.message.event.torrent.TorrentDownloadProgressEvent',
|
||||
'platypush.message.event.torrent.TorrentResumedEvent',
|
||||
'platypush.message.event.torrent.TorrentPausedEvent',
|
||||
'platypush.message.event.torrent.TorrentSeedingStartEvent',
|
||||
'platypush.message.event.torrent.TorrentStateChangeEvent',
|
||||
'platypush.message.event.torrent.TorrentDownloadStopEvent',
|
||||
'platypush.message.event.torrent.TorrentDownloadCompletedEvent')
|
||||
|
||||
this.subscribe(this.onTorrentRemove,'on-torrent-remove',
|
||||
'platypush.message.event.torrent.TorrentRemovedEvent')
|
||||
},
|
||||
|
||||
destroy() {
|
||||
this.unsubscribe('on-torrent-update')
|
||||
this.unsubscribe('on-torrent-remove')
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
@ -1 +1,2 @@
|
|||
$torrent-header-height: 3.3em;
|
||||
$torrent-nav-width: 2.8em;
|
||||
|
|
|
@ -38,6 +38,7 @@ module.exports = {
|
|||
'^/ws/requests': wsProxy,
|
||||
'^/ws/shell': wsProxy,
|
||||
'^/execute': httpProxy,
|
||||
'^/file': httpProxy,
|
||||
'^/auth': httpProxy,
|
||||
'^/camera/': httpProxy,
|
||||
'^/sound/': httpProxy,
|
||||
|
|
|
@ -6,10 +6,19 @@ class TorrentMediaSearcher(MediaSearcher):
|
|||
def search(self, query, **kwargs):
|
||||
self.logger.info('Searching torrents for "{}"'.format(query))
|
||||
|
||||
torrents = get_plugin(self.media_plugin.torrent_plugin if self.media_plugin else 'torrent')
|
||||
torrents = get_plugin(
|
||||
self.media_plugin.torrent_plugin if self.media_plugin else 'torrent'
|
||||
)
|
||||
if not torrents:
|
||||
raise RuntimeError('Torrent plugin not available/configured')
|
||||
return torrents.search(query, ).output
|
||||
|
||||
return [
|
||||
torrent
|
||||
for torrent in torrents.search(
|
||||
query,
|
||||
).output
|
||||
if torrent.get('is_media')
|
||||
]
|
||||
|
||||
|
||||
# vim:sw=4:ts=4:et:
|
||||
|
|
Loading…
Reference in a new issue