podgrab/client/index.html

821 lines
26 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>PodGrab</title>
{{template "commoncss" .}}
<style>
.button-delete {
color: #d11a2a;
}
.podcasts .button {
padding: 0 15px;
}
img {
display: none;
}
/* Larger than tablet */
@media (min-width: 750px) {
img {
display: block;
}
}
[v-cloak] {
display: none;
}
.grid .imageContainer{
max-height: 160px;
margin-bottom: 10px;
}
.grid .contentContainer, .grid .titleContainer{
display: none;
}
.grid .titleContainer{
height: 70px;
}
.grid .titleContainer h5{
font-size: 2rem;
}
.grid .podcasts{
box-shadow: 0 0 0 0px rgb(0 0 0 / 20%);
}
.grid .podcasts:hover{
box-shadow: #ccc 10px 5px 16px 1px;
transition:all 0.25s;
transition-timing-function: ease-in-out;
}
.grid .tags{
font-size: 0.85em;
padding-bottom: 10px;
display: inline-block;
}
.mobile.tags{
margin-bottom: 10px;
}
.alignRight{
text-align: right;
}
.alignLeft{
text-align: left;
}
.tag-editor{
position: absolute;
border:1px solid;
padding: 10px;
width: 300px;
z-index: 1000;
}
.tag-editor a.pill{
margin-right: 5px;
background: blue;
border-radius: 5px;
padding: 2px 5px;
text-decoration: none;
display: inline-block;
margin-bottom: 5px;
}
a.pill i{
background-color: inherit;
margin-right: 2px;
}
.tag-editor .available a.pill{
background: green;
}
.tag-editor .existing a.pill{
background: palevioletred;
}
.tag-editor>div{
margin-bottom: 15px;;
}
.list hr{
margin-top: 0.5rem;
margin-bottom: 1.5rem;
}
.list h3{
margin-bottom: 0rem;
}
.list p{
margin-bottom: 1rem;
}
.list .u-full-width{
width: 80%;
}
.paused{
opacity: 50%;
}
</style>
</head>
<body>
<div class="container">
{{template "navbar" .}}
<br />
<div id="app" v-cloak>
<div class="row">
<div class="columns six">&nbsp;</div>
<div class="columns six" :class="isMobile?'alignLeft':'alignRight'">
<select v-model="sortOrder" name="" id="">
<option v-for="option in sortOptions" v-bind:value="option.key">
${option.label}
</option>
</select>
<select v-model="filterTag" v-if="allTags.length">
<option value="">All</option>
<option v-for="option in allTags" v-bind:value="option.ID">
${option.Label} (${option.Podcasts.length})
</option>
</select>
<select v-if="!isMobile" v-model="layout" name="" id="">
<option v-for="option in layoutOptions" v-bind:value="option">
${option.capitalize()}
</option>
</select>
</div>
</div>
<div class="row" :class="layout">
<template v-for="podcastGroup in podcastGroups" >
<div class="row">
<template v-for="podcast in podcastGroup" >
<div :key="podcast.ID" class="podcasts" :title="podcast.Title" v-bind:class="{row:layout=='list', two:layout=='grid', columns:layout=='grid'}" v-bind:id="'podcast-'+podcast.ID" >
<div class="columns imageContainer" v-bind:class="{two:layout=='list', twelve:layout=='grid'}">
<a style="text-decoration: none" :href="'/podcasts/'+podcast.ID+'/view'">
<img
onerror="onImageError(this)"
class="u-full-width"
:src="getPodcastImage(podcast)"
v-bind:alt="podcast.Title"
/>
</a>
</div>
<div class="columns" v-bind:class="{ten:layout=='list', twelve:layout=='grid',paused:podcast.IsPaused}">
<div class="titleContainer">
<a style="text-decoration: none" :href="'/podcasts/'+podcast.ID+'/view'">
<h3 v-if="layout=='list'">${podcast.Title}</h3>
<h5 v-if="layout=='grid'">${podcast.Title} </h5>
</a>
</div>
<div class="contentContainer">
<p class="useMore">${podcast.Summary}</p></div>
<div class="row">
<div class="columns" v-bind:class="{two:layout=='list', twelve:layout=='grid'}">
<span v-if="podcast.LastEpisode" :title="'Last Episode aired on '+getFormattedLastEpisodeDate(podcast)">${getFormattedLastEpisodeDate(podcast)}</span>
</div>
<div
class="columns" v-bind:class="{two:layout=='list', twelve:layout=='grid'}"
:title="getEpisodeCountTooltip(podcast)"
>
<template v-if="podcast.DownloadingEpisodesCount">
(${podcast.DownloadingEpisodesCount})/</template>${podcast.DownloadedEpisodesCount}/${podcast.AllEpisodesCount}
episodes
</div>
<div class="columns" v-bind:class="{four:layout=='list', twelve:layout=='grid'}">
<tagger :class="isMobile?'mobile':'desktop'" v-bind:podcast="podcast" v-on:getalltags="getAllTags()"></tagger>
</div>
<div class="columns" v-bind:class="{four:layout=='list', twelve:layout=='grid'}">
<button
class="button button-delete deletePodcast"
:data-id="podcast.ID"
title="Delete Podcast and episode files"
>
<i class="fas fa-trash"></i>
</button>
<div :id="'deleteDdl-'+podcast.ID" style="display: none">
<ul>
<li style="list-style: none;"> <button class="button" :data-id="podcast.ID" onclick="deletePodcast(this)" >Delete Files and Podcast</button></li>
<li style="list-style: none;"> <button class="button" :data-id="podcast.ID" onclick="deletePodcastEpisodes(this)">Delete Files, Keep Podcast</button></li>
<li style="list-style: none;"> <button class="button" :data-id="podcast.ID" onclick="deleteOnlyPodcast(this)">Keep Files, Delete Podcast</button></li>
</ul>
</div>
<!-- <button
class="button button-delete"
title="Delete only episode files"
@click="deletePodcastEpisodes(podcast.ID)"
>
<i class="fas fa-folder-minus"></i>
</button> -->
<button
class="button"
title="Download all episode files"
@click="downloadAllEpisodes(podcast.ID)"
>
<i class="fas fa-download"></i>
</button>
<button
class="button"
title="Play all episodes"
@click="playPodcast(podcast.ID)"
>
<i class="fas fa-play"></i>
</button>
<button
class="button"
title="Add all episodes to existing player playlist"
v-if="playerExists"
@click="enquePodcast(podcast.ID)"
>
<i class="fas fa-plus"></i>
</button>
<button
class="button"
title="New episodes for this podcast are not being downloaded automatially. Click to resume download."
v-if="podcast.IsPaused"
@click="togglePause(podcast,false)"
>
<i style="color: greenyellow;" class="fas fa-dot-circle"></i>
</button>
<button
class="button"
title="Click to pause automatic downloading for this podcast."
v-if="!podcast.IsPaused"
@click="togglePause(podcast,true)"
>
<i style="color: red;" class="fas fa-dot-circle"></i>
</button>
<button
class="button"
title="Click to see details."
@click="showDetails(podcast)"
>
<i class="fas fa-info"></i>
</button>
</div>
</div>
</div>
</div>
<hr v-if="layout=='list'">
</template>
</div>
<br>
</template>
</div>
<template v-if="allPodcasts.length && !podcasts.length">
<div class="welcome">
<h5>No results!</h5>
<p>There doesn't seem to be any podcast for this filter criteria.</p></div>
</template>
</template>
<template v-if="!allPodcasts.length">
<div class="welcome">
<h5>Welcome</h5>
<p>It seems you have just setup Podgrab for the first time.</p>
<p>
Before you start adding and downloading podcasts I recommend that you
give a quick look to the <a href="/settings"><strong>Settings</strong> here</a> so that you can customize the
downloading behavior of the software as per your needs.
</p>
<p>
<a href="/add">Click here</a> to add
a new podcast to start downloading.
</p>
<p>
Please feel free to report any issues or request any features on our github page <a target="_blank" href="https://github.com/akhilrex/podgrab">here</a>
</p>
</div>
</template>
<Modal v-model="showDetail">
<table class="table" v-if="detailPodcast">
<tr>
<td>Title</td>
<td>${detailPodcast.Title}</td>
</tr>
<tr>
<td>Author</td>
<td>${ detailPodcast.Author }</td>
</tr>
<tr>
<td>Original Url</td>
<td> <a target="_blank" :href="detailPodcast.URL">Link</a></td>
</tr>
<tr>
<td>Summary</td>
<td style="word-wrap: break-word;">${ detailPodcast.Summary }</td>
</tr>
<tr>
<td>Added On</td>
<td>${ getFormattedDate(detailPodcast.CreatedAt)}</td>
</tr>
<tr>
<td>Last Episode</td>
<td>${ getFormattedLastEpisodeDate(detailPodcast)}</td>
</tr>
<tr>
<td>Paused</td>
<td> ${ detailPodcast.IsPaused?'Yes':'No' }</td>
</tr>
<tr>
<td>Podgrab Feed</td>
<td> <a target="_blank" :href="'/podcasts/'+detailPodcast.ID+'/rss'">Link</a></td>
</tr>
</table>
</Modal>
</div>
</div>
{{template "scripts"}}
<script src="/webassets/popper.min.js"></script>
<script src="/webassets/tippy-bundle.umd.min.js"></script>
<script src="/webassets/stopword.js"></script>
<script src="/webassets/modal/vue-modal.umd.min.js"></script>
<link rel="stylesheet" href="/webassets/modal/vue-modal.css">
<template id="editTags">
<div class="tags">
<div @click="editTags" style="cursor: pointer;">
<i class="fas fa-tags"></i>
${ commaSeparatedTags() }
</div>
<div v-if="editing" class="tag-editor">
<h5>Tags: ${podcast.Title}</h5>
<div class="available">
Add: <a style="cursor: pointer;" href="#" @click.prevent="addTag(tag.ID, $event);return false;" v-for="tag in availableTags" class="pill"><i class="fa fa-plus-circle"></i>${tag.Label}</a>
</div>
<div class="existing" v-if="tags.length">
Remove: <a style="cursor: pointer;" @click.prevent="removeTag(tag.ID, $event);return false;" href="#" v-for="tag in tags" class="pill"><i class="fa fa-minus-circle"></i>${tag.Label}</a>
</div>
<div class="create">
<form @submit.prevent="createNewTag" method="post">
<input type="text" name="newTag" id="" placeholder="Create New Tag" v-model="newTag">
<input type="submit" value="Add">
</form>
</div>
<button class="button" @click="editing=false">Close</button>
</div>
</div>
</template>
<script>
Vue.component('Modal', VueModal)
Vue.component('tagger',{
delimiters: ["${", "}"],
data:function(){
return {
newTag:'',
allTags:[],
tags:[],
availableTags:[],
editing:false,
}
},
template: '#editTags',
props:['podcast'],
computed:{
},
methods:{
createNewTag(){
var self=this;
if(!self.newTag){
return;
}
axios
.post("/tags",{label:self.newTag})
.then(function (response) {
self.tags.push(response.data);
self.addTag(response.data.ID);
self.getAllTags();
}).catch(showError);
},
setAvailableTags(){
existingTags= this.tags.map(x=>x.ID);
this.availableTags= this.allTags.filter(x=>existingTags.indexOf(x.ID)===-1);
this.$emit('getalltags')
}
,
commaSeparatedTags(){
if(!this.tags.length){
return "";
}
toReturn= this.tags.map(function(x){return x.Label}).join(", ");
return toReturn;
},
editTags(){
this.editing=!this.editing;
if(this.editing){
this.getAllTags();
}
},
getAllTags(){
var self=this;
axios
.get("/tags")
.then(function (response) {
self.allTags=response.data;
self.setAvailableTags();
})
},
addTag(tagId,e){
var self=this;
axios
.post("/podcasts/"+this.podcast.ID+"/tags/"+tagId)
.then(function (response) {
var i=-1;
for(i=0;i<self.allTags.length;i++){
if(self.allTags[i].ID===tagId){
self.tags.push(self.allTags[i]);
break;
}
}
self.setAvailableTags();
}).catch(showError);
return false;
},
removeTag(tagId,e){
var self=this;
axios
.delete("/podcasts/"+this.podcast.ID+"/tags/"+tagId)
.then(function (response) {
var i=-1;
for(i=0;i<self.tags.length;i++){
if(self.tags[i].ID===tagId){
break;
}
}
self.tags.splice(i,1)
self.setAvailableTags();
});
return false;
},
},
mounted(){
this.tags=this.podcast.Tags;
}
});
</script>
<script>
var app = new Vue({
delimiters: ["${", "}"],
el: "#app",
computed:{
podcastGroups(){
var i,j,temparray,chunk = 6;
var toReturn=[];
for (i=0,j=this.podcasts.length; i<j; i+=chunk) {
toReturn.push(this.podcasts.slice(i,i+chunk));
// do whatever
}
return toReturn
}
},
created(){
this.podcasts=this.allPodcasts;
const self=this;
this.socket= getWebsocketConnection(function(event){
const message= getWebsocketMessage("Register","Home")
self.socket.send(message);
},function(x){
const msg= JSON.parse(x.data)
if(msg.messageType=="NoPlayer"){
self.playerExists=false;
}
if(msg.messageType=="PlayerExists"){
self.playerExists=true;
}
});
},
methods:{
showDetails(podcast){
this.detailPodcast=podcast;
this.showDetail=true;
},
getPodcastImage(item){
return "/podcasts/"+item.ID+"/image"
},
removePodcast(id) {
const index= this.podcasts.findIndex(x=>x.ID===id);
this.podcasts.splice(index,1);
},
enquePodcast(id){
if(!this.playerExists){
return
}
this.socket.send(getWebsocketMessage("Enqueue",`{"podcastId":"${id}"}`))
},
togglePause(item,isPaused){
var self=this;
var url= isPaused?`/podcasts/${item.ID}/pause`:`/podcasts/${item.ID}/unpause`;
axios
.get(url)
.then(function (response) {
item.IsPaused=isPaused;
})
.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,
});
}
})
},
filterPodcasts(){
if(this.filterTag===""){
this.podcasts=this.allPodcasts;
}else{
var filtered=[];
for (var podast of this.allPodcasts) {
for(var tag of podast.Tags){
if(tag.ID===this.filterTag){
filtered.push(podast);
break;
}
}
}
this.podcasts=filtered;
}
this.sortPodcasts();
},
sortPodcasts(order){
var compareFunction;
switch(order){
case "dateAdded-asc":compareFunction=(a,b)=>Date.parse(a.CreatedAt)-Date.parse(b.CreatedAt);break;
case "dateAdded-desc":compareFunction=(a,b)=>Date.parse(b.CreatedAt)-Date.parse(a.CreatedAt);break;
case "lastEpisode-asc":compareFunction=(a,b)=>Date.parse(a.LastEpisode)-Date.parse(b.LastEpisode);break;
case "lastEpisode-desc":compareFunction=(a,b)=>Date.parse(b.LastEpisode)-Date.parse(a.LastEpisode);break;
case "episodesCount-asc":compareFunction=(a,b)=>a.AllEpisodesCount-b.AllEpisodesCount;break;
case "episodesCount-desc":compareFunction=(a,b)=>b.AllEpisodesCount-a.AllEpisodesCount;break;
case "name-asc":compareFunction=(a,b)=>{
var nameAArr = sw.removeStopwords(a.Title.split(' ')) ;
var nameBArr = sw.removeStopwords(b.Title.split(' '));
nameA= nameAArr.length?nameAArr[0].toUpperCase():"";
nameB= nameBArr.length?nameBArr[0].toUpperCase():"";
if (nameA < nameB) {
return -1;
}
if (nameA > nameB) {
return 1;
}
// names must be equal
return 0;
};break;
case "name-desc":compareFunction=(a,b)=>{
var nameAArr = sw.removeStopwords(b.Title.split(' ')) ;
var nameBArr = sw.removeStopwords(a.Title.split(' '));
nameA= nameAArr.length?nameAArr[0].toUpperCase():"";
nameB= nameBArr.length?nameBArr[0].toUpperCase():"";
if (nameA < nameB) {
return -1;
}
if (nameA > nameB) {
return 1;
}
// names must be equal
return 0;
};break;
}
this.podcasts.sort(compareFunction)
},
getReadableSize(bytes){
if(bytes<1024){
return `${bytes} bytes`;
}
bytes=bytes/1024
if(bytes<1024){
return `${bytes.toFixed(2)} KB`;
}
bytes=bytes/1024
if(bytes<1024){
return `${bytes.toFixed(2)} MB`;
}
bytes=bytes/1024
if(bytes<1024){
return `${bytes.toFixed(2)} GB`;
}
bytes=bytes/1024
return `${bytes.toFixed(2)} TB`;
},
getEpisodeCountTooltip(podcast){
var title=`${podcast.DownloadedEpisodesCount} episodes (${this.getReadableSize(podcast.DownloadedEpisodesSize)}) downloaded out of total ${podcast.AllEpisodesCount} episodes (${this.getReadableSize(podcast.AllEpisodesSize)})`
if(podcast.DownloadingEpisodesCount){
title+= '\n'+podcast.DownloadingEpisodesCount+' episodes ('+ this.getReadableSize(podcast.DownloadingEpisodesSize)+') in the queue.'
}
return title
},
getFormattedLastEpisodeDate(podcast){
const options={month:"short", day:"numeric", year:"numeric"}
//todo: this is a really dirty hack which needs to be fixed when we work on the episode page
var dt=new Date(Date.parse(podcast.LastEpisode.substr(0,10)));
return dt.toDateString()
},
getFormattedDate(date){
const options={month:"short", day:"numeric", year:"numeric"}
//todo: this is a really dirty hack which needs to be fixed when we work on the episode page
var dt=new Date(Date.parse(date.substr(0,10)));
return dt.toDateString()
},
downloadAllEpisodes(id) { downloadAllEpisodes(id);},
deletePodcast(id){ deletePodcast(id,()=>{
const index= this.podcasts.findIndex(x=>x.ID===id);
this.podcasts.splice(index,1);
});},
deletePodcastEpisodes(id){ deletePodcastEpisodes(id)},
playPodcast(id){openPlayer("",id)},
getAllTags(){
var self=this;
axios
.get("/tags")
.then(function (response) {
self.allTags=response.data;
})
},
},
mounted(){
if(localStorage && localStorage.sortOrder){
this.sortOrder=localStorage.sortOrder;
this.sortPodcasts(this.sortOrder);
}
if(localStorage && localStorage.layout){
this.layout=localStorage.layout;
}else{
this.layout='list';
}
if(localStorage && localStorage.filterTag){
this.filterTag=localStorage.filterTag;
}else{
this.filterTag='';
}
if (screen.width <= 760) {
this.isMobile= true
} else {
this.isMobile= false
}
if (screen.width <= 760) {
this.layout='list'
}
this.getAllTags();
this.$nextTick(function () {
checkUseMore();
tippy(".deletePodcast",{
allowHTML: true,
content(reference) {
const id = reference.getAttribute('data-id');
const template = document.getElementById('deleteDdl-'+id);
return template.innerHTML;
},
trigger:'click',
interactive: true
})
})
},
watch:{
sortOrder(newOrder,oldOrder){
if(newOrder===oldOrder){
return;
}
if(localStorage){
localStorage.sortOrder=newOrder
}
this.sortPodcasts(newOrder);
},
filterTag(newTag,oldTag){
if(newTag===oldTag){
return;
}
if(localStorage){
localStorage.filterTag=newTag
}
this.filterPodcasts();
},
layout(newLayout,oldLayout){
if(newLayout===oldLayout){
return;
}
if(localStorage){
localStorage.layout=newLayout
}
},
},
data: {
socket:null,
detailPodcast:null,
showDetail:false,
playerExists:false,
isMobile:false,
layoutOptions:["list","grid"],
layout:"grid",
sortOrder:"dateAdded-asc",
allTags:[],
filterTag:'',
sortOptions:[
{
key:"name-asc",
label:"Name (A-Z)"
},
{
key:"name-desc",
label:"Name (Z-A)"
},
{
key:"lastEpisode-asc",
label:"Latest Episode (Old First)"
},
{
key:"lastEpisode-desc",
label:"Latest Episode (New First)"
},
{
key:"dateAdded-asc",
label:"Date Added (Old First)"
},
{
key:"dateAdded-desc",
label:"Date Added (New First)"
},
{
key:"episodesCount-asc",
label:"Episodes Count (Asc)"
},
{
key:"episodesCount-desc",
label:"Episodes Count (Desc)"
},
],
podcasts:[],
{{ $len := len .podcasts}}
allPodcasts: {{if gt $len 0}} {{ .podcasts }} {{else}} [] {{end}},
}})
</script>
</body>
</html>