forked from platypush/platypush
[media
UI] Major redesign of the search results.
This commit is contained in:
parent
f7a25a478d
commit
d7093d18c5
9 changed files with 579 additions and 200 deletions
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="no-items-container">
|
<div class="no-items-container">
|
||||||
<div class="no-items fade-in">
|
<div class="no-items fade-in" :class="{shadow: withShadow}">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -9,6 +9,12 @@
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
name: "NoItems",
|
name: "NoItems",
|
||||||
|
props: {
|
||||||
|
withShadow: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -45,7 +51,10 @@ export default {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
border-radius: 1em;
|
border-radius: 1em;
|
||||||
box-shadow: $border-shadow-bottom;
|
|
||||||
|
&.shadow {
|
||||||
|
box-shadow: $border-shadow-bottom;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -33,26 +33,32 @@
|
||||||
@source-toggle="sources[$event] = !sources[$event]" />
|
@source-toggle="sources[$event] = !sources[$event]" />
|
||||||
|
|
||||||
<div class="body-container" :class="{'expanded-header': $refs.header?.filterVisible}">
|
<div class="body-container" :class="{'expanded-header': $refs.header?.filterVisible}">
|
||||||
<Results :results="results" :selected-result="selectedResult" @select="onResultSelect($event)"
|
<Results :results="results"
|
||||||
@play="play" @info="$refs.mediaInfo.isVisible = true" @view="view" @download="download"
|
:selected-result="selectedResult"
|
||||||
:sources="sources" v-if="selectedView === 'search'" />
|
:sources="sources"
|
||||||
|
:loading="loading"
|
||||||
|
@select="onResultSelect($event)"
|
||||||
|
@play="play"
|
||||||
|
@view="view"
|
||||||
|
@download="download"
|
||||||
|
v-if="selectedView === 'search'" />
|
||||||
|
|
||||||
<TorrentView :plugin-name="torrentPlugin" :is-media="true" @play="play"
|
<TorrentView :plugin-name="torrentPlugin"
|
||||||
|
:is-media="true"
|
||||||
|
@play="play"
|
||||||
v-else-if="selectedView === 'torrents'" />
|
v-else-if="selectedView === 'torrents'" />
|
||||||
|
|
||||||
<Browser :plugin-name="torrentPlugin" :is-media="true" :filter="browserFilter"
|
<Browser :plugin-name="torrentPlugin"
|
||||||
@path-change="browserFilter = ''" @play="play($event)" v-else-if="selectedView === 'browser'" />
|
:is-media="true"
|
||||||
|
:filter="browserFilter"
|
||||||
|
@path-change="browserFilter = ''"
|
||||||
|
@play="play($event)"
|
||||||
|
v-else-if="selectedView === 'browser'" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</MediaView>
|
</MediaView>
|
||||||
|
|
||||||
<div class="media-info-container">
|
|
||||||
<Modal title="Media info" ref="mediaInfo">
|
|
||||||
<Info :item="results[selectedResult]" v-if="selectedResult != null" />
|
|
||||||
</Modal>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="subtitles-container">
|
<div class="subtitles-container">
|
||||||
<Modal title="Available subtitles" :visible="showSubtitlesModal" ref="subtitlesSelector"
|
<Modal title="Available subtitles" :visible="showSubtitlesModal" ref="subtitlesSelector"
|
||||||
@close="showSubtitlesModal = false">
|
@close="showSubtitlesModal = false">
|
||||||
|
@ -65,21 +71,8 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="play-url-container">
|
<div class="play-url-container">
|
||||||
<Modal title="Play URL" ref="playUrlModal" @open="$refs.playUrlInput.focus()">
|
<Modal title="Play URL" ref="playUrlModal" @open="onPlayUrlModalOpen">
|
||||||
<form @submit.prevent="playUrl(urlPlay)">
|
<UrlPlayer :value="urlPlay" @input="urlPlay = $event.target.value" @play="playUrl($event)" />
|
||||||
<div class="row">
|
|
||||||
<label>
|
|
||||||
Play URL (use the file:// prefix for local files)
|
|
||||||
<input type="text" v-model="urlPlay" ref="playUrlInput" autofocus />
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row footer">
|
|
||||||
<button type="submit" :disabled="!urlPlay?.length">
|
|
||||||
<i class="fa fa-play"></i> Play
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</Modal>
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -90,20 +83,33 @@
|
||||||
import Loading from "@/components/Loading";
|
import Loading from "@/components/Loading";
|
||||||
import Modal from "@/components/Modal";
|
import Modal from "@/components/Modal";
|
||||||
import Utils from "@/Utils";
|
import Utils from "@/Utils";
|
||||||
|
|
||||||
|
import Browser from "@/components/File/Browser";
|
||||||
|
import Header from "@/components/panels/Media/Header";
|
||||||
import MediaUtils from "@/components/Media/Utils";
|
import MediaUtils from "@/components/Media/Utils";
|
||||||
import MediaView from "@/components/Media/View";
|
import MediaView from "@/components/Media/View";
|
||||||
import Header from "@/components/panels/Media/Header";
|
|
||||||
import Info from "@/components/panels/Media/Info";
|
|
||||||
import Nav from "@/components/panels/Media/Nav";
|
import Nav from "@/components/panels/Media/Nav";
|
||||||
import Results from "@/components/panels/Media/Results";
|
import Results from "@/components/panels/Media/Results";
|
||||||
import Subtitles from "@/components/panels/Media/Subtitles";
|
import Subtitles from "@/components/panels/Media/Subtitles";
|
||||||
import TorrentView from "@/components/panels/Torrent/View";
|
import TorrentView from "@/components/panels/Torrent/View";
|
||||||
import Browser from "@/components/File/Browser";
|
import UrlPlayer from "@/components/panels/Media/UrlPlayer";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "Media",
|
name: "Media",
|
||||||
mixins: [Utils, MediaUtils],
|
mixins: [Utils, MediaUtils],
|
||||||
components: {Browser, Loading, MediaView, Header, Results, Modal, Info, Nav, TorrentView, Subtitles},
|
components: {
|
||||||
|
Browser,
|
||||||
|
Header,
|
||||||
|
Loading,
|
||||||
|
MediaView,
|
||||||
|
Modal,
|
||||||
|
Nav,
|
||||||
|
Results,
|
||||||
|
Subtitles,
|
||||||
|
TorrentView,
|
||||||
|
UrlPlayer,
|
||||||
|
},
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
pluginName: {
|
pluginName: {
|
||||||
type: String,
|
type: String,
|
||||||
|
@ -249,6 +255,18 @@ export default {
|
||||||
this.selectedPlayer.status = status
|
this.selectedPlayer.status = status
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onPlayUrlModalOpen() {
|
||||||
|
const modal = this.$refs.playUrlModal
|
||||||
|
this.urlPlay = ''
|
||||||
|
modal.$nextTick(() => {
|
||||||
|
const input = modal.$el.querySelector('input[type=text]')
|
||||||
|
if (input) {
|
||||||
|
input.focus()
|
||||||
|
input.select()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
onTorrentQueued(event) {
|
onTorrentQueued(event) {
|
||||||
this.notify({
|
this.notify({
|
||||||
title: 'Torrent queued for download',
|
title: 'Torrent queued for download',
|
||||||
|
@ -347,19 +365,17 @@ export default {
|
||||||
if (this.selectedResult == null || this.selectedResult !== result) {
|
if (this.selectedResult == null || this.selectedResult !== result) {
|
||||||
this.selectedResult = result
|
this.selectedResult = result
|
||||||
this.selectedSubtitles = null
|
this.selectedSubtitles = null
|
||||||
|
} else {
|
||||||
|
this.selectedResult = null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
showPlayUrlModal() {
|
showPlayUrlModal() {
|
||||||
this.$refs.playUrlModal.show()
|
this.$refs.playUrlModal.show()
|
||||||
this.$refs.playUrlInput.value = ''
|
|
||||||
this.$nextTick(() => {
|
|
||||||
this.$refs.playUrlInput.value = ''
|
|
||||||
this.$refs.playUrlInput.focus()
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async playUrl(url) {
|
async playUrl(url) {
|
||||||
|
this.urlPlay = url
|
||||||
this.loading = true
|
this.loading = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -452,16 +468,6 @@ export default {
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.media-info-container) {
|
|
||||||
.modal-container {
|
|
||||||
.body {
|
|
||||||
max-width: calc(100vw - 2px);
|
|
||||||
padding: 0;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.subtitles-container) {
|
:deep(.subtitles-container) {
|
||||||
.body {
|
.body {
|
||||||
padding: 0 !important;
|
padding: 0 !important;
|
||||||
|
@ -471,45 +477,4 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.play-url-container) {
|
|
||||||
.body {
|
|
||||||
padding: 1em !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
form {
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
border: none;
|
|
||||||
border-radius: 0;
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type=text] {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
[type=submit] {
|
|
||||||
background: initial;
|
|
||||||
border-color: initial;
|
|
||||||
border-radius: 1.5em;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: $default-hover-fg-2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer {
|
|
||||||
display: flex;
|
|
||||||
justify-content: right;
|
|
||||||
padding: 0;
|
|
||||||
margin-top: 1em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.media-info-container) {
|
|
||||||
.modal {
|
|
||||||
max-width: 70em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,9 +1,16 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="media-info">
|
<div class="media-info">
|
||||||
<div class="row header">
|
<div class="row header">
|
||||||
<MediaImage :item="item" />
|
<div class="image-container">
|
||||||
|
<MediaImage :item="item" @play="$emit('play')" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="title">
|
<div class="title">
|
||||||
|
<i :class="typeIcons[item.type]"
|
||||||
|
:title="item.type"
|
||||||
|
v-if="typeIcons[item?.type]">
|
||||||
|
|
||||||
|
</i>
|
||||||
<a :href="item.url" target="_blank" v-if="item.url" v-text="item.title" />
|
<a :href="item.url" target="_blank" v-if="item.url" v-text="item.title" />
|
||||||
<span v-else v-text="item.title" />
|
<span v-else v-text="item.title" />
|
||||||
</div>
|
</div>
|
||||||
|
@ -79,11 +86,10 @@
|
||||||
<div class="right side" v-text="item.genres.join(', ')" />
|
<div class="right side" v-text="item.genres.join(', ')" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row" v-if="item?.channelId">
|
<div class="row" v-if="channel">
|
||||||
<div class="left side">Channel</div>
|
<div class="left side">Channel</div>
|
||||||
<div class="right side">
|
<div class="right side">
|
||||||
<a :href="`https://www.youtube.com/channel/${item.channelId}`" target="_blank"
|
<a :href="channel.url" target="_blank" v-text="channel.title || channel.url" />
|
||||||
v-text="item.channelTitle || `https://www.youtube.com/channel/${item.channelId}`" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -92,9 +98,9 @@
|
||||||
<div class="right side" v-text="item.year" />
|
<div class="right side" v-text="item.year" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row" v-if="item?.publishedAt">
|
<div class="row" v-if="publishedDate">
|
||||||
<div class="left side">Published at</div>
|
<div class="left side">Published at</div>
|
||||||
<div class="right side" v-text="formatDate(item.publishedAt, true)" />
|
<div class="right side" v-text="publishedDate" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row" v-if="item?.file">
|
<div class="row" v-if="item?.file">
|
||||||
|
@ -140,17 +146,58 @@
|
||||||
import Utils from "@/Utils";
|
import Utils from "@/Utils";
|
||||||
import MediaUtils from "@/components/Media/Utils";
|
import MediaUtils from "@/components/Media/Utils";
|
||||||
import MediaImage from "./MediaImage";
|
import MediaImage from "./MediaImage";
|
||||||
|
import Icons from "./icons.json";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "Info",
|
name: "Info",
|
||||||
components: {MediaImage},
|
components: {MediaImage},
|
||||||
mixins: [Utils, MediaUtils],
|
mixins: [Utils, MediaUtils],
|
||||||
|
emits: ['play'],
|
||||||
props: {
|
props: {
|
||||||
item: {
|
item: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => {},
|
default: () => {},
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
typeIcons: Icons,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
channel() {
|
||||||
|
let ret = null
|
||||||
|
if (this.item?.channelId)
|
||||||
|
ret = {
|
||||||
|
url: `https://www.youtube.com/channel/${this.item.channelId}`,
|
||||||
|
}
|
||||||
|
else if (this.item?.channel_url)
|
||||||
|
ret = {
|
||||||
|
url: this.item.channel_url,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ret)
|
||||||
|
return null
|
||||||
|
|
||||||
|
if (this.item?.channelTitle)
|
||||||
|
ret.title = this.item.channelTitle
|
||||||
|
else if (this.item?.channel)
|
||||||
|
ret.title = this.item.channel
|
||||||
|
|
||||||
|
return ret
|
||||||
|
},
|
||||||
|
|
||||||
|
publishedDate() {
|
||||||
|
if (this.item?.publishedAt)
|
||||||
|
return this.formatDate(this.item.publishedAt, true)
|
||||||
|
if (this.item?.created_at)
|
||||||
|
return this.formatDate(this.item.created_at, true)
|
||||||
|
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -159,10 +206,6 @@ export default {
|
||||||
|
|
||||||
.media-info {
|
.media-info {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
@include from($tablet) {
|
|
||||||
width: 640px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.row {
|
.row {
|
||||||
|
@ -231,6 +274,14 @@ export default {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
|
.image-container {
|
||||||
|
@include from($desktop) {
|
||||||
|
.image-container {
|
||||||
|
width: 420px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
font-size: 1.5em;
|
font-size: 1.5em;
|
||||||
|
@ -238,6 +289,11 @@ export default {
|
||||||
margin-top: 0.5em;
|
margin-top: 0.5em;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
|
|
||||||
|
@include from($desktop) {
|
||||||
|
flex: 1;
|
||||||
|
padding-left: 1em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
|
|
|
@ -0,0 +1,193 @@
|
||||||
|
<template>
|
||||||
|
<div class="item media-item" :class="{selected: selected}" v-if="!hidden">
|
||||||
|
<div class="thumbnail">
|
||||||
|
<MediaImage :item="item" @play="$emit('play')" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="body">
|
||||||
|
<div class="row title">
|
||||||
|
<div class="col-11 left side" v-text="item.title" @click="$emit('select')" />
|
||||||
|
<div class="col-1 right side">
|
||||||
|
<Dropdown title="Actions" icon-class="fa fa-ellipsis-h">
|
||||||
|
<DropdownItem icon-class="fa fa-play" text="Play" @click="$emit('play')"
|
||||||
|
v-if="item.type !== 'torrent'" />
|
||||||
|
<DropdownItem icon-class="fa fa-download" text="Download" @click="$emit('download')"
|
||||||
|
v-if="item.type === 'torrent'" />
|
||||||
|
<DropdownItem icon-class="fa fa-window-maximize" text="View in browser" @click="$emit('view')"
|
||||||
|
v-if="item.type === 'file'" />
|
||||||
|
<DropdownItem icon-class="fa fa-info-circle" text="Info" @click="$emit('select')" />
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row subtitle" v-if="item.channel">
|
||||||
|
<a class="channel" :href="item.channel_url" target="_blank">
|
||||||
|
<img :src="item.channel_image" class="channel-image" v-if="item.channel_image" />
|
||||||
|
<span class="channel-name" v-text="item.channel" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Dropdown from "@/components/elements/Dropdown";
|
||||||
|
import DropdownItem from "@/components/elements/DropdownItem";
|
||||||
|
import Icons from "./icons.json";
|
||||||
|
import MediaImage from "./MediaImage";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {Dropdown, DropdownItem, MediaImage},
|
||||||
|
emits: ['play', 'select', 'view', 'download'],
|
||||||
|
props: {
|
||||||
|
item: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
hidden: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
selected: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
typeIcons: Icons,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import "vars";
|
||||||
|
|
||||||
|
.media-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
cursor: initial !important;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-bottom: 1px solid transparent !important;
|
||||||
|
|
||||||
|
&.selected {
|
||||||
|
box-shadow: $border-shadow-bottom;
|
||||||
|
background: $selected-bg;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: none !important;
|
||||||
|
box-shadow: $border-shadow;
|
||||||
|
border-radius: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumbnail {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
.row {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 1.1em;
|
||||||
|
font-weight: bold;
|
||||||
|
padding: .5em 0;
|
||||||
|
flex: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.side {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
&.left {
|
||||||
|
max-height: 3em;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: " [...]";
|
||||||
|
margin-right: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.right {
|
||||||
|
align-items: flex-end;
|
||||||
|
margin-right: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.dropdown-container) {
|
||||||
|
.item {
|
||||||
|
flex-direction: row;
|
||||||
|
box-shadow: none;
|
||||||
|
cursor: pointer !important;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: $hover-bg !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
border: 0;
|
||||||
|
padding: 0;
|
||||||
|
background: none;
|
||||||
|
opacity: .7;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $default-hover-fg-2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
font-size: .9em;
|
||||||
|
color: $default-fg-2;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: .5em;
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
.channel {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.channel-name {
|
||||||
|
display: inline-flex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-image {
|
||||||
|
width: 2em;
|
||||||
|
height: 2em;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin-right: .5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,5 +1,17 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="image-container">
|
<div class="image-container" :class="{ 'with-image': !!item?.image }">
|
||||||
|
<div class="play-overlay" @click="$emit('play', item)">
|
||||||
|
<i class="fas fa-play" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span class="icon type-icon" v-if="typeIcons[item?.type]">
|
||||||
|
<a :href="item.url" target="_blank" v-if="item.url">
|
||||||
|
<i :class="typeIcons[item.type]" :title="item.type">
|
||||||
|
|
||||||
|
</i>
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
|
||||||
<img class="image" :src="item.image" :alt="item.title" v-if="item?.image" />
|
<img class="image" :src="item.image" :alt="item.title" v-if="item?.image" />
|
||||||
<div class="image" v-else>
|
<div class="image" v-else>
|
||||||
<div class="inner">
|
<div class="inner">
|
||||||
|
@ -7,7 +19,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span class="imdb-link" v-if="item?.imdb_id">
|
<span class="icon imdb-link" v-if="item?.imdb_id">
|
||||||
<a :href="`https://www.imdb.com/title/${item.imdb_id}`" target="_blank">
|
<a :href="`https://www.imdb.com/title/${item.imdb_id}`" target="_blank">
|
||||||
<i class="fab fa-imdb" />
|
<i class="fab fa-imdb" />
|
||||||
</a>
|
</a>
|
||||||
|
@ -19,37 +31,67 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import Icons from "./icons.json";
|
||||||
import MediaUtils from "@/components/Media/Utils";
|
import MediaUtils from "@/components/Media/Utils";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
mixins: [MediaUtils],
|
mixins: [Icons, MediaUtils],
|
||||||
|
emits: ['play'],
|
||||||
props: {
|
props: {
|
||||||
item: {
|
item: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => {},
|
default: () => {},
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
typeIcons: Icons,
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import "vars";
|
@import "vars";
|
||||||
|
|
||||||
.imdb-link {
|
.icon {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
width: 30px;
|
||||||
right: 0;
|
height: 30px;
|
||||||
height: 1em;
|
font-size: 30px;
|
||||||
font-size: 2em;
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
border-radius: 0.25em;
|
||||||
|
color: $default-media-img-fg;
|
||||||
|
|
||||||
&:hover {
|
a {
|
||||||
color: $default-hover-fg;
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
color: $default-media-img-fg;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $default-hover-fg;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
i {
|
i {
|
||||||
background: #ffff00;
|
margin: 2.5px;
|
||||||
border-radius: 0.25em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fa-imdb {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
margin: 1px 2.5px 3px 2.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fa-youtube {
|
||||||
|
margin-left: 1px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.imdb-link {
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.duration {
|
.duration {
|
||||||
|
@ -63,20 +105,43 @@ export default {
|
||||||
border-radius: 0.25em;
|
border-radius: 0.25em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.type-icon {
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
font-size: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
.image-container {
|
.image-container {
|
||||||
width: 100%;
|
max-width: 100%;
|
||||||
min-height: 240px;
|
min-height: 200px;
|
||||||
|
aspect-ratio: 16 / 9;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
img {
|
&.with-image {
|
||||||
@include from($tablet) {
|
background: black;
|
||||||
height: 480px;
|
|
||||||
|
.icon {
|
||||||
|
background: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.play-overlay {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.image {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
div.image {
|
div.image {
|
||||||
height: 400px;
|
width: 100%;
|
||||||
color: $default-media-img-fg;
|
color: $default-media-img-fg;
|
||||||
font-size: 5em;
|
font-size: 5em;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -84,14 +149,38 @@ div.image {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
||||||
.inner {
|
.inner {
|
||||||
width: calc(100% - 20px);
|
width: 100%;
|
||||||
height: calc(100% - 20px);
|
height: 100%;
|
||||||
min-height: 240px;
|
|
||||||
background: $default-media-img-bg;
|
background: $default-media-img-bg;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
border-radius: 1em;
|
border-radius: 0.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.play-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
border-radius: 2em;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s ease-in-out;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
i {
|
||||||
|
font-size: 5em;
|
||||||
|
color: $default-media-img-fg;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,42 +1,46 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="media-results">
|
<div class="media-results">
|
||||||
<div class="no-content" v-if="!results?.length">
|
<Loading v-if="loading" />
|
||||||
|
<NoItems v-else-if="!results?.length" :with-shadow="false">
|
||||||
No search results
|
No search results
|
||||||
|
</NoItems>
|
||||||
|
|
||||||
|
<div class="media-grid" v-else>
|
||||||
|
<Item v-for="(item, i) in results"
|
||||||
|
:key="i"
|
||||||
|
:item="item"
|
||||||
|
:selected="selectedResult === i"
|
||||||
|
:hidden="!sources[item.type]"
|
||||||
|
@select="$emit('select', i)"
|
||||||
|
@play="$emit('play', item)"
|
||||||
|
@view="$emit('view', item)"
|
||||||
|
@download="$emit('download', item)" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row item" :class="{selected: selectedResult === i, hidden: !sources[result.type]}"
|
<Modal ref="infoModal" title="Media info" @close="$emit('select', null)">
|
||||||
v-for="(result, i) in results" :key="i" @click="$emit('select', i)">
|
<Info :item="results[selectedResult]"
|
||||||
<div class="col-10 left side">
|
@play="$emit('play', results[selectedResult])"
|
||||||
<div class="icon">
|
v-if="selectedResult != null" />
|
||||||
<i :class="typeIcons[result.type]" />
|
</Modal>
|
||||||
</div>
|
|
||||||
<div class="title" v-text="result.title" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-2 right side">
|
|
||||||
<Dropdown title="Actions" icon-class="fa fa-ellipsis-h" @click="$emit('select', i)">
|
|
||||||
<DropdownItem icon-class="fa fa-play" text="Play" @click="$emit('play', result)"
|
|
||||||
v-if="result?.type !== 'torrent'" />
|
|
||||||
<DropdownItem icon-class="fa fa-download" text="Download" @click="$emit('download', result)"
|
|
||||||
v-if="result?.type === 'torrent'" />
|
|
||||||
<DropdownItem icon-class="fa fa-window-maximize" text="View in browser" @click="$emit('view', result)"
|
|
||||||
v-if="result?.type === 'file'" />
|
|
||||||
<DropdownItem icon-class="fa fa-info" text="Info" @click="$emit('info', result)" />
|
|
||||||
</Dropdown>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Dropdown from "@/components/elements/Dropdown";
|
import Info from "@/components/panels/Media/Info";
|
||||||
import DropdownItem from "@/components/elements/DropdownItem";
|
import Item from "./Item";
|
||||||
|
import Loading from "@/components/Loading";
|
||||||
|
import Modal from "@/components/Modal";
|
||||||
|
import NoItems from "@/components/elements/NoItems";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "Results",
|
components: {Info, Item, Loading, Modal, NoItems},
|
||||||
components: {Dropdown, DropdownItem},
|
emits: ['select', 'play', 'view', 'download'],
|
||||||
emits: ['select', 'info', 'play', 'view', 'download'],
|
|
||||||
props: {
|
props: {
|
||||||
|
loading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
|
||||||
results: {
|
results: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default: () => [],
|
default: () => [],
|
||||||
|
@ -52,22 +56,20 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
mounted() {
|
||||||
return {
|
this.$watch('selectedResult', (value) => {
|
||||||
typeIcons: {
|
if (value == null)
|
||||||
'file': 'fa fa-hdd',
|
this.$refs.infoModal?.close()
|
||||||
'torrent': 'fa fa-magnet',
|
else
|
||||||
'youtube': 'fab fa-youtube',
|
this.$refs.infoModal?.show()
|
||||||
'plex': 'fa fa-plex',
|
})
|
||||||
'jellyfin': 'fa fa-jellyfin',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import "src/style/items";
|
@import "src/style/items";
|
||||||
|
@import "vars";
|
||||||
|
|
||||||
.media-results {
|
.media-results {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -75,55 +77,37 @@ export default {
|
||||||
background: $background-color;
|
background: $background-color;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
|
||||||
.item {
|
.media-grid {
|
||||||
display: flex;
|
width: 100%;
|
||||||
align-items: center;
|
display: grid;
|
||||||
|
row-gap: 1em;
|
||||||
|
column-gap: 1.5em;
|
||||||
|
padding: 1em;
|
||||||
|
|
||||||
&.selected {
|
@include until($tablet) {
|
||||||
background: $selected-bg;
|
grid-template-columns: repeat(1, minmax(0, 1fr));
|
||||||
}
|
}
|
||||||
|
|
||||||
.side {
|
@media (min-width: 640px) and (max-width: $tablet) {
|
||||||
display: inline-flex;
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
align-items: center;
|
}
|
||||||
|
|
||||||
&.left {
|
@include between($tablet, $desktop) {
|
||||||
overflow: hidden;
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
text-overflow: " [...]";
|
}
|
||||||
}
|
|
||||||
|
|
||||||
&.right {
|
@include between($desktop, $widescreen) {
|
||||||
justify-content: flex-end;
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
margin-right: .5em;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.dropdown-container) {
|
@include from($widescreen) {
|
||||||
.item {
|
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
border: 0;
|
|
||||||
padding: 0;
|
|
||||||
background: none;
|
|
||||||
opacity: .7;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: $default-hover-fg-2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.no-content {
|
.info-container {
|
||||||
height: 100%;
|
width: 100%;
|
||||||
}
|
cursor: initial;
|
||||||
|
|
||||||
.icon {
|
|
||||||
.fa-youtube {
|
|
||||||
color: #d21;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -0,0 +1,72 @@
|
||||||
|
<template>
|
||||||
|
<form class="url-player" @submit.prevent="$emit('play', value)">
|
||||||
|
<div class="row">
|
||||||
|
<label>
|
||||||
|
Play URL (use the file:// prefix for local files)
|
||||||
|
<input type="text"
|
||||||
|
v-model="value"
|
||||||
|
ref="playUrlInput"
|
||||||
|
autofocus />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row footer">
|
||||||
|
<button type="submit" :disabled="!value?.length">
|
||||||
|
<i class="fa fa-play"></i> Play
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
emits: ['input', 'play'],
|
||||||
|
props: {
|
||||||
|
playUrl: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
value: this.playUrl,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.url-player {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
|
||||||
|
.body {
|
||||||
|
padding: 1em !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=text] {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
[type=submit] {
|
||||||
|
background: initial;
|
||||||
|
border-color: initial;
|
||||||
|
border-radius: 1.5em;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $default-hover-fg-2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: right;
|
||||||
|
padding: 0;
|
||||||
|
margin-top: 1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"file": "fa fa-hdd",
|
||||||
|
"torrent": "fa fa-magnet",
|
||||||
|
"youtube": "fab fa-youtube",
|
||||||
|
"plex": "fa fa-plex",
|
||||||
|
"jellyfin": "fa fa-jellyfin"
|
||||||
|
}
|
|
@ -3,3 +3,7 @@ $media-nav-width: 2.8em;
|
||||||
$filter-header-height: 3em;
|
$filter-header-height: 3em;
|
||||||
$default-media-img-bg: #d0dad8;
|
$default-media-img-bg: #d0dad8;
|
||||||
$default-media-img-fg: white;
|
$default-media-img-fg: white;
|
||||||
|
|
||||||
|
.fa-youtube {
|
||||||
|
color: #d21;
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue