├── README.md ├── style ├── themes.css ├── icons.css ├── playlist.css ├── player.css └── main.css ├── script ├── header.tis ├── playlists.tis ├── virtual-list.tis ├── download.tis ├── requests.tis ├── tray.tis ├── requests-vlist.tis ├── script.tis ├── player.tis └── data.tis ├── Recast.svg ├── main.htm └── tray.htm /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Recast 3 | Desktop podcast application 4 | 5 | > Version : 1.1 6 | > [Release / Download](https://github.com/MustafaHi/Recast/releases) 7 | > [Change Log & Features](https://github.com/MustafaHi/Recast/wiki/Change-log-&-Features) 8 | > [Support / Help](https://github.com/MustafaHi/Recast/wiki/Support) 9 | > [Dev Guide](https://github.com/MustafaHi/Recast/wiki/Dev-Guide) 10 | > More information in the [wiki](https://github.com/MustafaHi/Recast/wiki) 11 | 12 | Recast is a rich, light and cross-platform podcast client for desktop (Windows/Mac/Linux) 13 | [Download Now](https://github.com/MustafaHi/Recast/releases/latest) 14 | 15 |  16 |  17 |  18 |  19 | 20 | ## Features 21 | 22 | - Discover (using custom query) 23 | - Search (iTunes playlist) 24 | - Playlists 25 | - Download 26 | - Subscription (+custom RSS feed) 27 | - Tray player 28 | - Lite Mode 29 | - Proxy 30 | - Themes 31 | - light < 10MB 32 | 33 | ## Credit 34 | 35 | Recast @MIT licensed, by [@MustafaHi](https://github.com/MustafaHi) 36 | [Sciter](https://sciter.com) (framework) 37 | [iTunes](https://itunes.com) (playlist) 38 | -------------------------------------------------------------------------------- /style/themes.css: -------------------------------------------------------------------------------- 1 | /* 2 | Namming Convention 3 | `BG` : Background; 4 | `K` : Color; 5 | `F` : Fill; 6 | `H` : Hover; 7 | 8 | `W` : Width; 9 | `H` : Height; 10 | */ 11 | 12 | 13 | html[theme=Dark] { 14 | var(BG): #222; 15 | var(K) : #fff; 16 | var(aK): #eee; 17 | 18 | var(clearBG): #111; 19 | var(border) : #444; 20 | background: color(BG); 21 | var(bodyBG): rgba(10, 10, 10, 0.5); 22 | var(headerButtonHBG): rgba(255,255,255, 0.1); 23 | var(headerStroke) : #fff; 24 | var(headerStrokeW) : 2dip; 25 | 26 | var(buttonK) : #fff; 27 | var(buttonHK) : #000; 28 | var(buttonBG) : #222; 29 | var(buttonHBG): #fff; 30 | var(currentBG): #111; 31 | 32 | var(itemBG): #333; 33 | 34 | var(playerBG): #111; 35 | var(playerHBG): #222; 36 | var(playerK) : #fff; 37 | color: #fff; 38 | } 39 | html[theme=Dark] #iSearch { text-selection-caret-color: #fff; } 40 | 41 | body[vertical] { 42 | flow: vertical; 43 | } 44 | 45 | body[vertical] navigator { 46 | flow: horizontal; 47 | size: unset; 48 | /* height: 40dip; */ 49 | height: auto; 50 | overflow: hidden; 51 | /* background: unset; */ 52 | /* box-sizing: border-box; */ 53 | /* clip-box: border-box; */ 54 | } 55 | 56 | body[vertical] #iSearch { 57 | margin-left: *; 58 | border:none; 59 | /* border-left: 1px solid; */ 60 | /* text-selection-caret-color: #fff; */ 61 | } 62 | -------------------------------------------------------------------------------- /style/icons.css: -------------------------------------------------------------------------------- 1 | icon { 2 | display: inline-block; 3 | size: 15dip; 4 | background-size: contain; 5 | background-repeat: no-repeat; 6 | background-position: center; 7 | stroke: inherit; 8 | fill: inherit; 9 | } 10 | 11 | 12 | icon.play { margin-right: 5dip;background-image: url(path:M838 1871 l-33 -29 -3 -635 -2 -635 25 -30 c30 -36 70 -47 111 -32 32 12 613 586 646 638 42 68 37 76 -285 401 -161 161 -310 306 -331 322 -50 37 -87 37 -128 0z m339 -848 l-177 -178 0 355 0 355 177 -178 178 -177 -178 -177z); } 13 | icon.left { background-image: url(path:M1434 1871 c-21 -16 -170 -161 -331 -322 -322 -325 -327 -332 -285 -401 12 -18 157 -169 324 -334 321 -320 336 -331 394 -302 46 22 68 60 59 105 -6 33 -42 74 -278 311 l-271 272 266 268 c146 147 271 278 278 291 18 35 4 86 -31 115 -40 34 -77 33 -125 -3z); } 14 | icon.right { background-image: url(path:M841 1874 c-35 -29 -49 -80 -31 -115 7 -13 132 -144 278 -291 l266 -268 -271 -272 c-236 -237 -272 -278 -278 -311 -9 -45 13 -83 59 -105 58 -29 73 -18 394 302 167 165 312 316 324 334 42 69 37 76 -285 401 -161 161 -310 306 -331 322 -48 36 -85 37 -125 3z); } 15 | icon.x { size: 10dip; background-image: url(path:M0 0 L10 10 M10 0 L0 10); } 16 | 17 | [started] icon.play { stroke-width:1dip; transform: rotate(90deg); background-image: url(path:M340 1673 c-19 -18 -33 -41 -36 -64 -5 -31 -1 -42 24 -70 l30 -34 842 0 842 0 30 34 c25 28 29 39 24 70 -3 23 -17 46 -36 64 l-30 27 -830 0 -830 0 -30 -27z M340 873 c-19 -18 -33 -41 -36 -64 -5 -31 -1 -42 24 -70 l30 -34 842 0 842 0 30 34 c25 28 29 39 24 70 -3 23 -17 46 -36 64 l-30 27 -830 0 -830 0 -30 -27z); } 18 | [started] span { content: 'STOP'; } 19 | 20 | 21 | -------------------------------------------------------------------------------- /script/header.tis: -------------------------------------------------------------------------------- 1 | 2 | // Recast v1.1 3 | // https://github.com/MustafaHi/Recast 4 | 5 | 6 | // Naming Convention 7 | // Upper case variables such as below are global 8 | 9 | // html selectors prefixed with small letter then upper letter 10 | // just to define their type; 11 | // c : Context Menu 12 | // m : Popup Menu 13 | // b : Button 14 | // 15 | // n : Name 16 | // a : Author 17 | 18 | 19 | const Version = "1.1"; 20 | 21 | var Performance = false; 22 | var Feed = new Element("feed"); // Store and Parse xml feed 23 | var Tape = $(#tape); // Video element (player) 24 | var Tracker = $(#tracker); // Slider (progress) 25 | var Podcasts = []; // Store JSON request result 26 | var Playlist = $(playlist); 27 | var Queue = $(queue); 28 | var iSearch = $(#iSearch); 29 | 30 | include "tray.tis"; 31 | 32 | include "data.tis"; 33 | // include "requests.tis"; 34 | include "requests-vlist.tis"; 35 | include "player.tis"; 36 | include "playlists.tis"; 37 | include "download.tis"; 38 | include "script.tis"; 39 | 40 | 41 | 42 | function self.ready() { 43 | $$(navigator>li)[0].sendEvent("click"); 44 | initFiles(); 45 | DB.load(); 46 | DB.checkSubs(); 47 | Ping(); 48 | } 49 | 50 | 51 | 52 | function Element.focus() { this.state.focus = true; } 53 | 54 | async function Ping() { 55 | // Ping google analytics, only gives the country, Sciter.machineName() as ID, you can replace with random ID or just remove the function. 56 | try { 57 | await view.request({ 58 | url: "https://www.google-analytics.com/collect?v=1&t=pageview&tid=UA-61531253-4&cid=" + Sciter.machineName() +"&dp=%2Fv" + Version 59 | }); 60 | } 61 | catch (e) { } 62 | } 63 | -------------------------------------------------------------------------------- /script/playlists.tis: -------------------------------------------------------------------------------- 1 | 2 | event click $(#mPlaylists>li) { // Add to playlist 3 | var episode = this.$o(episode); 4 | var podcast = { 5 | name : episode.$(name).text, 6 | author: $(author>name).text, 7 | audio : episode.@#audio, 8 | list : this.id, 9 | down : false, 10 | time : 0 11 | }; 12 | DB.addPodcast(podcast, true); 13 | $(#mPlaylists).closePopup(); 14 | $(#lists>li:current).sendEvent("click"); 15 | } 16 | 17 | event click $(#lists>li) { // View playlist 18 | // if (this.state.current) return true; 19 | if (var c = $(#lists>li:current)) c.state.current = false; 20 | this.state.current = true; 21 | var list = DB.data.p.filter(p => p.list == this.id); 22 | DB.podcasts(list, "playlist"); 23 | } 24 | 25 | // Playlist context menu 26 | event click $(#cPlaylist>.rename) { // Call Rename popup 27 | var el = this.$o(#lists>li); 28 | el.popup($(#mRename), 2); 29 | var input = $(#iRename); 30 | input.@#placeholder = el.text; 31 | input.text = el.text; 32 | input.focus(); 33 | } 34 | event click $(#bRename) { 35 | var el = this.$o(#lists>li).id; 36 | var (i, l) = DB.data.l.find(l => l.id == el); 37 | DB.data.l[i].name = $(#iRename).text; 38 | DB.save(); 39 | DB.playlists(); 40 | } 41 | event click $(#cPlaylist>.delete) { 42 | var el = this.$o(#lists>li); 43 | if (el.state.current) { $(#lists>li).state.current = true; } 44 | var ID = el.id; 45 | if (ID == "queue" || ID == "subscriptions") return true; 46 | var (i, l) = DB.data.l.find(l => l.id == ID); 47 | DB.data.l.remove(i); 48 | for (var p in DB.data.p.filter(p => p.list == ID)) // Delete podcasts in playlist 49 | DB.data.p.removeByValue(p); 50 | DB.save(); 51 | DB.playlists(); 52 | } 53 | 54 | 55 | // Playlist Buttons 56 | event click $(#bPlayPL) { // play playlist 57 | var list = DB.data.p.filter(p => p.list == $(#lists>li:current).id); 58 | DB.podcasts(list, "queue"); 59 | play(); 60 | } 61 | 62 | event click $(#addPlaylist) { this.popup($(#mAddPlaylist),(8 << 16) | 2); $(#iPlaylistName).focus(); } 63 | event click $(#bAddPlaylist) { if ($(#iPlaylistName).text.length > 1) { DB.addPlaylist(); $(#mAddPlaylist).closePopup(); } } 64 | 65 | 66 | event click $(item .delete) { 67 | var item = this.$o(item); 68 | var (i, p) = DB.data.p.find(p => p.audio == item.@#audio); 69 | DB.data.p.remove(i); 70 | if (var c = $(queue>item[audio="{item.@#audio}"])) { 71 | if (c.state.current) { 72 | $(#bPlayPL).postEvent("click"); 73 | } 74 | c.remove(); 75 | } 76 | item.remove(); 77 | DB.save(); 78 | } 79 | 80 | -------------------------------------------------------------------------------- /script/virtual-list.tis: -------------------------------------------------------------------------------- 1 | class VList : Element { 2 | 3 | var me; 4 | function attached() { 5 | me = this; 6 | } 7 | 8 | function render(it) { 9 | // if (it.$(enclosure)) { 10 | return 11 | 12 | {it.$(title)?.text} 13 | {it.$(pubDate)?.text.splice(16)} 14 | 15 | 16 | 17 | PLAY 18 | DOWNLOAD 19 | 20 | 21 | PLAYLIST 22 | LINK 23 | 24 | 25 | {it.$(description)?.text} 26 | ; 27 | // } 28 | } 29 | 30 | function scrollDown(index, n) { 31 | if (index == undefined) index = 0; 32 | var elements = []; 33 | for (var i = 0; i= Items.length) break; 35 | if (Items[index]) 36 | elements.push(render(Items[index])); 37 | } 38 | me.append(elements); 39 | return { moreafter: (Items.length - index) }; 40 | } 41 | 42 | function scrollUp(index, n) { 43 | if (index == undefined) index = Items.length - 1; 44 | var elements = []; 45 | for (var i = 0; i= Items.length) break; 60 | if (Items[start]) 61 | elements.push(render(Items[index])); 62 | } 63 | me.content(elements); 64 | return { 65 | morebefore: (start <=0 ? 0 : start), 66 | moreafter : (Items.length - index) 67 | }; 68 | } 69 | 70 | event contentrequired(evt) { 71 | if (!Items) return false; 72 | var {length, start, where} = evt.data; 73 | if (start == -1 || start >= Items.length) return false; 74 | // debug log: evt.data; 75 | length = 20; 76 | if (where > 0) 77 | evt.data = scrollDown(start, length); 78 | else if (where < 0) 79 | evt.data = scrollUp(start, length); 80 | else 81 | evt.data = scrollTo(start, length); 82 | return true; 83 | }; 84 | 85 | } -------------------------------------------------------------------------------- /script/download.tis: -------------------------------------------------------------------------------- 1 | 2 | event click $(.bDownload) { 3 | var episode = this.$o(episode); 4 | var podcast = { 5 | name : episode.$(name).text, 6 | author: $(author>name).text, 7 | audio : episode.@#audio, 8 | list : "", 9 | down : true, 10 | time : 0 11 | }; 12 | DB.addPodcast(podcast, false); 13 | $(#downloads>list).$append({podcast.name}{podcast.author}PLAY); 14 | download(podcast); 15 | this.remove(); 16 | } 17 | 18 | async function download(podcast) { 19 | var el = $(#downloads item[audio='{podcast.audio}'] .bPlay); 20 | el.@#download = true; 21 | var path = URL.toPath(DB.data.o.downloadDir + "/" + podcast.name + "." + URL.parse(podcast.audio).ext); 22 | function progress(loaded, total) { 23 | el = $(#downloads item[audio='{podcast.audio}'] .bPlay); 24 | el.text = String.printf("%02d%%",((loaded || 1)/(total || 1).toFloat())*100); 25 | } 26 | function success(md5) { 27 | if (md5) { 28 | el = $(#downloads item[audio='{podcast.audio}'] .bPlay); 29 | el.@#download = undefined; 30 | el.text = "PLAY"; 31 | el.$o(item).@#audio = path; 32 | var (i, p) = DB.data.p.find(p => p.audio == podcast.audio); 33 | DB.data.p[i].audio = path; 34 | DB.save(); 35 | } else { el.text = "ERROR"; debug DOWNLOAD: "Error"; } 36 | } 37 | function error(e) { 38 | debug Error: e.toString(); 39 | var (i, p) = DB.data.p.find(p => p.audio == podcast.audio); 40 | if (!p.list) DB.data.p.remove(i); 41 | DB.save(); 42 | } 43 | try { 44 | if (Linux.is) 45 | { 46 | var p = System.Process.exec("curl", ["-w","\"%{http_code}\"","-L", "-o", path, podcast.audio]); 47 | p.on("stdout", (data) => { 48 | if (data == "\"200\"") { 49 | p.terminate(); 50 | success(1); 51 | } 52 | }); 53 | p.on("stderr", (data) => { 54 | el.text = data[..4] + "%"; 55 | }); 56 | } 57 | else 58 | { 59 | var query = { 60 | type : #get, 61 | url : podcast.audio, 62 | toFile: path, 63 | success : success, 64 | progress: progress, 65 | error : error 66 | }; 67 | if (DB.data.o.path) { 68 | var p = DB.data.o.path.split(":"); 69 | query.proxyHost = p[0]; 70 | query.proxyPort = p[1].toInteger(); 71 | } 72 | await view.request(query); 73 | } 74 | } catch(err) { debug: err.toString(); } 75 | } 76 | 77 | -------------------------------------------------------------------------------- /style/playlist.css: -------------------------------------------------------------------------------- 1 | playlist { 2 | display: block; 3 | background: color(clearBG); 4 | position: absolute; 5 | overflow: hidden; 6 | top: 100%; 7 | size: *; 8 | /* overflow: scroll; */ 9 | transition: top linear 155ms; 10 | } 11 | browser[show="playlist"] playlist { top: 0; } 12 | playlist:busy::before { 13 | /* font-rendering-mode: snap-pixel; */ 14 | font-rendering-mode: enhanced; 15 | content: 'LOADING'; 16 | font-size: 2em; 17 | top: 1.15em; 18 | display:block; 19 | left: 0; 20 | size: *; 21 | position: absolute; 22 | background: color(clearBG); 23 | text-align: center; 24 | vertical-align: middle; 25 | flow: vertical; 26 | } 27 | playlist[failed]::before { 28 | content: 'FAILED TO LOAD CONTENT'; 29 | background: #f54c5d; 30 | color: #fff; 31 | } 32 | 33 | playlist header { 34 | padding: 0.5em; 35 | text-align: center; 36 | cursor: pointer; 37 | flow: 0; 38 | } 39 | playlist content { 40 | display: block; 41 | /* overflow-y: auto; */ 42 | overflow: hidden; 43 | size: *; 44 | flow:horizontal; 45 | } 46 | author { 47 | flow: vertical; 48 | display: block; 49 | max-width: 30%; 50 | width: 4*; 51 | height: *; 52 | padding: 2em 0 0 2em; 53 | /* overflow: hidden; */ 54 | } 55 | author name { 56 | font-size: 1.2em; 57 | margin: 1em 0; 58 | font-weight: bold; 59 | display: block; 60 | /* font-family: Arial; */ 61 | } 62 | author description { 63 | display: block; 64 | max-height: 5em; 65 | overflow: hidden; 66 | text-overflow: ellipsis; 67 | } 68 | 69 | episodes { 70 | display: none; 71 | flow: vertical; 72 | size:*; 73 | behavior: virtual-list; 74 | /* overflow: scroll-indicator; */ 75 | padding: 2em 0; 76 | overflow-x: hidden; 77 | overflow-y: scroll; 78 | /* scroll-manner: scroll-manner( animation:false, step:14dip, wheel-step: 14dip ); */ 79 | prototype: VList url(../script/virtual-list.tis); 80 | } 81 | browser[show="playlist"] episodes { display: block; } 82 | episode { 83 | display: block; 84 | /* height: max-content; */ 85 | width: *; 86 | border: 1dip solid color(border); 87 | padding: 1em; 88 | margin: 0 1em 1em 1em; 89 | /* behavior:form; */ 90 | /* font-rendering-mode: enhanced; */ 91 | } 92 | episode name, episode date { 93 | font-size: 1.2em; 94 | } 95 | episode block { 96 | margin-bottom: 1em; 97 | font-weight: 500; 98 | } 99 | episode description { 100 | overflow: hidden; 101 | display: block; 102 | font-size: 0.9em; 103 | color: color(aK); 104 | behavior: htmlarea; 105 | font-rendering-mode: classic; 106 | max-height: 8em; 107 | } 108 | episode description:expanded { max-height: unset; } 109 | episode description img { display: none; visibility: none; } 110 | episode description br { display: none; } 111 | episode description p { margin: 5dip 0; } -------------------------------------------------------------------------------- /script/requests.tis: -------------------------------------------------------------------------------- 1 | var records = []; 2 | 3 | function includeAttributes(recordNo, record, episode) { episode.@#audio = record.audio; episode.$(description).html = record.info || " "; } 4 | 5 | var Episodes = VirtualList { 6 | container: $(episodes), 7 | bufferSize: 50, 8 | setupItemView: includeAttributes 9 | }; 10 | Episodes.value = []; 11 | 12 | function Get(params) { 13 | var defaultParams = { type:#get, output:#stream }; 14 | var query = {...defaultParams, ...params}; 15 | if (DB.data.o.proxy) { 16 | var p = DB.data.o.proxy.split(":"); 17 | query.proxyHost = p[0]; 18 | query.proxyPort = p[1].toInteger(); 19 | } 20 | return view.request(query); 21 | } 22 | 23 | 24 | async function getPodcast(link) { 25 | try { 26 | var r = await Get({url: link}); 27 | Feed.html = r[0].toString().replace("", ""); 28 | // Items = Feed.$$(channel>item); 29 | // Items = []; 30 | for (var it in Feed.$$(channel>item)) { 31 | if (it.$(enclosure)) 32 | records.push({audio: it.$(enclosure).@#url, 33 | name: it.$(title)?.text, date: it.$(pubDate)?.text.splice(16), 34 | info: it.$(description)?.text}); 35 | } 36 | Episodes.value = records; 37 | // $(episodes).vlist.navigate("start"); 38 | 39 | // update author 40 | var author = $(author); 41 | var podcast = Podcasts.find(p => p.feedUrl == link); 42 | if (!podcast) { podcast = DB.data.s.find(p => p.feed == link); } 43 | author.$(>img).@#src = podcast.artworkUrl600 || podcast.image600; 44 | author.$(>name).html = podcast.collectionName || podcast.author; 45 | author.$(>description).html = Feed.$(channel>description).text; 46 | 47 | $(.bSubscribe).state.current = DB.data.s.some(p => p.feed == link); 48 | Playlist.state.busy = false; 49 | r[0].close(); 50 | } catch (e) { 51 | Playlist.@#failed = true; 52 | debug error: e.toString(); 53 | } 54 | } 55 | 56 | async function getPodcasts(searchTerm, id, limit = 11) { 57 | var link = "http://itunes.apple.com/search"; 58 | var header = { 59 | "Accept": "application/json", 60 | "Content-Type": "application/json" 61 | } 62 | var param = { 63 | "term" : searchTerm, 64 | "media": "podcast", 65 | "limit": limit 66 | } 67 | try { 68 | var r = await Get({url: link, params: param, headers: header}); 69 | r = (parseData(r[0])).results; 70 | Podcasts = [...Podcasts,...r]; 71 | var h = !id ? $(#home) : $(#{id} .content); 72 | h.clear(); 73 | var image = Performance ? "artworkUrl100" : "artworkUrl600"; 74 | for (var p in r) 75 | if (p.feedUrl) 76 | h.$append({p.collectionName}{p.artistName}); 77 | } catch (e) { 78 | debug error: e.toString(); 79 | } 80 | } 81 | 82 | -------------------------------------------------------------------------------- /script/tray.tis: -------------------------------------------------------------------------------- 1 | // Sciter Tray Menu v3.2 2 | // https://github.com/MustafaHi/Sciter-Tray 3 | 4 | var trayMenu = $(#trayPopup); // keep null to use tray.htm or give it a selector to use a popup 5 | var trayCentered = true; // True to center the menu to the TrayIcon 6 | var trayPlayer = null; 7 | // self.url is not relative if this is inside a folder you must name the folder self.url("folder/tray.htm") 8 | 9 | function initTray(trayIcon = true) { 10 | if(trayIcon) { 11 | view.trayIcon { 12 | image: self.loadImage(self.url("Recast.svg")), 13 | text: "Welcome Recast!" 14 | }; 15 | } else { 16 | view.trayIcon(#remove); 17 | } 18 | } 19 | 20 | view << event trayicon-click(evt) { 21 | // if the main app window not showing on top add view.windowTopmost = true; then view.windowTopmost = false; 22 | // if (evt.buttons == 1) {view.state = View.WINDOW_SHOWN; view.focus = self; view.windowTopmost = true; view.windowTopmost = false; } 23 | // else { 24 | if(evt.buttons == 1) { 25 | if (!trayPlayer) { 26 | function show() { trayPlayer.close(); view.state = View.WINDOW_SHOWN; } 27 | function hide() { trayPlayer.close(); view.state = View.WINDOW_HIDDEN; } 28 | function inactive() { trayPlayer.close(); trayPlayer=null; } 29 | function close() { trayPlayer.close(); view.close(); } 30 | trayPlayer = view.window{ 31 | url: self.url("tray.htm"), 32 | state: View.WINDOW_HIDDEN, 33 | parameters: { show: show, hide: hide, inactive: inactive, close: close } 34 | }; 35 | var x, y; 36 | var (w,h) = trayPlayer.box(#dimension, #border); 37 | var (tx,ty) = view.trayIcon(#place); 38 | var (sX1,sY1,sX2,sY2) = view.screenBox(#workarea, #rect); 39 | if (trayCentered) { 40 | tx < sX1 ? x = sX1 : (tx > sX2 ? x = (sX2 - w) : x = (tx-(w/2.1))); 41 | ty < (sY1*4) ? y = (ty + 32) : (ty > sY2 ? y = (sY2 - h) : y = ty-h); 42 | tx < sX1 || tx - sX2 > 20 ? y += (h/2) : ""; 43 | } else { 44 | tx < (sX1*4) ? x = tx : (tx > sX2 ? x = (sX2 - w) : x = tx); 45 | ty < (sY1*4) ? y = (ty + 32) : (ty > sY2 ? y = (sY2 - h) : y = ty-h); 46 | } 47 | trayPlayer.move(x.toInteger(), y, w, h); 48 | trayPlayer.state = View.WINDOW_SHOWN; 49 | trayPlayer.windowTopmost = true; 50 | trayPlayer.focus = self; 51 | } 52 | } 53 | else { 54 | // view.focus = trayMenu; 55 | var (tx,ty) = view.trayIcon(#place); 56 | var (sx,sy) = view.box(#position,#client,#screen); 57 | self.popup(trayMenu, (trayCentered ? 2 : 1), tx - sx, ty - sy); 58 | // self.popup(trayMenu, 1, evt.x - sx, evt.y - sy - 10); 59 | } 60 | // } 61 | } 62 | 63 | 64 | event click $(#show) { view.state = View.WINDOW_SHOWN; trayMenu.closePopup(); } 65 | event click $(#hide) { view.state = View.WINDOW_HIDDEN; trayMenu.closePopup(); } 66 | event click $(#exit) { view.close(); } 67 | 68 | -------------------------------------------------------------------------------- /script/requests-vlist.tis: -------------------------------------------------------------------------------- 1 | 2 | var Items = []; // used by virtual-list.tis 3 | 4 | function Get(params) { 5 | var defaultParams = { type:#get, output:#stream }; 6 | var query = {...defaultParams, ...params}; 7 | if (DB.data.o.proxy) { 8 | var p = DB.data.o.proxy.split(":"); 9 | query.proxyHost = p[0]; 10 | query.proxyPort = p[1].toInteger(); 11 | } 12 | return view.request(query); 13 | } 14 | function GetCurl(param) { 15 | var request = promise(); 16 | var result = ""; 17 | var p = System.Process.exec("curl", param); 18 | p.on("stdout", (data) => { 19 | if (data == "Not Found" || data == "\"000\"") 20 | request(false, [data]); 21 | else { 22 | if (data.slice(-4) == "200\"") { 23 | result += data.splice(-4); 24 | p.terminate(); 25 | request(true, [result]); 26 | } else 27 | result += data; 28 | } 29 | }); 30 | return request; 31 | } 32 | 33 | async function getPodcast(link) { 34 | try { 35 | if (Linux.is) 36 | { 37 | var r = await GetCurl(["-L", "--silent", "-w","\"%{http_code}\"", link]); 38 | Feed.html = r.replace("", ""); 39 | } 40 | else 41 | { 42 | var r = await Get({url: link}); 43 | Feed.html = r[0].toString().replace("", ""); 44 | r[0].close(); 45 | } 46 | // Items = Feed.$$(channel>item); 47 | Items = []; 48 | for (var it in Feed.$$(channel>item)) { 49 | if (it.$(enclosure)) Items.push(it); 50 | } 51 | $(episodes).vlist.navigate("start"); 52 | 53 | // update author 54 | var author = $(author); 55 | var podcast = Podcasts.find(p => p.feedUrl == link); 56 | if (!podcast) { podcast = DB.data.s.find(p => p.feed == link); } 57 | author.$(>img).@#src = podcast.artworkUrl600 || podcast.image600; 58 | author.$(>name).html = podcast.collectionName || podcast.author; 59 | author.$(>description).html = Feed.$(channel>description).text; 60 | 61 | $(.bSubscribe).state.current = DB.data.s.some(p => p.feed == link); 62 | Playlist.state.busy = false; 63 | } catch (e) { 64 | Playlist.@#failed = true; 65 | debug error: e.toString(); 66 | } 67 | } 68 | 69 | async function getPodcasts(searchTerm, id, limit = 11) { 70 | var link = "http://itunes.apple.com/search"; 71 | var header = { 72 | "Accept": "application/json", 73 | "Content-Type": "application/json" 74 | } 75 | var param = { 76 | "term" : searchTerm, 77 | "media": "podcast", 78 | "limit": limit 79 | } 80 | try { 81 | var r = await Get({url: link, params: param, headers: header}); 82 | r = (parseData(r[0])).results; 83 | Podcasts = [...Podcasts,...r]; 84 | var h = !id ? $(#home) : $(#{id} .content); 85 | h.clear(); 86 | var image = Performance ? "artworkUrl100" : "artworkUrl600"; 87 | for (var p in r) 88 | if (p.feedUrl) 89 | h.$append({p.collectionName}{p.artistName}); 90 | } catch (e) { 91 | debug error: e.toString(); 92 | } 93 | } 94 | 95 | -------------------------------------------------------------------------------- /Recast.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /style/player.css: -------------------------------------------------------------------------------- 1 | player { 2 | position: relative; 3 | display: block; 4 | height: 5em; 5 | background: color(playerBG); 6 | /* color: color(playerK); */ 7 | /* fill: color(playerK); */ 8 | color: color(playerK); 9 | fill : color(playerK); 10 | flow:horizontal; 11 | vertical-align: middle; 12 | } 13 | /* player icon { fill: #000 !important; } */ 14 | player btn { 15 | display: inline-block; 16 | font-weight: bolder; 17 | padding: 1em; 18 | /* background: red; */ 19 | line-height: 1em; 20 | cursor: pointer; 21 | border-radius: 1em; 22 | flow: horizontal; 23 | } 24 | player btn:hover { 25 | /* background-color: rgba(255,255,255, 0.5); */ 26 | background-color: color(playerHBG); 27 | } 28 | #tapePlay { 29 | width: 4.25em; 30 | } 31 | #tapePlay span { height: 1dip; } 32 | #tape { 33 | display: none; 34 | } 35 | #tracker { 36 | width: 20em; 37 | height: 0.3em; 38 | /* background: #f0f1fd; */ 39 | background: color(itemBG); 40 | /* aspect: DrawProgress; */ 41 | /* background-color: rgba(255,255,255, 0.5); */ 42 | } 43 | #tracker > .slider { 44 | /* border-left-color: lightcoral; */ 45 | background-color: color(playerK); 46 | border-radius: 2dip; 47 | height: 16dip; 48 | width: 9dip; 49 | border: 0; 50 | transition: width linear 50ms; 51 | } 52 | #tracker:hover > .slider { 53 | width: 5dip; 54 | } 55 | #tracker:hover > button::after { 56 | content: parent-attr(time); 57 | font-weight: bold; 58 | width: 4em; 59 | font-family: Consolas, monospace; 60 | display: block; 61 | background: #fff; 62 | box-shadow: 0px 1px 1px #d1e6f8; 63 | border-radius: 0.5em; 64 | padding: 0.5em; 65 | margin-top: -3em; 66 | /* margin-left: -2.33em; */ 67 | margin-left: -2.5em; 68 | } 69 | #tracker::before { 70 | /* content: ''; */ 71 | /* background: #f0f1fd; */ 72 | left: 0; 73 | width: 2em; 74 | height: 0.3em; 75 | } 76 | #time { width: 7em; } 77 | 78 | queue { 79 | display: block; 80 | height: 4em; 81 | min-height: 4em; 82 | transition: height linear 100ms 0.3s; 83 | overflow: none; 84 | vertical-align: bottom; 85 | flow: vertical; 86 | margin-bottom: 0.5em; 87 | width: *; 88 | /* size: *; */ 89 | margin-right: 2.5em; 90 | background: color(playerHBG); 91 | border-radius: 4dip; 92 | } 93 | queue:hover { 94 | position: relative; 95 | height: min-content; 96 | } 97 | queue:hover item { display: block; } 98 | queue item { 99 | /* overflow: hidden; */ 100 | height: 3em; 101 | padding: 0.5em 1em; 102 | /* background: red; */ 103 | display: none; 104 | } 105 | queue item:hover { 106 | /* background-color: #f54c5d; */ 107 | background-color: #000; 108 | color: #fff; 109 | } 110 | queue item:hover n { color: #fff; } 111 | queue item:hover a { color: #eee; } 112 | queue item:current { display: block; } 113 | queue item:hover a { color: #ddd; font-rendering-mode: classic; } 114 | 115 | #trayPopup { 116 | width: 150dip; 117 | border-radius: 4dip; 118 | overflow: hidden; 119 | box-shadow: 0 4px 6px rgba(50,50,93,0.7), 0 1px 3px rgba(0,0,0,0.4); 120 | } 121 | #trayPopup li { 122 | display: block; 123 | padding: 10dip; 124 | background: #fff; 125 | font-size: 15dip; 126 | font-weight: 500; 127 | color: #222; 128 | } 129 | #trayPopup li:hover { 130 | background: #ddd; 131 | } 132 | #trayPopup .header { font-weight: bold; font-size: 12dip; } 133 | #trayPopup b { 134 | display: block; 135 | border-bottom: 1dip solid #aaa; 136 | } 137 | #trayPopup button { 138 | min-width: 4.5em; 139 | } -------------------------------------------------------------------------------- /script/script.tis: -------------------------------------------------------------------------------- 1 | // Recast 2 | // https://github.com/MustafaHi/Recast 3 | // Refer to Header.tis for main script 4 | 5 | event click $(navigator>li) { 6 | $(browser).@#show = this.@#for; 7 | if (this.state.current) {return true;} 8 | 9 | if (var c = $(navigator>li:current)) { c.state.current = false; } 10 | if (var e = $(browser>section:expanded)) { e.state.expanded = false; } 11 | this.state.current = true; 12 | $(browser>#{this.@#for}).state.expanded = true; 13 | } 14 | event keyup $(#iSearch)(e) { 15 | if (e.keyCode == Event.VK_ENTER) { 16 | if (this.value.length == 0) {DB.discover(); return true;} 17 | Podcasts = []; 18 | getPodcasts(this.value, false, 50); 19 | } 20 | } 21 | 22 | event click $(playlist>header) { $(browser).@#show = "shows"; } 23 | 24 | var prevLink; 25 | event click $(podcast) { 26 | var link = this.@#feed; 27 | if (prevLink == link) { 28 | $(browser).@#show = "playlist"; 29 | return true; 30 | } 31 | prevLink = link; 32 | getPodcast(link); 33 | Playlist.state.busy = true; 34 | Playlist.@#failed = undefined; 35 | Playlist.@#feed = link; 36 | $(browser).@#show = "playlist"; 37 | } 38 | 39 | event click $(.bSubscribe) { 40 | if (this.state.current) { // unsubscribe 41 | DB.unsubscribe(Playlist.@#feed); 42 | this.state.current = false; 43 | } else { // subscribe 44 | var p = Podcasts.find(p => p.feedUrl == Playlist.@#feed); 45 | var podcast = { 46 | feed: p.feedUrl, 47 | name: p.collectionName, 48 | author: p.artistName, 49 | update: new Date().toISOString(), 50 | image100: p.artworkUrl100, 51 | image600: p.artworkUrl600 52 | }; 53 | DB.subscribe(podcast); 54 | this.state.current = true; 55 | } 56 | DB.subscriptions(); 57 | } 58 | 59 | event click $(.bPlay) { 60 | var p = this.$o(episode) || this.$o(item); 61 | if (p.tag == "episode") { 62 | Queue.@#list = ""; 63 | Queue.$content({p.$(name).text}{$(author>name).text}); 64 | $(queue>item).state.current = true; 65 | } else if (p.tag == "item") { 66 | if (this.@#download) return true; 67 | if ($(browser).@#show == "downloads") { 68 | Queue.@#list = ""; 69 | Queue.$content({p.$(n).text}{$(a).text}); 70 | $(queue>item).state.current = true; 71 | } else { 72 | var list; 73 | if ($(browser).@#show == "subscriptions") 74 | list = DB.data.p.filter(p => p.list == "subscriptions"); 75 | else 76 | list = DB.data.p.filter(p => p.list == $(#lists>li:current).id); 77 | DB.podcasts(list, "queue"); 78 | $(queue>item[audio="{p.@#audio}"]).postEvent("click"); 79 | } 80 | } 81 | play(); 82 | } 83 | 84 | event click $(.bPlaylist) { this.$p(button).popup($(#mPlaylists), 5); } 85 | event click $(.bLink) { view.clipboard(#put, {text: this.$p(episode).@#audio}); } 86 | 87 | event click $(episode>description) { this.state.expanded = true; } 88 | event click $(a[href]) { Sciter.launch(this.@#href); return true; } 89 | 90 | 91 | self.on("keydown", (e) => { 92 | if (e.keyCode == Event.VK_ESCAPE) { $(browser).@#show = "undefined"; } 93 | }); 94 | 95 | event click $(cell .more) { 96 | iSearch.value = this.$o(block).$(b).html; 97 | iSearch.sendKeyEvent({type: Event.KEY_UP, keyCode: Event.VK_ENTER}); 98 | } 99 | 100 | event dblClick $([for=home]) { 101 | if (iSearch.value.length == 0) return true; 102 | iSearch.value = ""; 103 | iSearch.sendKeyEvent({type: Event.KEY_UP, keyCode: Event.VK_ENTER}); 104 | } 105 | 106 | function self.closing(reason) { 107 | if (reason == #by-code) { 108 | var (sx, sy, sw, sh) = view.box(#rectw, #border, #screen); 109 | DB.data.o.size = [sw,sh]; 110 | DB.data.o.position = [sx,sy]; 111 | DB.save(); 112 | } 113 | } 114 | 115 | event click $(button#close) { 116 | DB.data.o.trayPlayer ? view.state = View.WINDOW_HIDDEN : view.close(); 117 | } -------------------------------------------------------------------------------- /main.htm: -------------------------------------------------------------------------------- 1 | 2 | 3 | Recast 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | Show 13 | Hide 14 | Exit 15 | 16 | 17 | 18 | Rename 19 | Delete 20 | 21 | 22 | 23 | 24 | 25 | ADD 26 | 27 | 28 | 29 | RENAME 30 | 31 | 32 | 33 | 34 | Recast 35 | 36 | 37 | 38 | 39 | 40 | 41 | Discover 42 | Playlists 43 | Subscriptions 44 | Downloads 45 | Options 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | PLAY 55 | Add 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | Downloads 65 | 66 | 67 | Options 68 | 69 | Lite Mode 70 | Animation 71 | Tray Player 72 | Vertical Flow 73 | Discover Query : 74 | 75 | 76 | Download Directory : 77 | 78 | Change 79 | Proxy : 80 | 81 | 82 | Theme : 83 | 84 | Default 85 | Dark 86 | 87 | 88 | 91 | 92 | 93 | CLOSE 94 | 95 | 96 | 97 | 98 | 99 | 100 | SUBSCRIBE 101 | Alert 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | PLAY 113 | DOWNLOAD 114 | 115 | 116 | PLAYLIST 117 | LINK 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | PLAY 131 | 132 | 133 | 134 | 135 | 136 | 137 | -------------------------------------------------------------------------------- /script/player.tis: -------------------------------------------------------------------------------- 1 | 2 | event click $(#tapePlay) { 3 | if (!$(queue>item:current)) return; 4 | if (!this.@#started) 5 | { 6 | this.@#started = true; 7 | if (Tape.videoPosition() > 1) { 8 | Tape.videoPlay(); 9 | Tape.timer(0.5s, pulse); 10 | Tape.timer(10s, saveTime); 11 | } else { play(); } 12 | } 13 | else 14 | { 15 | this.@#started = undefined; 16 | Tape.videoStop(); 17 | saveTime(); 18 | } 19 | } 20 | event click $(#bPrev) { 21 | if (!$(queue>item:current)) return; 22 | if (Tape.videoPosition() > 10) Tape.videoPlay(0); 23 | else if (var item = Queue.$$(item)[Queue.$(item:current).index+1]) item.sendEvent("click"); 24 | } 25 | event click $(#bNext) { 26 | if (!$(queue>item:current)) return; 27 | if (var item = Queue.$$(item)[Queue.$(item:current).index-1]) item.sendEvent("click"); 28 | } 29 | 30 | event change $(#tracker) { 31 | if (!$(queue>item:current)) return; 32 | var (tm, ts) = hms(this.value); 33 | this.@#time = String.printf("%02d:%02d", tm, ts); 34 | } 35 | event click $(#tracker) { 36 | if ($(#tapePlay).@#started) { 37 | Tape.videoPlay(this.value); 38 | } else { 39 | $(#tapePlay).sendEvent("click"); 40 | } 41 | } 42 | 43 | 44 | event click $(queue>item) { 45 | if (this.state.current) return true; 46 | $(queue>item:current).state.current = false; 47 | this.state.current = true; 48 | play(); 49 | } 50 | 51 | Tape.onControlEvent = function(evt) { // not working [require a frame to be rendered] 52 | switch(evt.type) { 53 | case Event.VIDEO_INITIALIZED: () => {debug: "init"; return false}; 54 | case Event.VIDEO_STARTED: () => {debug: "started"; return false}; 55 | case Event.VIDEO_STOPPED: () => {debug: "stopped"; return false}; 56 | } 57 | } 58 | 59 | var TrackerBTN = Tracker.$(button); 60 | function DrawProgress() { 61 | var button = TrackerBTN; 62 | this.paintContent = function(gfx) 63 | { 64 | var (x1,y1,x2,y2) = button.box(#rect,#border,#parent); 65 | var (w,h) = this.box(#dimension,#inner); 66 | gfx.fillColor( button.style["border-left-color"] ) 67 | .rectangle(0,0,(x1+x2)/2,h); 68 | } 69 | } 70 | 71 | function play() { 72 | if (var p = $(queue>item:current)) { 73 | $(#tapePlay).@#started = true; 74 | Tape.videoUnload(); 75 | Tape.videoLoad(p.@#audio); 76 | Tape.videoPlay(p.@#time.toInteger()); 77 | Tape.timer(0.5s, pulse); 78 | Tape.timer(10s, saveTime); 79 | DB.data.o.current = { 80 | name: p.$(n).text, 81 | author: p.$(a).text, 82 | audio: p.@#audio, 83 | list: Queue.@#list, 84 | time: p.@#time 85 | } 86 | DB.save(); 87 | } 88 | // } else if ($(#tapePlay).@#started) { $(#tapePlay).postEvent("click"); } 89 | } 90 | 91 | function saveTime() { 92 | var time = Tape.videoPosition(); 93 | if (!$(queue).@#list) DB.data.o.current.time = time; 94 | else { 95 | var (i,p ) = DB.data.p.find(p => p.audio == $(queue>item:current).@#audio); 96 | DB.data.p[i].time = time; 97 | } 98 | DB.save(); 99 | $(queue>item:current).@#time = time; 100 | if (!$(#tapePlay).@#started) return false; 101 | return true; 102 | } 103 | 104 | function hms(seconds) { 105 | var m,s; 106 | s = seconds % 60; seconds /= 60; 107 | m = seconds % 60; return ( m, s ); 108 | } 109 | 110 | //| Update Tracker and Time | repeat every 0.5s while podcast is playing 111 | function pulse() { 112 | //| videoIsPlaying() not working [because no frame is rendered] 113 | // if (!Tape.videoIsPlaying()) return false; 114 | if (!$(#tapePlay).@#started) return false; 115 | var m = (Tape.videoDuration() + 0.5).toInteger(); 116 | var t = (Tape.videoPosition() + 0.5).toInteger(); 117 | if( ! Tracker.state.hover ) { 118 | Tracker.sliderRange(0, m); 119 | Tracker.value = t; 120 | } 121 | var (tm,ts) = hms(t); var (mm,ms) = hms(m); 122 | Tracker.@#time = String.printf("%02d:%02d", tm, ts); 123 | $(#time).value = String.printf("%02d:%02d of %02d:%02d", tm,ts,mm,ms); 124 | if (t == m) { if (var item = Queue.$$(item)[Queue.$(item:current).index-1]) item.sendEvent("click"); } 125 | return true; 126 | } 127 | 128 | const Linux = { 129 | is: System.PLATFORM == "Linux", 130 | player: undefined, 131 | time: 0, 132 | duration: 0, 133 | audio: "", 134 | } 135 | 136 | Linux.listen = () => { 137 | function toSeconds(time) { 138 | var m = time.split(":").map(t=>t.toInteger()); 139 | return (m[0]*3600 + m[1]*60 + m[2]); 140 | } 141 | Linux.player.on("stderr", (d) => { 142 | if (d[..2] == "A:") { 143 | var time = d.match(/\d+\:\d+\:\d+/g); 144 | Linux.time = toSeconds(time[0]); 145 | Linux.duration = toSeconds(time[1]); 146 | } 147 | }) 148 | } 149 | 150 | function setupLinux() { 151 | if (Linux.is) { 152 | Tape.videoPlay = (time = false) => { 153 | time = typeof time == #integer ? time.toString() : Linux.time.toString(); 154 | Tape.videoStop(); 155 | Linux.player = System.Process.exec("mpv", ["--vid=no", "--start", time, Linux.audio]); 156 | Linux.listen(); 157 | } 158 | Tape.videoPosition = (time = false) => { 159 | if (typeof time == #integer) 160 | Linux.time = time; 161 | return Linux.time; 162 | } 163 | Tape.videoDuration = () => { 164 | return Linux.duration; 165 | } 166 | Tape.videoLoad = (url) => { Linux.audio = url; } 167 | Tape.videoUnload = () => { 168 | if (Linux.player !== undefined && Linux.player.active == true) { 169 | Linux.player.terminate(); 170 | Linux.player = undefined; 171 | } 172 | } 173 | Tape.videoStop = Tape.videoUnload; 174 | } 175 | } 176 | 177 | setupLinux(); -------------------------------------------------------------------------------- /tray.htm: -------------------------------------------------------------------------------- 1 | 2 | 3 | Recast Tray Player 4 | 5 | 132 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | Title of the episode 216 | Author here 217 | 218 | 219 | 220 | PLAY 221 | 222 | 223 | 224 | 225 | 226 | 227 | 00:00 - 00:00 228 | 229 | 230 | -------------------------------------------------------------------------------- /style/main.css: -------------------------------------------------------------------------------- 1 | html { 2 | font-family: Segoe UI, Cantarell; 3 | 4 | var(BG): #e6eef5; 5 | var(K) : #000; 6 | var(aK): #222; 7 | var(clearBG): #fff; 8 | var(border) : #eee; 9 | 10 | var(bodyBG): rgba(240, 250, 255, 0.60); 11 | var(headerButtonHBG): rgba(0,0,0, 0.1); 12 | var(headerStroke) : #111; 13 | var(headerStrokeW) : 1dip; 14 | 15 | var(buttonK) : #000; 16 | var(buttonHK) : #fff; 17 | var(buttonBG) : #fff; 18 | var(buttonHBG): #000; 19 | /* var(currentBG): rgb(246, 252, 255); */ 20 | var(currentBG): #fff; 21 | 22 | var(itemBG): #e6eef5; 23 | 24 | /* var(playerBG): #1c70bf; */ 25 | var(playerBG): #fff; 26 | var(playerHBG): #e6eef5; 27 | var(playerK) : #000; 28 | line-height: 16dip; 29 | /* color: #fff; */ 30 | /* font-size: 9pt; */ 31 | } 32 | html[window-state="maximized"] { padding: 0.5em; } 33 | body { 34 | margin: 0; 35 | flow: horizontal; 36 | backdrop-filter: blur(50dip); 37 | /* background-color: color(bodyBG); */ 38 | background-color: color(clearBG); 39 | transition: background-color linear 300ms; 40 | color: color(K); 41 | } 42 | /* body:hover {background-color: rgba(240, 250, 255, 0.65);} */ 43 | header { 44 | vertical-align: middle; 45 | /* height: 24dip; */ 46 | flow:horizontal; 47 | color: color(K); 48 | /* background: linear-gradient(90deg, #FEE140 0%, #FA709A 100%); */ 49 | background: color(BG); 50 | } 51 | header icon { 52 | display:block; 53 | visibility: hidden; 54 | /* size:5em; */ 55 | size: 24dip; 56 | /* padding: 0.5em; */ 57 | /* margin: 0.2em 0.3em; */ 58 | /* position: absolute; */ 59 | /* z-index: 1; */ 60 | background-image: url("../Recast.svg"); 61 | image-rendering:optimize-quality; 62 | /* transform: rotate(30deg); */ 63 | } 64 | header caption { padding-left: 0.5em; width:*; } 65 | header button[role] { 66 | /* size:24dip; */ 67 | padding:0; 68 | margin: 0; 69 | border:none; 70 | border-radius: 0; 71 | /* background:50% 50% no-repeat; background-size:24dip; */ 72 | background: none; 73 | 74 | behavior:clickable; 75 | display:block; 76 | height:*; 77 | width:window-button-width; 78 | foreground-size: 11dip; 79 | foreground-repeat: no-repeat; 80 | foreground-position:50% 50%; 81 | stroke: color(headerStroke); 82 | fill: none; 83 | stroke-width: var(headerStrokeW); 84 | transition: background-color linear 80ms; 85 | position: relative; 86 | z-index: 1; 87 | } 88 | header button[role]:hover { background-color: color(headerButtonHBG); } 89 | button[role="window-minimize"] { foreground-image: url(path:M0 0 M0 5 H10 M10 10); } 90 | button[role="window-maximize"] { foreground-image: url(path:M0 0 H10 V10 H0 Z); } 91 | html[window-state="maximized"] header button[role="window-maximize"] { foreground-image: url(path:M0 2 h8 v8 h-8 Z M2 2 v-2 h8 v8 h-2); } 92 | #close { stroke-width:1.44dip; foreground-image: url(path:M0 0 L10 10 M10 0 L0 10); } 93 | header button[role="window-close"]:hover { background:rgb(232,17,35); stroke:#fff; } 94 | 95 | footer { margin-top: *; } 96 | body img { width: 100%; image-rendering:optimize-quality; } 97 | body button:not([type]):not([role]) { 98 | background: color(buttonBG); 99 | color: color(buttonK); 100 | cursor: pointer; 101 | vertical-align: middle; 102 | /* flow: horizontal; */ 103 | } 104 | body button:not([type]):not([role]):hover, body button:current { 105 | background: color(buttonHBG); 106 | color: color(buttonHK); 107 | border-color: color(buttonHBG); 108 | } 109 | .bPlaylist { behavior: none; } 110 | input[placeholder]:empty { content: attr(placeholder); color: #888 !important; } 111 | popup[role=overflow-tooltip], 112 | popup[role=overflow-multiline-tooltip] { 113 | background-color: #f8f8ff; 114 | padding: 0.4em; 115 | } 116 | 117 | menu, popup.menu { 118 | overflow: hidden; 119 | background: #fff; 120 | min-width: 8em; 121 | width: max-content; 122 | font-weight: 600; 123 | border-radius: 3dip; 124 | letter-spacing: 1dip; 125 | box-shadow:rgba(50, 50, 93, 0.22) 0px 4px 6px 0px, rgba(0, 0, 0, 0.1) 0px 1px 3px 0px; 126 | border: 1dip solid #aaa; 127 | padding: 0.5em; 128 | flow: vertical; 129 | } 130 | popup.menu input { 131 | padding: 0.5em; 132 | border-radius: 0; 133 | margin-bottom: 0.5em; 134 | font-weight: 500; 135 | } 136 | popup.menu button { 137 | background: #fff; 138 | border-radius: 0; 139 | width: *; 140 | } 141 | popup.menu button:hover { background: #e6eef5; } 142 | menu, #mPlaylists { padding: 0; background: color(BG); } 143 | menu li, #mPlaylists > li { padding: 0.5em; overflow: hidden; } 144 | menu li:hover, #mPlaylists > li:hover { 145 | background-color: #000; 146 | color: #fff; 147 | } 148 | 149 | block { 150 | vertical-align: middle; 151 | display: block; 152 | flow: horizontal; 153 | border-spacing: *; 154 | } 155 | block button:not(:last-child) { 156 | margin-right: 5dip; 157 | } 158 | 159 | a, n { overflow-x: hidden; white-space: nowrap; text-overflow: ellipsis; } 160 | n { color: color(K); } 161 | a { color: color(aK); } 162 | item n { 163 | display: block; 164 | font-size: 1.1em; 165 | font-weight: 600; 166 | margin-bottom: 0.2em; 167 | } 168 | item a { 169 | display: block; 170 | font-size: 0.9em; 171 | } 172 | 173 | 174 | /* Body */ 175 | 176 | navigator { 177 | display: block; 178 | /* padding-top: 3.3em; */ 179 | size: 180dip *; 180 | background: color(BG); 181 | /* background: #fff; */ 182 | } 183 | navigator li { 184 | /* z-index: 99; */ 185 | /* position: relative; */ 186 | padding: 1em; 187 | } 188 | navigator li:hover, navigator li:current { 189 | background: color(currentBG); 190 | } 191 | #iSearch { 192 | border-radius: 0; 193 | border-width: 1dip 0; 194 | width: *; 195 | padding: 1em; 196 | background: none; 197 | font-size: 9pt; 198 | color: color(K); 199 | } 200 | 201 | browser { 202 | display: block; 203 | /* box-shadow: 0px 5px 51px -20px rgba(117,117,117,0.3); */ 204 | overflow: hidden; 205 | /* position: relative; */ 206 | size: *; 207 | } 208 | browser>section { overflow-y: auto; padding: 1em; visibility: none; size: *; } 209 | browser>section:expanded {visibility: visible;} 210 | cell { 211 | display: block; 212 | margin-bottom: 1em; 213 | height: 260dip; 214 | overflow: hidden; 215 | } 216 | cell block { 217 | padding: 0.5em 1em; 218 | background: color(BG); 219 | margin: 0 1em 1em 0; 220 | border-radius: 4dip; 221 | } 222 | browser podcast { 223 | display: inline-block; 224 | height: 205dip; 225 | width: 160dip; 226 | /* background: red; */ 227 | overflow: hidden; 228 | /* border-spacing: *; */ 229 | margin-right: 1em; 230 | margin-bottom: 0.7em; 231 | flow:vertical; 232 | /* border-spacing: *; */ 233 | cursor: pointer; 234 | } 235 | browser podcast > img { 236 | border-radius: 5dip; 237 | } 238 | podcast > n { 239 | display: block; 240 | font-weight: 600; 241 | margin-top: 0.4em; 242 | margin-bottom: 0.2em; 243 | } 244 | podcast > a { 245 | display: block; 246 | font-size: 0.9em; 247 | } 248 | 249 | 250 | #subscriptions { 251 | flow: horizontal; 252 | overflow-y: scroll; 253 | padding: 0 1.4em 0 0; 254 | } 255 | #subscriptions > * { 256 | size: *; 257 | } 258 | #podcasts { 259 | padding: 1em; 260 | border-right: 2dip solid color(BG); 261 | } 262 | #playlists, #downloads { background: color(clearBG); } 263 | #playlists>block { 264 | vertical-align: middle; 265 | padding: 0 0.5em; 266 | background: color(playerHBG); 267 | } 268 | #lists { 269 | flow: horizontal; 270 | } 271 | #lists li { 272 | /* font-family: Arial; */ 273 | padding: 1em; 274 | font-weight: 600; 275 | font-rendering-mode: enhanced; 276 | font-size: 10pt; 277 | context-menu: selector(#cPlaylist); 278 | } 279 | #lists li:hover, #lists li:current { 280 | background: color(clearBG); 281 | color: color(K); 282 | } 283 | list { 284 | display: block; 285 | flow: vertical; 286 | padding: 1em; 287 | } 288 | list item { 289 | background: color(itemBG); 290 | display: block; 291 | padding: 0.5em 1em; 292 | margin-top: 0.5em; 293 | } 294 | 295 | /* #options { background: #fff; } */ 296 | form { 297 | margin-top: 4em; 298 | max-width: 30em; 299 | font-size: 1.2em; 300 | font-rendering-mode: enhanced; 301 | flow: row(label, input select, button br); 302 | border-spacing: 1em; 303 | 304 | } 305 | form button|checkbox { padding-left: 1.7em; } 306 | form label { vertical-align: middle; height: *; width: 170dip;} 307 | form input:not([type="integer"]), form input caption { 308 | padding: 0.5em; 309 | border-radius: 0; 310 | display: block; 311 | } 312 | form input { width: *; font-family: inherit; } 313 | form select { 314 | border-radius: 0; 315 | height: *; 316 | background: #fff; 317 | } 318 | form select button { padding-right: 0.5em; } 319 | form select caption { vertical-align: middle; } 320 | 321 | -------------------------------------------------------------------------------- /script/data.tis: -------------------------------------------------------------------------------- 1 | const dbPath = System.home("Recast.json"); 2 | 3 | function initFiles(forced = false) { 4 | if (!System.scanFiles(dbPath) || forced) { 5 | var file = Stream.openFile(dbPath, "wu"); 6 | // // o = options 7 | // // l = playlists { id,name } 8 | // // s = subscriptions { name,author,feed,update,image100,image600 } // imageXX represent size, update is last check Date().toISOString() 9 | // // p = podcasts { name,author,audio,list,down,time } 10 | file.print(JSON.stringify(DB.data," ")); 11 | file.close(); 12 | } 13 | } 14 | 15 | namespace DB { 16 | var data = { 17 | o: { 18 | performance: false, 19 | animation: true, 20 | trayPlayer: false, 21 | verticalFlow: false, 22 | downloadDir: System.home(), 23 | discover: "Global, Money, Tech, Crime", 24 | proxy: "", 25 | current: {}; 26 | theme: "Default", 27 | size: [905, 735], 28 | position: [self.toPixels(905dip, #width), self.toPixels(735dip, #height)] 29 | }, 30 | l: [{ 31 | id: "queue", 32 | name: "Queue" 33 | }, { 34 | id: "subscriptions", 35 | name: "Subscription" 36 | }], 37 | s: [], 38 | p: [] 39 | }; 40 | function load() { 41 | var file = Stream.openFile(dbPath, "ur"); 42 | MergeObjects(data, parseData(file)); 43 | file.close(); 44 | 45 | move(); 46 | apply(); 47 | discover(); 48 | playlists(); 49 | subscriptions(); 50 | if (data.o.current.list) { 51 | var cur = data.o.current; 52 | podcasts(data.p.filter(p => p.list == cur.list), "queue", cur.audio); 53 | } 54 | else if (data.o.current.audio) { 55 | var cur = data.o.current; 56 | Queue.$content({cur.name}{cur.author}); 57 | $(queue>item[audio="{cur.audio}"]).state.current = true; 58 | } 59 | else 60 | podcasts(data.p.filter(p => p.list == "queue"), "queue"); 61 | downloads(); 62 | $(#setting).value = data.o; 63 | } 64 | function apply() { 65 | Performance = data.o.performance; 66 | view.animationsEnabled = data.o.animation; 67 | self.@#theme = data.o.theme; 68 | $(body).@#vertical = data.o.verticalFlow || undefined; 69 | initTray(data.o.trayPlayer); 70 | // getPodcasts(); 71 | // save(); 72 | } 73 | function save() { 74 | var file = Stream.openFile(dbPath, "wu"); 75 | file.print(JSON.stringify(data, " ")); 76 | file.close(); 77 | } 78 | 79 | function discover() { 80 | var html = ""; 81 | for (var d in data.o.discover.split(",")) { 82 | d = d.trim(); 83 | var id = "c" + d.replace(" ", "-"); 84 | // html += String.printf('%sMORE', "c"+d, d); 85 | html += String.$({d}MORE); 86 | getPodcasts(d, id); 87 | } 88 | $(#home).html = html; 89 | // for (var d in data.o.discover.split(",")) getPodcasts(d.trim(), "c"+d.trim().replace(" ", "-")); 90 | } 91 | function playlists() { 92 | var selected = $(#lists>li:current)?.id; 93 | $(#mPlaylists).clear(); 94 | $(#lists).clear(); 95 | for (var l in data.l.filter(l => l.id !== "subscriptions")) { 96 | var el = {l.name}; 97 | $(#mPlaylists).append(el); 98 | $(#lists).append(el); 99 | } 100 | selected ? $(#lists>#{selected}).sendEvent("click") : $(#lists>li).sendEvent("click"); 101 | } 102 | function addPlaylist() { 103 | var list = { 104 | id: "L" + rand(90000), 105 | name: $(#iPlaylistName).text 106 | }; 107 | data.l.push(list); 108 | save(); 109 | playlists(); 110 | } 111 | function deletePlaylist(ID) { 112 | var (i, l) = data.l.find(l => l.id == ID); 113 | data.l.remove(i); 114 | save(); 115 | } 116 | 117 | function subscriptions() { 118 | var pods = $(#podcasts); 119 | pods.clear(); 120 | var eps = $(#episodes); 121 | eps.clear(); 122 | var episodes = DB.data.p.filter(p => p.list == "subscriptions"); 123 | 124 | var image = Performance ? "image100" : "image600"; 125 | for (var p in data.s) 126 | pods.$append({p.name}{p.author}); 127 | for (var p in episodes) 128 | eps.$append({p.name}{p.author}PLAY); 129 | } 130 | async function checkSubs() { 131 | var newEpisodes = []; 132 | for (var s in data.s) { 133 | try { 134 | var r = await Get({url: s.feed}); 135 | Feed.html = r[0].toString(); 136 | var lastCheck = new Date(s.update); 137 | for (var p in Feed.$$(channel>item)) 138 | { 139 | var d = new Date(p.$(pubDate).text); 140 | if (Date.diff(lastCheck, d, #minutes) > 0) { 141 | s.update = new Date().toISOString(); 142 | var podcast = { 143 | name : p.$(title).text, 144 | author: s.name, 145 | audio : p.$(enclosure).@#url, 146 | list : "subscriptions", 147 | down : false, 148 | time : 0 149 | }; 150 | newEpisodes.push(podcast); 151 | } else break; 152 | } 153 | } catch (e){ 154 | debug checkSubs: e.toString(); 155 | } 156 | } 157 | if (newEpisodes.length > 0) 158 | { 159 | for (var podcast in newEpisodes) DB.addPodcast(podcast, false); 160 | save(); 161 | subscriptions(); 162 | } 163 | } 164 | function subscribe(podcast) { 165 | data.s.push(podcast); 166 | save(); 167 | } 168 | function unsubscribe(podcast) { 169 | var (i, p) = data.s.find(p => p.feed == podcast); 170 | data.s.remove(i); 171 | save(); 172 | } 173 | 174 | function podcasts(list, type, current = false) { 175 | if (type == "playlist") { 176 | $(#playlists>list).clear(); 177 | for (var p in list) 178 | $(#playlists>list).$append({p.name}{p.author}PLAY); 179 | } else if (type == "queue") { 180 | Queue.clear(); 181 | if (list.length == 0) { return true; } 182 | for (var p in list) 183 | Queue.$prepend({p.name}{p.author}); 184 | if (current) Queue.$(>item[audio="{current}"]).state.current = true; 185 | else Queue.$$(item).last.state.current = true; 186 | Queue.@#list = list[0].list; 187 | } 188 | } 189 | function addPodcast(podcast, list) { 190 | var (i, p) = data.p.find(p => p.audio == podcast.audio); 191 | if (i >= 0) { 192 | 193 | if (list) { 194 | data.p[i].list = podcast.list; 195 | data.p[i].time = podcast.time; 196 | } else { data.p[i].down = podcast.down; } 197 | 198 | } else { data.p.push(podcast); } 199 | 200 | if (Queue.@#list == podcast.list) { addToQueue(podcast); } 201 | save(); 202 | } 203 | // function updateTime(audioUrl, time) { 204 | // var (i, p) = data.p.find(p => p.audio == audioUrl); 205 | // data.p[i].time = time; 206 | // save(); 207 | // } 208 | function addToQueue(p) { // p for podcast 209 | Queue.$prepend({p.name}{p.author}); 210 | } 211 | 212 | function downloads() { 213 | var d = $(#downloads>list); 214 | d.clear(); 215 | for (var p in data.p.filter(p => p.down == true)) 216 | d.$append({p.name}{p.author}PLAY); 217 | } 218 | function move() { 219 | var p = data.o.position; 220 | var s = data.o.size; 221 | var (sx,sy,sw,sh) = View.screenBox(0,#workarea,#rectw); 222 | 223 | // If window size close to screen size, then maximize window 224 | if (sw-100 < s[0] && sh-100 < s[1]) { 225 | // view.move( p[0], p[1], 905, 735, true ); 226 | view.state = View.WINDOW_MAXIMIZED; 227 | } else { 228 | // view.move( p[0], p[1], s[0], s[1], true ); 229 | } 230 | } 231 | } 232 | 233 | 234 | event change $(#setting) { 235 | MergeObjects(DB.data.o, this.value); 236 | DB.apply(); 237 | DB.save(); 238 | } 239 | event click $(#changeDir) { 240 | var fn = view.selectFolder("Select Download folder" , System.home()); 241 | if (fn) { 242 | DB.data.o.downloadDir = URL.toPath(fn); 243 | DB.save(); 244 | $(#setting).value = DB.data.o; 245 | } 246 | } 247 | 248 | DB.checkSubs(); 249 | self.timer(600s, DB.checkSubs); 250 | 251 | 252 | function MergeObjects(obj1, obj2) { 253 | for (var p in obj2) { 254 | try { 255 | if (typeof obj2[p] == #object) 256 | obj1[p] = MergeObjects(obj1[p], obj2[p]); 257 | else 258 | obj1[p] = obj2[p]; 259 | } catch(e) { obj1[p] = obj2[p]; } 260 | } 261 | return obj1; 262 | } 263 | --------------------------------------------------------------------------------
00:00 - 00:00