'use strict'; /* Services */ angular.module('podcasts.services', ['podcasts.database']) .service('downloader2', ['$http', '$q', 'xmlParser', function($http, $q, xmlParser) { return { downloadFile: function(url) { var deferred = $q.defer(); $http.get(url, {'responseType': 'blob'}) .success(function(file) { deferred.resolve(file); }) .error(function() { deferred.reject(); }); return deferred.promise; }, downloadXml: function(url) { var deferred = $q.defer(); $http.get(url) .success(function(xml) { deferred.resolve(xmlParser.parse(xml)); }) .error(function(data, status, headers, config) { deferred.reject(); }); return deferred.promise; } } }]) .service('feedItems', ['db', function(db) { return { db: db, get: function(id, onSuccess, onFailure) { this.db.getCursor("feedItem", function(ixDbCursorReq) { if(typeof ixDbCursorReq !== "undefined") { ixDbCursorReq.onsuccess = function (e) { var cursor = ixDbCursorReq.result || e.result; if (cursor) { onSuccess(cursor.value); } if (typeof onFailure === 'function') { onFailure(); } } } }, null, IDBKeyRange.only(id)); }, addFromXml: function(xml, feedId, onSuccess) { var newFeedItem = {}, searchableXml = angular.element(xml); newFeedItem.guid = searchableXml.find('guid').text(); newFeedItem.feedId = feedId; newFeedItem.title = searchableXml.find('title').text(); newFeedItem.link = searchableXml.find('link').text(); newFeedItem.date = Date.parse(searchableXml.find('pubDate').text()); newFeedItem.description = searchableXml.find('description').text(); newFeedItem.audioUrl = searchableXml.find('enclosure').attr('url'); this.db.put("feedItem", newFeedItem, undefined, function() { onSuccess(newFeedItem); }); }, list: function($scope) { this.db.getCursor("feedItem", function(ixDbCursorReq) { if(typeof ixDbCursorReq !== "undefined") { ixDbCursorReq.onsuccess = function (e) { var cursor = ixDbCursorReq.result || e.result; if (cursor) { $scope.queue.push(cursor.value); $scope.$apply(); cursor.continue(); } } } }); } } }]) .directive('scroll', function() { return function(scope, element, attrs, feedItems) { var scroll = new iScroll(element[0]); }; }) .directive('pullToRefresh', function() { return function(scope, element, attrs, feedItems) { var myScroll, pullDownEl, pullDownOffset; pullDownEl = document.getElementById('pullDown'); pullDownOffset = pullDownEl.offsetHeight; //TODO: get ID from context somehow? myScroll = new iScroll(element[0], { useTransition: true, topOffset: pullDownOffset, onRefresh: function () { if (pullDownEl.className.match('loading')) { pullDownEl.className = ''; pullDownEl.querySelector('.pullDownLabel').innerHTML = 'Pull down to refresh...'; } }, onScrollMove: function () { if (this.y > 5 && !pullDownEl.className.match('flip')) { pullDownEl.className = 'flip'; pullDownEl.querySelector('.pullDownLabel').innerHTML = 'Release to refresh...'; this.minScrollY = 0; } else if (this.y < 5 && pullDownEl.className.match('flip')) { pullDownEl.className = ''; pullDownEl.querySelector('.pullDownLabel').innerHTML = 'Pull down to refresh...'; this.minScrollY = -pullDownOffset; } }, onScrollEnd: function () { if (pullDownEl.className.match('flip')) { pullDownEl.className = 'loading'; pullDownEl.querySelector('.pullDownLabel').innerHTML = 'Loading...'; scope.downloadItems(function(feedItem, feed) { if (undefined === feed) { myScroll.refresh(); } else { pullDownEl.querySelector('.pullDownLabel').innerHTML = 'Loading ' + feed.title + '...'; } }); } } }); scope.$watch( function() { return scope.queue; }, function() { myScroll.refresh(); }, true ); } }) .directive('hold', function() { return function(scope, element, attrs) { var startTime, moved, holdTimer = false; element.bind('touchstart', function(event) { startTime = new Date().getTime(); clearTimeout(holdTimer); holdTimer = setTimeout(function() { scope.$eval(attrs.hold); }, 500); }); element.bind('touchmove', function() { clearTimeout(holdTimer); }); element.bind('touchend', function(event) { if (new Date().getTime() - startTime > 500) { event.preventDefault(); } else { clearTimeout(holdTimer); element[0].click(); } }); } }) .service('feeds', ['db', 'downloader2', 'xmlParser', 'feedItems', function(db, downloader2, xmlParser, feedItems) { return { db: db, feeds: [], add: function(url) { var feedService = this; var finishSave = function(newFeed) { db.put("feed", newFeed, undefined, function(key) { newFeed.id = key; feedService.feeds.push(newFeed); feedService.downloadItems(newFeed); }); }; // TODO: verify URL format somewhere var promise = downloader2.downloadXml(url); promise.then(function(xml) { var channelChildren = xml.find('channel').children(), newFeed = {}, imageUrl; angular.forEach(channelChildren, function(value, key) { if ('itunes:image' === angular.element(value)[0].nodeName.toLowerCase()) { imageUrl = angular.element(value).attr('href'); } if ('itunes:author' === angular.element(value)[0].nodeName.toLowerCase()) { newFeed.author = angular.element(value).text(); } }); newFeed.url = url; newFeed.title = channelChildren.find('title').text(); newFeed.summary = channelChildren.find('description').text(); var file = downloader2.downloadFile(imageUrl); file.then(function(fileBlob) { newFeed.image = fileBlob; finishSave(newFeed); }, function() { finishSave(newFeed); }); }, function() { console.log('Could not fetch XML for feed, adding just URL for now'); var newFeed = {}; newFeed.url = url; finishSave(newFeed); }); }, get: function(id, onSuccess, onFailure) { id = parseInt(id, 10); this.db.getCursor("feed", function(ixDbCursorReq) { if(typeof ixDbCursorReq !== "undefined") { ixDbCursorReq.onsuccess = function (e) { var cursor = ixDbCursorReq.result || e.result; if (cursor) { var feed = cursor.value; if (typeof feed.image === 'string') { feed.image = new Blob([feed.image]); } db.getCursor("feedItem", function(ixDbCursorReq) { feed.items = []; if(typeof ixDbCursorReq !== "undefined") { ixDbCursorReq.onsuccess = function (e) { var cursor = ixDbCursorReq.result || e.result; if (cursor) { feed.items.push(cursor.value); cursor.continue(); } else { onSuccess(feed); } } } }, null, IDBKeyRange.only(feed.id), undefined, 'ixFeedId'); //onSuccess(cursor.value); } else { onFailure(cursor); } } ixDbCursorReq.onerror = function (e) { onFailure(e); } } }, undefined, IDBKeyRange.only(id)); }, list: function($scope) { var feeds = this.feeds; db.getCursor("feed", function(ixDbCursorReq) { if(typeof ixDbCursorReq !== "undefined") { ixDbCursorReq.onsuccess = function (e) { var cursor = ixDbCursorReq.result || e.result; if (cursor) { if (typeof cursor.value.image === 'string') { cursor.value.image = new Blob([cursor.value.image], {type: 'application/octet-stream'}); } feeds.push(cursor.value); $scope.$apply(); cursor.continue(); } } } }); }, /** * * @param feedItems * @param updateStatus function that gets called for each item it goes through * Takes the feedItem as the argument */ downloadAllItems: function(feedItems, updateStatus) { var feedService = this; db.getCursor("feed", function(ixDbCursorReq) { if(typeof ixDbCursorReq !== "undefined") { ixDbCursorReq.onsuccess = function (e) { var cursor = ixDbCursorReq.result || e.result; if (cursor) { feedService.downloadItems(cursor.value, updateStatus); cursor.continue(); } else { updateStatus(); } } } }); }, downloadItems: function(feedItem, updateStatus) { var promise = downloader2.downloadXml(feedItem.url); promise.then(function(data) { angular.forEach( data.find('item'), function(element, index) { if (index < 3) { // TODO: this should be a global setting feedItems.addFromXml(element, feedItem.id, function(item) { if (typeof updateStatus === 'function') { updateStatus(item, feedItem); } }); } } ); }); } } }]) .value('xmlParser', { parse: function(data) { return angular.element(new window.DOMParser().parseFromString(data, "text/xml")); } }) .directive('blob', function() { return function postLink(scope, element, attrs) { var updateImage = function () { var blob = scope.$eval(attrs.blob); if (blob !== undefined) { var imgUrl = window.URL.createObjectURL(blob); element.attr('src', imgUrl); window.URL.revokeObjectURL(imgUrl); } }; scope.$watch( function() { return scope.$eval(attrs.blob); }, function() { updateImage(); }, true ); }; }) .directive('setting', function() { return { restrict: 'A', require: '?ngModel', priority: 1, link: function(scope, element, attrs, ngModel) { if (!ngModel) { return; } ngModel.$render = function() { console.log(ngModel); if (ngModel.$modelValue.value !== '') { ngModel.$viewValue = ngModel.$modelValue.value; } }; } }; }) .service('settings', ['db', function(db) { return { db: db, set: function (name, value, key) { if (key) { var setting = {'id': key, 'name': name, 'value': value}; } else { var setting = {'name': name, 'value': value}; } this.db.put("setting", setting); }, get: function (name, onSuccess, onFailure) { this.db.getCursor("setting", function(ixDbCursorReq) { if(typeof ixDbCursorReq !== "undefined") { ixDbCursorReq.onsuccess = function (e) { var cursor = ixDbCursorReq.result || e.result; if (cursor) { onSuccess(cursor.value); } else { onFailure(); } } ixDbCursorReq.onerror = function (e) { onFailure(); } } }, undefined, IDBKeyRange.only(name), undefined, 'ixName'); }, setAllValuesInScope: function(scope) { this.db.getCursor("setting", function(ixDbCursorReq) { if(typeof ixDbCursorReq !== "undefined") { ixDbCursorReq.onsuccess = function (e) { var cursor = ixDbCursorReq.result || e.result; if (cursor) { scope[cursor.value.name] = cursor.value; scope.$apply(); cursor.continue(); } } } }); } } }]) .service('player', ['db', function(db) { return { db: db, audio: angular.element(document.getElementById('audioPlayer')), nowPlaying: {position: 0, duration: 0, title: '', description: '', feed: '', date: 0}, play: function (feedItem, $scope) { if (feedItem) { var audioSrc; if (feedItem.audio) { var URL = window.URL || window.webkitURL; audioSrc = URL.createObjectURL(feedItem.audio); } else { audioSrc = feedItem.audioUrl; } this.audio.attr('src', audioSrc); this.updateSong(feedItem, $scope); if (feedItem.position) { this.audio.bind('canplay', function(event) { this.currentTime = feedItem.position; }); } } this.audio[0].play(); var db = this.db; this.audio.bind('pause', function(event) { feedItem.position = Math.floor(event.target.currentTime); db.put("feedItem", feedItem); }); }, pause: function() { this.audio[0].pause(); }, playing: function() { return !this.audio[0].paused; }, updateSong: function(feedItem, $scope) { this.nowPlaying.title = feedItem.title; var audio = this.audio[0], player = this; setTimeout(function() { player.nowPlaying.duration = audio.duration; }, 100); this.nowPlaying.feedItem = feedItem; this.nowPlaying.description = feedItem.description; this.nowPlaying.feed = feedItem.feed; this.nowPlaying.date = feedItem.date; this.updatePosition($scope); }, updatePosition: function($scope) { var audio = this.audio[0], player = this; setInterval(function() { player.nowPlaying.position = audio.currentTime; $scope.$apply(); }, 1000); } } }]) .service('pageSwitcher', ['$location', '$route', function($location, $route) { return { //TODO: change these getElementById's to something else pageSwitcher: document.getElementById('pageSwitcher'), pages: ['queue', 'settings', 'feeds'], $route: $route, currentPage: null, backPage: null, change: function(current) { this.currentPage = current; var nextPage = this.getNextPage(this.currentPage); angular.element(document.getElementById('pageswitch-icon-' + this.currentPage)) .addClass('next').removeClass('next1 next2'); angular.element(document.getElementById('pageswitch-icon-' + nextPage)) .addClass('next1').removeClass('next next2'); angular.element(document.getElementById('pageswitch-icon-' + this.getNextPage(nextPage))) .addClass('next2').removeClass('next next1'); }, setBack: function(backPage) { this.backPage = backPage; console.log(this); }, getNextPage: function(current) { var nextPage, pages = this.pages, validRoute = false; angular.forEach(pages, function(value, key) { if (current === value) { var nextKey = key + 1; if (pages[nextKey]) { nextPage = pages[nextKey]; } else { nextPage = pages[0]; } } }); angular.forEach(this.$route.routes, function(value, key) { if (key === '/' + nextPage) { validRoute = true; } }); if (!validRoute) { console.error('no valid route found for pageSwitcher: ' + nextPage); } return nextPage; }, goToPage: function(page) { if (!page) { if (this.backPage) { page = this.backPage; } else { page = this.getNextPage(this.currentPage); } } if (this.backPage) { this.backPage = null; } $location.path('/'+page); } } }]) .service('downloader', ['db', '$http', 'settings', function(db, $http, settings) { return { db: db, http: $http, settings: settings, allowedToDownload: function(result) { settings.get('downloadOnWifi', function(setting) { if (setting.value) { // check if we're on wifi result(false); } else { result(true); } }, function() { result(true); // Default value is "allowed" - maybe change this? }); }, downloadAll: function() { var downloader = this; this.allowedToDownload(function(value) { if (!value) { alert('not Downloading because not on WiFi'); } else { var itemsToDownload = []; downloader.db.getCursor("feedItem", function(ixDbCursorReq) { if(typeof ixDbCursorReq !== "undefined") { ixDbCursorReq.onsuccess = function (e) { var cursor = ixDbCursorReq.result || e.result; if (cursor) { if (!cursor.value.audio && cursor.value.audioUrl) { itemsToDownload.push(cursor.value); } cursor.continue(); } else { downloader.downloadFiles(itemsToDownload); } } } }); } }); }, downloadFiles: function(itemsToDownload) { var item = itemsToDownload.shift(), downloader = this; if (!item) { return; } this.http.get(item.audioUrl, {'responseType': 'blob'}).success(function(data) { item.audio = data; db.put("feedItem", item); downloader.downloadFiles(itemsToDownload); }); } }; }]) .filter('time', function() { return function(input, skip) { var seconds, minutes, hours; seconds = Math.floor(input); if (seconds > 120) { minutes = Math.floor(seconds/60); seconds = seconds % 60; } if (minutes > 60) { minutes = Math.floor(minutes/60); hours = minutes % 60; } if (hours) { return hours + ':' + minutes + ':' + seconds; } else if (minutes) { return minutes + ':' + seconds; } else { if (skip) { return seconds; } return seconds + 's'; } } }) .filter('timeAgo', function() { return function(timestamp) { var diff = ((new Date().getTime()) - (new Date(timestamp).getTime())) / 1000, day_diff = Math.floor(diff / 86400); return day_diff == 0 && ( diff < 60 && "just now" || diff < 120 && "1 minute ago" || diff < 3600 && Math.floor( diff / 60 ) + " minutes ago" || diff < 7200 && "1 hour ago" || diff < 86400 && Math.floor( diff / 3600 ) + " hours ago") || day_diff == 1 && "Yesterday" || day_diff < 7 && day_diff + " days ago" || day_diff < 31 && Math.ceil( day_diff / 7 ) + " weeks ago" || "older than a month"; } }); angular.module('podcasts.database', []). run(function() { var dbConfig = (function () { //Create IndexedDB ObjectStore and Indexes via ixDbEz ixDbEz.createObjStore("feed", "id", true); ixDbEz.createIndex("feed", "ixUrl", "url", true); ixDbEz.createObjStore("feedItem", "id", true); ixDbEz.createIndex("feedItem", "ixGuid", "guid", true); ixDbEz.createIndex("feedItem", "ixFeedId", "feedId"); ixDbEz.createObjStore("setting", "id", true); ixDbEz.createIndex("setting", "ixName", "name", true); }); //Create or Open the local IndexedDB database via ixDbEz ixDbEz.startDB("podcastDb", 7, dbConfig, undefined, undefined, false); }) .value('db', ixDbEz); angular.module('podcasts.updater', []). run(function($timeout) { var checkFeeds = function() { console.log('TODO: trigger download here'); $timeout(checkFeeds, 1800000); // run every half an hour }; checkFeeds(); });