diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1fde536 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/build +/deploy \ No newline at end of file diff --git a/fxos/manifest.webapp b/fxos/manifest.webapp index 6557d23..2cf3a34 100755 --- a/fxos/manifest.webapp +++ b/fxos/manifest.webapp @@ -2,7 +2,7 @@ "name": "FoxCasts", "description": "A fully-featured podcast app for your FirefoxOS device.", - "version": "1.2.0", + "version": "1.3.0", "launch_path": "/index.html", "icons": { "256": "/assets/icon-256.png", diff --git a/icon.png b/icon.png index b210019..bf2317b 100755 Binary files a/icon.png and b/icon.png differ diff --git a/lib/xmlprequest/XmlpRequest.js b/lib/xmlprequest/XmlpRequest.js new file mode 100644 index 0000000..b5979fd --- /dev/null +++ b/lib/xmlprequest/XmlpRequest.js @@ -0,0 +1,79 @@ +/** + Allows for cross-domain JSONp style requests for XML documents, via YQL, + converted and returned in JSON format. + */ + +enyo.kind({ + name: "enyo.XmlpRequest", + kind: enyo.JsonpRequest, + published: { + /** + If true, on a query failure, it will re-query the XML url/params + to get the plain-text content and return it in the format of + + {"error":"unescaped-plain-text-content"} + */ + detailedErrors:true + }, + //* @protected + callbackName: "callback", + //* @public + //* Executes the XML document request via YQL, with an optional parameters object + go: function(inParams) { + var parts = this.url.split("?"); + var uri = parts.shift() || ""; + var args = parts.join("?").split("&"); + args.push(enyo.Ajax.objectToQuery(inParams || {})); + this.xmlUrl = [uri, args.join("&")].join("?"); + this.url = "http://query.yahooapis.com/v1/public/yql?q=select%20*%20from%20xml%20where%20url%3D'" + + encodeURIComponent(this.xmlUrl) + "'&format=json"; + inParams = {}; + this.inherited(arguments); + }, + //* @protected + respond: function(inValue) { + if(inValue && inValue.query) { + inValue = inValue.query.results || undefined; + if(!inValue && this.detailedErrors) { + var fallbackUrl = "http://query.yahooapis.com/v1/public/yql?q=use%20%22http" + + "%3A%2F%2Fwww.datatables.org%2Fdata%2Fhtmlstring.xml%22%20" + + "as%20htmlstring%3B%20select%20*%20from%20htmlstring%20where" + + "%20url%3D'"+ encodeURIComponent(this.xmlUrl) + "'&format=json"; + this.fallbackTextRequest = new enyo.JsonpRequest({url:fallbackUrl, + callbackName:"callback"}); + this.fallbackTextRequest.response(this, "fallbackTextCallback"); + this.fallbackTextRequest.go(); + return; + } + } + this.inherited(arguments); + }, + fallbackTextCallback: function(inSender, inResponse) { + if(inResponse && inResponse.query) { + inResponse = inResponse.query.results || undefined; + if(inResponse && inResponse.result) { + inResponse = inResponse.result; + //plain text is within auto-generated html, so extract it + var prefix = "\n \n \n \n \n \n \n \n \n \n \n \n </head>\n \n \n <body>\n \n \n <p>" + var suffix = "</p>\n \n \n </body>\n \n\n</html>"; + inResponse = inResponse.replace(prefix, ""); + + inResponse = inResponse.replace(suffix, ""); + var iOpen = inResponse.indexOf("<"); + var iClose = inResponse.lastIndexOf(">"); + + //if still contains html, we know it wasn't plain text to begin with + if(iOpen>-1 && iOpen<iClose) { + inResponse = {"error":"Query is not in XML format"}; + } else { + inResponse = {"error":inResponse}; + } + } else if(inResponse && !inResponse.result) { + inResponse = {"error":"Unknown XML query failure"}; + } else if(!inResponse) { + inResponse = {"error":"Unable to connect to server"}; + } + } + this.respond(inResponse); + } +}); \ No newline at end of file diff --git a/lib/xmlprequest/package.js b/lib/xmlprequest/package.js new file mode 100644 index 0000000..1522e22 --- /dev/null +++ b/lib/xmlprequest/package.js @@ -0,0 +1,3 @@ +enyo.depends( + 'XmlpRequest.js' +); diff --git a/luneos/appinfo.json b/luneos/appinfo.json new file mode 100644 index 0000000..61f5151 --- /dev/null +++ b/luneos/appinfo.json @@ -0,0 +1,11 @@ +{ + "title": "FoxCasts", + "id": "com.choorp.app.foxcasts", + "version": "1.3.0", + "vendor": "Garrett Downs", + "vendor_url": "https://github.com/choorp/foxcasts", + "type": "web", + "main": "index.html", + "icon": "icon.png", + "uiRevision": "2" +} \ No newline at end of file diff --git a/luneos/framework_config.json b/luneos/framework_config.json new file mode 100644 index 0000000..c2503a6 --- /dev/null +++ b/luneos/framework_config.json @@ -0,0 +1,7 @@ +{ + "logLevel": 99, + "debuggingEnabled": true, + "timingEnabled": false, + "logEvents": true, + "escapeHTMLInTemplates" : true +} \ No newline at end of file diff --git a/source/package.js b/source/package.js index 3674b77..1892ad6 100755 --- a/source/package.js +++ b/source/package.js @@ -1,9 +1,7 @@ enyo.depends( - // Layout library "$lib/layout", - // Onyx UI library - "$lib/onyx", // To theme Onyx using Theme.less, change this line to $lib/onyx/source, - // CSS/LESS style files + "$lib/onyx", + "$lib/xmlprequest", "style", // Model and data definitions "data", diff --git a/source/style/main.less b/source/style/main.less index 99e57d3..befc7f9 100755 --- a/source/style/main.less +++ b/source/style/main.less @@ -125,6 +125,7 @@ a { position: relative; margin: 2px 2px -2px 2px; width: 32%; /* desired width */ + max-width: 200px; } .podcast-tile:before { content: ""; diff --git a/source/views/App.js b/source/views/App.js index 33c97e4..24eb507 100755 --- a/source/views/App.js +++ b/source/views/App.js @@ -8,7 +8,7 @@ enyo.kind({ fit: true, components:[ {kind: "Panels", name: "menuSlider", fit:true, classes: "", arrangerKind: "CarouselArranger", index: 1, draggable: true, narrowFit: false, onTransitionFinish: "", components: [ - {name: "menu", kind: "Menu", style: "width: 80vw;", onChangePanel: "changePanel", onRefreshAll: "checkForUpdates"}, + {name: "menu", kind: "Menu", style: "width: 80vw; max-width: 300px;", onChangePanel: "changePanel", onRefreshAll: "checkForUpdates"}, {kind: "Panels", fit:true, classes: "panels-container", arrangerKind: "CardArranger", index: 1, draggable: false, onTransitionFinish: "panelChanged", components: [ {name: "player", kind: "Player"}, {name: "subscriptionsGrid", kind: "SubscriptionsGrid", onOpenPodcast: "showPodcastDetail"}, diff --git a/source/views/FeedParser.js b/source/views/FeedParser.js index f7ca060..657710e 100755 --- a/source/views/FeedParser.js +++ b/source/views/FeedParser.js @@ -1,76 +1,151 @@ -var ParseFeed = function(xml, podcast, dateLimit) { - // console.log("Begin parseFeed"); +var ParseFeed = function(xml, podcast, dateLimit, dataType) { + console.log("Begin parseFeed"); + console.log(xml); if (!dateLimit) { dateLimit = 0; } - var items = xml.getElementsByTagName("item"); + var episodes = []; - for (var i=0; i< items.length; i++) { - var e = items[i]; - // console.log(e); + // if (dataType == 'xml') { + if (xml.contentType && xml.contentType == 'text/xml') { + console.log('Parsing episodes as XML'); - var date, author, title, description, abort; - try { - date = e.getElementsByTagName("pubDate")[0].textContent; - date = Date.parse(date); - // console.log("Most recent: " + podcast.mostRecent + " This Episode: " + date); - // console.log("Datelimit: " + dateLimit); - if (date <= dateLimit) { - console.log("Episode " + i + " is not new. Stopping."); - break; - } else { - // console.log("Episode " + i + " is new. Continuing."); + var items = xml.getElementsByTagName("item"); + for (var i = 0; i < items.length; i++) { + var e = items[i]; + // console.log(e); + + var date, author, title, description, abort; + try { + date = e.getElementsByTagName("pubDate")[0].textContent; + date = Date.parse(date); + // console.log("Most recent: " + podcast.mostRecent + " This Episode: " + date); + // console.log("Datelimit: " + dateLimit); + if (date <= dateLimit) { + console.log("Episode " + i + " is not new. Stopping."); + break; + } else { + // console.log("Episode " + i + " is new. Continuing."); + } + } catch (e) { + date = "?"; + abort = true; + } + try { + author = e.getElementsByTagName("author")[0].textContent; + } catch (e) { + author = "?"; + } + try { + title = e.getElementsByTagName("title")[0].textContent; + } catch (e) { + title = "?"; + } + try { + description = e.getElementsByTagName("description")[0].textContent; + } catch (e) { + description = "?"; + } + + // Let's see if this episode is even worth keeping around + try { + enclosure = e.getElementsByTagName("enclosure")[0].getAttribute("url"); + abort = false; + } catch (e) { + abort = true; } - } catch (e) { - date = "?"; - abort = true; - } - try { - author = e.getElementsByTagName("author")[0].textContent; - } catch (e) { - author = "?"; - } - try { - title = e.getElementsByTagName("title")[0].textContent; - } catch (e) { - title = "?"; - } - try { - description = e.getElementsByTagName("description")[0].textContent; - } catch (e) { - description = "?"; - } - // Let's see if this episode is even worth keeping around - try { - enclosure = e.getElementsByTagName("enclosure")[0].getAttribute("url"); - abort = false; - } catch (e) { - abort = true; + if (!abort) { + episodes.push({ + name: podcast.name || podcast.collectionName, + date: date, + author: author, + title: title, + description: description, + logo100: podcast.artworkUrl100, + logo600: podcast.artworkUrl600, + length: e.getElementsByTagName("enclosure")[0].getAttribute("length"), + type: e.getElementsByTagName("enclosure")[0].getAttribute("type"), + fileUrl: e.getElementsByTagName("enclosure")[0].getAttribute("url"), + localUrl: "", + progress: 0, + duration: 0, + played: "false", + downloaded: "false", + inprogress: "false" + }); + } } + } else { + console.log('Parsing episodes as JSON'); + + var items = xml.rss.channel.item; + for (var i = 0; i < items.length; i++) { + var e = items[i]; + // console.log(e); - if (!abort) { - episodes.push({ - name: podcast.name || podcast.collectionName, - date: date, - author: author, - title: title, - description: description, - logo100: podcast.artworkUrl100, - logo600: podcast.artworkUrl600, - length: e.getElementsByTagName("enclosure")[0].getAttribute("length"), - type: e.getElementsByTagName("enclosure")[0].getAttribute("type"), - fileUrl: e.getElementsByTagName("enclosure")[0].getAttribute("url"), - localUrl: "", - progress: 0, - duration: 0, - played: "false", - downloaded: "false", - inprogress: "false" - }); + var date, author, title, description, abort; + try { + date = e.pubDate; + date = Date.parse(date); + // console.log("Most recent: " + podcast.mostRecent + " This Episode: " + date); + // console.log("Datelimit: " + dateLimit); + if (date <= dateLimit) { + console.log("Episode " + i + " is not new. Stopping."); + break; + } else { + // console.log("Episode " + i + " is new. Continuing."); + } + } catch (e) { + date = "?"; + abort = true; + } + try { + author = e.author[0]; + } catch (e) { + author = "?"; + } + try { + title = e.title; + } catch (e) { + title = "?"; + } + try { + description = e.description; + } catch (e) { + description = "?"; + } + + // Let's see if this episode is even worth keeping around + if (!e.enclosure.url) { + abort = true; + } else { + abort = false; + } + + if (!abort) { + episodes.push({ + name: podcast.name || podcast.collectionName, + date: date, + author: author, + title: title, + description: description, + logo100: podcast.artworkUrl100, + logo600: podcast.artworkUrl600, + length: e.enclosure.length, + type: e.enclosure.type, + fileUrl: e.enclosure.url, + localUrl: "", + progress: 0, + duration: 0, + played: "false", + downloaded: "false", + inprogress: "false" + }); + } } } diff --git a/source/views/PodcastPreview.js b/source/views/PodcastPreview.js index 514cdb5..28a320d 100755 --- a/source/views/PodcastPreview.js +++ b/source/views/PodcastPreview.js @@ -78,29 +78,48 @@ enyo.kind({ this.$.episodes.destroyClientControls(); this.$.spinner.setShowing(true); - var xmlhttp = new XMLHttpRequest({mozSystem: true}); - xmlhttp.open("GET", this.podcast.feedUrl, true); - xmlhttp.responseType = "xml"; - xmlhttp.onreadystatechange = enyo.bind(this, function(response) { - if (xmlhttp.readyState == 4 && xmlhttp.status == 200) { - // console.log(xmlhttp); - var parser = new DOMParser(); - var xml = parser.parseFromString(xmlhttp.response, "text/xml"); - this.gotEpisodes(xml); - } - }); - xmlhttp.send(); + if (enyo.platform.firefoxOS) { + var xmlhttp = new XMLHttpRequest({mozSystem: true}); + xmlhttp.open("GET", this.podcast.feedUrl, true); + xmlhttp.responseType = "xml"; + xmlhttp.onreadystatechange = enyo.bind(this, function (response) { + if (xmlhttp.readyState == 4 && xmlhttp.status == 200) { + // console.log(xmlhttp); + var parser = new DOMParser(); + var xml = parser.parseFromString(xmlhttp.response, "text/xml"); + this.gotEpisodes(xml, 'xml'); + } + }); + xmlhttp.send(); + } else { + this.log('Not FxOS'); + var request = new enyo.XmlpRequest({ + url: this.podcast.feedUrl + }); + request.response(enyo.bind(this, function(inRequest, inResponse) { + console.log(inResponse); + this.gotEpisodes(inResponse, 'json'); + })); + request.go(); + } }, - gotEpisodes: function(xml) { + gotEpisodes: function(response, type) { // Try to get the summary - var summary = xml.getElementsByTagName("summary"); - if (summary.length > 0) { - this.podcast.summary = summary[0].textContent; + if(type == 'xml') { + var summary = response.getElementsByTagName("summary"); + if (summary.length > 0) { + this.podcast.summary = summary[0].textContent; + } else { + this.podcast.summary = "No summary available."; + } } else { - this.podcast.summary = "No summary available."; + this.podcast.summary = response.rss.channel.summary || 'No summary available'; } - this.episodes = ParseFeed(xml, this.podcast); + this.processEpisodes(response, type); + }, + processEpisodes: function(response, type) { + this.episodes = ParseFeed(response, this.podcast, 0, type); this.log("Found " + this.episodes.length + " episodes."); // this.log(this.episodes[0]); diff --git a/source/views/Search.js b/source/views/Search.js index a8340a7..9c76654 100755 --- a/source/views/Search.js +++ b/source/views/Search.js @@ -38,13 +38,7 @@ enyo.kind({ var url = "https://itunes.apple.com/search?media=podcast&term=" + query; - if (enyo.platform.webos) { - var request = new enyo.JsonpRequest({ - url: url - }); - request.response(this, "gotResults"); - request.go(); - } else { + if (enyo.platform.firefoxOS) { var xmlhttp = new XMLHttpRequest({mozSystem: true}); xmlhttp.open("GET", url, true); xmlhttp.setRequestHeader("Content-type", "application/json"); @@ -52,20 +46,26 @@ enyo.kind({ xmlhttp.onreadystatechange = enyo.bind(this, function(response) { if (xmlhttp.readyState == 4 && xmlhttp.status == 200) { console.log(xmlhttp); - this.gotResults(xmlhttp.response); + this.gotResults(xmlhttp.response.results); } }); xmlhttp.send(); - } + } else { + var request = new enyo.JsonpRequest({ + url: url + }); + request.response(enyo.bind(this, function(req, res) { + this.gotResults(res.results); + })); + request.go(); + } }, - gotResults: function(response) { + gotResults: function(results) { this.$.spinner.setShowing(false); - - this.log(response); - // response = JSON.parse(response); - // this.log(response); - this.results = response.results; - var r = response.results; + + this.log('gotResults', results); + this.results = results; + var r = results; this.$.results.destroyClientControls(); diff --git a/source/views/Services.js b/source/views/Services.js index 4294e3e..7b27a14 100644 --- a/source/views/Services.js +++ b/source/views/Services.js @@ -427,27 +427,40 @@ PodcastManager.updateNextPodcast = function() { }; PodcastManager.requestPodcastRefresh = function(_this, podcast) { - // console.log("PodcastManager.requestPodcastRefresh(): Event = " + podcast.name); - - var xmlhttp = new XMLHttpRequest({mozSystem: true}); - xmlhttp.open("GET", podcast.feedUrl, true); - xmlhttp.responseType = "xml"; - xmlhttp.onreadystatechange = enyo.bind(this, function(response) { - // this.log(response); - if (xmlhttp.readyState == 4 && xmlhttp.status == 200) { - var parser = new DOMParser(); - var xml = parser.parseFromString(xmlhttp.response, "text/xml"); - PodcastManager.processPodcastRefresh(_this, podcast, xml); - } - }); - xmlhttp.send(); + console.log("PodcastManager.requestPodcastRefresh(): Event = " + podcast.name); + + if (enyo.platform.firefoxOS) { + var xmlhttp = new XMLHttpRequest({mozSystem: true}); + xmlhttp.open("GET", podcast.feedUrl, true); + xmlhttp.responseType = "xml"; + xmlhttp.onreadystatechange = enyo.bind(this, function (response) { + if (xmlhttp.readyState == 4 && xmlhttp.status == 200) { + // console.log(xmlhttp); + var parser = new DOMParser(); + var xml = parser.parseFromString(xmlhttp.response, "text/xml"); + // this.gotEpisodes(xml, 'xml'); + PodcastManager.processPodcastRefresh(_this, podcast, xml); + } + }); + xmlhttp.send(); + } else { + console.log('Not FxOS'); + var request = new enyo.XmlpRequest({ + url: podcast.feedUrl + }); + request.response(enyo.bind(this, function(inRequest, inResponse) { + console.log(inResponse); + // this.gotEpisodes(inResponse, 'json'); + PodcastManager.processPodcastRefresh(_this, podcast, inResponse); + })); + request.go(); + } }; -PodcastManager.processPodcastRefresh = function(_this, podcast, xml) { - // console.log("PodcastManager.processPodcastRefresh(): Event = " + podcast.name); - - var items = xml.getElementsByTagName("item"); - var episodes = ParseFeed(xml, podcast, podcast.latest); +PodcastManager.processPodcastRefresh = function(_this, podcast, data) { + console.log("PodcastManager.processPodcastRefresh(): Event = " + podcast.name); + + var episodes = ParseFeed(data, podcast, podcast.latest); // No new episodes if (episodes.length === 0) { diff --git a/source/views/deploy.json b/source/views/deploy.json new file mode 100644 index 0000000..c4ff58b --- /dev/null +++ b/source/views/deploy.json @@ -0,0 +1,6 @@ +{ + "enyo": "./enyo", + "packagejs": "./package.js", + "assets": ["./icon.png", "./index.html", "./assets", "./appinfo.json", "./framework_config.json"], + "libs": ["./lib/layout", "./lib/onyx", "./lib/xmlprequest"] +} \ No newline at end of file diff --git a/tools/deploy.sh b/tools/deploy.sh index 3f31170..1313489 100755 --- a/tools/deploy.sh +++ b/tools/deploy.sh @@ -66,5 +66,31 @@ while [ "$1" != "" ]; do cp -a "$SRC/webOS/." "$DEST" esac + case $1 in --luneos ) + echo "Packaging for LuneOS" + + # Copy LuneOS files + DEST="$SRC/deploy/" + cp -a "$SRC/luneos/." "$DEST" + + # Package it as an ipk + palm-package -o ./deploy ./deploy + esac + case $1 in --luneos-deploy ) + echo "Packaging for LuneOS" + + # Copy LuneOS files + DEST="$SRC/deploy/" + cp -a "$SRC/luneos/." "$DEST" + + # Package it as an ipk + palm-package -o ./deploy ./deploy + + # Locate the new ipk and store in a variable + ipkLocation="$(find ./deploy -maxdepth 1 -name '*.ipk' -print -quit)" + + # Copy the ipk over to the LuneOS emulator + scp -P 5522 $ipkLocation root@localhost:/media/internal + esac shift done \ No newline at end of file