diff --git a/backend/backgrounds.js b/backend/backgrounds.js new file mode 100644 index 0000000..992735d --- /dev/null +++ b/backend/backgrounds.js @@ -0,0 +1,221 @@ +'use strict'; + +const request = require('request'); +const fs = require('fs'); +const path = require('path'); +const fsExtra = require('fs-extra'); +const download = require('download'); +const electron = require('electron'); +const sha1 = require('sha1'); +const debug = require('debug')('farnsworth:backgrounds'); +const _ = require('lodash'); + +const app = electron.app; +const ipc = electron.ipcMain; + +// URL where our backgrounds are +const BACKGROUND_URL = 'https://raw.githubusercontent.com/dconnolly/chromecast-backgrounds/master/backgrounds.json'; +const BACKGROUNDS_SAVE_PATH = app.getPath('userData') + '/backgrounds.json'; +const BACKGROUNDS_DIR = app.getPath('userData') + '/backgrounds'; +const BACKGROUND_DOWNLOAD_THREADS = 4; + +function cleanBackgrounds(backgrounds) { + console.log('Cleaning leftover backgrounds...'); + + // Clean up any backgrounds we don't have in the list anymore. + fs.readDir(BACKGROUND_SAVE_PATH, function(error, files) { + if(!error) { + _.each(files, function(f) { + var filename = path.basename(f, path.extname(f)); + + if(!_.find(backgrounds, function(entry) { + return path.basename(f) === entry.filename; + })) { + console.log(`Deleting ${f}`); + fs.unlink(f); + } + }); + } + }); +} + +// Download all of the available backgrounds locally just in case we +// need them when we don't have an internet connection. +function downloadBackgrounds() { + var ready = false; + + ipc.on('background-service-ready', function(ev) { + var bgService = ev.sender; + + function doDownload(backgrounds, index, bgAvailable) { + return new Promise(function(reject, resolve) { + if(index < backgrounds.length) { + if(backgrounds[index].downloaded) { + doDownload(backgrounds, index + 1).then(resolve, reject); + } else { + // Since the URLs contain a variety of filenames, for easiest + // portability, we're just going to generate a new filename + // based on the sha1 hash of the URL. Overkill? Maybe. + // Problematic for other reasons? Also possible. + // + // As one of the dads would say: "Good enough for who it's for." + debug(' - Downloading...'); + backgrounds[index].filename = `${sha1(backgrounds[index].url)}${path.extname(backgrounds[index].url)}`; + var filename = path.join(BACKGROUNDS_DIR, backgrounds[index].filename); + + console.log(`Downloading ${backgrounds[index].url} to ${filename}...`); + download(backgrounds[index].url) + .pipe(fs.createWriteStream(filename)) + .on('close', function() { + // Set the probably unnecessary downloaded flag to true. + backgrounds[index].downloaded = true; + + // If we haven't told the renderer that a bg is available, do + // so now. + if(!bgAvailable) { + bgAvailable = true; + bgService.send('background-available', index, backgrounds[index]); + } + + bgService.send('background-data-available', backgrounds); + + // Save the backgrounds file. Possibly a minor performance impact + // serializing JSON and writing to disk every time we download an + // image but...see above dad quote. + fs.writeFile(BACKGROUNDS_SAVE_PATH, JSON.stringify(backgrounds), function(error) { + if(error) { + bgWindow.send('backgrounds-error', 'Could not save backgrounds list', error); + } + + doDownload(backgrounds, index + 1).then(resolve, reject); + }); + }).on('error', function(error) { + console.log(`Error downloading ${backgrounds[index].url}:`, error); + bgWindow.send('backgrounds-error', `Error downloading ${backgrounds[index].url}`, error); + doDownload(backgrounds, index + 1).then(resolve, reject); + }); + } + } else { + // Resolve only when we've processed all images. + debug('Completed all files, resolving.'); + resolve(); + } + }); + } + + console.log('Loading background images...'); + + // Try to be as quick as possible finding the first background. + // First, see if we already have background data. + fs.readFile(BACKGROUNDS_SAVE_PATH, function(error, data) { + var backgrounds = null; + var bgAvailable = false; + + if(!error) { + debug('Found existing backgrounds file, loading...'); + + // We do, try and load it and, if successful, tell the + // renderer that we have data and look for an actual + // downloaded image file. If we have at least one, let + // the renderer know we're ready to go. + try { + backgrounds = JSON.parse(data); + + bgService.send('background-data-available', backgrounds); + + var background = _.findIndex(backgrounds, function(entry) { + if(!entry.downloaded || !entry.filename) { + return false; + } + + try { + fs.accessSync(path.join(BACKGROUNDS_DIR, entry.filename), fs.R_OK); + return true; + } catch(e) { + return false; + } + }); + + if(background !== -1) { + debug('Found at least one available background image, notifying renderer.'); + bgAvailable = true; + bgService.send('background-available', backgrounds[background]); + } + } catch(e) { + backgrounds = null; + } + } + + // Now, load up the background file from the URL and, if successful, + // save it or update ours by adding new backgrounds and removing those + // that don't exist. + request(BACKGROUND_URL, function(error, response, body) { + if(error || response.statusCode !== 200) { + console.error('Error loading backgrounds from ' + BACKGROUND_URL, error); + bgService.send('backgrounds-error', 'Error loading backgrounds from ' + BACKGROUND_URL, error); + } else { + var newBgData = JSON.parse(body); + + // If we already have data, go through it and update it with + // data from the server. + if(backgrounds !== null) { + debug(`Updating data with new entries from remote. Existing count: ${backgrounds.length}, remote length: ${newBgData.length}`); + _.each(newBgData, function(entry) { + if(!_.find(backgrounds, function(existing) { + return entry.url === existing.url; + })) { + backgrounds.push(entry); + } + }); + + debug(`New count: ${backgrounds.length}, removing backgrounds that have been removed from remote.`); + _.remove(backgrounds, function(entry) { + return !_.find(newBgData, function(newEntry) { + return newEntry.url === entry.url; + }); + }); + + debug(`New count: ${backgrounds.length}.`); + } else { + backgrounds = newBgData; + } + + debug(`Total backgrounds: ${backgrounds.length}`); + // Notify the renderer that new data is available. Even + // though we've already notified them once, give them + // a chance to deal with new data being available. + bgService.send('background-data-available', backgrounds); + + // Save the background file locally. + fs.writeFile(BACKGROUNDS_SAVE_PATH, JSON.stringify(backgrounds, null, 4), function(error) { + if(error) { + bgWindow.send('backgrounds-error', 'Could not save backgrounds list', error); + } + }); + + // Create the backgrounds directory + fsExtra.mkdirs(BACKGROUNDS_DIR, function(error) { + if(error) { + console.error('Could not create directory for backgrounds: ', error); + bgService.send('backgrounds-error', 'Could not create directory for backgrounds', error); + } else { + // Download all of the background images, succeeding whether or not + // the promise resolves or rejects. + doDownload(backgrounds, 0, bgAvailable).then(function() { + debug(`Background downloading succeeded.`); + bgService.send('backgrounds-downloaded'); + + cleanBackgrounds(backgrounds); + }, function(error) { + bgService.send('backgrounds-error', `Unhandled error downloading backgrounds: ${error}`); + throw error; + }); + } + }); + } + }); + }); + }); +} + +module.exports = downloadBackgrounds; diff --git a/main.js b/main.js index 2cabc6a..73827ab 100644 --- a/main.js +++ b/main.js @@ -2,13 +2,8 @@ const debug = require('debug')('farnsworth:main'); const electron = require('electron'); -const request = require('request'); -const fs = require('fs'); -const path = require('path'); -const fsExtra = require('fs-extra'); -const download = require('download'); -const sha1 = require('sha1'); -const _ = require('lodash'); +const spawn = require('spawn-shell'); +const downloadBackgrounds = require('./backend/backgrounds'); // Module to control application life. const app = electron.app; @@ -16,11 +11,6 @@ const app = electron.app; const ipc = electron.ipcMain; // Module to create native browser window. const BrowserWindow = electron.BrowserWindow; -// URL where our backgrounds are -const BACKGROUND_URL = 'https://raw.githubusercontent.com/dconnolly/chromecast-backgrounds/master/backgrounds.json'; -const BACKGROUNDS_SAVE_PATH = app.getPath('userData') + '/backgrounds.json'; -const BACKGROUNDS_DIR = app.getPath('userData') + '/backgrounds'; -const BACKGROUND_DOWNLOAD_THREADS = 4; // Keep a global reference of the window object, if you don't, the window will // be closed automatically when the JavaScript object is garbage collected. @@ -48,9 +38,6 @@ function createWindow () { // and load the index.html of the app. mainWindow.loadURL('file://' + __dirname + '/ui/index.html'); - // Open the DevTools. - mainWindow.webContents.openDevTools(); - // Emitted when the window is closed. mainWindow.on('closed', function() { // Dereference the window object, usually you would store windows @@ -64,210 +51,29 @@ function createWindow () { someWindow.webContents.goBack(); } }); - - mainWindow.on('background-service-ready', function(ev) { - downloadBackgrounds(ev.sender); - }); } -function cleanBackgrounds(backgrounds) { - console.log('Cleaning leftover backgrounds...'); - - // Clean up any backgrounds we don't have in the list anymore. - fs.readDir(BACKGROUND_SAVE_PATH, function(error, files) { - if(!error) { - _.each(files, function(f) { - var filename = path.basename(f, path.extname(f)); - - if(!_.find(backgrounds, function(entry) { - return path.basename(f) === entry.filename; - })) { - console.log(`Deleting ${f}`); - fs.unlink(f); - } - }); - } - }); -} +// Launch an application as requested by the renderer +ipc.on('launch-application', function(ev, command) { + command = command.replace(/ /g, '\\ '); -// Download all of the available backgrounds locally just in case we -// need them when we don't have an internet connection. -function downloadBackgrounds() { - ipc.on('background-service-ready', function(ev) { - var bgService = ev.sender; + debug(`Launching ${command}...`); - function doDownload(backgrounds, index, bgAvailable) { - debug(`Checking for background at index ${index}`); + var child = spawn(`"${command}"`); - return new Promise(function(reject, resolve) { - if(index < backgrounds.length) { - if(backgrounds[index].downloaded) { - debug(' - Already downloaded, skipping.'); - doDownload(backgrounds, index + 1).then(resolve, reject); - } else { - // Since the URLs contain a variety of filenames, for easiest - // portability, we're just going to generate a new filename - // based on the sha1 hash of the URL. Overkill? Maybe. - // Problematic for other reasons? Also possible. - // - // As one of the dads would say: "Good enough for who it's for." - debug(' - Downloading...'); - backgrounds[index].filename = `${sha1(backgrounds[index].url)}${path.extname(backgrounds[index].url)}`; - var filename = path.join(BACKGROUNDS_DIR, backgrounds[index].filename); + child.on('close', function(code) { + mainWindow.restore(); + mainWindow.focus(); - console.log(`Downloading ${backgrounds[index].url} to ${filename}...`); - download(backgrounds[index].url) - .pipe(fs.createWriteStream(filename)) - .on('close', function() { - debug(' - Download complete.'); + debug(`Command ${command} exited with code ${code}.`); - // Set the probably unnecessary downloaded flag to true. - backgrounds[index].downloaded = true; - - // If we haven't told the renderer that a bg is available, do - // so now. - if(!bgAvailable) { - bgAvailable = true; - bgService.send('background-available', index, backgrounds[index]); - } - - bgService.send('background-data-available', backgrounds); - - // Save the backgrounds file. Possibly a minor performance impact - // serializing JSON and writing to disk every time we download an - // image but...see above dad quote. - fs.writeFile(BACKGROUNDS_SAVE_PATH, JSON.stringify(backgrounds), function(error) { - if(error) { - bgWindow.send('backgrounds-error', 'Could not save backgrounds list', error); - } - - doDownload(backgrounds, index + 1).then(resolve, reject); - }); - }).on('error', function(error) { - console.log(`Error downloading ${backgrounds[index].url}:`, error); - bgWindow.send('backgrounds-error', `Error downloading ${backgrounds[index].url}`, error); - doDownload(backgrounds, index + 1).then(resolve, reject); - }); - } - } else { - // Resolve only when we've processed all images. - debug('Completed all files, resolving.'); - resolve(); - } - }); + if(code !== 0) { + ev.sender.send('application-launch-error', code); } - - console.log('Loading background images...'); - - // Try to be as quick as possible finding the first background. - // First, see if we already have background data. - fs.readFile(BACKGROUNDS_SAVE_PATH, function(error, data) { - var backgrounds = null; - var bgAvailable = false; - - if(!error) { - debug('Found existing backgrounds file, loading...'); - - // We do, try and load it and, if successful, tell the - // renderer that we have data and look for an actual - // downloaded image file. If we have at least one, let - // the renderer know we're ready to go. - try { - backgrounds = JSON.parse(data); - bgService.send('background-data-available', backgrounds); - - if(_.find(backgrounds, function(entry) { - if(!entry.downloaded || !entry.filename) { - return false; - } - - try { - fs.accessSync(entry.filename, fs.R_OK); - return true; - } catch(e) { - return false; - } - })) { - debug('Found at least one available background image, notifying renderer.'); - bgAvailable = true; - bgService.send('background-available', backgrounds[index]); - } - } catch(e) { - backgrounds = null; - } - } - - // Now, load up the background file from the URL and, if successful, - // save it or update ours by adding new backgrounds and removing those - // that don't exist. - request(BACKGROUND_URL, function(error, response, body) { - if(error || response.statusCode !== 200) { - console.error('Error loading backgrounds from ' + BACKGROUND_URL, error); - bgService.send('backgrounds-error', 'Error loading backgrounds from ' + BACKGROUND_URL, error); - } else { - var newBgData = JSON.parse(body); - - // If we already have data, go through it and update it with - // data from the server. - if(backgrounds !== null) { - debug(`Updating data with new entries from remote. Existing count: ${backgrounds.length}, remote length: ${newBgData.length}`); - _.each(newBgData, function(entry) { - if(!_.find(backgrounds, function(existing) { - return entry.url === existing.url; - })) { - backgrounds.push(entry); - } - }); - - debug(`New count: ${backgrounds.length}, removing backgrounds that have been removed from remote.`); - _.remove(backgrounds, function(entry) { - return !_.find(newBgData, function(newEntry) { - return newEntry.url === entry.url; - }); - }); - - debug(`New count: ${backgrounds.length}.`); - } else { - backgrounds = newBgData; - } - - debug(`Total backgrounds: ${backgrounds.length}`); - // Notify the renderer that new data is available. Even - // though we've already notified them once, give them - // a chance to deal with new data being available. - bgService.send('background-data-available', backgrounds); - - // Save the background file locally. - fs.writeFile(BACKGROUNDS_SAVE_PATH, JSON.stringify(backgrounds, null, 4), function(error) { - if(error) { - bgWindow.send('backgrounds-error', 'Could not save backgrounds list', error); - } - }); - - // Create the backgrounds directory - fsExtra.mkdirs(BACKGROUNDS_DIR, function(error) { - if(error) { - console.error('Could not create directory for backgrounds: ', error); - bgService.send('backgrounds-error', 'Could not create directory for backgrounds', error); - } else { - // Download all of the background images, succeeding whether or not - // the promise resolves or rejects. - doDownload(backgrounds, 0, bgAvailable).then(function() { - debug(`Background downloading succeeded.`); - bgService.send('backgrounds-downloaded'); - - cleanBackgrounds(backgrounds); - }, function(error) { - bgService.send('backgrounds-error', `Unhandled error downloading backgrounds: ${error}`); - throw error; - }); - } - }); - } - }); - }); }); -} + + mainWindow.minimize(); +}); // This method will be called when Electron has finished // initialization and is ready to create browser windows. diff --git a/package.json b/package.json index e468ad5..c114aa2 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "request": "^2.72.0", "roboto-fontface": "^0.4.5", "sha1": "^1.1.1", - "slash": "^1.0.0" + "slash": "^1.0.0", + "spawn-shell": "^1.1.2" } } diff --git a/ui/scripts/controllers/edit-tile.js b/ui/scripts/controllers/edit-tile.js index 8ee1b8b..d55ab3d 100644 --- a/ui/scripts/controllers/edit-tile.js +++ b/ui/scripts/controllers/edit-tile.js @@ -79,6 +79,16 @@ angular.module('farnsworth') self.categories[tile.category].tiles.push(tile); } + _.each(self.categories, function(category) { + var existingTile = _.findIndex(category.tiles, function(candidate) { + return candidate === tile; + }); + + if(existingTile !== -1 && category.name !== category.tiles[existingTile].category) { + category.tiles.splice(existingTile, 1); + } + }) + SettingsService.save().then(function() { $window.history.back(); }).catch(function(error) { diff --git a/ui/scripts/controllers/home.js b/ui/scripts/controllers/home.js index 0f083cb..f59eca8 100644 --- a/ui/scripts/controllers/home.js +++ b/ui/scripts/controllers/home.js @@ -9,8 +9,10 @@ angular.module('farnsworth') .controller('HomeController', function($location, $mdToast, $mdDialog, $timeout, $q, $scope, $route, $document, hotkeys, duScrollDuration, duScrollEasing, SettingsService, BackgroundsService, HotkeyDialog) { - var slash = require('slash'); // Convert Windows paths to something we can use in `file:///` urls. - var app = require('electron').remote.app; // Needed to close the application. + var slash = require('slash'); // Convert Windows paths to something we can use in `file:///` urls. + var electron = require('electron'); + var app = electron.remote.app; // Needed to close the application. + var ipc = electron.ipcRenderer; // For app launching. var self = this; var constants = { @@ -35,6 +37,13 @@ angular.module('farnsworth') self.selectedTileIndex = 0; // The index of the currently selected tile. self.editingCategories = false; // Whether or not we're editing the list of categories. + ipc.on('appliation-launch-error', function(code) { + $mdToast.show( + $mdToast.simple() + .textContent('The application terminated with an error.') + .hideDelay(3000)); + }); + SettingsService.get().then(function(settings) { self.settings = settings; @@ -67,6 +76,10 @@ angular.module('farnsworth') }); }); + /** + * Set a random background image based on the backgrounds + * we've downloaded in the main app. + */ self.setRandomBackground = function() { BackgroundsService.getRandomBackground().then(function(background) { self.background = background; @@ -141,8 +154,8 @@ angular.module('farnsworth') }] }; - // Make sure we ignore transient categories when building the list - // as we're going to add them later. + // Make a list of categories so it's easier to navigate + // up and down. self.categoryList = self.makeCategoryList(); self.selectedCategory = self.categoryList[self.selectedCategoryIndex]; @@ -494,6 +507,8 @@ angular.module('farnsworth') combo: 'enter', description: 'Edit the selected category', callback: function() { + // If we don't have any category selected, then we've chosen + // to stop editing categories, reset things. if(self.selectedCategory === null) { self.editingCategories = false; self.selectedCategoryIndex = self.categoryList.length - 1; @@ -504,6 +519,8 @@ angular.module('farnsworth') self.setupBindings(); return; } else if(self.moving) { + // Otherwise, if we're moving categories, then it's time + // to save the new location. SettingsService.save().catch(function() { $mdToast.show( $mdToast.simple() @@ -514,6 +531,9 @@ angular.module('farnsworth') self.moving = false; }); } else { + // So we have a category selected, and we're not moving, + // so it's time to let the user choose what they want to + // do with the seelcted categories, bring up the popup. self.disableBindings(); HotkeyDialog() @@ -534,16 +554,20 @@ angular.module('farnsworth') .then(function(result) { switch(result.caption) { case 'Arrange': + // Switch into move mode and restart category editing self.moving = true; self.editCategories(self.selectedCategoryIndex); break; case 'Rename': + // Bring up the rename dialog self.renameCategory(); break; case 'Delete': + // Bring up the delete confirmation. self.deleteCategory(); break; default: + // Cancel the popup and go back to normal self.editCategories(self.selectedCategoryIndex); } @@ -578,10 +602,13 @@ angular.module('farnsworth') }); } }).then(function() { + // Make sure we update the category name in all the tiles as well _.each(self.selectedCategory.tiles, function(tile) { tile.category = self.selectedCategory.name; }); + // Save the category, reinitialize our categories, and + // go back to category editing mode return SettingsService.save().then(function() { self.init(); self.editCategories(self.selectedCategoryIndex); @@ -700,6 +727,8 @@ angular.module('farnsworth') if(_.has(self, func)) { self[func](); } + } else { + ipc.send('launch-application', tile.command); } }; diff --git a/ui/scripts/services/backgrounds.js b/ui/scripts/services/backgrounds.js index 522c650..879fce1 100644 --- a/ui/scripts/services/backgrounds.js +++ b/ui/scripts/services/backgrounds.js @@ -1,5 +1,8 @@ 'use strict'; +/** + * Service for accessing the backgrounds we've downloaded...in the background. + */ angular.module('farnsworth') .service('BackgroundsService', function($q, $mdToast) { var electron = require('electron'); @@ -9,29 +12,39 @@ angular.module('farnsworth') var ipc = electron.ipcRenderer; var app = electron.remote.app; + // We keep this here to make loading of backgrounds a bit faster...though + // since we now load this from disk first in main as well, it doesn't + // make much sense...TODO: Remove this and just get it from main. const BACKGROUND_FILE = app.getPath('userData') + '/backgrounds.json'; + + // TODO: Load this form main too, possibly as part of the background data? const BACKGROUNDS_DIR = app.getPath('userData') + '/backgrounds'; var service = {}; - var bgAvailable = $q.defer(); - var allBgsLoaded = $q.defer(); - var bgDataAvailable = $q.defer(); - var lastBackground = null; - var backgroundData = null; + var bgAvailable = $q.defer(); // Promise that is resolved when we have data. + var allBgsLoaded = $q.defer(); // Promise resolved when all backgrounds have been loaded. + var bgDataAvailable = $q.defer(); // Promise resolved when at least one background is available. + var lastBackground = null; // The last background we displayed, so we don't cycle to the same one twice. + var backgroundData = null; // List of all backgrounds and their metadata. service.waitForBackgroundData = bgDataAvailable.promise; service.waitForBackground = bgAvailable.promise; service.waitForAllBackgrounds = allBgsLoaded.promise; + // Listen for at least one background to be available and resolve + // the promise. ipc.on('background-available', function(sender, index, background) { bgAvailable.resolve(background, index); }); + // Listen for all backgrounds to be downloaded and resolve that promise. ipc.on('backgrounds-downloaded', function() { allBgsLoaded.resolve(); }); + // If any error occurs, reject any promises that haven't yet been + // resolved and display the error. ipc.on('background-error', function(sender, text, details) { bgDataAvailable.reject(text, details); bgAvailable.reject(text, details); @@ -42,20 +55,33 @@ angular.module('farnsworth') .textContent(`${text}: ${details}`) .hideDelay(3000)); }); - + + // Listen for background data to be available and resolve the promise + // as well as store the data. ipc.on('background-data-available', function(sender, data) { backgroundData = data; // TODO: Is it Kosher to reference data from the main process like this? bgDataAvailable.resolve(backgroundData); }); + /** + * Get a single random background image. + * + * @return {object} Background metadata, including the full path. + */ service.getRandomBackground = function() { var defered = $q.defer(); + // We're going to wait for the data to be available *and* at least + // one background to be available. var waits = [service.waitForBackgroundData, service.waitForBackground]; $q.all(waits).then(function(bgData) { bgData = bgData[0]; + // Get a list of all files in the directory. + // TODO: I believe this is legacy logic and isn't necessary, + // the `fs.accessSync` call verifies that the file exists, so + // we should probably remove this. fs.readdir(BACKGROUNDS_DIR, function(error, entries) { if(error) { defered.reject('Cannot read background directory.', error); @@ -64,22 +90,30 @@ angular.module('farnsworth') return defered.reject('No images found in directory but images supposedly available?'); } + // Choose a random background to start with. var index = Math.floor(Math.random() * (bgData.length - 1)); - var checks = 0; - var background = null; - + var checks = 0; // Make sure we don't keep checking forever if we can never find a background + var background = null; // The background to use + + // Keep looping so long as we haven't found a background that isn't the last + // background we've used and we haven't mmade as many checks as there are entries + // + // TODO: This logic is flawed as we can check the same background multiple times, + // increasing the number of checks without insuring we don't re-check a failed + // background over and over. while((!background || background === lastBackground) && checks < entries.length) { checks++; if(bgData[index].downloaded) { var filename = path.join(BACKGROUNDS_DIR, bgData[index].filename); + // Make sure this background actually exists try { fs.accessSync(filename, fs.R_OK); background = { filename: filename, - author: index < bgData.length ? bgData[index] : null + metadata: index < bgData.length ? bgData[index] : null }; } catch(e) {} } @@ -97,6 +131,10 @@ angular.module('farnsworth') return defered.promise; }; + // Tell main that we're ready to receive events, this will in turn + // trigger it to begin loading backgrounds. We do this so that + // it doesn't send us events when we're not yet ready to receive them, + // thereby avoiding a race condition. ipc.send('background-service-ready'); return service; diff --git a/ui/scripts/services/hotkey-dialog.js b/ui/scripts/services/hotkey-dialog.js index df52466..630a2fa 100644 --- a/ui/scripts/services/hotkey-dialog.js +++ b/ui/scripts/services/hotkey-dialog.js @@ -125,21 +125,76 @@ angular.module('farnsworth') } }; + /** + * Set the text prompt displayed to the user. + * + * @param {string} prompt The prompt. + * @return {HotkeyDialog} This so that we can chain calls + */ HotkeyDialog.prototype.prompt = function(prompt) { this.dialog.locals.text = prompt; return this; }; + /** + * Set a custom template URL to use. + * + * @note Setting this would obviously make the prompt + * irrelevant unless you use `{{ controller.text }}` in + * your template. + * + * @param {string} template The template URL. + * @return {HotkeyDialog} This so that we can chain calls + */ HotkeyDialog.prototype.template = function(template) { this.dialog.templateUrl = template; return this; }; + /** + * List of all dialog actions to generate buttons at the bottom + * of the dialog. The form of `actions` should look like the following: + * + * ```js + * [{ + * caption: 'Arrange', + * icon: 'swap_horiz' + * }, { + * caption: 'Edit', + * icon: 'edit' + * }, { + * caption: 'Cancel', + * icon: 'cancel' + * }, { + * caption: 'Delete', + * icon: 'delete' + * }] + * ``` + * + * The icon is optional and the promise returned by `show()` will be + * resolved with the value of `caption` that the user selected. + * + * @param {[object]} actions List of actions as defined above. + * @return {HotkeyDialog} This so that we can chain calls + */ HotkeyDialog.prototype.actions = function(actions) { this.dialog.locals.dialogActions = actions; return this; }; + /** + * Optionally specify a promise on which we'll wait before + * enabling hotkeys. This allows us to prevent an action + * being chosen before a user has a chance to release a button + * that may have opened this dialog, or other reasons. + * + * This may be called with no argument to create and immediately + * resolve a promise internally. This is useful to defer creation + * of the dialog until the next digest cycle. + * + * @param {Promise} promise (Optional, see above) The promise on which to wait + * @return {HotkeyDialog} This so that we can chain calls + */ HotkeyDialog.prototype.wait = function(promise) { if(_.isObjectLike(promise)) { this.waitPromise = promise; @@ -152,15 +207,27 @@ angular.module('farnsworth') return this; }; + /** + * Set the value of the `aria-label` attribute. + * + * @param {string} ariaLabel The value of the attribute + * @return {HotkeyDialog} This so that we can chain calls + */ HotkeyDialog.prototype.aria = function(ariaLabel) { this.dialog.locals.ariaLabel = ariaLabel; return this; }; + /** + * Show the dialog, returning a promise that will be resolved + * with the user's selected action, much like `$mdDialog`. + * + * @return {Promise} The promise resolved when the user selects an action + */ HotkeyDialog.prototype.show = function() { return $mdDialog.show(this.dialog); }; - + return function() { return new HotkeyDialog(); } diff --git a/ui/scripts/services/settings.js b/ui/scripts/services/settings.js index f2a10ff..7d595a6 100644 --- a/ui/scripts/services/settings.js +++ b/ui/scripts/services/settings.js @@ -33,8 +33,12 @@ angular.module('farnsworth') if(!service.settings || reload) { fs.readFile(path, function(error, data) { + // If we fail reading this file, we should try to read from the backup var useBackup = false; + // If the file doesn't exist at all, we assume that this is a first + // run. TODO: We should probably use the backup instead just in case + // the file was deleted for some reason. if(error) { service.settings = {}; defered.resolve(service.settings); @@ -42,9 +46,17 @@ angular.module('farnsworth') try { service.settings = JSON.parse(data); + // If the result is not a plain object or the categories + // aren't, then something is corrupt, use the backup. + // + // This mostly occurred during development before the + // format was stabilized but does help prevent getting + // into weird states the user can't get out of without + // manual surgery. if(!_.isPlainObject(service.settings) || !_.isPlainObject(service.settings.categories)) { useBackup = 'Saved settings are not valid.'; } else { + // If we successfully read the file, save it as a backup. service.save(true); defered.resolve(service.settings); @@ -54,6 +66,8 @@ angular.module('farnsworth') } if(useBackup !== false) { + // Let the user know that their main settings file + // is hosed. $mdToast.show( $mdToast.simple() .textContent(useBackup) @@ -66,6 +80,8 @@ angular.module('farnsworth') try { service.settings = JSON.parse(data); + // This time, if things don't work, just + // start up as if it's a first run. if(!_.isPlainObject(service.settings)) { service.settings = {}; } diff --git a/ui/views/settings.html b/ui/views/settings.html index 12a636d..ae1a523 100644 --- a/ui/views/settings.html +++ b/ui/views/settings.html @@ -5,8 +5,9 @@
Hide Category Names - +
+ Save Cancel