<!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" v-bind:src="podcast.Image" 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> <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:{ 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 nameA = a.Title.toUpperCase(); // ignore upper and lowercase var nameB = b.Title.toUpperCase(); // ignore upper and lowercase if (nameA < nameB) { return -1; } if (nameA > nameB) { return 1; } // names must be equal return 0; };break; case "name-desc":compareFunction=(a,b)=>{ var nameA = b.Title.toUpperCase(); // ignore upper and lowercase var nameB = a.Title.toUpperCase(); // ignore upper and lowercase 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>