parent
ab91ca49c9
commit
d4bb3d8665
@ -0,0 +1,684 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>{{.title}} - PodGrab</title>
|
||||
{{template "commoncss" .}}
|
||||
<link rel="stylesheet" href="/webassets/vue-multiselect.min.css">
|
||||
|
||||
<style>
|
||||
img {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5 {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
h4 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
p {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
hr {
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.podcastItem .button {
|
||||
padding: 0 15px;
|
||||
}
|
||||
|
||||
.IsPlayed-true {
|
||||
color: #555555;
|
||||
}
|
||||
|
||||
/* Larger than tablet */
|
||||
@media (min-width: 750px) {
|
||||
img {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
.button-enqueue{
|
||||
display: none;
|
||||
}
|
||||
body.playerExists .button-enqueue{
|
||||
display: inline-block;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
{{template "navbar" .}}
|
||||
|
||||
<br />{{$setting := .setting}}
|
||||
|
||||
<div id="app">
|
||||
|
||||
|
||||
<form @submit.prevent="submitFilters()">
|
||||
<div class="row">
|
||||
<input class="columns five" type="search" @input="searchQueryUpated()" v-model="filter.q" placeholder="Search">
|
||||
<div class="columns three"> <vue-multiselect v-model="selectedSorting" :options="sortOptions" :searchable="false"
|
||||
:multiple="false" :close-on-select="true" :clear-on-select="true" :allow-empty="false" :show-labels="false"
|
||||
placeholder="Sort By" label="Label" track-by="Value" :preselect-first="true">
|
||||
</vue-multiselect></div>
|
||||
<div class="columns one"> <vue-multiselect :show-pointer="false" v-model="filter.count" :options="pagingOptions" :searchable="false" :allow-empty="false" :close-on-select="true" :show-labels="false" placeholder="Pick a value"></vue-multiselect>
|
||||
</div>
|
||||
|
||||
<div class="columns three">
|
||||
<input type="submit" value="Filter" class="button">
|
||||
<button class="button" @click="resetFilters()">Reset</button>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
<div class="row">
|
||||
<vue-multiselect class="columns three" v-model="selectedPodcasts" :options="podcasts"
|
||||
:multiple="true" :close-on-select="false" :clear-on-select="false"
|
||||
:preserve-search="true" placeholder="Podcasts" label="Title" track-by="ID" :preselect-first="false">
|
||||
<template slot="selection" slot-scope="{ values, search, isOpen }">
|
||||
<span class="multiselect__single" v-if="values.length && !isOpen">${ values.length } options selected</span>
|
||||
</template>
|
||||
</vue-multiselect>
|
||||
<vue-multiselect class="columns three" v-model="selectedTags" :options="tags" :multiple="true" :close-on-select="false" :clear-on-select="false" :preserve-search="true" placeholder="Tags" label="Label" track-by="ID" :preselect-first="false">
|
||||
<template slot="selection" slot-scope="{ values, search, isOpen }"><span class="multiselect__single" v-if="values.length && !isOpen">${ values.length } options selected</span></template>
|
||||
</vue-multiselect>
|
||||
<div class="columns three"> <vue-multiselect v-model="selectedDownloadStatus" :options="downloadStatusOptions" :searchable="false"
|
||||
:multiple="false" :close-on-select="true" :clear-on-select="true" :allow-empty="false" :show-labels="false"
|
||||
placeholder="Sort By" label="Label" track-by="Value" :preselect-first="true">
|
||||
</vue-multiselect></div>
|
||||
<div class="columns three"> <vue-multiselect v-model="selectedPlayedStatus" :options="playedStatusOptions" :searchable="false"
|
||||
:multiple="false" :close-on-select="true" :clear-on-select="true" :allow-empty="false" :show-labels="false"
|
||||
placeholder="Sort By" label="Label" track-by="Value" :preselect-first="true">
|
||||
</vue-multiselect></div>
|
||||
</div>
|
||||
</form>
|
||||
<hr>
|
||||
|
||||
|
||||
<template v-for="item in podcastItems" >
|
||||
<div class="podcasts row podcastItem" >
|
||||
<div class="columns two">
|
||||
<img
|
||||
onerror="onImageError(this)"
|
||||
class="u-full-width"
|
||||
:src=" getEpisodeImage(item)"
|
||||
v-bind:alt="item.Title"
|
||||
lazy="lazy"
|
||||
/>
|
||||
</div>
|
||||
<div class="columns ten">
|
||||
<div class="row">
|
||||
<div class="columns eight">
|
||||
<h4>
|
||||
<i
|
||||
v-if="item.IsPlayed"
|
||||
title="Played"
|
||||
style="color: green"
|
||||
class="fas fa-check-circle"
|
||||
></i>
|
||||
${item.Title} <template v-if="item.Podcast && item.Podcast.Title"> // ${item.Podcast.Title}</template>
|
||||
</h4>
|
||||
</div>
|
||||
<div class="columns three">
|
||||
<small :title="item.PubDate">${getRelativeDate(item.PubDate)}</small
|
||||
>
|
||||
</div>
|
||||
<div class="columns one">
|
||||
<small> ${getFormattedDuration(item.Duration)}</small>
|
||||
</div>
|
||||
</div>
|
||||
<p class="useMore">${item.Summary }</p>
|
||||
|
||||
<a
|
||||
v-if="item.IsPlayed"
|
||||
class="button button"
|
||||
title="Mark as not listened"
|
||||
@click="changePlayedStatus(item)"
|
||||
><i class="fas fa-envelope"></i
|
||||
></a>
|
||||
|
||||
<a
|
||||
v-if="!item.IsPlayed"
|
||||
class="button button"
|
||||
title="Mark as listened"
|
||||
@click="changePlayedStatus(item)"
|
||||
><i class="fas fa-envelope-open"></i
|
||||
></a>
|
||||
<a
|
||||
v-if="!isBookmarked(item)"
|
||||
class="button button"
|
||||
title="Bookmark Episode"
|
||||
@click="changeBookmarkStatus(item)"
|
||||
><i class="far fa-bookmark"></i
|
||||
></a>
|
||||
|
||||
<a
|
||||
v-if="isBookmarked(item)"
|
||||
class="button button"
|
||||
title="Remove Bookmark"
|
||||
@click="changeBookmarkStatus(item)"
|
||||
><i class="fas fa-bookmark"></i
|
||||
></a>
|
||||
|
||||
<a
|
||||
v-if="item.DownloadPath"
|
||||
class="button"
|
||||
:href="removeStartingSlash(item.DownloadPath)"
|
||||
download
|
||||
title="Download episode file"
|
||||
><i class="fas fa-download"></i
|
||||
></a>
|
||||
<a
|
||||
v-if="item.DownloadPath"
|
||||
class="button button"
|
||||
@click="deleteFile(item)"
|
||||
title="Delete Podcast Episode File"
|
||||
><i class="fas fa-trash"></i
|
||||
></a>
|
||||
<a
|
||||
v-if="settings.AutoDownload && !item.DownloadPath"
|
||||
class="button button"
|
||||
@click="downloadToDisk(item)"
|
||||
title="Download to server"
|
||||
download
|
||||
><i class="fas fa-cloud-download-alt"></i
|
||||
></a>
|
||||
<a
|
||||
v-if="!settings.AutoDownload && !item.DownloadPath && item.DownloadStatus===3"
|
||||
class="button button"
|
||||
@click="downloadToDisk(item)"
|
||||
title="Download to server"
|
||||
download
|
||||
><i class="fas fa-cloud-download-alt"></i
|
||||
></a>
|
||||
<a
|
||||
class="button button"
|
||||
@click="openPlayer(item)"
|
||||
title="Play Episode"
|
||||
><i class="fas fa-play"></i
|
||||
></a>
|
||||
<a
|
||||
class="button button-enqueue"
|
||||
@click="enqueueEpisode(item)"
|
||||
title="Add Episode to existing player playlist"
|
||||
><i class="fas fa-plus"></i
|
||||
></a>
|
||||
</div> <div class="columns one"></div>
|
||||
</div>
|
||||
<hr />
|
||||
</template>
|
||||
<p v-if="podcastItems.length===0">Nothing to see here! Maybe try changing the filter criteria.</p>
|
||||
|
||||
|
||||
|
||||
<div class="row" v-if="podcastItems.length">
|
||||
<div class="columns twelve" style="text-align: center">
|
||||
|
||||
<a v-if="filter.previousPage"
|
||||
@click=" goToPage(1)"
|
||||
class="button"
|
||||
>First</a
|
||||
>
|
||||
|
||||
<a v-if="filter.previousPage"
|
||||
@click="goToPreviousPage()"
|
||||
class="button"
|
||||
>Previous</a
|
||||
>
|
||||
|
||||
|
||||
<select name="page" id="pageDdl" v-model="filter.page" >
|
||||
<option :key="index" v-for="index in filter.totalPages" >${index}</option>
|
||||
</select>
|
||||
|
||||
<a
|
||||
v-if="filter.nextPage"
|
||||
class="button"
|
||||
@click="goToNextPage()"
|
||||
>Next</a
|
||||
>
|
||||
|
||||
|
||||
<a
|
||||
v-if="filter.totalPages>filter.page"
|
||||
class="button"
|
||||
@click=" goToPage(filter.totalPages)"
|
||||
>Last</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{{template "scripts"}}
|
||||
<script src="/webassets/luxon.min.js"></script>
|
||||
<script src="/webassets/vue-multiselect.min.js"></script>
|
||||
|
||||
|
||||
<script>
|
||||
Vue.component('vue-multiselect', window.VueMultiselect.default)
|
||||
|
||||
var app = new Vue({
|
||||
delimiters: ["${", "}"],
|
||||
el: "#app",
|
||||
computed:{
|
||||
page(){
|
||||
return this.filter.page;
|
||||
}
|
||||
},
|
||||
watch:{
|
||||
page(newPage,oldPage){
|
||||
if(newPage==1){
|
||||
this.filter.previousPage=0;
|
||||
}else{
|
||||
this.filter.previousPage=newPage-1;
|
||||
}
|
||||
if(newPage==this.filter.totalPages){
|
||||
this.filter.nextPage=0;
|
||||
}else{
|
||||
this.filter.nextPage=newPage+1;
|
||||
}
|
||||
this.getData()
|
||||
|
||||
},
|
||||
selectedPodcasts(current,old){
|
||||
var arr=[];
|
||||
for (let index = 0; index < current.length; index++) {
|
||||
const element = current[index];
|
||||
arr.push(element.ID);
|
||||
}
|
||||
this.filter['podcastIds']=arr;
|
||||
this.getData()
|
||||
},
|
||||
selectedTags(current,old){
|
||||
var arr=[];
|
||||
for (let index = 0; index < current.length; index++) {
|
||||
const element = current[index];
|
||||
arr.push(element.ID);
|
||||
}
|
||||
this.filter['tagIds']=arr;
|
||||
this.getData()
|
||||
},
|
||||
selectedSorting(current,old){
|
||||
this.filter.sorting=current.Value;
|
||||
this.getData()
|
||||
},
|
||||
selectedDownloadStatus(current,old){
|
||||
this.filter.isDownloaded=current.Value;
|
||||
this.getData()
|
||||
},
|
||||
selectedPlayedStatus(current,old){
|
||||
this.filter.isPlayed=current.Value;
|
||||
this.getData()
|
||||
},
|
||||
},
|
||||
mounted(){
|
||||
if(localStorage && localStorage.episodesFilter){
|
||||
this.filter=JSON.parse(localStorage.episodesFilter);
|
||||
}
|
||||
|
||||
if(this.filter.podcastIds!=null){
|
||||
var selectedPodcasts=[];
|
||||
for(var i=0;i<this.podcasts.length;i++){
|
||||
for(var j=0;j<this.filter.podcastIds.length;j++){
|
||||
if(this.filter.podcastIds[j]===this.podcasts[i].ID){
|
||||
selectedPodcasts.push( this.podcasts[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
this.selectedPodcasts=selectedPodcasts
|
||||
}
|
||||
|
||||
if(this.filter.tagIds!=null){
|
||||
|
||||
var selectedTags=[];
|
||||
for(var i=0;i<this.tags.length;i++){
|
||||
for(var j=0;j<this.filter.tagIds.length;j++){
|
||||
if(this.filter.tagIds[j]===this.tags[i].ID){
|
||||
selectedTags.push( this.tags[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
this.selectedTags=selectedTags
|
||||
}
|
||||
for(var i=0;i<this.sortOptions.length;i++){
|
||||
if(this.sortOptions[i].Value===this.filter.sorting){
|
||||
this.selectedSorting=this.sortOptions[i]
|
||||
}
|
||||
}
|
||||
|
||||
for(var i=0;i<this.downloadStatusOptions.length;i++){
|
||||
if(this.downloadStatusOptions[i].Value===this.filter.isDownloaded.toString()){
|
||||
this.selectedDownloadStatus=this.downloadStatusOptions[i]
|
||||
}
|
||||
}
|
||||
|
||||
for(var i=0;i<this.playedStatusOptions.length;i++){
|
||||
if(this.playedStatusOptions[i].Value===this.filter.isPlayed.toString()){
|
||||
this.selectedPlayedStatus=this.playedStatusOptions[i]
|
||||
}
|
||||
}
|
||||
|
||||
this.getData()
|
||||
|
||||
|
||||
},
|
||||
methods:{
|
||||
getRelativeDate(dt){
|
||||
return luxon.DateTime.fromISO(dt).toRelative()
|
||||
},
|
||||
getFormattedDuration(duration){
|
||||
var obj=luxon.Duration.fromObject({seconds:duration}).shiftTo("hours","minutes","seconds").toObject();
|
||||
str="";
|
||||
if(obj.hours>0){
|
||||
str+=obj.hours+":"
|
||||
}
|
||||
str+=obj.minutes+":"+obj.seconds;
|
||||
return str;
|
||||
},
|
||||
getEpisodeImage(item){
|
||||
return "/podcastitems/"+item.ID+"/image"
|
||||
},
|
||||
changePlayedStatus(item){
|
||||
changePlayedStatus(item.ID,!item.IsPlayed,()=>item.IsPlayed=!item.IsPlayed)
|
||||
},
|
||||
goToPreviousPage(pageNumber){
|
||||
this.filter.page=this.filter.previousPage;
|
||||
},
|
||||
goToNextPage(pageNumber){
|
||||
this.filter.page=this.filter.nextPage;
|
||||
},
|
||||
goToPage(pageNumber){
|
||||
this.filter.page=pageNumber;
|
||||
},
|
||||
changeBookmarkStatus(item){
|
||||
isBookmarked= this.isBookmarked(item);
|
||||
|
||||
let self=this;
|
||||
changeBookmarkStatus(item.ID,!isBookmarked,function(){
|
||||
if(isBookmarked){
|
||||
item.BookmarkDate=self.nildate
|
||||
}else{
|
||||
item.BookmarkDate="new Date().toDateString()"
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
isBookmarked(item){
|
||||
return item.BookmarkDate!==this.nildate
|
||||
},
|
||||
deleteFile(item){
|
||||
deleteFile(item.ID)
|
||||
},
|
||||
downloadToDisk(item){
|
||||
downloadToDisk(item.ID)
|
||||
},
|
||||
openPlayer(item){
|
||||
openPlayer(item.ID)
|
||||
},
|
||||
enqueuePlayer(item){
|
||||
enqueuePlayer(item.ID)
|
||||
},
|
||||
searchQueryUpated(){
|
||||
var self=this;
|
||||
clearTimeout(this.debounce)
|
||||
this.debounce = setTimeout(() => {
|
||||
self.getData()
|
||||
}, 600)
|
||||
},
|
||||
saveFilter(data){
|
||||
if(localStorage){
|
||||
localStorage.episodesFilter=JSON.stringify(data);
|
||||
}
|
||||
},
|
||||
submitFilters(){
|
||||
this.filter.page=1;
|
||||
this.getData();
|
||||
|
||||
},
|
||||
updateUrl(){
|
||||
var url= new URL(window.top.location.href);
|
||||
var copy= Vue.util.extend({},this.filter);
|
||||
delete copy.totalPages
|
||||
delete copy.totalCount
|
||||
delete copy.nextPage
|
||||
delete copy.previousPage
|
||||
url.search=new URLSearchParams(copy);
|
||||
history.pushState(null,null,url.href)
|
||||
},
|
||||
resetFilters(){
|
||||
this.filter.q="";
|
||||
this.selectedPodcasts=[];
|
||||
this.selectedTags=[];
|
||||
this.selectedDownloadStatus=this.downloadStatusOptions[0];
|
||||
this.selectedPlayedStatus=this.playedStatusOptions[0];
|
||||
},
|
||||
removeStartingSlash(url){
|
||||
if(url[0]==='/'){
|
||||
return url
|
||||
}
|
||||
return "/"+url
|
||||
},
|
||||
getData(){
|
||||
var self=this;
|
||||
axios
|
||||
.get("/podcastitems",{params:this.filter})
|
||||
.then(function (response) {
|
||||
self.podcastItems= response.data.podcastItems;
|
||||
self.filter=response.data.filter;
|
||||
self.saveFilter(self.filter);
|
||||
self.updateUrl();
|
||||
})
|
||||
.catch(function (error) {
|
||||
if (error.response && error.response.data && error.response.data.message) {
|
||||
Vue.toasted.show(error.response.data.message, {
|
||||
theme: "bubble",
|
||||
type: "error",
|
||||
position: "top-right",
|
||||
duration: 5000,
|
||||
});
|
||||
}
|
||||
})
|
||||
.then(function () {});
|
||||
}
|
||||
},
|
||||
data: {
|
||||
socket:null,
|
||||
debouce:null,
|
||||
nildate:"0001-01-01T00:00:00Z",
|
||||
playerExists:false,
|
||||
isMobile:false,
|
||||
sortOrder:"dateAdded-asc",
|
||||
selectedSorting:"release_desc",
|
||||
selectedPodcasts:[],
|
||||
selectedTags:[],
|
||||
selectedDownloadStatus:"",
|
||||
selectedPlayedStatus:"",
|
||||
|
||||
{{ $len := len .podcastItems}}
|
||||
podcastItems: {{if gt $len 0}} {{ .podcastItems }} {{else}} [] {{end}},
|
||||
settings:{{.setting}},
|
||||
filter:{{.filter}},
|
||||
podcasts:{{.podcasts}},
|
||||
tags:{{.tags}},
|
||||
sortOptions:{{.sortOptions}},
|
||||
pagingOptions:[10,20,50,100],
|
||||
downloadStatusOptions:[{"Label":"All","Value":"nil"},{"Label":"Downloaded Only","Value":"true"},{"Label":"Not Downloaded","Value":"false"}],
|
||||
playedStatusOptions:[{"Label":"All","Value":"nil"},{"Label":"Played Only","Value":"true"},{"Label":"Unplayed only","Value":"false"}],
|
||||
}})
|
||||
</script>
|
||||
|
||||
<script>
|
||||
function debounce(func, wait, immediate) {
|
||||
var timeout;
|
||||
return function() {
|
||||
var context = this, args = arguments;
|
||||
var later = function() {
|
||||
timeout = null;
|
||||
if (!immediate) func.apply(context, args);
|
||||
};
|
||||
var callNow = immediate && !timeout;
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
if (callNow) func.apply(context, args);
|
||||
};
|
||||
};
|
||||
function downloadToDisk(id) {
|
||||
axios
|
||||
.get("/podcastitems/" + id + "/download")
|
||||
.then(function (response) {
|
||||
Vue.toasted.show("Podcast download enqueued.", {
|
||||
theme: "bubble",
|
||||
type: "info",
|
||||
position: "top-right",
|
||||
duration: 5000,
|
||||
});
|
||||
var row = document.getElementById("podcast-" + id);
|
||||
row.remove();
|
||||
})
|
||||
.catch(function (error) {
|
||||
if (error.response && error.response.data && error.response.data.message) {
|
||||
Vue.toasted.show(error.response.data.message, {
|
||||
theme: "bubble",
|
||||
type: "error",
|
||||
position: "top-right",
|
||||
duration: 5000,
|
||||
});
|
||||
}
|
||||
})
|
||||
.then(function () {});
|
||||
return false;
|
||||
}
|
||||
function deleteFile(id) {
|
||||
axios
|
||||
.get("/podcastitems/" + id + "/delete")
|
||||
.then(function (response) {
|
||||
Vue.toasted.show("Podcast file deleted.", {
|
||||
theme: "bubble",
|
||||
type: "success",
|
||||
position: "top-right",
|
||||
duration: 5000,
|
||||
});
|
||||
var row = document.getElementById("podcast-" + id);
|
||||
row.remove();
|
||||
})
|
||||
.catch(function (error) {
|
||||
if (error.response && error.response.data && error.response.data.message) {
|
||||
Vue.toasted.show(error.response.data.message, {
|
||||
theme: "bubble",
|
||||
type: "error",
|
||||
position: "top-right",
|
||||
duration: 5000,
|
||||
});
|
||||
}
|
||||
})
|
||||
.then(function () {});
|
||||
return false;
|
||||
}
|
||||
function changePlayedStatus(id, status,success) {
|
||||
var endpoint = status ? "markPlayed" : "markUnplayed";
|
||||
axios
|
||||
.get("/podcastitems/" + id + "/" + endpoint, {
|
||||
isPlayed: status,
|
||||
})
|
||||
.then(function (response) {
|
||||
Vue.toasted.show("Podcast played status updated.", {
|
||||
theme: "bubble",
|
||||
type: "info",
|
||||
position: "top-right",
|
||||
duration: 5000,
|
||||
});
|
||||
if(success){
|
||||
success();
|
||||
}
|
||||
})
|
||||
.catch(function (error) {
|
||||
if (error.response && error.response.data && error.response.data.message) {
|
||||
|
||||
Vue.toasted.show(error.response.data.message, {
|
||||
theme: "bubble",
|
||||
type: "error",
|
||||
position: "top-right",
|
||||
duration: 5000,
|
||||
});
|
||||
}
|
||||
})
|
||||
.then(function () {});
|
||||
return false;
|
||||
}
|
||||
function changeBookmarkStatus(id, status,success) {
|
||||
var endpoint = status ? "bookmark" : "unbookmark";
|
||||
axios
|
||||
.get("/podcastitems/" + id + "/" + endpoint, {
|
||||
isPlayed: status,
|
||||
})
|
||||
.then(function (response) {
|
||||
msg= status?"Bookmark Added": "Bookmark removed";
|
||||
Vue.toasted.show(msg, {
|
||||
theme: "bubble",
|
||||
type: "info",
|
||||
position: "top-right",
|
||||
duration: 5000,
|
||||
});
|
||||
if(success){
|
||||
success()
|
||||
}
|
||||
})
|
||||
.catch(function (error) {
|
||||
if (error.response && error.response.data && error.response.data.message) {
|
||||
Vue.toasted.show(error.response.data.message, {
|
||||
theme: "bubble",
|
||||
type: "error",
|
||||
position: "top-right",
|
||||
duration: 5000,
|
||||
});
|
||||
}
|
||||
})
|
||||
.then(function () {});
|
||||
return false;
|
||||
}
|
||||
|
||||
</script>
|
||||
<script>
|
||||
|
||||
const socket= getWebsocketConnection(function(event){
|
||||
const message= getWebsocketMessage("Register","Home")
|
||||
socket.send(message);
|
||||
},function(x){
|
||||
const msg= JSON.parse(x.data)
|
||||
if(msg.messageType=="NoPlayer"){
|
||||
document.body.classList.remove("playerExists")
|
||||
}
|
||||
if(msg.messageType=="PlayerExists"){
|
||||
document.body.classList.add("playerExists")
|
||||
}
|
||||
});
|
||||
function enqueueEpisode(id){
|
||||
if(!socket){
|
||||
return
|
||||
}
|
||||
socket.send(getWebsocketMessage("Enqueue",`{"itemId":"${id}"}`))
|
||||
}
|
||||
function enquePodcast(id){
|
||||
if(!socket){
|
||||
return
|
||||
}
|
||||
socket.send(getWebsocketMessage("Enqueue",`{"podcastId":"${id}"}`))
|
||||
}
|
||||
function playPodcast(id){
|
||||
openPlayer("",id)
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,58 @@
|
||||
package model
|
||||
|
||||
import "math"
|
||||
|
||||
type Pagination struct {
|
||||
Page int `uri:"page" query:"page" json:"page" form:"page" default:1`
|
||||
Count int `uri:"count" query:"count" json:"count" form:"count" default:20`
|
||||
NextPage int `uri:"nextPage" query:"nextPage" json:"nextPage" form:"nextPage"`
|
||||
PreviousPage int `uri:"previousPage" query:"previousPage" json:"previousPage" form:"previousPage"`
|
||||
TotalCount int `uri:"totalCount" query:"totalCount" json:"totalCount" form:"totalCount"`
|
||||
TotalPages int `uri:"totalPages" query:"totalPages" json:"totalPages" form:"totalPages"`
|
||||
}
|
||||
|
||||
type EpisodeSort string
|
||||
|
||||
const (
|
||||
RELEASE_ASC EpisodeSort = "release_asc"
|
||||
RELEASE_DESC EpisodeSort = "release_desc"
|
||||
DURATION_ASC EpisodeSort = "duration_asc"
|
||||
DURATION_DESC EpisodeSort = "duration_desc"
|
||||
)
|
||||
|
||||
type EpisodesFilter struct {
|
||||
Pagination
|
||||
IsDownloaded *string `uri:"isDownloaded" query:"isDownloaded" json:"isDownloaded" form:"isDownloaded"`
|
||||
IsPlayed *string `uri:"isPlayed" query:"isPlayed" json:"isPlayed" form:"isPlayed"`
|
||||
Sorting EpisodeSort `uri:"sorting" query:"sorting" json:"sorting" form:"sorting"`
|
||||
Q string `uri:"q" query:"q" json:"q" form:"q"`
|
||||
TagIds []string `uri:"tagIds" query:"tagIds[]" json:"tagIds" form:"tagIds[]"`
|
||||
PodcastIds []string `uri:"podcastIds" query:"podcastIds[]" json:"podcastIds" form:"podcastIds[]"`
|
||||
}
|
||||
|
||||
func (filter *EpisodesFilter) VerifyPaginationValues() {
|
||||
if filter.Count == 0 {
|
||||
filter.Count = 20
|
||||
}
|
||||
if filter.Page == 0 {
|
||||
filter.Page = 1
|
||||
}
|
||||
if filter.Sorting == "" {
|
||||
filter.Sorting = RELEASE_DESC
|
||||
}
|
||||
}
|
||||
|
||||
func (filter *EpisodesFilter) SetCounts(totalCount int64) {
|
||||
totalPages := int(math.Ceil(float64(totalCount) / float64(filter.Count)))
|
||||
nextPage, previousPage := 0, 0
|
||||
if filter.Page < totalPages {
|
||||
nextPage = filter.Page + 1
|
||||
}
|
||||
if filter.Page > 1 {
|
||||
previousPage = filter.Page - 1
|
||||
}
|
||||
filter.NextPage = nextPage
|
||||
filter.PreviousPage = previousPage
|
||||
filter.TotalCount = int(totalCount)
|
||||
filter.TotalPages = totalPages
|
||||
}
|
File diff suppressed because one or more lines are too long
@ -0,0 +1,402 @@
|
||||
fieldset[disabled] .multiselect {
|
||||
pointer-events: none;
|
||||
}
|
||||
.multiselect__spinner {
|
||||
position: absolute;
|
||||
right: 1px;
|
||||
top: 1px;
|
||||
width: 48px;
|
||||
height: 35px;
|
||||
background: #fff;
|
||||
display: block;
|
||||
}
|
||||
.multiselect__spinner:after,
|
||||
.multiselect__spinner:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
margin: -8px 0 0 -8px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 100%;
|
||||
border-color: #41b883 transparent transparent;
|
||||
border-style: solid;
|
||||
border-width: 2px;
|
||||
box-shadow: 0 0 0 1px transparent;
|
||||
}
|
||||
.multiselect__spinner:before {
|
||||
animation: a 2.4s cubic-bezier(0.41, 0.26, 0.2, 0.62);
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
.multiselect__spinner:after {
|
||||
animation: a 2.4s cubic-bezier(0.51, 0.09, 0.21, 0.8);
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
.multiselect__loading-enter-active,
|
||||
.multiselect__loading-leave-active {
|
||||
transition: opacity 0.4s ease-in-out;
|
||||
opacity: 1;
|
||||
}
|
||||
.multiselect__loading-enter,
|
||||
.multiselect__loading-leave-active {
|
||||
opacity: 0;
|
||||
}
|
||||
.multiselect,
|
||||
.multiselect__input,
|
||||
.multiselect__single {
|
||||
font-family: inherit;
|
||||
font-size: 16px;
|
||||
-ms-touch-action: manipulation;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
.multiselect {
|
||||
box-sizing: content-box;
|
||||
display: block;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
min-height: 40px;
|
||||
text-align: left;
|
||||
color: #35495e;
|
||||
}
|
||||
.multiselect * {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.multiselect:focus {
|
||||
outline: none;
|
||||
}
|
||||
.multiselect--disabled {
|
||||
opacity: 0.6;
|
||||
}
|
||||
.multiselect--active {
|
||||
z-index: 1;
|
||||
}
|
||||
.multiselect--active:not(.multiselect--above) .multiselect__current,
|
||||
.multiselect--active:not(.multiselect--above) .multiselect__input,
|
||||
.multiselect--active:not(.multiselect--above) .multiselect__tags {
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
.multiselect--active .multiselect__select {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
.multiselect--above.multiselect--active .multiselect__current,
|
||||
.multiselect--above.multiselect--active .multiselect__input,
|
||||
.multiselect--above.multiselect--active .multiselect__tags {
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
.multiselect__input,
|
||||
.multiselect__single {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
min-height: 20px;
|
||||
line-height: 20px;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
padding: 0 0 0 5px;
|
||||
width: 100%;
|
||||
transition: border 0.1s ease;
|
||||
box-sizing: border-box;
|
||||
margin-bottom: 8px;
|
||||
vertical-align: top;
|
||||
}
|
||||
.multiselect__input::-webkit-input-placeholder {
|
||||
color: #35495e;
|
||||
}
|
||||
.multiselect__input:-ms-input-placeholder {
|
||||
color: #35495e;
|
||||
}
|
||||
.multiselect__input::placeholder {
|
||||
color: #35495e;
|
||||
}
|
||||
.multiselect__tag ~ .multiselect__input,
|
||||
.multiselect__tag ~ .multiselect__single {
|
||||
width: auto;
|
||||
}
|
||||
.multiselect__input:hover,
|
||||
.multiselect__single:hover {
|
||||
border-color: #cfcfcf;
|
||||
}
|
||||
.multiselect__input:focus,
|
||||
.multiselect__single:focus {
|
||||
border-color: #a8a8a8;
|
||||
outline: none;
|
||||
}
|
||||
.multiselect__single {
|
||||
padding-left: 5px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.multiselect__tags-wrap {
|
||||
display: inline;
|
||||
}
|
||||
.multiselect__tags {
|
||||
min-height: 40px;
|
||||
display: block;
|
||||
padding: 8px 40px 0 8px;
|
||||
border-radius: 5px;
|
||||
border: 1px solid #e8e8e8;
|
||||
|
||||
font-size: 14px;
|
||||
}
|
||||
.multiselect__tag {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
padding: 4px 26px 4px 10px;
|
||||
border-radius: 5px;
|
||||
margin-right: 10px;
|
||||
color: #fff;
|
||||
line-height: 1;
|
||||
/* background: #41b883; */
|
||||
margin-bottom: 5px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
max-width: 100%;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.multiselect__tag-icon {
|
||||
cursor: pointer;
|
||||
margin-left: 7px;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
width: 22px;
|
||||
text-align: center;
|
||||
line-height: 22px;
|
||||
transition: all 0.2s ease;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.multiselect__tag-icon:after {
|
||||
content: "\D7";
|
||||
color: #266d4d;
|
||||
font-size: 14px;
|
||||
}
|
||||
.multiselect__tag-icon:focus,
|
||||
.multiselect__tag-icon:hover {
|
||||
background: #369a6e;
|
||||
}
|
||||
.multiselect__tag-icon:focus:after,
|
||||
.multiselect__tag-icon:hover:after {
|
||||
color: #fff;
|
||||
}
|
||||
.multiselect__current {
|
||||
min-height: 40px;
|
||||
overflow: hidden;
|
||||
padding: 8px 12px 0;
|
||||
padding-right: 30px;
|
||||
white-space: nowrap;
|
||||
border-radius: 5px;
|
||||
border: 1px solid #e8e8e8;
|
||||
}
|
||||
.multiselect__current,
|
||||
.multiselect__select {
|
||||
line-height: 16px;
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
margin: 0;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
.multiselect__select {
|
||||
position: absolute;
|
||||
width: 40px;
|
||||
height: 38px;
|
||||
right: 1px;
|
||||
top: 1px;
|
||||
padding: 4px 8px;
|
||||
text-align: center;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
.multiselect__select:before {
|
||||
position: relative;
|
||||
right: 0;
|
||||
top: 65%;
|
||||
color: #999;
|
||||
margin-top: 4px;
|
||||
border-style: solid;
|
||||
border-width: 5px 5px 0;
|
||||
border-color: #999 transparent transparent;
|
||||
content: "";
|
||||
}
|
||||
.multiselect__placeholder {
|
||||
color: #adadad;
|
||||
display: inline-block;
|
||||
margin-bottom: 10px;
|
||||
padding-top: 2px;
|
||||
}
|
||||
.multiselect--active .multiselect__placeholder {
|
||||
display: none;
|
||||
}
|
||||
.multiselect__content-wrapper {
|
||||
position: absolute;
|
||||
display: block;
|
||||
background: #fff;
|
||||
width: 100%;
|
||||
max-height: 240px;
|
||||
overflow: auto;
|
||||
border: 1px solid #e8e8e8;
|
||||
border-top: none;
|
||||
border-bottom-left-radius: 5px;
|
||||
border-bottom-right-radius: 5px;
|
||||
z-index: 1;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
.multiselect__content {
|
||||
list-style: none;
|
||||
display: inline-block;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
min-width: 100%;
|
||||
vertical-align: top;
|
||||
}
|
||||
.multiselect--above .multiselect__content-wrapper {
|
||||
bottom: 100%;
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
border-top-left-radius: 5px;
|
||||
border-top-right-radius: 5px;
|
||||
border-bottom: none;
|
||||
border-top: 1px solid #e8e8e8;
|
||||
}
|
||||
.multiselect__content::webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
.multiselect__element {
|
||||
display: block;
|
||||
}
|
||||
.multiselect__option {
|
||||
display: block;
|
||||
padding: 12px;
|
||||
min-height: 40px;
|
||||
line-height: 16px;
|
||||
text-decoration: none;
|
||||
text-transform: none;
|
||||
vertical-align: middle;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.multiselect__option:after {
|
||||
top: 0;
|
||||
right: 0;
|
||||
position: absolute;
|
||||
line-height: 40px;
|
||||
padding-right: 12px;
|
||||
padding-left: 20px;
|
||||
font-size: 13px;
|
||||
}
|
||||
.multiselect__option--highlight {
|
||||
/* background: #41b883; */
|
||||
outline: none;
|
||||
color: #fff;
|
||||
}
|
||||
.multiselect__option--highlight:after {
|
||||
content: attr(data-select);
|
||||
/* background: #41b883; */
|
||||
color: #fff;
|
||||
}
|
||||
.multiselect__option--selected {
|
||||
/* background: #f3f3f3;
|
||||
color: #35495e; */
|
||||
font-weight: 700;
|
||||
}
|
||||
.multiselect__option--selected:after {
|
||||
content: attr(data-selected);
|
||||
color: silver;
|
||||
}
|
||||
.multiselect__option--selected.multiselect__option--highlight {
|
||||
/* background: #cccccc; */
|
||||
/* color: #fff; */
|
||||
}
|
||||
.multiselect__option--selected.multiselect__option--highlight:after {
|
||||
/* background: #cccccc; */
|
||||
content: attr(data-deselect);
|
||||
/* color: #fff; */
|
||||
}
|
||||
.multiselect--disabled {
|
||||
background: #ededed;
|
||||
pointer-events: none;
|
||||
}
|
||||
.multiselect--disabled .multiselect__current,
|
||||
.multiselect--disabled .multiselect__select,
|
||||
.multiselect__option--disabled {
|
||||
background: #ededed;
|
||||
color: #a6a6a6;
|
||||
}
|
||||
.multiselect__option--disabled {
|
||||
cursor: text;
|
||||
pointer-events: none;
|
||||
}
|
||||
.multiselect__option--group {
|
||||
background: #ededed;
|
||||
color: #35495e;
|
||||
}
|
||||
.multiselect__option--group.multiselect__option--highlight {
|
||||
background: #35495e;
|
||||
color: #fff;
|
||||
}
|
||||
.multiselect__option--group.multiselect__option--highlight:after {
|
||||
background: #35495e;
|
||||
}
|
||||
.multiselect__option--disabled.multiselect__option--highlight {
|
||||
background: #dedede;
|
||||
}
|
||||
.multiselect__option--group-selected.multiselect__option--highlight {
|
||||
background: #ff6a6a;
|
||||
color: #fff;
|
||||
}
|
||||
.multiselect__option--group-selected.multiselect__option--highlight:after {
|
||||
background: #ff6a6a;
|
||||
content: attr(data-deselect);
|
||||
color: #fff;
|
||||
}
|
||||
.multiselect-enter-active,
|
||||
.multiselect-leave-active {
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
.multiselect-enter,
|
||||
.multiselect-leave-active {
|
||||
opacity: 0;
|
||||
}
|
||||
.multiselect__strong {
|
||||
margin-bottom: 8px;
|
||||
line-height: 20px;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
[dir="rtl"] .multiselect {
|
||||
text-align: right;
|
||||
}
|
||||
[dir="rtl"] .multiselect__select {
|
||||
right: auto;
|
||||
left: 1px;
|
||||
}
|
||||
[dir="rtl"] .multiselect__tags {
|
||||
padding: 8px 8px 0 40px;
|
||||
}
|
||||
[dir="rtl"] .multiselect__content {
|
||||
text-align: right;
|
||||
}
|
||||
[dir="rtl"] .multiselect__option:after {
|
||||
right: auto;
|
||||
left: 0;
|
||||
}
|
||||
[dir="rtl"] .multiselect__clear {
|
||||
right: auto;
|
||||
left: 12px;
|
||||
}
|
||||
[dir="rtl"] .multiselect__spinner {
|
||||
right: auto;
|
||||
left: 1px;
|
||||
}
|
||||
@keyframes a {
|
||||
0% {
|
||||
transform: rotate(0);
|
||||
}
|
||||
to {
|
||||
transform: rotate(2turn);
|
||||
}
|
||||
}
|
File diff suppressed because one or more lines are too long
Loading…
Reference in new issue