<!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;
}
.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%;
}
< / style >
< / head >
< body >
< div class = "container" >
{{template "navbar" .}}
< br / >
< div id = "app" v-cloak >
< div class = "row" >
< div class = "columns six" > < / 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'}" >
< 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 >
< / 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 >
< / 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 >
< 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('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:{
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}"}`))
},
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()
},
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,
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 >