with a class of "sunlight-container" where
- //the pre created by this code is placed
- $("#frame").prepend($("
").text(data));
- //format Javascript syntax
- Sunlight.highlightAll();
- var codeBlock = $(".sunlight-javascript");
- //select the line/column
- //figure out how many characters in that line/column is
- var text = codeBlock.text(),
- characterCount = 0,
- lastNewLine = 0;
- for(var lineIndex = 1; lineIndex < line; ++lineIndex)
- {
- //get the newline character at the end of line #lineIndex
- lastNewLine = text.indexOf("\n", lastNewLine + 1);
- characterCount = lastNewLine;
- }
- characterCount += column;
-
- //figure out which node has the character we are looking for
- //the parsed document is filled with
elements for syntax
- //highlighting
- var childNodes = codeBlock[0].childNodes;
- for(var i = 0, length = childNodes.length; i < length; ++i)
- {
- var node = childNodes[i];
- //anything that's not a text node is a span with text inside
- if(node.nodeType != 3)
- node = node.childNodes[0];
- //get the amount of text inside the text node
- characterCount -= node.length;
- //if we've run out of our character count, that's the node that we want
- if(characterCount <= 0)
- {
- var scroller = $(".sunlight-code-container");
- //get the position of the text node relative to the scroller
- //the text has to be wrapped in a span first, as TextNodes don't
- //have offsetTop
- var nodePos = $(node).wrap(" ").parent().offset().top -
- codeBlock.offset().top;
- //center the text in the scrolling window
- scroller.scrollTop(Math.max(nodePos - scroller.height() * 0.5, 0));
- //remove the text wrapping that we added
- $(node).unwrap();
-
- //put the text caret in the correct location
- var range = document.createRange(),
- sel = window.getSelection();
- range.setStart(node, -characterCount + 1);
- range.collapse(true);
- sel.removeAllRanges();
- sel.addRange(range);
- break;
- }
- }
- }
- });
- });
- };
-
- // Reference to the prototype
- var p = extend(RemoteTrace, Module);
-
- // The collection of all themes
- var allFilters = ['general','debug','info','warning','error'];
-
- /**
- * When the WebSocketServer is initialized
- * @method _onInit
- * @private
- * @param {WebSocket} ws The websocket instance
- */
- p._onInit = function(ws)
- {
- ws.on('message', this._onMessage.bind(this));
- };
-
- /**
- * Handler for the filters
- * @method _onFiltersChanged
- * @private
- * @param {event} e Jquery click event
- */
- p._onFiltersChanged = function(e)
- {
- e.preventDefault();
- var a = $(e.currentTarget);
- if (a.data('toggle-all'))
- {
- this.setFilters(allFilters);
- }
- else
- {
- a.toggleClass('selected');
-
- var filters = [];
- this.filters.filter('.selected').each(function(){
- filters.push($(this).data('filter'));
- });
- this.setFilters(filters);
- }
- };
-
- /**
- * Set the filters to show
- * @method setFilters
- * @param {array} filters The collection of string filters to show
- */
- p.setFilters = function(filters)
- {
- var i;
-
- // Remove all
- for(i = 0; i < allFilters.length; i++)
- {
- this.output.removeClass('show-' + allFilters[i]);
- }
-
- // Set based on the input filteres
- for(i = 0; i < filters.length; i++)
- {
- var f = this.filters.filter("[data-filter='" + filters[i] + "']");
-
- if (f)
- {
- f.addClass('selected');
- this.output.addClass('show-'+filters[i]);
- }
- }
- localStorage.setItem('filters', filters);
- };
-
- /**
- * Handler for the theme selection
- * @method _onThemesChanged
- * @private
- * @param {event} e Jquery click event
- */
- p._onThemesChanged = function(e)
- {
- e.preventDefault();
- this.setTheme($(e.currentTarget).data('theme'));
- };
-
- /**
- * Set the current theme
- * @method setTheme
- * @param {String} theme The theme name, CSS class name
- */
- p.setTheme = function(theme)
- {
- this.themes.removeClass('selected')
- .filter("[data-theme='" + theme + "']")
- .addClass('selected');
-
- this.output
- .removeClass(this.theme)
- .addClass(theme);
-
- this.theme = theme;
-
- localStorage.setItem('theme', theme);
- };
-
- /**
- * Clear the output
- * @method clear
- */
- p.clear = function()
- {
- this.output.empty();
- this.groupStack.length = 0;
- this.saveButton.addClass('disabled');
- this.clearButton.addClass('disabled');
- };
-
- /**
- * Save the current log
- * @method save
- */
- p.save = function()
- {
- // Lets massage the output so it looks better in a log file
- var output = [];
- this.output.clone().children().each(function(){
- var child = $(this);
-
- // extra line breaks for session
- if (child.hasClass('session'))
- {
- child.append("\n").prepend("\n");
- }
- // Add some character around date
- var date = child.children('.timestamp');
- date.text(" : " + date.text() + " : ");
- output.push(child.text());
- });
- output = output.join("\n");
-
- if (APP)
- {
- // Browse for file and save output
- Browser.saveAs(
- function(file)
- {
- var fs = require('fs');
- var path = require('path');
-
- if (!/\.txt$/.test(file))
- {
- file += ".txt";
- }
- localStorage.setItem('workingDir', path.dirname(file));
- fs.writeFileSync(file, output);
- },
- (new Date()).toUTCString() + ".txt",
- localStorage.getItem('workingDir') || undefined
- );
- }
- if (WEB)
- {
- console.log(output);
- }
- };
-
- function extendNumber(input, length)
- {
- var output = input.toString();
- while(output.length < length)
- output = "0" + output;
- return output;
- }
-
- /**
- * Callback when a message is received by the server
- * @method _onMessage
- * @private
- * @param {String} result The result object to be parsed as JSON
- */
- p._onMessage = function(result)
- {
- var output = this.output,
- height = output.outerHeight(),
- scrollTop = output.scrollTop(),
- scrollHeight = output[0].scrollHeight,
- atBottom = scrollTop + height >= scrollHeight;
-
- result = JSON.parse(result);
-
- var level = (result.level || "GENERAL").toLowerCase(),
- stack = result.stack,
- now = new Date();
- if(result.time)
- now.setTime(result.time);
- now = now.toDateString() + " " + extendNumber(now.getHours(), 2) + ":" +
- extendNumber(now.getMinutes(), 2) + ":" + extendNumber(now.getSeconds(), 2) +
- "." + extendNumber(now.getMilliseconds(), 3);
-
- this.saveButton.removeClass('disabled');
- this.clearButton.removeClass('disabled');
-
- if (level === "session")
- {
- this.logSession(now);
- }
- else if(level == "clear")
- {
- this.clear();
- }
- else if(level == "group" || level == "groupcollapsed")
- {
- var log = this.prepareAndLogMessage(now, result.message, "general", stack),
- groupId = "group_" + this.nextGroupId++;
- var chevron = $(" ");
- chevron.append(
- $(" "),
- $(" ")
- );
- log.prepend(chevron);
-
- var group = $("
");
- this.getLogParent().append(group);
- this.groupStack.push(group);
-
- if(level == "groupcollapsed")
- {
- chevron.addClass("collapsed");
- group.collapse("hide");
- }
- }
- else if(level == "groupend")
- {
- this.groupStack.pop();
- }
- else if (Array.isArray(result.message))
- {
- this.prepareAndLogMessage(now, result.message, level, stack);
- }
-
- if (this.maxLogs)
- {
- this.output.html(this.output.children(".log").slice(-this.maxLogs));
- }
-
- // Scroll to the bottom of the log display
- if (atBottom)
- {
- this.output.scrollTop(this.output[0].scrollHeight);
- }
- };
-
- p.prepareAndLogMessage = function(now, messages, level, stack)
- {
- if(!messages || !messages.length)
- {
- return this.logMessage(now, [""], level, stack);
- }
-
- var message = messages[0], j, tokens, token, sub;
-
- //if the first message is a string, then check it for string formatting tokens
- if (typeof message == "string")
- {
- tokens = message.match(/%[sdifoObxec]/g);
-
- if (tokens)
- {
- for (j = 0; j < tokens.length; j++)
- {
- token = tokens[j];
- sub = messages[1];
-
- // CSS substitution check
- if (token == "%c")
- {
- sub = '';
- message += ' ';
- }
- // Do object substitution
- else if (token == "%o" || token == "%O")
- {
- sub = this.prepareObject(sub)[0].outerHTML;
- }
- else if(token == "%d" || token == "%i")
- {
- sub = parseInt(sub);
- }
- message = message.replace(token, String(sub));
-
- messages.splice(1, 1);
- }
- }
- messages[0] = message;
- }
- return this.logMessage(now, messages, level, stack);
- };
-
- /**
- * Log a new message
- * @method logMessage
- * @param {String} now The current time name
- * @param {Array} messages The message to log
- * @param {String} level The level to use
- * @param {Array} [stack] The stack trace for the log.
- */
- p.logMessage = function(now, messages, level, stack)
- {
- var message = "", i, length, messageDom;
- for(i = 0, length = messages.length; i < length; ++i)
- {
- if(i > 0)
- message += " ";
- if (typeof messages[i] === "object")
- {
- message += this.prepareObject(messages[i])[0].outerHTML;
- }
- else
- message += messages[i];
- }
- var log = $("
")
- .addClass(level)
- .append(
- $(" ").text(level.toUpperCase()),
- $(" ").text(now)
- );
- messageDom = $(" ").html(message);
- if(stack && stack.length)
- {
- var stackLinkText = stack[0].file;
- if(stackLinkText.indexOf("/") >= 0)
- stackLinkText = stackLinkText.substring(stackLinkText.lastIndexOf("/") + 1);
- stackLinkText += ":" + stack[0].lineLocation;
- log.append($(" ").text(stackLinkText))
- .append(messageDom);
- var groupId = "group_" + this.nextGroupId++;
- var group = $("
");
- for(i = 0, length = stack.length; i < length; ++i)
- {
- stackLinkText = stack[i].file;
- if(stackLinkText.indexOf("/") >= 0)
- stackLinkText = stackLinkText.substring(stackLinkText.lastIndexOf("/") + 1);
- stackLinkText += ":" + stack[i].lineLocation;
- var line = $("
");
- var stackLink = $(" ")
- .text(stackLinkText)
- .attr("data-file", stack[i].file)
- .attr("data-location", stack[i].lineLocation);
- line.text(stack[i].function)
- .append(stackLink);
- group.append(line);
- }
- log.append(group);
-
- messageDom.attr("data-toggle", "collapse").attr("data-target", "#" + groupId);
- if(level != "error")
- {
- messageDom.addClass("collapsed");
- group.collapse("hide");
- }
- }
- else
- log.append(messageDom);
- this.getLogParent().append(log);
- return log;
- };
-
- /**
- * Turns an object into an HTML element suitable for display.
- * @method prepareObject
- * @param {Object} input The input object.
- * @return {jquery} The jquery element for the object
- */
- p.prepareObject = function(input)
- {
- var output = $("
");
-
- var group = $("
");
- for(var key in input)
- {
- var line = $("
");
- if(typeof input[key] == "object")
- {
- line.append(key + ": ", this.prepareObject(input[key]));
- }
- else
- {
- line.append(key + ": " + input[key]);
- }
- group.append(line);
- }
- if(group.children().length)
- {
- var groupId = "group_" + this.nextGroupId++;
- group.attr("id", groupId);
- var chevron = $(" " + (Array.isArray(input) ? "Array [" : "Object {") + " ");
- chevron.prepend(
- $(" "),
- $(" ")
- );
- output.prepend(chevron);
- output.append(group);
- group.append(Array.isArray(input) ? "]" : "}");
- }
- else
- group.append(Array.isArray(input) ? "Array []" : "Object {}");
- return output;
- };
-
- /**
- * Gets the JQuery element that logs should be added to.
- * @method getLogParent
- * @return {jquery} The div to add logs to.
- */
- p.getLogParent = function()
- {
- if(this.groupStack.length)
- return this.groupStack[this.groupStack.length - 1];
- return this.output;
- };
-
- /**
- * Log a new session
- * @method logSession
- * @param {String} now The current time name
- */
- p.logSession = function(now)
- {
- if (this.clearOnNewSession)
- {
- this.clear();
- }
- this.output.append(
- $("
")
- .text("New Session Began at " + now)
- );
- };
-
- /**
- * Close the application
- * @method shutdown
- */
- p.shutdown = function()
- {
- if (this.server)
- {
- this.server.close();
- this.server = null;
- }
- this.close(true);
- };
-
- // Create the new Remote trace
- Module.create(RemoteTrace);
-
-}());
\ No newline at end of file
diff --git a/src/js/tasks/TaskRunner.js b/src/js/tasks/TaskRunner.js
deleted file mode 100644
index 2ec5725..0000000
--- a/src/js/tasks/TaskRunner.js
+++ /dev/null
@@ -1,215 +0,0 @@
-(function($){
-
- if (APP)
- {
- // Import node modules
- var ansi2html = require('ansi2html');
- var watch = require('node-watch');
- var path = require('path');
- }
-
- var Module = springroll.Module,
- Settings = springroll.tasks.Settings,
- ProjectManager = springroll.tasks.ProjectManager,
- TerminalWindow = springroll.tasks.TerminalWindow,
- Interface = springroll.tasks.Interface,
- Utils = springroll.tasks.Utils,
- TerminalManager = springroll.tasks.TerminalManager;
-
- /**
- * The main application
- * @class TaskRunner
- * @extends springroll.Module
- * @namespace springroll.tasks
- */
- var TaskRunner = function()
- {
- Module.call(this);
-
- /**
- * The tasks container
- * @property {jquery} tasks
- */
- this.tasks = $("#tasks");
-
- /**
- * The project manager
- * @property {springroll.tasks.ProjectManager} projectManager
- */
- this.projectManager = new ProjectManager(this);
-
- /**
- * The console output manager
- * @property {springroll.tasks.TerminalManager} terminalManager
- */
- this.terminalManager = new TerminalManager(this);
-
- /**
- * The interface instance
- * @property {springroll.tasks.Interface} ui
- */
- this.ui = new Interface(this);
-
- /**
- * The opened terminal window
- * @property {springroll.tasks.TerminalWindow} terminal
- */
- this.terminal = null;
-
- if (APP)
- {
- // Create the menu
- this.initMenubar(false, true);
- }
-
- // Load the project
- var project = localStorage.getItem('project');
- if (!project)
- {
- throw "Not a valid project: " + project;
- }
- this.addProject(project);
- };
-
- // Reference to the prototype
- var p = extend(TaskRunner, Module);
-
- /**
- * Add a project to the list of projects
- * @method addProject
- * @param {string} projectPath The directory of the project to load
- */
- p.addProject = function(projectPath)
- {
- this.projectManager.add(
- projectPath,
- this.addedProject.bind(this)
- );
- };
-
- /**
- * Check to see if a file changed
- * @method watchProject
- * @param {string} projectPath The project path to watch
- */
- p.watchProject = function(projectPath)
- {
- var self = this;
- watch(
- path.join(projectPath, "Gruntfile.js"),
- this.refreshTasks.bind(this)
- );
- };
-
- /**
- * Refresh the project tasks
- * @method refreshTasks
- */
- p.refreshTasks = function()
- {
- // Stop any running tasks
- this.terminalManager.killTasks();
- this.terminalManager.tasks = {};
-
- // Clear the project
- this.projectManager.project = null;
-
- // Reload
- this.addProject(localStorage.getItem('project'));
- };
-
- /**
- * Add the project to the interface
- * @method addedProject
- * @param {object} project The project properties
- */
- p.addedProject = function(project)
- {
- $(Utils.getTemplate('tasks', project))
- .appendTo(this.tasks);
-
- this.watchProject(project.path);
- };
-
- /**
- * Put the commandline log into the terminal window
- * @method putCliLog
- * @param {String} data The log data to add
- * @param {String} taskName The name of the task
- */
- p.putCliLog = function(data, taskName)
- {
- var output = ansi2html(data);
- $('' + output + '
').appendTo($('#console_' + taskName));
- this.terminalScrollToBottom(taskName);
- };
-
- /**
- * Scroll to the bottom of the console output
- * @method terminalScrollToBottom
- * @param {String} taskName The name of the task
- */
- p.terminalScrollToBottom = function(taskName)
- {
- _.throttle(function(){
- $('#console_' + taskName).scrollTop(999999999);
- }, 100)();
- };
-
- /**
- * Start running or stop running a task
- * @method toggleTask
- * @param {String} taskName The name of the task
- */
- p.toggleTask = function(taskName)
- {
- var item = $('#task_item_' + taskName);
-
- if (item.hasClass('running'))
- {
- this.terminalManager.stopTask(taskName);
- item.removeClass('running error');
- }
- else
- {
- this.terminalManager.runTask(
- taskName,
- function()
- {
- //start event
- item.addClass('running')
- .removeClass('error');
- },
- function()
- {
- //end event
- item.removeClass('running');
- },
- function()
- {
- //error event
- item.addClass('error')
- .removeClass('running');
- }
- );
- }
- };
-
- /**
- * Shutdown the
- * @method shutdown
- */
- p.shutdown = function()
- {
- if (this.terminal)
- {
- this.terminal.destroy();
- }
- this.terminalManager.killTasks();
- this.close(true);
- };
-
- // Create a new module
- Module.create(TaskRunner);
-
-}(jQuery));
\ No newline at end of file
diff --git a/src/js/tasks/input/ProjectManager.js b/src/js/tasks/input/ProjectManager.js
deleted file mode 100644
index 6f72fe9..0000000
--- a/src/js/tasks/input/ProjectManager.js
+++ /dev/null
@@ -1,223 +0,0 @@
-(function(){
-
- if (APP)
- {
- var exec = require('child_process').exec;
- var path = require('path');
- var fs = require('fs');
- var async = require('async');
- var isWin = /^win/.test(process.platform);
- }
-
- // Import classes
- var Utils = springroll.tasks.Utils,
- Settings = springroll.tasks.Settings;
-
- /**
- * Add projects to the interface
- * @class ProjectManager
- * @namespace springroll.tasks
- * @constructor
- * @param {springroll.tasks.TaskRunner} app The instance of the app
- */
- var ProjectManager = function(app)
- {
- /**
- * Reference to the application
- * @property {springroll.tasks.TaskRunner}
- */
- this.app = app;
-
- /**
- * The collection of currently loaded projects
- * @property {Array} project
- */
- this.project = null;
-
- /**
- * The path to the grunt executable
- * @property {string} gruntBin
- */
- this.gruntBin = path.resolve('.','node_modules','grunt-cli','bin','grunt');
-
- /**
- * The body dom
- * @property {jquery} body
- */
- this.body = $(document.body);
-
- /**
- * The dom for failed message display
- * @property {jquery} failedMessage
- */
- this.failedMessage = $('#failedMessage');
- };
-
- // The reference to the prototype
- var p = ProjectManager.prototype = {};
-
- /**
- * Add a project to the interface
- * @method add
- * @param {String} dir The directory of project file
- * @param {function} success The callback when added
- */
- p.add = function(dir, success)
- {
- // Reset the classes
- this.body.removeClass('installing failed loading');
-
- var self = this, project;
- var name = path.basename(dir);
- this.project = project = {
- name: name,
- path: dir,
- tasks: []
- };
-
- async.waterfall([
- // Check for access to node
- function(done)
- {
- exec('node --version',
- function(err, stdout, stderr)
- {
- if (err)
- {
- console.error("Unable to access node");
- return done("Unable to access NodeJS on this machine. Is it installed?");
- }
- done(null);
- }
- );
- },
- // check that node modules exists within the project
- function(done)
- {
- var nodeModules = path.join(project.path, 'node_modules');
- fs.exists(nodeModules, function(exists)
- {
- done(null, exists);
- });
- },
- function(exists, done)
- {
- // install the node_modules within the project
- // if the node modules don't exist
- if (!exists)
- {
- self.body.addClass('installing');
- exec('npm install', { cwd : project.path }, done);
- }
- else
- {
- done(null);
- }
- }
- ],
- function(err)
- {
- if (err)
- {
- console.error(String(err));
- return self.failed(err);
- }
- self.body.removeClass('installing');
- self.proceed(success);
- });
- };
-
- /**
- * The tasks loading or installing failed
- * @method failed
- * @param {string} msg The failed message
- */
- p.failed = function(msg)
- {
- this.body.removeClass('loading installing')
- .addClass('failed');
-
- this.failedMessage.text(msg);
- };
-
- /**
- * Proceed with the add
- * @method proceed
- * @private
- * @param {Object} project
- * @param {function} success
- */
- p.proceed = function(success)
- {
- this.body.addClass('loading');
-
- var self = this;
- var project = this.project;
- var gruntPath = path.join(project.path, 'node_modules', 'grunt');
- var gruntFile = path.join(project.path, 'Gruntfile.js');
-
- async.waterfall([
- function(done)
- {
- fs.exists(gruntPath, function(exists)
- {
- if (!exists)
- {
- return done('Unable to find local grunt.');
- }
- done(null);
- });
- },
- function(done)
- {
- fs.exists(gruntFile, function(exists)
- {
- if (!exists)
- {
- return done('Unable to find Gruntfile.js.');
- }
- done(null);
- });
- },
- function(done)
- {
- exec('node ' + self.gruntBin + ' _springroll_usertasks',
- { cwd : project.path },
- function(err, stdout, strderr)
- {
- if (err)
- {
- done("Invalid project, must contain Grunt task '_springroll_usertasks'.");
- }
- var tasksContent = stdout.split("\n");
- var tasks;
- try
- {
- tasks = JSON.parse(tasksContent[1]);
- }
- catch(e)
- {
- console.error(e);
- }
- project.tasks = tasks;
- done(null, project);
- }
- );
- }
- ],
- function(err, project)
- {
- if (err)
- {
- console.error(err);
- return self.failed(err);
- }
- self.body.removeClass('loading');
- success(project);
- });
- };
-
- // Assign to the global space
- namespace('springroll.tasks').ProjectManager = ProjectManager;
-
-})();
\ No newline at end of file
diff --git a/src/js/tasks/input/TerminalManager.js b/src/js/tasks/input/TerminalManager.js
deleted file mode 100644
index 6bda782..0000000
--- a/src/js/tasks/input/TerminalManager.js
+++ /dev/null
@@ -1,177 +0,0 @@
-(function(){
-
- if (APP)
- {
- // Import node modules
- var spawn = require("child_process").spawn;
- var exec = require("child_process").exec;
- var isWin = /^win/.test(process.platform);
- }
-
- /**
- * Manage the terminal window
- * @class TerminalManager
- * @namespace springroll.tasks
- * @constructor
- * @param {springroll.tasks.TaskRunner} app The instance of the app
- */
- var TerminalManager = function(app)
- {
- /**
- * Reference to the application
- * @property {springroll.tasks.TaskRunner}
- */
- this.app = app;
-
- /**
- * The command to use based on the platform, either grunt.cmd or grunt
- * @property {String} command
- */
- this.command = null;
-
- if (APP)
- {
- // Get the command based on the platform
- this.command = isWin ? 'grunt.cmd' : 'grunt';
- }
-
- /**
- * The list of current processes by project id
- * @property {dict} tasks
- */
- this.tasks = {};
- };
-
- // Reference to the prototype
- var p = TerminalManager.prototype;
-
- /**
- * Kill all the workers for all projects
- * @method killTasks
- */
- p.killTasks = function()
- {
- _.forEach(this.tasks,
- function(task, name)
- {
- if (task.status == 'running')
- {
- this.killTask(name);
- }
- }.bind(this)
- );
- };
-
- /**
- * Run a task
- * @method runTask
- * @param {String} name The name of the task
- * @param {Function} startCb The starting callback function
- * @param {Function} endCb The ending callback function
- * @param {Function} errorCb The error callback function
- */
- p.runTask = function(name, startCb, endCb, errorCb)
- {
- var app = this.app;
- var project = app.projectManager.project;
-
- startCb();
-
- var terminal = spawn(
- this.command,
- [name],
- {cwd: project.path}
- );
-
- var task = this.tasks[name];
-
- if (_.isUndefined(task))
- {
- task = {
- name: name,
- terminal: terminal,
- status: 'running'
- };
- }
- else
- {
- task.terminal = terminal;
- task.status = 'running';
- }
-
- terminal.stdout.setEncoding('utf8');
- terminal.stdout.on(
- 'data',
- function(data)
- {
- app.putCliLog(data, name);
- }
- );
-
- terminal.stderr.on(
- 'data',
- function(data)
- {
- app.putCliLog(data, name);
- errorCb();
- }
- );
-
- terminal.on(
- 'close',
- function(code)
- {
- endCb();
- terminal.status = 'stop';
- }
- );
- };
-
- /**
- * Stop a task
- * @method stopTask
- * @param {String} name The name of the task
- */
- p.stopTask = function(name)
- {
- var task = this.tasks[name];
-
- if (!_.isUndefined(task))
- {
- try
- {
- this.killTask(name);
- task.status = "stop";
- }
- catch(e)
- {
- alert("process end error!");
- }
- }
- };
-
- /**
- * Kill a task
- * @method killTask
- * @param {String} name The name of the task
- */
- p.killTask = function(name)
- {
- if (APP)
- {
- if (isWin)
- {
- var pid = this.tasks[name].terminal.pid;
- exec('taskkill /pid ' + pid + ' /T /F');
- }
- else
- {
- this.tasks[name].terminal.kill();
- }
- }
- };
-
- // Assign to global space
- namespace('springroll.tasks').TerminalManager = TerminalManager;
-
-}());
\ No newline at end of file
diff --git a/src/js/tasks/input/TerminalWindow.js b/src/js/tasks/input/TerminalWindow.js
deleted file mode 100644
index 1b92f8e..0000000
--- a/src/js/tasks/input/TerminalWindow.js
+++ /dev/null
@@ -1,180 +0,0 @@
-(function(){
-
- if (APP)
- {
- // Import modules
- var gui = require('nw.gui');
- }
-
- // Import classes
- var Settings = springroll.tasks.Settings;
-
- /**
- * The Terminal Window manages the output of a task into it's own console window
- * @class TerminalWindow
- * @namespace springroll.tasks
- * @constructor
- * @param {String} taskName The task name
- */
- var TerminalWindow = function(taskName)
- {
- /**
- * The name of the task
- * @property {String} taskName
- */
- this.taskName = taskName;
-
- /**
- * The jQuery node for the task output
- * @property {jquery} output
- */
- this.output = null;
-
- /**
- * The DOM element on the output window
- * @property {DOM} terminal
- */
- this.terminal = null;
-
- /**
- * Reference to the nodejs window
- * @property {nw.gui.Window} main
- */
- this.main = null;
-
- /**
- * The observer to watch changes in the output
- * @property {MutationObserver} observer
- */
- this.observer = null;
-
- // create the new window
- this.create();
- };
-
- // Reference to the prototype
- var p = TerminalWindow.prototype = {};
-
- /**
- * The window reference
- * @property {String} WINDOW_ALIAS
- * @private
- * @static
- */
- var WINDOW_ALIAS = 'TasksTerminalWindow';
-
- /**
- * Open the dialog
- * @method create
- */
- p.create = function()
- {
- // Open the new window
- this.main = gui.Window.get(
- window.open('tasks-terminal.html'), {
- show : false
- }
- );
- this.main.on('close', this.close.bind(this));
- this.main.on('loaded', this.onLoaded.bind(this));
- };
-
- /**
- * Open after the DOM is loaded on the new window
- * @method onLoaded
- */
- p.onLoaded = function()
- {
- Settings.loadWindow(WINDOW_ALIAS, this.main);
-
- // Get the dom output
- this.terminal = this.main.window.document.getElementById('terminal');
-
- // Setup the observer
- this.observer = new MutationObserver(this.onUpdate.bind(this));
-
- // Open the constructor task
- this.open(this.taskName);
- };
-
- /**
- * Everytime the output is updated
- * @method onUpdate
- * @private
- */
- p.onUpdate = function()
- {
- this.terminal.innerHTML = this.output.innerHTML;
-
- // Scroll to the bottom of the output window
- this.terminal.scrollTop = this.terminal.scrollHeight;
- };
-
- /**
- * New
- * @method open
- * @param {String} taskName The task name
- */
- p.open = function(taskName)
- {
- if (this.observer)
- {
- this.observer.disconnect();
- }
- this.terminal.innerHTML = "";
-
- this.taskName = taskName;
- this.output = document.getElementById('console_' + taskName);
-
- // Update the title
- //this.main.title = this.taskName;
- this.main.window.document.getElementById('title').innerHTML = taskName;
-
- // define what element should be observed by the observer
- // and what types of mutations trigger the callback
- this.observer.observe(this.output, {
- attributes: true,
- childList: true,
- characterData: true
- });
- this.onUpdate();
-
- // Reveal the window
- this.main.show();
- this.main.focus();
- };
-
- /**
- * Close the window
- * @method close
- * @private
- */
- p.close = function()
- {
- if (this.observer)
- {
- this.observer.disconnect();
- }
- this.output = null;
- this.terminal.innerHTML = "";
- Settings.saveWindow(WINDOW_ALIAS, this.main);
- this.main.hide();
- };
-
- /**
- * Destroy and don't use after this
- * @method destroy
- */
- p.destroy = function()
- {
- this.close();
- this.observer = null;
- this.terminal = null;
- this.main.close(true);
- this.main = null;
- };
-
- // Assign to global space
- namespace('springroll.tasks').TerminalWindow = TerminalWindow;
-
-}());
\ No newline at end of file
diff --git a/src/js/tasks/ui/Interface.js b/src/js/tasks/ui/Interface.js
deleted file mode 100644
index 1cddba3..0000000
--- a/src/js/tasks/ui/Interface.js
+++ /dev/null
@@ -1,88 +0,0 @@
-(function(){
-
- if (APP)
- {
- // Global node modules
- var fs = require('fs');
- var path = require("path");
- }
-
- // Import classes
- var TerminalWindow = springroll.tasks.TerminalWindow,
- Settings = springroll.tasks.Settings;
-
- /**
- * The main interface class
- * @class Interface
- * @namespace springroll.tasks
- * @constructor
- * @param {springroll.tasks.TaskRunner} app The instance of the app
- */
- var Interface = function(app)
- {
- $("#refreshTasks").click(function(){
- app.refreshTasks();
- });
-
- var body = $('body').on(
- 'dblclick',
- '.JS-Task-Toggle-Info',
- function()
- {
- var button = $(this).find('.JS-Task-Run');
- app.toggleTask(button.data('task-name'));
- return false;
- }
- )
- .on(
- 'click',
- '.JS-Task-Run',
- function()
- {
- app.toggleTask($(this).data('task-name'));
- return false;
- }
- )
- .on(
- 'click',
- '.JS-Task-Terminal',
- function(e)
- {
- var button = $(this);
- var taskName = button.data('task-name').toString();
-
- if (!app.terminal)
- {
- app.terminal = new TerminalWindow(taskName);
- }
- else
- {
- app.terminal.open(taskName);
- }
- return false;
- }
- )
- .on(
- 'click',
- '.JS-Task-Stop',
- function()
- {
- app.toggleTask($(this).data('task-name'));
- return false;
- }
- );
-
- $(document).on(
- 'dragover',
- function handleDragOver(event)
- {
- event.stopPropagation();
- event.preventDefault();
- }
- );
- };
-
- // Assign to namespace
- namespace('springroll.tasks').Interface = Interface;
-
-}());
\ No newline at end of file
diff --git a/src/js/tasks/utils/Settings.js b/src/js/tasks/utils/Settings.js
deleted file mode 100644
index 03e2061..0000000
--- a/src/js/tasks/utils/Settings.js
+++ /dev/null
@@ -1,58 +0,0 @@
-(function(){
-
- /**
- * For storing and retrieving data
- * @class Settings
- * @namespace springroll.tasks
- */
- var Settings = {};
-
- /**
- * Save the window settings
- * @method saveWindow
- * @static
- * @param {String} alias The alias for the window
- * @param {Window} win Node webkit window object
- * @param {Boolean} [isTerminal=false] If we're setting the terminal window
- */
- Settings.saveWindow = function(alias, win)
- {
- localStorage[alias] = JSON.stringify({
- width : win.width,
- height : win.height,
- x : win.x,
- y : win.y
- });
- };
-
- /**
- * Load the window to the saved size
- * @method loadWIndow
- * @static
- * @param {String} alias The alias for the window
- * @param {Window} win The GUI window object
- */
- Settings.loadWindow = function(alias, win)
- {
- var rect;
- try
- {
- rect = JSON.parse(localStorage[alias] || 'null');
- }
- catch(e)
- {
- alert('Error Reading Spock Window! Reverting to defaults.');
- }
- if (rect)
- {
- win.width = rect.width;
- win.height = rect.height;
- win.x = rect.x;
- win.y = rect.y;
- }
- };
-
- // Assign to global space
- namespace('springroll.tasks').Settings = Settings;
-
-})();
\ No newline at end of file
diff --git a/src/js/tasks/utils/Utils.js b/src/js/tasks/utils/Utils.js
deleted file mode 100644
index ff47124..0000000
--- a/src/js/tasks/utils/Utils.js
+++ /dev/null
@@ -1,54 +0,0 @@
-(function(){
-
- if (APP)
- {
- // Import node modules
- var md5 = require("MD5");
- var fs = require("fs");
- }
-
- /**
- * Basic app utilities
- * @class Utils
- * @namespace springroll.tasks
- */
- var Utils = {};
-
- /**
- * The collection of templates for markup
- * @static
- * @property {dict} templates
- * @private
- */
- Utils.templates = {};
-
- /**
- * Get the templates and do the subtitutions
- * @method getTemplate
- * @static
- * @param {string} templateFileName The name of the template
- * @param {object} obj The object to template
- * @return {string} The templated string
- */
- Utils.getTemplate = function(templateFileName, obj)
- {
- if (!Utils.templates[templateFileName])
- {
- var template = fs.readFileSync(
- "assets/html/" + templateFileName + ".html",
- {encoding: "utf-8"}
- );
-
- Utils.templates[templateFileName] = template;
- return _.template(template)(obj);
- }
- else
- {
- return _.template(Utils.templates[templateFileName])(obj);
- }
- };
-
- // Assign to the global space
- namespace('springroll.tasks').Utils = Utils;
-
-}());
\ No newline at end of file
diff --git a/src/less/Module.less b/src/less/Module.less
deleted file mode 100644
index 021f2e0..0000000
--- a/src/less/Module.less
+++ /dev/null
@@ -1,4 +0,0 @@
-@import 'mixins';
-@import 'header';
-@import 'fonts';
-@import 'scrollbar';
\ No newline at end of file
diff --git a/src/less/SpringRollStudio.less b/src/less/SpringRollStudio.less
deleted file mode 100755
index a41dbcb..0000000
--- a/src/less/SpringRollStudio.less
+++ /dev/null
@@ -1,96 +0,0 @@
-@import 'fonts';
-
-body {
- -webkit-user-select:none;
- cursor:default;
- font-family:'OpenSans', sans-serif;
- font-weight: lighter;
- width:100%;
- height:100%;
- position:absolute;
- padding:0 10px;
- background-color:#fff;
- &.dragging {
- box-shadow: inset 0px 0px 10px 0px rgba(0,0,0,0.3);
- background-color:#eee;
- }
-}
-
-.modules {
- width:100%;
- margin-top: 10px;
- &:last-child {
- margin-bottom: 10px;
- }
- a, button {
- font-weight:lighter;
- }
-}
-
-.project {
- background:#363842;
- padding:10px 0;
- border-radius: 4px;
- border-top-left-radius: 0;
- border-top-right-radius: 0;
- text-align: center;
- margin:0;
- width:auto;
- color:#fff;
- border:0;
- .name:before {
- content:"Project:";
- margin-right:5px;
- color:#999;
- }
- .empty {
- display:none;
- color:#999;
- }
- &.empty {
- .empty {
- display:block;
- }
- .name {
- display:none;
- }
- }
-}
-
-header {
- .logo {
- display:block;
- margin:10px auto 0;
- width:100px;
- }
-}
-
-input[type="file"] {
- position:absolute;
- top:0;
- left:0;
-}
-
-nav {
- .btn {
- width:33.55%;
- text-align:center;
- &:first-child {
- border-bottom-left-radius: 0;
- }
- &:last-child {
- border-bottom-right-radius: 0;
- }
- }
-}
-
-.icon {
- margin-right:0.3em;
-}
-
-.version {
- text-align: center;
- margin-top: 7px;
- font-size:90%;
- color: #999;
-}
\ No newline at end of file
diff --git a/src/less/captions/Captions.less b/src/less/captions/Captions.less
deleted file mode 100644
index cf1fb2a..0000000
--- a/src/less/captions/Captions.less
+++ /dev/null
@@ -1,7 +0,0 @@
-@import '../mixins';
-@import 'global';
-@import 'header';
-@import 'list';
-@import 'controls';
-@import 'timeline';
-@import 'modal';
\ No newline at end of file
diff --git a/src/less/captions/controls.less b/src/less/captions/controls.less
deleted file mode 100644
index 58cba32..0000000
--- a/src/less/captions/controls.less
+++ /dev/null
@@ -1,12 +0,0 @@
-#controls {
- position:absolute;
- bottom:10px;
- width:100%;
- .toggle {
- &.active .on, .off { display:inline; }
- &.active .off, .on { display:none; }
- }
- .main, .sub {
- padding:0;
- }
-}
\ No newline at end of file
diff --git a/src/less/captions/global.less b/src/less/captions/global.less
deleted file mode 100644
index e29a9fd..0000000
--- a/src/less/captions/global.less
+++ /dev/null
@@ -1,40 +0,0 @@
-.btn:focus {
- outline: none !important;
-}
-
-body,
-html {
- height:100%;
- width:100%;
- overflow:hidden;
- font-family:'OpenSans', sans-serif;
-}
-
-body, body * {
- user-select: none;
- cursor:default;
-}
-
-.contentBorder {
- border:2px solid #ccc;
- border-radius:6px;
- background:#eee;
- overflow-x: scroll;
- overflow-y: hidden;
- box-sizing:border-box;
- padding:5px;
- position:absolute;
- left:15px;
- right:15px;
- margin:0;
-}
-
-#frame {
- min-width:510px;
- min-height:400px;
- position:absolute;
- top:0;
- left:0;
- width:100%;
- height:100%;
-}
\ No newline at end of file
diff --git a/src/less/captions/header.less b/src/less/captions/header.less
deleted file mode 100644
index 63da7b9..0000000
--- a/src/less/captions/header.less
+++ /dev/null
@@ -1,6 +0,0 @@
-/*header {
-
- #refreshButton {
- right:40px;
- }
-}*/
\ No newline at end of file
diff --git a/src/less/captions/list.less b/src/less/captions/list.less
deleted file mode 100644
index 713c0ed..0000000
--- a/src/less/captions/list.less
+++ /dev/null
@@ -1,49 +0,0 @@
-#list {
- .contentBorder();
- transition: border-color 0.2s, background 0.2s;
- &.dragover {
- border-color: #84B0DF;
- background: #DCE7F3;
- }
- bottom:205px;
- top:50px;
- -webkit-column-width: 10em;
- -moz-column-width: 10em;
- column-width: 10em;
- -webkit-column-gap: 1rem;
- -moz-column-gap: 1rem;
- column-gap: 1rem;
- max-width: none;
- button {
- display:block;
- width:100%;
- margin:0 0 5px 0;
- break-inside: avoid-column;
- -webkit-column-break-inside: avoid;
- }
-}
-
-#instructions {
- position:absolute;
- top:50px;
- width:100%;
- margin:0;
- padding:0;
- bottom:205px;
- text-align: center;
- z-index: 2;
- display:none;
- .unselectable();
- .text {
- position:absolute;
- top:50%;
- width:100%;
- -webkit-transform:translateY(-50%);
- color:#999;
- font-size:150%;
- line-height: 1;
- }
- body.empty & {
- display: block;
- }
-}
\ No newline at end of file
diff --git a/src/less/captions/modal.less b/src/less/captions/modal.less
deleted file mode 100644
index 74d3a68..0000000
--- a/src/less/captions/modal.less
+++ /dev/null
@@ -1,11 +0,0 @@
-.modal {
- .input-group-addon:first-child {
- width:120px;
- }
- .form-row {
- margin-bottom:10px;
- &:last-child {
- margin-bottom:0;
- }
- }
-}
\ No newline at end of file
diff --git a/src/less/captions/timeline.less b/src/less/captions/timeline.less
deleted file mode 100644
index 3e76ec8..0000000
--- a/src/less/captions/timeline.less
+++ /dev/null
@@ -1,79 +0,0 @@
-#timeline {
- .contentBorder();
- background-image: linear-gradient(90deg,
- rgba(0, 0, 0, 0.15) 0.5%,
- transparent 1%
- );
- background-size: 100px 100px;
- height:130px;
- bottom:65px;
- a, button {
- position:absolute;
- z-index:3;
- margin:0 0 0 4px;
- }
- #resizeRight,
- #resizeLeft {
- top: 5px;
- height: 105px;
- width: 23px;
- padding: 0;
- cursor: ew-resize;
- width: 15px;
- box-shadow: none;
- }
- #resizeRight {
- border-top-left-radius: 0;
- border-bottom-left-radius: 0;
- }
- #resizeLeft {
- border-top-right-radius: 0;
- border-bottom-right-radius: 0;
- }
- #removeButton {
- top:5px;
- margin-left:8px;
- }
- #captions {
- position:absolute;
- z-index:1;
- textarea {
- user-select:none;
- font-family:'OpenSans', Helvetica, sans-serif;
- resize: none;
- font-size: 12px;
- text-align:center;
- padding: 6px;
- overflow: hidden;
- width:200px;
- position: absolute;
- background: #428bca;
- box-shadow:none;
- color:#fff;
- height:30px;
- transition: background 0.2s, height 0.2s, color 0.2s;
- top:0;
- &:focus, &.current {
- cursor:text;
- user-select: text;
- padding: 6px 12px;
- font-family:'OpenSans', sans-serif;
- font-weight: lighter;
- font-size: 18px;
- color:#333;
- background:rgba(255,255,255,0.5);
- height:105px;
- top:0;
- }
- }
- }
- #wave {
- position:absolute;
- z-index:0;
- top:35px;
- padding-right:200px;
- }
- &.disabled #wave{
- display:none;
- }
-}
\ No newline at end of file
diff --git a/src/less/fonts.less b/src/less/fonts.less
deleted file mode 100644
index 22db631..0000000
--- a/src/less/fonts.less
+++ /dev/null
@@ -1,48 +0,0 @@
-@font-face {
- font-family: 'SourceCodePro';
- src: url('../fonts/sourcecodepro-regular-webfont.eot');
- src: url('../fonts/sourcecodepro-regular-webfont.eot?#iefix') format('embedded-opentype'),
- url('../fonts/sourcecodepro-regular-webfont.woff2') format('woff2'),
- url('../fonts/sourcecodepro-regular-webfont.woff') format('woff'),
- url('../fonts/sourcecodepro-regular-webfont.ttf') format('truetype'),
- url('../fonts/sourcecodepro-regular-webfont.svg#source_code_proregular') format('svg');
- font-weight: normal;
- font-style: normal;
-}
-
-@font-face {
- font-family: 'SourceCodePro';
- src: url('../fonts/sourcecodepro-bold-webfont.eot');
- src: url('../fonts/sourcecodepro-bold-webfont.eot?#iefix') format('embedded-opentype'),
- url('../fonts/sourcecodepro-bold-webfont.woff2') format('woff2'),
- url('../fonts/sourcecodepro-bold-webfont.woff') format('woff'),
- url('../fonts/sourcecodepro-bold-webfont.ttf') format('truetype'),
- url('../fonts/sourcecodepro-bold-webfont.svg#source_code_probold') format('svg');
- font-weight: bold;
- font-style: normal;
-}
-
-
-@font-face {
- font-family: 'OpenSans';
- src: url('../fonts/opensans-regular-webfont.eot');
- src: url('../fonts/opensans-regular-webfont.eot?#iefix') format('embedded-opentype'),
- url('../fonts/opensans-regular-webfont.woff2') format('woff2'),
- url('../fonts/opensans-regular-webfont.woff') format('woff'),
- url('../fonts/opensans-regular-webfont.ttf') format('truetype'),
- url('../fonts/opensans-regular-webfont.svg#OpenSans') format('svg');
- font-weight: normal;
- font-style: normal;
-}
-
-@font-face {
- font-family: 'OpenSans';
- src: url('../fonts/opensans-light-webfont.eot');
- src: url('../fonts/opensans-light-webfont.eot?#iefix') format('embedded-opentype'),
- url('../fonts/opensans-light-webfont.woff2') format('woff2'),
- url('../fonts/opensans-light-webfont.woff') format('woff'),
- url('../fonts/opensans-light-webfont.ttf') format('truetype'),
- url('../fonts/opensans-light-webfont.svg#OpenSans') format('svg');
- font-weight: lighter;
- font-style: normal;
-}
\ No newline at end of file
diff --git a/src/less/header.less b/src/less/header.less
deleted file mode 100644
index 47e331a..0000000
--- a/src/less/header.less
+++ /dev/null
@@ -1,48 +0,0 @@
-header {
- h1 {
- z-index:1;
- width:100%;
- position:absolute;
- text-align:center;
- color:#fff;
- font-family: 'OpenSans', sans-serif;
- font-weight: lighter;
- font-size:20px;
- padding: 0;
- margin: 0;
- margin-top: 10px;
- letter-spacing: -0.02em;
- .unselectable();
- }
- height:40px;
- background-color: #428bca;
-
- &.fixed {
- position:fixed;
- width:100%;
- top:0;
- left:0;
- z-index:100;
- }
- button {
- position:absolute;
- z-index:2;
- border:0;
- top:4px;
- right:4px;
- font-size:120%;
- padding:4px 8px;
- background: transparent;
- opacity:0.6;
- color:#fff;
- transition:opacity 0.2s, background 0.2s;
- &:focus, &:hover {
- opacity:1;
- background:rgba(0, 0, 0, 0.5);
- }
- span {
- cursor:pointer;
- color: #fff;
- }
- }
-}
\ No newline at end of file
diff --git a/src/less/mixins.less b/src/less/mixins.less
deleted file mode 100644
index bc6aa35..0000000
--- a/src/less/mixins.less
+++ /dev/null
@@ -1,6 +0,0 @@
-.unselectable() {
- -webkit-user-select: none;
- user-select:none;
- pointer-events:none;
- cursor:default;
-}
\ No newline at end of file
diff --git a/src/less/new/NewProject.less b/src/less/new/NewProject.less
deleted file mode 100644
index c37ea2f..0000000
--- a/src/less/new/NewProject.less
+++ /dev/null
@@ -1,133 +0,0 @@
-@import '../mixins';
-
-body {
- overflow: hidden;
- font-family: 'Open Sans', sans-serif;
- font-weight: normal;
-}
-
-.modal-body.main {
- padding-bottom:0;
-}
-
-label.disabled {
- color:#999;
-}
-
-label {
- display:block;
- .on { display:none }
- .off { display:inline-block }
- &.active {
- .on { display:inline-block }
- .off { display:none }
- }
-}
-
-#modules {
- height:220px;
- border:1px solid #ccc;
- border-radius:4px;
- overflow-y: scroll;
- -webkit-user-select: none;
- .checkbox {
- border-bottom: 1px solid #ddd;
- padding: 5px 10px 5px;
- position: relative;
- &:last-child {
- border: 0;
- }
- .name {
- font-size:85%;
- font-weight:bold;
- }
- .id {
- font-size: 11px;
- margin-left: 5px;
- color: #999;
- border: 1px solid #ddd;
- border-top: 0;
- padding: 2px 8px;
- border-radius: 4px;
- border-top-left-radius: 0;
- border-top-right-radius: 0;
- display: block;
- position: absolute;
- top: 0;
- right: 0;
- }
- .description {
- display: block;
- font-size: 85%;
- color: #666;
- padding-top: 5px;
- }
- }
-}
-
-#dropTemplate {
- position: relative;
- min-height: 150px;
- border: 4px solid #ccc;
- width: 100%;
- color: #999;
- background-color: #F7F7F7;
- font-size: 120%;
- transition:background-color 0.2s, color 0.2s, border-color 0.2s;
- cursor:pointer;
- margin-bottom:20px;
- .folder { display:none }
- .instructions { display:block }
- .error { display:none }
- &.dragging {
- background-color:#EEFFFA;
- border-color:#6BD2B4;
- color:#5EB9A2;
- }
- &.has-folder {
- border-color:#73ADEC;
- color:#5E84C8;
- background-color: #E9F2FF;
- .folder { display:block }
- .instructions { display:none }
- .error { display:none }
- }
- &.has-error {
- border-color:#DF8D8D;
- color: #D17B7B;
- background-color:#FFEBEB;
- .folder { display:none }
- .instructions { display:none }
- .error { display:block }
- }
-}
-
-.templates {
- max-height:280px;
- overflow:hidden;
- overflow-y: auto;
- .template {
- padding: 5px 0 0;
- height: 45px;
- border-top: 1px solid #ddd;
- &:last-child {
- border-bottom:1px solid #ddd;
- }
- .name {
- font-size: 110%;
- display: block;
- float: left;
- padding-top: 5px;
- }
- }
-}
-
-.center-vertical {
- transform:translateY(-50%);
- -webkit-transform:translateY(-50%);
- position:absolute;
- display:block;
- width: 100%;
- text-align:center;
- top:50%;
-}
\ No newline at end of file
diff --git a/src/less/preview/Preview.less b/src/less/preview/Preview.less
deleted file mode 100644
index b3a5959..0000000
--- a/src/less/preview/Preview.less
+++ /dev/null
@@ -1,268 +0,0 @@
-@import 'preview-icons';
-@import 'preview-mixins';
-
-@uiHeight: 40px;
-@smBreak: 500px;
-
-* {
- /* make transparent link selection, adjust last value opacity 0 to 1.0 */
- -webkit-tap-highlight-color: rgba(0,0,0,0);
-}
-
-body {
- /* prevent callout to copy image, etc when tap to hold */
- -webkit-touch-callout: none;
- /* prevent webkit from resizing text to fit */
- -webkit-text-size-adjust: none;
- /* prevent copy paste, to allow, change 'none' to 'text' */
- .user-select(none);
- position:absolute;
- background-color:#000;
- font-family: 'OpenSans', sans-serif;
- font-weight: lighter;
- font-size:12px;
- height:200%; // hack for scroll-up hiding address bar
- margin:0px;
- padding:0px;
- color:white;
- width:100%;
- text-align: left;
- overflow: hidden;
-}
-
-.drop-down {
- position:absolute;
- display:none;
- padding: 1px 1px 1px 0;
- background:#222;
- &.on {
- display:block;
- }
- height:auto;
- width:374px;
- padding:10px;
- margin-left: 1px;
- .box-sizing(border-box);
- border-bottom-left-radius: 6px;
- border-bottom-right-radius: 6px;
- .form-group {
- font-size:12px;
- color:#ccc;
- .btn {
- .label { font-size: 18px }
- .icon { font-size: 20px }
- }
- }
- .form-title {
- font-size:16px;
- text-align: left;
- color: #ccc;
- margin: 5px 0 15px;
- .close {
- color: #fff;
- }
- }
- @media screen and (max-width: @smBreak) {
- .scrollDropDowns();
- }
- .touch & {
- .scrollDropDowns();
- }
-}
-
-.scrollDropDowns()
-{
- width:100%;
- margin-left:0;
- position:fixed;
- top:@uiHeight;
- bottom:0;
- left:0;
- right:0;
- overflow: auto;
- overflow-x: hidden;
- -webkit-overflow-scrolling: touch;
-}
-
-#pausedScreen {
- z-index: 2;
- position: absolute;
- left: 0;
- top:0;
- bottom:0;
- width:100%;
- height:0;
- overflow:hidden;
- background-color:transparent;
- .no-touch & {
- .transition(background-color 0.4s);
- }
- .container {
- display:none;
- position:absolute;
- top:50%;
- text-align: center;
- margin-top:-45px;
- height:90px;
- width:100%;
- h2 {
- margin:0 0 0.5em;
- font-size:180%;
- font-weight:300;
- }
- }
- .paused & {
- height:auto;
- background-color:rgba(0,0,0,0.6);
- .container {
- display:block;
- }
- }
- .loading & {
- .container { display:none }
- }
- @media screen and (max-width: @smBreak) {
- h2 {
- font-size:150%;
- }
- }
-}
-
-#browserLink {
- position:absolute;
- padding:4px 0;
- bottom:0;
- z-index:3;
- width:100%;
- text-align: center;
- color: #555;
- .user-select(initial);
- background:#000;
-}
-
-#frame {
- position:fixed;
- z-index:1;
- width:100%;
- height:100%;
- top:0;
- left:0;
- background: #222;
- &.show-controls {
- .controls {
- display:block;
- }
- #pausedScreen {
- top:@uiHeight;
- }
- .appWrapper {
- top:@uiHeight;
- }
- }
- .controls {
- .internal & {
- .btn.main {
- width: percentage(1/7);
- &.drop-handle {
- width: percentage(1/14);
- }
- }
- }
- display:none;
- position:absolute;
- top:0;
- right:0;
- height:@uiHeight;
- z-index:3;
- font-size:0;
- width:375px;
- .btn.main {
- outline:none;
- cursor:pointer;
- border-radius:0;
- border:0;
- display:inline-block;
- height:100%;
- font-size:24px;
- width: percentage(1/5);
- -webkit-box-sizing: border-box;
- box-sizing: border-box;
- border-left: 1px solid rgba(0,0,0, 0.6);
- margin:0;
- padding:0;
-
- // The drop down handle for the audio
- // and captions additional options
- &.drop-handle {
- margin-left:0;
- padding:0 7px;
- border-left: 1px solid rgba(0,0,0, 0.1);
- width: percentage(1/10);
- .arrow-down {
- width: 0;
- height: 0;
- border-left: 5px solid transparent;
- border-right: 5px solid transparent;
- border-top: 5px solid #fff;
- display: inline-block;
- margin-bottom: 7px;
- }
- &.disabled {
- border-left-color: #777;
- .arrow-down {
- border-top-color:#999;
- }
- }
- }
- .glyphicon {
- font-size:20px;
- }
- .icon {
- width:26px;
- display:inline-block;
- }
- }
- .unpaused .off,
- .unmuted .off { display:none !important }
- .paused .on,
- .muted .on { display:none !important }
- @media screen and (max-width: @smBreak) {
- width:100%;
- }
- }
-}
-
-#appTitle {
- display:none;
- position: absolute;
- top: 5px;
- left: 15px;
- color: #fff;
- font-size: 160%;
- .show-title & {
- display:block;
- }
- @media screen and (max-width: @smBreak) {
- display:none !important;
- }
-}
-
-.appWrapper {
- position: absolute;
- top:0;
- bottom:0;
- .internal & {
- bottom:25px;
- }
- right:0;
- left:0;
- z-index:0;
- background:#000;
-}
-
-#appContainer {
- position: absolute;
- width:100%;
- height:100%;
- z-index:0;
-}
\ No newline at end of file
diff --git a/src/less/preview/preview-icons.less b/src/less/preview/preview-icons.less
deleted file mode 100644
index 510b6cf..0000000
--- a/src/less/preview/preview-icons.less
+++ /dev/null
@@ -1,71 +0,0 @@
-@font-face {
- font-family: 'preview';
- src:url('../fonts/preview.eot?dfjxtz');
- src:url('../fonts/preview.eot?#iefixdfjxtz') format('embedded-opentype'),
- url('../fonts/preview.woff?dfjxtz') format('woff'),
- url('../fonts/preview.ttf?dfjxtz') format('truetype'),
- url('../fonts/preview.svg?dfjxtz#preview') format('svg');
- font-weight: normal;
- font-style: normal;
-}
-
-[class^="icon-"], [class*=" icon-"] {
- font-family: 'preview';
- speak: none;
- font-style: normal;
- font-weight: normal;
- font-variant: normal;
- text-transform: none;
- line-height: 1;
-
- /* Better Font Rendering =========== */
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
-}
-
-.icon-captions-off:before {
- content: "\e600";
-}
-.icon-captions:before {
- content: "\e601";
-}
-.icon-close:before {
- content: "\e602";
-}
-.icon-help:before {
- content: "\e603";
-}
-.icon-music-off:before {
- content: "\e604";
-}
-.icon-music:before {
- content: "\e605";
-}
-.icon-sfx-off:before {
- content: "\e606";
-}
-.icon-sfx:before {
- content: "\e607";
-}
-.icon-sound-off:before {
- content: "\e608";
-}
-.icon-sound:before {
- content: "\e609";
-}
-.icon-vo-off:before {
- content: "\e60a";
-}
-.icon-vo:before {
- content: "\e60b";
-}
-.icon-gear:before {
- content: "\e60c";
-}
-.icon-play:before {
- content: "\ea1c";
-}
-.icon-pause:before {
- content: "\ea1d";
-}
-
diff --git a/src/less/preview/preview-mixins.less b/src/less/preview/preview-mixins.less
deleted file mode 100644
index c6a1ae0..0000000
--- a/src/less/preview/preview-mixins.less
+++ /dev/null
@@ -1,27 +0,0 @@
-.user-select(@u){
- -webkit-user-select: @u;
- -moz-user-select: @u;
- -ms-user-select: @u;
- user-select: @u;
-}
-
-.box-sizing(@u){
- -webkit-box-sizing:@u;
- -moz-box-sizing:@u;
- -ms-box-sizing:@u;
- box-sizing:@u;
-}
-
-.transform(@u){
- -webkit-transform: @u;
- -moz-transform: @u;
- -ms-transform: @u;
- transform: @u;
-}
-
-.transition(@u){
- -webkit-transition:@u;
- -moz-transition:@u;
- -ms-transition:@u;
- transition:@u;
-}
\ No newline at end of file
diff --git a/src/less/remote/RemoteTrace.less b/src/less/remote/RemoteTrace.less
deleted file mode 100755
index 4fde0f5..0000000
--- a/src/less/remote/RemoteTrace.less
+++ /dev/null
@@ -1,4 +0,0 @@
-@import '../mixins';
-@import 'base';
-@import 'trace';
-@import 'controls';
\ No newline at end of file
diff --git a/src/less/remote/base.less b/src/less/remote/base.less
deleted file mode 100644
index 97f981a..0000000
--- a/src/less/remote/base.less
+++ /dev/null
@@ -1,17 +0,0 @@
-.btn:focus {
- outline: none !important;
-}
-
-html, body {
- overflow: hidden;
-}
-
-#frame {
- min-width:400px;
- min-height:275px;
- position:absolute;
- top:0;
- left:0;
- width:100%;
- height:100%;
-}
\ No newline at end of file
diff --git a/src/less/remote/controls.less b/src/less/remote/controls.less
deleted file mode 100644
index 4bab8e6..0000000
--- a/src/less/remote/controls.less
+++ /dev/null
@@ -1,48 +0,0 @@
-.controls {
- width: 100%;
- position:absolute;
- left:0;
- top:40px;
- height:40px;
- background:#EAEAEA;
- z-index: 2;
- border-bottom: 1px solid #ccc;
-
- .buttons {
- margin-top:3px;
- margin-left:3px;
- }
- .btn.disabled {
- background:#CACACA;
- }
- @media screen and (min-width:560px){
- .btn {
- width:60px;
- }
- }
- .maxLogs {
- float:right;
- width: 140px;
- margin-top: 3px;
- margin-right:3px;
-
- @media screen and (max-width:560px){
- width:130px;
- .input-group-addon {
- font-size:80%;
- }
- }
- }
- #maxLogs {
- text-align:center
- }
-
- .dropdown-menu a {
- .on {display:none}
- .off {display:inline}
- &.selected {
- .on {display:inline}
- .off {display:none}
- }
- }
-}
\ No newline at end of file
diff --git a/src/less/remote/trace.less b/src/less/remote/trace.less
deleted file mode 100644
index c6fba3e..0000000
--- a/src/less/remote/trace.less
+++ /dev/null
@@ -1,165 +0,0 @@
-.user-deselect() {
- -webkit-user-select:none;
- -moz-user-select:none;
- -o-user-select:none;
- -ms-user-select:none;
- user-select:none;
-}
-
-#trace {
- overflow: auto;
- position:absolute;
- top:80px;
- bottom:0;
- left:0;
- right:0;
- padding:10px;
-
- .error,
- .warning,
- .info,
- .debug,
- .general {
- display:none;
- }
-
- &.show-error .error,
- &.show-warning .warning,
- &.show-info .info,
- &.show-debug .debug,
- &.show-general .general {
- display: block;
- }
-
- .group {
- padding-left:20px;
- display: block;
- overflow: hidden;
- }
-
- .log {
- margin-bottom:5px;
- font-size: 12px;
- font-family: "SourceCodePro", monospace;
- font-weight:normal;
- white-space:nowrap;
-
- &.session {
- font-weight:700;
- border-bottom:1px solid #999;
- margin:10px 0;
- }
- .type {
- font-weight:bold;
- padding-right: 10px;
- display:block;
- float: left;
- }
- .timestamp {
- display: none;
- opacity: 0.8;
- padding-right: 10px;
- font-size: 10px;
- float: left;
- }
- .message {
- margin: 0 auto;
- display: block;
- width: 100%;
- }
-
- .stackLink {
- float: right;
- cursor: pointer;
- text-decoration: underline;
- }
-
- .stack {
- .line {
- display: block;
- }
- }
-
- .groupToggle,
- .objectToggle {
- .user-deselect();
- cursor: pointer;
- padding-right: 5px;
- .down { display:inline; }
- .right { display:none; }
- &.collapsed {
- .down { display:none; }
- .right { display:inline; }
- }
- }
-
- .groupToggle {
- display: block;
- float: left;
- }
-
- .object {
- display: inline-block;
- vertical-align: top;
- .line {
- display: block;
- }
- }
- }
- // Options for the trace
- &.wrap .log {
- white-space:pre;
- }
- &.timestamps .timestamp {
- display: block;
- }
-
- // Default color scheme
- &.default {
- background:#fff;
- .session,
- .general { color: #000000;}
- .info { color: #009933;}
- .debug { color: #0066CC;}
- .warning {color: #FF9900;}
- .error {color: #CC0000;}
- .stack .line:hover { background:#ddd; }
- }
- &.solarizedDark {
- background:#002B36;
- .session,
- .general {color: #93A1A1;}
- .info {color: #2AA198;}
- .debug {color: #859900;}
- .warning {color: #DC322F;}
- .error {color: #D33682;}
- .stack .line:hover { background:#ddd; }
- }
- &.solarizedLight {
- background:#FDF6E3;
- .session,
- .general {color: #586E75;}
- .info {color: #2AA198;}
- .debug {color: #859900;}
- .warning {color: #DC322F;}
- .error {color: #D33682;}
- .stack .line:hover { background:#ddd; }
- }
-}
-
-.sunlight-code-container {
- overflow-y: auto;
- height: 500px;
- border-width: 1px;
- border-style: solid;
-}
-
-.sunlight-highlight-javascript {
- //no standard padding because that messes with the line numbers
- padding: 0px;
-}
-
-.sunlight-container {
- //display fully on top of the other remote trace ui
- z-index: 4;
-}
\ No newline at end of file
diff --git a/src/less/scrollbar.less b/src/less/scrollbar.less
deleted file mode 100644
index 7ee8cc2..0000000
--- a/src/less/scrollbar.less
+++ /dev/null
@@ -1,23 +0,0 @@
-::-webkit-scrollbar {
- width: 12px;
- height: 12px;
-}
-::-webkit-scrollbar-button {
- width: 12px;
- height: 12px;
-}
-::-webkit-scrollbar-track {
- background:transparent;
- border-radius:10px;
-}
-::-webkit-scrollbar-thumb {
- background:rgba(0, 0, 0, 0.2);
- border-radius:10px;
- border: 2px solid transparent;
- background-clip: content-box;
-}
-::-webkit-scrollbar-thumb:hover {
- background:rgba(0, 0, 0, 0.5);
- border: 2px solid transparent;
- background-clip: content-box;
-}
diff --git a/src/less/tasks/TaskRunner.less b/src/less/tasks/TaskRunner.less
deleted file mode 100644
index adee9ee..0000000
--- a/src/less/tasks/TaskRunner.less
+++ /dev/null
@@ -1,4 +0,0 @@
-@import '../mixins';
-@import 'icons';
-@import 'terminal';
-@import 'base';
\ No newline at end of file
diff --git a/src/less/tasks/base.less b/src/less/tasks/base.less
deleted file mode 100644
index 284bfc4..0000000
--- a/src/less/tasks/base.less
+++ /dev/null
@@ -1,125 +0,0 @@
-body {
- width: 100%;
- height: 100%;
- background: #F5F6F8;
- -webkit-user-select: none;
- font-family:'OpenSans', sans-serif;
- font-weight: lighter;
-
- // Hide the messages
- .loading,
- .failed,
- .installing {
- display:none;
- }
-
- // Show the messages
- &.loading .loading,
- &.failed .failed,
- &.installing .installing {
- display:block;
- }
-}
-
-.status {
- z-index:0;
- .state {
- text-align:center;
- font-size:110%;
- position:absolute;
- width:100%;
- top:50%;
- margin-top:20px;
- -webkit-transform:translateY(-50%);
- transform:translateY(-50%);
- }
- .failed {
- color:#c00;
- }
- .spinner {
- background:url(../images/loader.gif) no-repeat top center;
- padding-top:40px;
- }
-}
-
-.stripes(){
- background-color: #ddd;
- background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
- -webkit-animation: progress-bar-stripes 0.7s linear infinite;
- background-size: 40px 40px;
- -webkit-transition: width .6s ease;
- transition: width .6s ease;
-}
-
-@-webkit-keyframes progress-bar-stripes {
- from {
- background-position: 40px 0
- }
- to {
- background-position: 0 0
- }
-}
-.progress {
- height: 20px;
- margin-bottom: 20px;
- overflow: hidden;
- background-color: #f5f5f5;
- border-radius: 4px;
- -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);
-}
-* {
- -webkit-box-sizing: border-box;
- cursor: default;
-}
-.controls {
- visibility: hidden;
- position: absolute;
- right: 5px;
- top: 5px;
- opacity: 0;
-}
-#tasks, .status {
- overflow: auto;
-}
-#tasks {
- z-index:1;
-}
-.tasks {
- overflow-y: auto;
- position: fixed;
- width: 100%;
- top: 40px;
- bottom: 0;
-}
-.tasks-item {
- background-color: #fff;
- padding: 5px;
- border-bottom:1px solid #d3d3d3;
-
- &.running,
- &.error {
- .stripes();
- }
- &.error {
- background-color: #d9534f;
- -webkit-animation: none;
- color: #fff;
- }
-
- .play { display:inline-block; }
- .stop { display:none; }
-
- &.running {
- .play { display:none; }
- .stop { display:inline-block; }
- }
- .name {
- padding:6px 0 0 10px;
- display:block;
- float:left;
- font-weight:normal;
- }
-}
-.console {
- display:none;
-}
\ No newline at end of file
diff --git a/src/less/tasks/icons.less b/src/less/tasks/icons.less
deleted file mode 100644
index 4a349d2..0000000
--- a/src/less/tasks/icons.less
+++ /dev/null
@@ -1,46 +0,0 @@
-@font-face {
- font-family: 'task-runner';
- src:url('../fonts/task-runner.eot?zhu4hz');
- src:url('../fonts/task-runner.eot?#iefixzhu4hz') format('embedded-opentype'),
- url('../fonts/task-runner.woff?zhu4hz') format('woff'),
- url('../fonts/task-runner.ttf?zhu4hz') format('truetype'),
- url('../fonts/task-runner.svg?zhu4hz#task-runner') format('svg');
- font-weight: normal;
- font-style: normal;
-}
-
-[class^="icon-"], [class*=" icon-"] {
- font-family: 'task-runner';
- speak: none;
- font-style: normal;
- font-weight: normal;
- font-variant: normal;
- text-transform: none;
- line-height: 1;
-
- /* Better Font Rendering =========== */
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
-}
-
-.icon-left:before {
- content: "\e602";
-}
-.icon-remove:before {
- content: "\e603";
-}
-.icon-right:before {
- content: "\e604";
-}
-.icon-run:before {
- content: "\e605";
-}
-.icon-stop:before {
- content: "\e606";
-}
-.icon-terminal:before {
- content: "\e607";
-}
-.icon-uniE600:before {
- content: "\e600";
-}
diff --git a/src/less/tasks/terminal.less b/src/less/tasks/terminal.less
deleted file mode 100644
index c1ccf79..0000000
--- a/src/less/tasks/terminal.less
+++ /dev/null
@@ -1,24 +0,0 @@
-.tasks-terminal {
- -webkit-user-select: all;
- user-select: all;
- background:#363842;
- ::-webkit-scrollbar-track {
- background:#363842;
- }
- pre {
- font-family: Menlo, Monaco, Consolas, "Lucida Console", "Courier New", "Microsoft Yahei", monospace;
- font-size: 12px;
- line-height: 1.4em;
- top:40px;
- bottom:0;
- width: 100%;
- position: fixed;
- overflow: hidden;
- overflow-y: auto;
- padding: 10px;
- box-sizing: border-box;
- background: #363842;
- color: #93A1A1;
- border:0;
- }
-}
diff --git a/src/main/index.js b/src/main/index.js
new file mode 100644
index 0000000..df3acb0
--- /dev/null
+++ b/src/main/index.js
@@ -0,0 +1,112 @@
+'use strict';
+
+import { app, protocol, BrowserWindow, Menu, MenuItem, ipcMain } from 'electron';
+import { createProtocol } from 'vue-cli-plugin-electron-builder/lib';
+import installExtension, { VUEJS_DEVTOOLS } from 'electron-devtools-installer';
+import { studio } from './studio';
+import { template, captionStudioTemplate } from './studio/menus/AppMenuTemplate';
+
+const isDevelopment = process.env.NODE_ENV !== 'production';
+
+// 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.
+let win;
+
+// Scheme must be registered before the app is ready
+protocol.registerSchemesAsPrivileged([
+ { scheme: 'app', privileges: { secure: true, standard: true } }
+]);
+
+// Create menus from templates
+const menu = Menu.buildFromTemplate(template);
+const captionStudioMenu = Menu.buildFromTemplate(captionStudioTemplate);
+Menu.setApplicationMenu(menu);
+
+//on caption studio open or close, set the appropriate menu
+ipcMain.on('captionStudio', (event, page) => {
+ Menu.setApplicationMenu(page ? captionStudioMenu : menu);
+});
+
+/**
+ * Creates electron window
+ */
+function createWindow() {
+ // Create the browser window.
+ win = new BrowserWindow({
+ width: 2048,
+ height: 968,
+ webPreferences: {
+ // Use pluginOptions.nodeIntegration, leave this alone
+ // See nklayman.github.io/vue-cli-plugin-electron-builder/guide/security.html#node-integration for more info
+ nodeIntegration: process.env.ELECTRON_NODE_INTEGRATION,
+ webviewTag: true,
+ webSecurity: false
+ }
+ });
+
+ // Initialize SpringRoll Studio.
+ studio.initialize(win);
+
+ if (process.env.WEBPACK_DEV_SERVER_URL) {
+ // Load the url of the dev server if in development mode
+ win.loadURL(process.env.WEBPACK_DEV_SERVER_URL);
+ if (!process.env.IS_TEST) {
+ win.webContents.openDevTools();
+ }
+ } else {
+ createProtocol('app');
+ // Load the index.html when not in development
+ win.loadURL('app://./index.html');
+ }
+
+ win.on('closed', () => {
+ win = null;
+ });
+}
+
+// Quit when all windows are closed.
+app.on('window-all-closed', () => {
+ // On macOS it is common for applications and their menu bar
+ // to stay active until the user quits explicitly with Cmd + Q
+ if (process.platform !== 'darwin') {
+ app.quit();
+ }
+});
+
+app.on('activate', () => {
+ // On macOS it's common to re-create a window in the app when the
+ // dock icon is clicked and there are no other windows open.
+ if (win === null) {
+ createWindow();
+ }
+});
+
+// This method will be called when Electron has finished
+// initialization and is ready to create browser windows.
+// Some APIs can only be used after this event occurs.
+app.on('ready', async () => {
+ if (isDevelopment && !process.env.IS_TEST) {
+ // Install Vue Devtools
+ try {
+ await installExtension(VUEJS_DEVTOOLS);
+ } catch (e) {
+ console.error('Vue Devtools failed to install:', e.toString());
+ }
+ }
+ createWindow();
+});
+
+// Exit cleanly on request from parent process in development mode.
+if (isDevelopment) {
+ if (process.platform === 'win32') {
+ process.on('message', (data) => {
+ if (data === 'graceful-exit') {
+ app.quit();
+ }
+ });
+ } else {
+ process.on('SIGTERM', () => {
+ app.quit();
+ });
+ }
+}
diff --git a/src/main/studio/index.js b/src/main/studio/index.js
new file mode 100644
index 0000000..0495174
--- /dev/null
+++ b/src/main/studio/index.js
@@ -0,0 +1,158 @@
+import { join } from 'path';
+import { ipcMain, dialog, BrowserWindow } from 'electron';
+import { EVENTS, DIALOGS } from '../../constants';
+import { projectInfo, gamePreview, captionInfo } from './storage';
+
+import ProjectTemplateCreator from './managers/ProjectTemplateCreator';
+import { existsSync } from 'fs';
+
+/**
+ * Main application Singleton. This object is responsible for setting up logic specific to SpringRoll Studio.
+ * @class SpringRollStudio
+ */
+class SpringRollStudio {
+ /**
+ * Initialize SpringRoll Studio.
+ * @memberof SpringRollStudio
+ */
+ initialize(window) {
+ /** @type {BrowserWindow} */
+ this.window = window;
+
+ this.templateCreator = new ProjectTemplateCreator(this);
+ this.templateCreator.logger = this.templateCreationLogger.bind(this);
+
+ this.setupListeners();
+ }
+
+ /**
+ * Wire up all studio level event listeners.
+ * @memberof SpringRollStudio
+ */
+ setupListeners() {
+ ipcMain.on(EVENTS.OPEN_DIALOG, this.openDialog.bind(this));
+ ipcMain.on(EVENTS.CREATE_PROJECT_TEMPLATE, this.createProjectTemplate.bind(this));
+ ipcMain.on(EVENTS.OPEN_CAPTION_STUDIO, this.openCaptionStudio.bind(this));
+ ipcMain.on(EVENTS.PREVIEW_TARGET_SET, this.previewTargetSet.bind(this));
+ }
+
+ /**
+ * Handler for the EVENTS.OPEN_DIALOG event.
+ * @param {string} type
+ * @memberof SpringRollStudio
+ * @private
+ */
+ openDialog(event, type) {
+ if (typeof type !== 'string') {
+ throw new Error(`[Studio] Invalid dialog type. Expected string. [type = ${typeof type}]`);
+ }
+
+ switch (type) {
+ case DIALOGS.PROJECT_LOCATION_SETTER:
+ const options = {
+ title: 'Select SpringRoll Project',
+ defaultPath: projectInfo.location,
+ properties: ['openDirectory']
+ };
+
+ const paths = dialog.showOpenDialogSync(this.window, options);
+ if (paths !== undefined) {
+ projectInfo.location = paths[0];
+ captionInfo.audioLocation = paths[0]; //when the project location changes also change the default audio files directory
+ captionInfo.captionLocation = '';
+ }
+ break;
+
+ case DIALOGS.AUDIO_LOCATION_SETTER:
+ const audio_options = {
+ title: 'Select SpringRoll Project Audio Files Location',
+ defaultPath: captionInfo.aduioLocation,
+ properties: ['openDirectory']
+ };
+
+ const audio_paths = dialog.showOpenDialogSync(this.window, audio_options);
+ if (audio_paths !== undefined) {
+ captionInfo.audioLocation = audio_paths[0];
+ this.window.webContents.send(EVENTS.UPDATE_AUDIO_LOCATION);
+ }
+ break;
+
+ default:
+ throw new Error(`[Studio] Unrecognized dialog type. [type = ${type}]`);
+ }
+ }
+
+ /**
+ * Handler for EVENTS.CREATE_PROJECT_TEMPLATE event.
+ * @memberof SpringRollStudio
+ */
+ async createProjectTemplate(event, data) {
+ const result = await this.templateCreator.create(data.type, data.location);
+ if (!result || result.err) {
+ let msg = `Could not create ${data.type} template at ${data.location}`;
+ if (result && result.err) {
+ msg = result.err;
+ }
+ dialog.showErrorBox('Failed to create template', msg);
+ }
+ else if (result.success) {
+ projectInfo.location = data.location;
+ captionInfo.audioLocation = data.location;
+ }
+ this.window.webContents.send(EVENTS.PROJECT_CREATION_COMPLETE, result && !!result.success);
+ }
+
+ /**
+ * Handler for EVENTS.OPEN_CAPTION_STUDIO event.
+ * @memberof SpringRollStudio
+ */
+ openCaptionStudio() {
+ this.window.webContents.send(EVENTS.NAVIGATE, 'caption-studio');
+ }
+
+ /**
+ *Handler for the EVENTS.PREVIEW_TARGET_SET event.
+ * @memberof SpringRollStudio
+ */
+ previewTargetSet(event, data) {
+ gamePreview.previewTarget = data.type;
+
+ switch (data.type) {
+ case 'deploy':
+ const deployPath = join(projectInfo.location, 'deploy');
+ // Make sure the deploy folder exists because attempting to host it.
+ if (!existsSync(deployPath)) {
+ dialog.showErrorBox(
+ 'Deploy folder not found.',
+ `Could not find a deploy folder in:\n${projectInfo.location}`
+ );
+ return;
+ }
+ gamePreview.previewURL = `file://${deployPath}`;
+ break;
+
+ case 'url':
+ if (data.url.indexOf('http:') === -1 && data.url.indexOf('https:') === -1) {
+ data.url = `http://${data.url}`;
+ }
+ gamePreview.previewURL = data.url;
+ break;
+ }
+
+ this.window.webContents.send(EVENTS.NAVIGATE, 'preview');
+ }
+
+ /**
+ *
+ * @param {string} log
+ * @memberof SpringRollStudio
+ */
+ templateCreationLogger(log) {
+ this.window.webContents.send(EVENTS.UPDATE_TEMPLATE_CREATION_LOG, log);
+ }
+}
+
+/**
+ * Export the singleton instance of SpringRoll Studio.
+ */
+export const studio = new SpringRollStudio();
\ No newline at end of file
diff --git a/src/main/studio/managers/ProjectTemplateCreator.js b/src/main/studio/managers/ProjectTemplateCreator.js
new file mode 100644
index 0000000..f8265da
--- /dev/null
+++ b/src/main/studio/managers/ProjectTemplateCreator.js
@@ -0,0 +1,238 @@
+import * as fs from 'fs';
+import { TEMPLATES } from '../../../constants';
+import { join } from 'path';
+import { app, net } from 'electron';
+import DecompressZip from 'decompress-zip';
+import ncp from 'ncp';
+import { promisify } from 'util';
+import rimraf from 'rimraf';
+
+/**
+ *
+ * @class ProjectTemplateCreator
+ */
+export default class ProjectTemplateCreator {
+ /**
+ * Creates an instance of ProjectTemplateCreator.
+ * @param {string} studio
+ * @memberof ProjectTemplateCreator
+ */
+ constructor(studio) {
+ this.studio = studio;
+ this.tempDir = join(app.getPath('userData'), 'tmp/');
+ }
+
+ /**
+ *
+ * @param {string[]} msg
+ * @memberof ProjectTemplateCreator
+ */
+ log(...msg) {
+ if (this.logger) {
+ this.logger(msg.join(' '));
+ }
+ else {
+ console.log(msg.join(' '));
+ }
+ }
+
+ /**
+ *
+ * @param {*} location
+ * @memberof ProjectTemplateCreator
+ */
+ isLocationEmpty(location) {
+ if (!fs.existsSync(location)) {
+ return true;
+ }
+ try {
+ const files = fs.readdirSync(location);
+ return files.length === 0;
+ }
+ catch (e) {
+ return false;
+ }
+ }
+
+ /**
+ *
+ * @param {string} type
+ * @param {string} location
+ * @returns
+ * @memberof ProjectTemplateCreator
+ */
+ create(type, location) {
+ return new Promise((resolve, reject) => {
+ if (!this.isLocationEmpty(location)) {
+ return reject({ err: 'New project location must be empty.' });
+ }
+ this.log(`Beginning ${type} template project creation:`);
+ this.log('Attempting to reach https://github.com.');
+
+ const request = net.request('https://github.com');
+ request.on('response', () => {
+ this.createFrom('github', type, location)
+ .then(resolve)
+ .catch((err) => {
+ if (err && err.err) {
+ this.log(err.err);
+ }
+ this.log('Failed to create project from GitHub. Falling back to local template archives.');
+ this.createFrom('file', type, location)
+ .then(resolve)
+ .catch(reject);
+ });
+ });
+ request.on('error', () => {
+ this.log('Could not reach https://github.com. Falling back to local template archives.');
+ this.createFrom('file', type, location)
+ .then(resolve)
+ .catch(reject);
+ });
+ request.end();
+ });
+ }
+
+ /**
+ *
+ * @param {string} from
+ * @param {string} type
+ * @param {string} location
+ * @memberof ProjectTemplateCreator
+ */
+ async createFrom(from, type, location) {
+ const path = await this.getTemplateZip(from, type);
+ try {
+ await this.extractTemplateFiles(path);
+ return await this.copyTemplateFilesTo(location);
+ }
+ catch (err) {
+ this.log('Failed to create new template project.');
+ return { err };
+ }
+ }
+
+ /**
+ *
+ * @param {string} from
+ * @param {string} type
+ * @memberof ProjectTemplateCreator
+ */
+ async getTemplateZip(from, type) {
+ const url = TEMPLATES[from][type];
+ switch (from) {
+ case 'github':
+ return await this.downloadTemplateZip(url, type);
+
+ case 'file':
+ if (process.env.NODE_ENV === 'production') {
+ return join(process.resourcesPath, url);
+ }
+ return join(__dirname, '../', url);
+ }
+ }
+
+ /**
+ *
+ * @param {string} url
+ * @returns
+ * @memberof ProjectTemplateCreator
+ */
+ downloadTemplateZip(url, type) {
+ this.log(`Downloading ${type} template files from ${url}`);
+
+ return new Promise(resolve => {
+ const session = this.studio.window.webContents.session;
+
+ const willDownload = (event, item, webContents) => {
+ const path = join(this.tempDir, 'template.zip');
+ item.setSavePath(path);
+
+ item.once('done', (event, state) => {
+ session.off('will-download', willDownload);
+
+ if (state === 'completed') {
+ this.log('Download complete successful.');
+
+ resolve(path);
+ }
+ else {
+ this.log('Failed to download template file from GitHub. Falling back to local archives.');
+
+ // If download fails, fallback to the local archive.
+ resolve(this.getTemplateZip('file', type));
+ }
+ });
+ };
+ session.on('will-download', willDownload);
+
+ session.downloadURL(url);
+ });
+ }
+
+ /**
+ *
+ * @param {string} source
+ * @returns
+ * @memberof ProjectTemplateCreator
+ */
+ extractTemplateFiles(source) {
+ return new Promise((resolve, reject) => {
+ const unzipper = new DecompressZip(source);
+ const extractLocation = join(this.tempDir, 'decompressed');
+
+ unzipper.on('error', (log) => {
+ this.log('Failed to extract template files.');
+
+ reject('Failed to extract template.');
+ });
+ unzipper.on('progress', (fileIndex, fileCount) => {
+ this.log(`Extracting progress: ${Math.ceil(((fileIndex + 1) / fileCount) * 100)}%`);
+ });
+ unzipper.on('extract', (log) => {
+ this.log('Extracting complete');
+
+ resolve();
+ });
+
+ this.log(`Extracting template files to ${extractLocation}`);
+
+ unzipper.extract({ path: extractLocation });
+ });
+ }
+
+ /**
+ *
+ * @param {string} location
+ * @memberof ProjectTemplateCreator
+ */
+ async copyTemplateFilesTo(location) {
+ const decompressed = join(this.tempDir, 'decompressed');
+
+ try {
+ const readDir = promisify(fs.readdir);
+ const files = await readDir(decompressed);
+
+ const path = join(decompressed, files[0]);
+
+ this.log(`Copying template files to ${location}`);
+
+ const copy = promisify(ncp);
+ await copy(path, location);
+
+ this.log('Cleaning up temp folder.');
+
+ const remove = promisify(rimraf);
+ await remove(this.tempDir);
+
+ this.log('Project creation complete.');
+
+ return { success: true };
+ }
+ catch (err) {
+ this.log('Failed to copy template files.');
+
+ return { err };
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/studio/menus/AppMenuTemplate.js b/src/main/studio/menus/AppMenuTemplate.js
new file mode 100644
index 0000000..02a9b4b
--- /dev/null
+++ b/src/main/studio/menus/AppMenuTemplate.js
@@ -0,0 +1,340 @@
+import { app } from 'electron';
+import { EVENTS } from '../../../constants';
+
+const isMac = process.platform === 'darwin';
+
+
+export const template = [
+ // { role: 'appMenu' }
+ ...(isMac ? [{
+ label: app.name,
+ submenu: [
+ { role: 'about' },
+ { type: 'separator' },
+ { role: 'hide' },
+ { role: 'hideothers' },
+ { role: 'unhide' },
+ { type: 'separator' },
+ { role: 'quit' }
+ ]
+ }] : []),
+ // { role: 'fileMenu' }
+ {
+ label: 'File',
+ submenu: [
+ {
+ label: 'Open...',
+ accelerator: isMac ? 'Cmd+O' : 'Cntrl+O',
+ click: async () => {
+ const { dialog, BrowserWindow } = require('electron');
+ const { projectInfo, captionInfo } = require('../storage');
+ const options = {
+ title: 'Select SpringRoll Project',
+ defaultPath: projectInfo.location,
+ properties: ['openDirectory']
+ };
+
+ const paths = dialog.showOpenDialogSync(BrowserWindow.getFocusedWindow(), options);
+ if (paths !== undefined) {
+ projectInfo.location = paths[0];
+ captionInfo.audioLocation = paths[0]; //when the project location changes also change the default audio files directory
+ }
+ }
+ },
+ {
+ label: 'New Project',
+ accelerator: isMac ? 'Cmd+N' : 'Cntrl+N',
+ click: async () => {
+ const { BrowserWindow } = require('electron');
+ BrowserWindow.getFocusedWindow().webContents.send(EVENTS.OPEN_TEMPLATE_DIALOG, true);
+ }
+ },
+ { type: 'separator' },
+ {
+ label: 'Choose Audio Directory',
+ click: async () => {
+ const { dialog, BrowserWindow } = require('electron');
+ const { captionInfo } = require('../storage');
+ const audio_options = {
+ title: 'Select SpringRoll Project Audio Files Location',
+ defaultPath: captionInfo.audioLocation,
+ properties: ['openDirectory']
+ };
+ const window = BrowserWindow.getFocusedWindow();
+ const audio_paths = dialog.showOpenDialogSync(window, audio_options);
+ if (audio_paths !== undefined) {
+ captionInfo.audioLocation = audio_paths[0];
+ window.webContents.send(EVENTS.UPDATE_AUDIO_LOCATION);
+ }
+ }
+ },
+ { type: 'separator' },
+ isMac ? { role: 'close' } : { role: 'quit' }
+ ]
+ },
+ // { role: 'editMenu' }
+ {
+ label: 'Edit',
+ submenu: [
+ { role: 'undo' },
+ { role: 'redo' },
+ { type: 'separator' },
+ { role: 'cut' },
+ { role: 'copy' },
+ { role: 'paste' },
+ ...(isMac ? [
+ { role: 'pasteAndMatchStyle' },
+ { role: 'delete' },
+ { role: 'selectAll' },
+ { type: 'separator' },
+ {
+ label: 'Speech',
+ submenu: [
+ { role: 'startSpeaking' },
+ { role: 'stopSpeaking' }
+ ]
+ }
+ ] : [
+ { role: 'delete' },
+ { type: 'separator' },
+ { role: 'selectAll' }
+ ])
+ ]
+ },
+ // { role: 'viewMenu' }
+ {
+ label: 'View',
+ submenu: [
+ { role: 'reload' },
+ { role: 'forceReload' },
+ { role: 'toggleDevTools' },
+ { type: 'separator' },
+ {
+ label: 'Preview Game',
+ accelerator: isMac ? 'Alt+Cmd+P' : 'Alt+Shift+P',
+ },
+ {
+ label: 'Caption Studio',
+ accelerator: isMac ? 'Alt+Cmd+C' : 'Alt+Shift+C',
+ },
+ { type: 'separator' },
+ { role: 'togglefullscreen' }
+ ]
+ },
+ // { role: 'windowMenu' }
+ {
+ label: 'Window',
+ submenu: [
+ { role: 'minimize' },
+ { role: 'zoom' },
+ ...(isMac ? [
+ { type: 'separator' },
+ { role: 'front' },
+ { type: 'separator' },
+ { role: 'window' }
+ ] : [
+ { role: 'close' }
+ ])
+ ]
+ },
+];
+
+/**
+ * Menu structure used in CaptionStudio page
+ */
+export const captionStudioTemplate = [
+ // { role: 'appMenu' }
+ ...(isMac ? [{
+ label: app.name,
+ submenu: [
+ { role: 'about' },
+ { type: 'separator' },
+ { role: 'services' },
+ { type: 'separator' },
+ { role: 'hide' },
+ { role: 'hideothers' },
+ { role: 'unhide' },
+ { type: 'separator' },
+ { role: 'quit' }
+ ]
+ }] : []),
+ // { role: 'fileMenu' }
+ {
+ label: 'File',
+ submenu: [
+ {
+ label: 'Choose Audio Directory',
+ click: async () => {
+ const { dialog, BrowserWindow } = require('electron');
+ const { captionInfo } = require('../storage');
+ const audio_options = {
+ title: 'Select SpringRoll Project Audio Files Location',
+ defaultPath: captionInfo.audioLocation,
+ properties: ['openDirectory']
+ };
+ const window = BrowserWindow.getFocusedWindow();
+ const audio_paths = dialog.showOpenDialogSync(window, audio_options);
+ if (audio_paths !== undefined) {
+ captionInfo.audioLocation = audio_paths[0];
+ window.webContents.send(EVENTS.UPDATE_AUDIO_LOCATION);
+ }
+ }
+ },
+ { type: 'separator' },
+ isMac ? { role: 'close' } : { role: 'quit' }
+
+ ]
+ },
+ // { role: 'editMenu' }
+ {
+ label: 'Edit',
+ submenu: [
+ { role: 'undo' },
+ { role: 'redo' },
+ { type: 'separator' },
+ { role: 'cut' },
+ { role: 'copy' },
+ { role: 'paste' },
+ ...(isMac ? [
+ { role: 'pasteAndMatchStyle' },
+ { role: 'delete' },
+ { role: 'selectAll' },
+ { type: 'separator' },
+ {
+ label: 'Speech',
+ submenu: [
+ { role: 'startSpeaking' },
+ { role: 'stopSpeaking' }
+ ]
+ }
+ ] : [
+ { role: 'delete' },
+ { type: 'separator' },
+ { role: 'selectAll' }
+ ])
+ ]
+ },
+ // { role: 'viewMenu' }
+ {
+ label: 'View',
+ submenu: [
+ { role: 'reload' },
+ { role: 'forceReload' },
+ { role: 'toggleDevTools' },
+ { type: 'separator' },
+ { role: 'resetZoom' },
+ { role: 'zoomIn' },
+ { role: 'zoomOut' },
+ { type: 'separator' },
+ { role: 'togglefullscreen' }
+ ]
+ },
+ // { role: 'windowMenu' }
+ {
+ label: 'Window',
+ submenu: [
+ { role: 'minimize' },
+ { role: 'zoom' },
+ ...(isMac ? [
+ { type: 'separator' },
+ { role: 'front' },
+ { type: 'separator' },
+ { role: 'window' }
+ ] : [
+ { role: 'close' }
+ ])
+ ]
+ },
+ {
+ label: 'Caption Studio',
+ submenu: [
+ {
+ label: 'Save Captions',
+ accelerator: process.platform === 'darwin' ? 'Cmd+S' : 'Cntrl+S',
+ click: async () => {
+ const { dialog, BrowserWindow } = require('electron');
+ const { captionInfo } = require('../storage');
+
+ const window = BrowserWindow.getFocusedWindow();
+
+ if (captionInfo.captionLocation) {
+ window.webContents.send(EVENTS.SAVE_CAPTION_DATA);
+
+ } else {
+ const options = {
+ title: 'Save As',
+ defaultPath: captionInfo.audioLocation + '/captions.json',
+ properties: ['createDirectory'],
+ filters: [
+ {name: 'JSON', extensions: ['json']}
+ ]
+ };
+
+ dialog.showSaveDialog(window, options).then(({ canceled, filePath }) => {
+ if (filePath !== undefined && !canceled) {
+ captionInfo.captionLocation = filePath;
+ window.webContents.send(EVENTS.SAVE_CAPTION_DATA, filePath);
+ }
+ });
+ }
+ }
+ },
+ {
+ label: 'Save Captions As...',
+ accelerator: process.platform === 'darwin' ? 'Cmd+Shift+S' : 'Cntrl+Shift+S',
+ click: async () => {
+ const { dialog, BrowserWindow } = require('electron');
+ const { captionInfo } = require('../storage');
+ const options = {
+ title: 'Save As',
+ defaultPath: captionInfo.captionLocation,
+ properties: ['createDirectory'],
+ filters: [
+ {name: 'JSON', extensions: ['json']}
+ ]
+ };
+ const window = BrowserWindow.getFocusedWindow();
+
+ dialog.showSaveDialog(window, options).then(({ canceled, filePath }) => {
+ if (filePath !== undefined && !canceled) {
+ captionInfo.captionLocation = filePath;
+ window.webContents.send(EVENTS.SAVE_CAPTION_DATA, filePath);
+ }
+ });
+ }
+ },
+ {
+ label: 'Open Caption File',
+ accelerator: isMac ? 'Cmd+O' : 'Cntrl+O',
+ click: async () => {
+ const { dialog, BrowserWindow } = require('electron');
+ const { captionInfo } = require('../storage');
+
+ const window = BrowserWindow.getFocusedWindow();
+ const options = {
+ title: 'Open Caption File',
+ defaultPath: captionInfo.captionLocation,
+ properties: ['openFile'],
+ filters: [
+ { name: 'JSON', extensions: [ 'json' ] }
+ ]
+ };
+ const caption_path = dialog.showOpenDialogSync(window, options);
+ if (caption_path !== undefined) {
+ captionInfo.captionLocation = caption_path[0];
+ window.webContents.send(EVENTS.OPEN_CAPTION_FILE, caption_path[0]);
+ }
+ }
+ },
+ { type: 'separator' },
+ {
+ label: 'Clear Captions',
+ click: () => {
+ const BrowserWindow = require('electron');
+ BrowserWindow.webContents.getFocusedWebContents().send(EVENTS.CLEAR_CAPTION_DATA);
+ }
+ },
+ ]
+ },
+];
+
diff --git a/src/main/studio/storage/CaptionInfo.js b/src/main/studio/storage/CaptionInfo.js
new file mode 100644
index 0000000..f75951a
--- /dev/null
+++ b/src/main/studio/storage/CaptionInfo.js
@@ -0,0 +1,62 @@
+import store from '../../../renderer/store';
+
+/**
+ * Proxy for accessing CaptionInfo store data. This will allow us to avoid
+ * importing and typing 'store.state.captionInfo'
+ * @class CaptionInfo
+ */
+class CaptionInfo {
+ /**
+ * Returns the audio files location path from the store.
+ * @readonly
+ * @memberof CaptionInfo
+ */
+ get audioLocation() { return store.state.captionInfo.audioLocation; }
+ /**
+ * Sets the value of the audio files location in the store.
+ * @memberof CaptionInfo
+ */
+ set audioLocation(val) {
+ if (typeof val !== 'string') {
+ throw new Error(`[CaptionInfo] Audio directory location must be a string. [val = ${typeof val}]`);
+ }
+ store.dispatch('setAudioLocation', { audioLocation: val });
+ }
+ /**
+ * Returns the caption file location path from the store.
+ * @readonly
+ * @memberof CaptionInfo
+ */
+ get captionLocation() { return store.state.captionInfo.captionLocation; }
+ /**
+ * Sets the value of the caption file location in the store.
+ * @memberof CaptionInfo
+ */
+ set captionLocation(val) {
+ if (typeof val !== 'string') {
+ throw new Error(`[CaptionInfo] Caption file location must be a string. [val = ${typeof val}]`);
+ }
+ store.dispatch('setCaptionLocation', { captionLocation: val });
+ }
+ /**
+ * Returns whether or not there are unsaved changes in caption studio
+ * @readonly
+ * @memberof CaptionInfo
+ */
+ get isUnsavedChanges() { return store.state.captionInfo.isUnsavedChanges; }
+ /**
+ * Sets the value of isUnsavedChanges
+ * @memberof CaptionInfo
+ */
+ set isUnsavedChanges(val) {
+ if (typeof val !== 'boolean') {
+ throw new Error(`[CaptionInfo] Caption file location must be a boolean. [val = ${typeof val}]`);
+ }
+ store.dispatch('setIsUnsavedChanges', { isUnsavedChanges: val });
+ }
+}
+
+/**
+ * Singleton proxy for accessing project info from the store.
+ */
+export const captionInfo = new CaptionInfo();
\ No newline at end of file
diff --git a/src/main/studio/storage/GamePreview.js b/src/main/studio/storage/GamePreview.js
new file mode 100644
index 0000000..64d9dc7
--- /dev/null
+++ b/src/main/studio/storage/GamePreview.js
@@ -0,0 +1,45 @@
+import store from '../../../renderer/store';
+
+/**
+ *
+ * @class GamePreview
+ */
+class GamePreview {
+ /**
+ *
+ * @readonly
+ * @memberof GamePreview
+ */
+ get previewTarget() { return store.state.gamePreview.previewTarget; }
+ /**
+ *
+ * @memberof GamePreview
+ */
+ set previewTarget(val) {
+ if (typeof val !== 'string') {
+ throw new Error(`[GamePreview] Preview target must be a string. [val = ${typeof val}]`);
+ }
+ store.dispatch('setPreviewTarget', { previewTarget: val });
+ }
+
+ /**
+ *
+ * @memberof GamePreview
+ */
+ get previewURL() { return store.state.gamePreview.previewURL; }
+ /**
+ *
+ * @memberof GamePreview
+ */
+ set previewURL(val) {
+ if (typeof val !== 'string') {
+ throw new Error(`[GamePreview] Preview URL must be a string. [val = ${typeof val}]`);
+ }
+ store.dispatch('setPreviewURL', { previewURL: val });
+ }
+}
+
+/**
+ *
+ */
+export const gamePreview = new GamePreview();
\ No newline at end of file
diff --git a/src/main/studio/storage/ProjectInfo.js b/src/main/studio/storage/ProjectInfo.js
new file mode 100644
index 0000000..7fdef54
--- /dev/null
+++ b/src/main/studio/storage/ProjectInfo.js
@@ -0,0 +1,30 @@
+import store from '../../../renderer/store';
+
+/**
+ * Proxy for accessing ProjectInfo store data. This will allow us to avoid
+ * importing and typing 'store.state.projectInfo'
+ * @class ProjectInfo
+ */
+class ProjectInfo {
+ /**
+ * Returns the project location path from the store.
+ * @readonly
+ * @memberof ProjectInfo
+ */
+ get location() { return store.state.projectInfo.location; }
+ /**
+ * Sets the value of the project location in the store.
+ * @memberof ProjectInfo
+ */
+ set location(val) {
+ if (typeof val !== 'string') {
+ throw new Error(`[ProjectInfo] Project location must be a string. [val = ${typeof val}]`);
+ }
+ store.dispatch('setProjectLocation', { location: val });
+ }
+}
+
+/**
+ * Singleton proxy for accessing project info from the store.
+ */
+export const projectInfo = new ProjectInfo();
\ No newline at end of file
diff --git a/src/main/studio/storage/index.js b/src/main/studio/storage/index.js
new file mode 100644
index 0000000..c253222
--- /dev/null
+++ b/src/main/studio/storage/index.js
@@ -0,0 +1,3 @@
+export * from './ProjectInfo';
+export * from './GamePreview';
+export * from './CaptionInfo';
\ No newline at end of file
diff --git a/src/renderer/App.vue b/src/renderer/App.vue
new file mode 100644
index 0000000..7815c55
--- /dev/null
+++ b/src/renderer/App.vue
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/renderer/assets/fonts/Aleo-Regular.otf b/src/renderer/assets/fonts/Aleo-Regular.otf
new file mode 100755
index 0000000..2e2a075
Binary files /dev/null and b/src/renderer/assets/fonts/Aleo-Regular.otf differ
diff --git a/src/renderer/assets/fonts/Assistant/Assistant-Bold.ttf b/src/renderer/assets/fonts/Assistant/Assistant-Bold.ttf
new file mode 100755
index 0000000..5c6be08
Binary files /dev/null and b/src/renderer/assets/fonts/Assistant/Assistant-Bold.ttf differ
diff --git a/src/renderer/assets/fonts/Assistant/Assistant-ExtraBold.ttf b/src/renderer/assets/fonts/Assistant/Assistant-ExtraBold.ttf
new file mode 100755
index 0000000..9828b3f
Binary files /dev/null and b/src/renderer/assets/fonts/Assistant/Assistant-ExtraBold.ttf differ
diff --git a/src/renderer/assets/fonts/Assistant/Assistant-ExtraLight.ttf b/src/renderer/assets/fonts/Assistant/Assistant-ExtraLight.ttf
new file mode 100755
index 0000000..5cdcc6a
Binary files /dev/null and b/src/renderer/assets/fonts/Assistant/Assistant-ExtraLight.ttf differ
diff --git a/src/renderer/assets/fonts/Assistant/Assistant-Light.ttf b/src/renderer/assets/fonts/Assistant/Assistant-Light.ttf
new file mode 100755
index 0000000..bc5e307
Binary files /dev/null and b/src/renderer/assets/fonts/Assistant/Assistant-Light.ttf differ
diff --git a/src/renderer/assets/fonts/Assistant/Assistant-Regular.ttf b/src/renderer/assets/fonts/Assistant/Assistant-Regular.ttf
new file mode 100755
index 0000000..582d612
Binary files /dev/null and b/src/renderer/assets/fonts/Assistant/Assistant-Regular.ttf differ
diff --git a/src/renderer/assets/fonts/Assistant/Assistant-SemiBold.ttf b/src/renderer/assets/fonts/Assistant/Assistant-SemiBold.ttf
new file mode 100755
index 0000000..547df98
Binary files /dev/null and b/src/renderer/assets/fonts/Assistant/Assistant-SemiBold.ttf differ
diff --git a/src/renderer/assets/fonts/Assistant/OFL.txt b/src/renderer/assets/fonts/Assistant/OFL.txt
new file mode 100755
index 0000000..075d1ae
--- /dev/null
+++ b/src/renderer/assets/fonts/Assistant/OFL.txt
@@ -0,0 +1,91 @@
+This Font Software is licensed under the SIL Open Font License, Version 1.1.
+This license is copied below, and is also available with a FAQ at:
+http://scripts.sil.org/OFL
+
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded,
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply
+to any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components as
+distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting -- in part or in whole -- any of the components of the
+Original Version, by changing formats or by porting the Font Software to a
+new environment.
+
+"Author" refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+PERMISSION & CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed, modify,
+redistribute, and sell modified and unmodified copies of the Font
+Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components,
+in Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the corresponding
+Copyright Holder. This restriction only applies to the primary font name as
+presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5) The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created
+using the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.
diff --git a/src/renderer/assets/img/256x256.png b/src/renderer/assets/img/256x256.png
new file mode 100644
index 0000000..2240e02
Binary files /dev/null and b/src/renderer/assets/img/256x256.png differ
diff --git a/src/renderer/assets/svg/wave1.svg b/src/renderer/assets/svg/wave1.svg
new file mode 100644
index 0000000..2b77ed1
--- /dev/null
+++ b/src/renderer/assets/svg/wave1.svg
@@ -0,0 +1,19 @@
+
+
+
+ Path 6
+ Created with Sketch.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/renderer/assets/svg/wave2.svg b/src/renderer/assets/svg/wave2.svg
new file mode 100644
index 0000000..ec28555
--- /dev/null
+++ b/src/renderer/assets/svg/wave2.svg
@@ -0,0 +1,19 @@
+
+
+
+ Path 6
+ Created with Sketch.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/renderer/class/Caption.js b/src/renderer/class/Caption.js
new file mode 100644
index 0000000..13152da
--- /dev/null
+++ b/src/renderer/class/Caption.js
@@ -0,0 +1,56 @@
+/**
+ * Object that represents a single files caption
+ *
+ * @export
+ * @property {number} end caption end time
+ * @property {number} start caption start time
+ * @property {string} content caption text as a string
+ * @param {string} name name of the file for this caption
+ * @param {Object} data the data relevant to this caption
+ * @param {string} data.content the html content of this caption
+ * @param {number} data.end when to stop playing the caption
+ * @param {number} data.start when to start playing the caption
+ * @class Caption
+ *
+ */
+export class Caption {
+ /**
+ *
+ * @param {*} name
+ * @param {*} param1
+ */
+ constructor(name, { content = ' ', end = 0, start = 0 } = {}) {
+ this.updateContent({ name, content, end, start });
+ }
+
+ /**
+ *
+ */
+ getData() {
+ const { content, start, end } = this;
+
+ return {
+ content,
+ end,
+ start
+ };
+ }
+ /**
+ *
+ * @param {*} param0
+ */
+ updateContent({
+ name = this.name,
+ content = this.content || ' ',
+ end = this.end || 0,
+ start = this.start || 0
+ } = {}) {
+ this.end = end;
+ this.name = name;
+ this.start = start;
+
+ this.content = content;
+
+ return this.getData();
+ }
+}
diff --git a/src/renderer/class/CaptionManager.js b/src/renderer/class/CaptionManager.js
new file mode 100644
index 0000000..0e332b2
--- /dev/null
+++ b/src/renderer/class/CaptionManager.js
@@ -0,0 +1,296 @@
+import { EventBus } from './EventBus';
+import store from '../store';
+
+/**
+ * Class that controls the creation, and management, of captions in the CaptionStudio Component.
+ * Acts as the state for the caption data. When a component updates a caption, changes the active caption, deletes a caption, changes files, etc. That
+ * choice is emitted to this class, which then emits an event to inform all affected components.
+ */
+class CaptionManager {
+ /**
+ *
+ */
+ constructor() {
+ this.data = {}; //All caption data, organized by file name.
+ this.activeCaption = undefined; //currently active caption name, created by stripping the file extension of the active file.
+ this.activeIndex = 0; //the index of the currently active caption.
+ this.file = {}; //Currently active file, selected in the FileDirectory component
+ this.currentTime = 0; //current time of the waveform component
+ EventBus.$on('caption_update', this.updateActiveCaption.bind(this));
+ EventBus.$on('caption_reset', this.reset.bind(this));
+ EventBus.$on('caption_add', this.addCaption.bind(this));
+ EventBus.$on('caption_add_index', this.addIndex.bind(this));
+ EventBus.$on('caption_get', this.emitData.bind(this));
+ EventBus.$on('file_selected', this.fileChanged.bind(this));
+ EventBus.$on('caption_move_index', this.moveIndex.bind(this));
+ EventBus.$on('caption_remove_index', this.removeAtIndex.bind(this));
+ EventBus.$on('caption_emit', this.emit.bind(this));
+ EventBus.$on('time_current', (e) => this.currentTime = e.time || 0);
+ EventBus.$on('json_update', this.onJSONUpdate.bind(this));
+ }
+
+ /**
+ *
+ * @param Object $event the object containing all data emitted by the origin
+ * @param String $origin String that contains the component origin. Used on some components to filter out their own updates.
+ *
+ * Function called when the user selects a new file in the FileDirectory. If no caption exits
+ * for that file, it creates a new Caption.
+ */
+ fileChanged($event, $origin = '') {
+
+ if (!$event.file) {
+ return;
+ }
+ const name = $event.file.name.replace(/.(ogg|mp3|mpeg)$/, '').trim();
+ if (!name || name === this.activeCaption) {
+ return;
+ }
+
+ this.file = $event.file;
+
+ if (!Array.isArray(this.data[name])) {
+ this.addCaption(name, $origin);
+ } else {
+ this.activeCaption = name;
+ this.activeIndex = 0;
+ this.emitCurrent($origin);
+ }
+ //used only when the JSON editor updates the selected file
+ EventBus.$emit('selected_file_updated');
+ }
+
+ /**
+ *
+ * @param Object $event the object containing all data emitted by the origin
+ * @param String $origin String that contains the component origin. Used on some components to filter out their own updates.
+ *
+ * Called when the JSON in the JsonPreview is edited directly. Since the Json editor only emits
+ * the entire JSON structure this method iterates over the entire JSON object and updates every
+ * Caption.
+ */
+ onJSONUpdate($event, $origin = '') {
+ if ($origin !== 'userOpen') {
+ store.dispatch('setIsUnsavedChanges', { isUnsavedChanges: true });
+ }
+
+ Object.keys($event).forEach((key) => {
+ $event[key].forEach((caption, index) => {
+
+ if (!this.data[key]) {
+ this.data[key] = [this.template];
+ }
+
+ const current = this.data[key];
+
+ this.data[key][index] = {
+ content: caption.content || current.content,
+ end: 'number' === typeof caption.end ? caption.end : current.end,
+ start: 'number' === typeof caption.start ? caption.start : current.start,
+ };
+ });
+ });
+
+ if ($origin === 'userOpen') {
+ this.emitOpenedJSON();
+ return;
+ }
+ this.currentCaptionIndex.edited = true;
+ this.emitCurrent($origin);
+ this.emitData($origin);
+ }
+ /**
+ *
+ * @param String key string that represents the curent active caption
+ * @param String $origin String that contains the component origin. Used on some components to filter out their own updates.
+ *
+ * Creates a new caption entry on the data object and updates the active caption and index to point
+ * at this new caption.
+ */
+ addCaption(key = this.activeCaption, $origin = '') {
+ if ('string' !== typeof key || !key) {
+ return;
+ }
+
+ this.data[key] = [this.template];
+
+ this.activeCaption = key;
+ this.activeIndex = 0;
+
+ this.emitCurrent($origin);
+ this.emitData($origin);
+ }
+
+ /**
+ * @param String $origin String that contains the component origin. Used on some components to filter out their own updates.
+ *
+ * Fired when the user hits the Add Caption Button in TextEditor. Moves the activeIndex forward,
+ * and creates a new empty caption. Also "saves" the previously active caption in the data object.
+ */
+ addIndex($origin = '') {
+ store.dispatch('setIsUnsavedChanges', { isUnsavedChanges: true });
+ this.data[this.activeCaption].push(this.template);
+ this.activeIndex++;
+ EventBus.$emit('file_captioned', { name: this.file.name, isCaptioned: true });
+ this.emitCurrent($origin);
+ this.emitData($origin);
+ }
+
+ /**
+ * @param String $origin String that contains the component origin. Used on some components to filter out their own updates.
+ *
+ * Called whenever the TextEditor component updates the content, or start/end times of the caption.
+ * simply upates the currently active caption with whatever new data is provided.
+ */
+ updateActiveCaption({ content, start, end }, $origin = '') {
+ store.dispatch('setIsUnsavedChanges', { isUnsavedChanges: true });
+ const current = this.currentCaptionIndex;
+
+ this.data[this.activeCaption][this.activeIndex] = {
+ content: content || current.content,
+ end: 'number' === typeof end ? end : current.end,
+ start: 'number' === typeof start ? start : current.start,
+ edited: true,
+ };
+
+ this.emitCurrent($origin);
+ }
+
+ /**
+ * Removes all captions from the data object and resets the active caption back to it's initial state.
+ */
+ reset() {
+ store.dispatch('setIsUnsavedChanges', { isUnsavedChanges: true });
+ this.data = {};
+ this.activeIndex = 0;
+ this.activeCaption = '';
+ }
+
+ /**
+ *
+ * @param Number $event Represents how far to move the index that points at the active caption.
+ * @param String $origin String that contains the component origin. Used on some components to filter out their own updates.
+ *
+ * Fired whenever a component needs to update the index that points to the active caption.
+ */
+ moveIndex( $event, $origin = '') {
+ if ('number' === typeof $event) {
+ const newIndex = this.activeIndex + $event;
+ if (0 > newIndex) {
+ this.activeIndex = 0;
+ } else if (newIndex > this.lastIndex) {
+ this.activeIndex = this.lastIndex;
+ } else {
+ this.activeIndex = newIndex;
+ }
+ this.emitCurrent($origin);
+ }
+ }
+
+ /**
+ *
+ * @param Number $event the index of the caption to be removed
+ * @param String $origin String that contains the component origin. Used on some components to filter out their own updates.
+ *
+ * Used to delete a single caption. Uses the index to look up which caption should be removed. Almost always will be the current caption.
+ */
+ removeAtIndex($origin = '') {
+ store.dispatch('setIsUnsavedChanges', { isUnsavedChanges: true });
+ if ('undefined' === typeof this.currentCaption[this.activeIndex]) {
+ return;
+ }
+
+ this.currentCaption.splice(this.activeIndex, 1);
+
+ if (0 < this.activeIndex) {
+ this.activeIndex--;
+ }
+ EventBus.$emit('file_captioned', { name: this.file.name, isCaptioned: !!(this.currentCaption.length > 1) });
+
+ this.emitCurrent($origin);
+ this.emitData($origin);
+ }
+
+ /**
+ * Emits the currently active caption data along with index, and file name information.
+ * This is used to replicate caption updates, and a change of active caption across all components.
+ */
+ emitCurrent($origin = '') {
+ EventBus.$emit('caption_changed', {
+ data: this.currentCaptionIndex,
+ file: this.file,
+ index: this.activeIndex,
+ lastIndex: this.lastIndex,
+ name: this.activeCaption,
+ time: this.currentTime,
+ }, $origin);
+ }
+
+ /**
+ * Emits the entirety of the current data object, which contains all caption files, and associated captions.
+ */
+ emitData( $origin = '' ) {
+ EventBus.$emit('caption_data', this.data, $origin);
+ }
+ /**
+ * Emits the entirety of the current data object, which contains all caption files, and associated captions.
+ */
+ emitOpenedJSON( $origin = '' ) {
+ EventBus.$emit('caption_data_opened', this.data, $origin);
+ }
+
+ /**
+ * Combines emitCurrent and emitData, as well as emitting the file_selected event.
+ * Used for the mounted hook of the CaptionStudio
+ */
+ emit() {
+ this.emitCurrent();
+ this.emitData();
+ EventBus.$emit('file_selected', { file: this.file });
+ }
+ /**
+ *
+ */
+ get lastIndex() {
+ return this.currentCaption?.length - 1 || 0;
+ }
+
+ /**
+ *
+ */
+ get currentCaption() {
+ return this.data[this.activeCaption];
+ }
+
+ /**
+ *
+ */
+ get currentCaptionIndex() {
+ if (this.data[this.activeCaption]) {
+ return this.data[this.activeCaption][this.activeIndex];
+ }
+ }
+
+ /**
+ *
+ */
+ set currentCaption(newArray) {
+ if (Array.isArray(newArray)) {
+ this.data[this.activeCaption] = newArray;
+ }
+ }
+
+ /**
+ *
+ */
+ get template() {
+ return {
+ start: 0,
+ end: 0,
+ content: ' ',
+ edited: false
+ };
+ }
+}
+
+export default new CaptionManager();
\ No newline at end of file
diff --git a/src/renderer/class/Directory.js b/src/renderer/class/Directory.js
new file mode 100644
index 0000000..9abbb0f
--- /dev/null
+++ b/src/renderer/class/Directory.js
@@ -0,0 +1,175 @@
+import { sortBy } from 'lodash-es';
+/**
+ * @export
+ * @class Directory
+ * @property {File[]} files
+ * @property {string} name
+ * @property {Object} dir
+ * @property {number} selected
+ *
+ * @param {Object} options
+ * @param {string} options.name
+ * @param {File[]} options.files
+ * @param {Object} options.directories
+ */
+export default class Directory {
+ /**
+ * @constructor
+ * @param {Object} [options={}]
+ * @param {string} [options.name=''] name of the directory
+ * @param {Object[]} [options.files=[]] array of files to use rather than generating them
+ * @param {Object} [options.directories={}] already instantiated directories
+ */
+ constructor({ name = '', files = [], directories = {} } = {}) {
+ this.name = name;
+ this.files = files;
+ this.dir = directories;
+ this.selected = 0;
+
+ if (this.files.length) {
+ this.sortFilesAlphabetically();
+ }
+ }
+
+ /**
+ *
+ * Adds a file to the directory and will create nested directories to properly represent the files local path if required
+ * @param {File} file
+ * @memberof Directory
+ */
+ addFile(file) {
+ const pathArray = file.relativePath.split('/');
+
+ pathArray.length -= 1;
+
+ //If a singular file, or multiple files were uploaded rather than a directory set the directory name to /
+ if (pathArray.length <= 0) {
+ pathArray.push('/');
+ }
+ let currentDir = this;
+
+ for (let i = 0, l = pathArray.length; i < l; i++) {
+ let dir = currentDir[pathArray[i]];
+ if ('undefined' === typeof dir) {
+ currentDir.addDirectory(new Directory({ name: pathArray[i] }));
+ dir = currentDir.dir[pathArray[i]];
+ }
+
+ currentDir = dir;
+ }
+ currentDir.files.push(file);
+ currentDir.sortFilesAlphabetically();
+ }
+
+ /**
+ * Adds an already existing Directory instance to this directory as a nested directory. It will not add it if there is already a directory with the same name
+ * @param {Directory} dir
+ * @memberof Directory
+ */
+ addDirectory(dir) {
+ if (
+ !(dir instanceof Directory) ||
+ (dir instanceof Directory && 'undefined' !== typeof this.dir[dir.name])
+ ) {
+ return;
+ }
+
+ this.dir[dir.name] = dir;
+ }
+
+ /**
+ * Sorts files stored in this directory Alphabetically
+ * @memberof Directory
+ */
+ sortFilesAlphabetically() {
+ this.files = sortBy(this.files, (f) => f.name);
+ }
+
+ /**
+ *
+ * Sets the current pointer of this directory to the specified index if it exists in this directory and returns that file
+ * @param {number} index
+ * @returns {File}
+ * @memberof Directory
+ */
+ selectByIndex(index) {
+ if (-1 < index && index < this.files.length) {
+ this.selected = index;
+ }
+
+ return this.currentFile();
+ }
+
+ /**
+ * Sets the current pointer of this directory to the specified file if it exists in this directory and returns it
+ * @param {File} file
+ * @returns {File}
+ * @memberof Directory
+ */
+ selectByFile(file) {
+ const index = this.getFileIndex(file);
+
+ if (-1 === index) {
+ return;
+ }
+
+ this.selected = index;
+ return this.currentFile();
+ }
+
+ /**
+ * Returns the currently active file in the directory
+ * @returns {File}
+ * @memberof Directory
+ */
+ currentFile() {
+ return this.files[this.selected];
+ }
+
+ /**
+ * Moves the directory pointer by plus 1 and returns the file. If it can't move forward it will return the current file
+ * @returns {File}
+ * @memberof Directory
+ */
+ nextFile() {
+ const next = this.selected + 1;
+ const nextFile = this.files[next];
+
+ if ('undefined' === typeof nextFile) {
+ return null;
+ }
+
+ this.selected = next;
+ return this.currentFile();
+ }
+
+ /**
+ * Utility function to find the index of a file in the files array
+ * Use over indexOf as files can be re-uploaded and their object reference chanes
+ * @param {File} file
+ * @returns {number}
+ */
+ getFileIndex(file) {
+ for (let i = 0, l = this.files.length; i < l; i++) {
+ if (this.files[i].name === file.name) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * Moves the directory pointer by minus 1 and returns the file. If it can't move back it will return the current file
+ * @returns {File}
+ * @memberof Directory
+ */
+ previousFile() {
+ if (0 >= this.selected) {
+ return this.files[this.selected];
+ }
+
+ this.selected -= 1;
+
+ return this.currentFile();
+ }
+}
diff --git a/src/renderer/class/EventBus.js b/src/renderer/class/EventBus.js
new file mode 100644
index 0000000..b2b50c9
--- /dev/null
+++ b/src/renderer/class/EventBus.js
@@ -0,0 +1,6 @@
+import Vue from 'vue';
+/**
+ *
+ */
+class EventBusManager extends Vue {}
+export const EventBus = new EventBusManager();
\ No newline at end of file
diff --git a/src/renderer/class/FileProcessor.js b/src/renderer/class/FileProcessor.js
new file mode 100644
index 0000000..346fb82
--- /dev/null
+++ b/src/renderer/class/FileProcessor.js
@@ -0,0 +1,130 @@
+import Directory from './Directory';
+import store from '../store/';
+import { EventBus } from '../class/EventBus';
+const fs = require('fs');
+const path = require('path');
+const FileType = require('file-type');
+
+/**
+ * @typedef FileProcessorOptions
+ * @property {RegExp} fileFilter
+ * @property {string} nameFilter
+ *
+ *
+ * @export
+ * @class FileProcessor
+ * @property {Directory} directory
+ * @property {RegExp} fileFilter
+ * @property {RegExp} nameFilter
+ * @param {FileList} files
+ * @param {FileProcessorOptions} options
+ */
+class FileProcessor {
+ /**
+ * @constructor
+ * @param {Object} options
+ * @param {RegExp} options.fileFilter
+ * @param {RegExp|string} options.nameFilter
+ */
+ constructor(
+ { fileFilter = /(audio\/(mp3|ogg|mpeg)|video\/ogg)$/, nameFilter = '' } = {}
+ ) {
+ this.clear();
+ this.fileFilter = fileFilter;
+ this.setNameFilter(nameFilter);
+ this.directory = new Directory();
+ this.hasFiles = false;
+ this.parentDirectoryName = store.state.captionInfo.audioLocation !== undefined ? path.basename(store.state.captionInfo.audioLocation) : '';
+ }
+
+ /**
+ * Processes a file list and returns a Directory containing all the files and nested directories to properly simulate the local directory
+ * @returns Promise
+ * @memberof FileProcessor
+ * @async
+ */
+ async generateDirectories() {
+ this.parentDirectoryName = store.state.captionInfo.audioLocation !== undefined ? path.basename(store.state.captionInfo.audioLocation) : '';
+
+ this.clear();
+
+ const files = await this.generateFileList(store.state.captionInfo.audioLocation);
+ EventBus.$emit('file_list_generated', files);
+
+ for (let i = 0, l = files.length; i < l; i++) {
+ if (
+ this.fileFilter.test(files[i].type.mime) &&
+ this.nameFilter.test(files[i].name)
+ ) {
+ this.directory.addFile(files[i]);
+ this.hasFiles = true;
+ }
+ }
+
+ return this.directory;
+ }
+
+ /**
+ * Grabs every file in the given directory path and formats it into an array of usable File-like objects
+ * @param {string} dirPath full path to directory
+ * @param {Object[]} [arrayOfFiles] array of prevoiously created file objects. Mostly used for recursive calls
+ * @returns Promise
+ * @memberof FileProcessor
+ * @async
+ */
+ async generateFileList(dirPath, arrayOfFiles = []) {
+ if (dirPath === undefined) {
+ return arrayOfFiles;
+ }
+ const fileList = fs.readdirSync(dirPath, { withFileTypes: true });
+
+ for (let i = 0, l = fileList.length; i < l; i++) {
+ if (fileList[i].isDirectory()) {
+ arrayOfFiles = await this.generateFileList(path.join(dirPath, fileList[i].name), arrayOfFiles);
+ } else {
+ const type = await FileType.fromFile(path.join(dirPath, fileList[i].name));
+ if (type) {
+ arrayOfFiles.push({
+ name: fileList[i].name,
+ fullPath: path.join(dirPath, fileList[i].name),
+ relativePath: path.join('' + this.parentDirectoryName, path.relative(store.state.captionInfo.audioLocation, path.join(dirPath, fileList[i].name))),
+ type,
+ });
+ }
+ }
+ }
+ return arrayOfFiles;
+ }
+
+
+ /**
+ * Clears the current Directory instance
+ * @memberof FileProcessor
+ */
+ clear() {
+ this.directory = new Directory();
+ this.hasFiles = false;
+ }
+
+ /**
+ * Updates the filter applied to file names
+ * @param {string} name
+ * @memberof FileProcessor
+ */
+ setNameFilter(name) {
+ if (!name.length) {
+ this.nameFilter = /^/g;
+ return;
+ }
+ this.nameFilter = new RegExp(`(${name})`, 'g');
+ }
+
+ /**
+ *
+ */
+ getDirectory() {
+ return this.directory;
+ }
+}
+
+export default new FileProcessor();
diff --git a/src/renderer/components/caption-studio/CaptionPreview.vue b/src/renderer/components/caption-studio/CaptionPreview.vue
new file mode 100644
index 0000000..4f6d48b
--- /dev/null
+++ b/src/renderer/components/caption-studio/CaptionPreview.vue
@@ -0,0 +1,183 @@
+
+
+
+
+
+
+ Previous
+
+
+ Next
+
+
+
+
+
+
+
+
+
+
diff --git a/src/renderer/components/caption-studio/FileDirectory.vue b/src/renderer/components/caption-studio/FileDirectory.vue
new file mode 100644
index 0000000..88a1cda
--- /dev/null
+++ b/src/renderer/components/caption-studio/FileDirectory.vue
@@ -0,0 +1,254 @@
+
+
+
+
+
+ {{ name }}
+
+
+
+
+ audiotrack
+
+ {{ value.file.name }}
+ done
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/renderer/components/caption-studio/FileExplorer.vue b/src/renderer/components/caption-studio/FileExplorer.vue
new file mode 100644
index 0000000..58e1f5a
--- /dev/null
+++ b/src/renderer/components/caption-studio/FileExplorer.vue
@@ -0,0 +1,283 @@
+
+
+
+ home
+
+
+
+
+ home
+
+
+
+
+
+ Save Changes
+
+
+
+ You have unsaved changes. Would you like to save?
+
+
+
+
+
+
+ {
+ onSaveClick();
+ onHomeClick();
+ }"
+ >
+ Save
+
+
+ Don't Save
+
+
+ Cancel
+
+
+
+
+
+
+
+
+
+
+ Change Audio Directory
+
+
+
+
+
+
+
diff --git a/src/renderer/components/caption-studio/JsonPreview.vue b/src/renderer/components/caption-studio/JsonPreview.vue
new file mode 100644
index 0000000..df0a6ec
--- /dev/null
+++ b/src/renderer/components/caption-studio/JsonPreview.vue
@@ -0,0 +1,445 @@
+
+
+
+
+
+
+
+ Warning
+
+
+ There are errors in the caption JSON. It is recommended you correct those before saving, otherwise your changes to those specific captions will not be saved.
+
+
+
+
+ Cancel
+
+ Save
+
+
+
+
+
+
+ Clear
+
+
+
+
+ Warning
+
+
+ This will clear all captions.
+
+
+
+
+ Cancel
+
+ Ok
+
+
+
+
+ Export Code
+
+
+
+
+
+
+
+
+
diff --git a/src/renderer/components/caption-studio/TextEditor.vue b/src/renderer/components/caption-studio/TextEditor.vue
new file mode 100644
index 0000000..ceffb77
--- /dev/null
+++ b/src/renderer/components/caption-studio/TextEditor.vue
@@ -0,0 +1,378 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ size.label }}
+
+
+
+
+ Bold
+
+ {{ }}
+
+
+
+ {{ characterCount }} / 40
+
+
+
+
+
+
+ warning It is recommended that caption lines are 40 characters or less
+ warning It is recommended that individual captions be no longer 2 lines
+
+
+
+
+
+
+
+ Remove Caption
+
+
+ Add Caption
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/renderer/components/caption-studio/TimeStamp.vue b/src/renderer/components/caption-studio/TimeStamp.vue
new file mode 100644
index 0000000..0923f5f
--- /dev/null
+++ b/src/renderer/components/caption-studio/TimeStamp.vue
@@ -0,0 +1,19 @@
+
+
+ {{ minutes }}:{{ seconds }}:{{ milliseconds }}
+
+
+
+
+
diff --git a/src/renderer/components/caption-studio/TimeStampInput.vue b/src/renderer/components/caption-studio/TimeStampInput.vue
new file mode 100644
index 0000000..b2e596b
--- /dev/null
+++ b/src/renderer/components/caption-studio/TimeStampInput.vue
@@ -0,0 +1,140 @@
+
+
+
+
+
+
+
diff --git a/src/renderer/components/caption-studio/WaveSurfer.vue b/src/renderer/components/caption-studio/WaveSurfer.vue
new file mode 100644
index 0000000..4f0e618
--- /dev/null
+++ b/src/renderer/components/caption-studio/WaveSurfer.vue
@@ -0,0 +1,198 @@
+
+
+
+
+
+
+ fast_rewind
+
+
+ skip_previous
+
+
+ {{ isPlaying ? 'pause' : 'play_arrow' }}
+
+
+ skip_next
+
+
+ fast_forward
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/renderer/components/dialogs/PreviewTargetDialog.vue b/src/renderer/components/dialogs/PreviewTargetDialog.vue
new file mode 100644
index 0000000..c4d69c5
--- /dev/null
+++ b/src/renderer/components/dialogs/PreviewTargetDialog.vue
@@ -0,0 +1,262 @@
+
+
+
+
Choose Preview Target
+
+
+ Deploy Folder
+
+
+ Custom URL
+
+
+
+
+
+
+ Cancel
+
+
+ Confirm
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/renderer/components/dialogs/TemplateProjectDialog.vue b/src/renderer/components/dialogs/TemplateProjectDialog.vue
new file mode 100644
index 0000000..64968b8
--- /dev/null
+++ b/src/renderer/components/dialogs/TemplateProjectDialog.vue
@@ -0,0 +1,453 @@
+
+
+
+
+
+
Create New SpringRoll Project
+
+
+
+
+
+
Project Location
+
+
+
+ Browse
+
+
+
+
+
+
+
+
+
+ Cancel
+
+
+ Confirm
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/renderer/components/pages/CaptionStudio.vue b/src/renderer/components/pages/CaptionStudio.vue
new file mode 100644
index 0000000..6e3ef2a
--- /dev/null
+++ b/src/renderer/components/pages/CaptionStudio.vue
@@ -0,0 +1,169 @@
+
+
+
+
+
+
+
+
+ Sound Preview
+
+
+
+ Text Preview
+
+
+
+
+ Text Editor
+
+
+
+ JSON Preview * - Unsaved Changes
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/renderer/components/pages/LandingPage.vue b/src/renderer/components/pages/LandingPage.vue
new file mode 100644
index 0000000..c70d050
--- /dev/null
+++ b/src/renderer/components/pages/LandingPage.vue
@@ -0,0 +1,277 @@
+
+
+
Version {{ appVersion }}
+
+
+
+
+
+
+
SpringRoll Studio
+
+
+
SpringRoll is a light-weight toolset for building accessible HTML5 games, focusing on utilities to help developers make games more accessible and deployable at scale.
+
+
+ Set Project Location
+ Preview Game
+ Create Project Template
+ Open Caption Studio
+
+
+
+
Project: {{ projectLocation }}
+ Audio: {{ audioLocation }}
+
+
+
+
+
+
+
diff --git a/src/renderer/components/pages/PreviewPage.vue b/src/renderer/components/pages/PreviewPage.vue
new file mode 100644
index 0000000..1f0cfd3
--- /dev/null
+++ b/src/renderer/components/pages/PreviewPage.vue
@@ -0,0 +1,509 @@
+
+
+
+
+
+ home
+ refresh
+
+
+
help
+
+ volume_up
+ volume_off
+
+
+ expand_more
+
+
+
+ closed_caption
+ closed_caption_disabled
+
+
+ expand_more
+
+
+
+ pause
+ play_arrow
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/renderer/main.js b/src/renderer/main.js
new file mode 100644
index 0000000..6ae772f
--- /dev/null
+++ b/src/renderer/main.js
@@ -0,0 +1,31 @@
+import Vue from 'vue';
+import vuetify from './plugins/vuetify'; // path to vuetify export
+import 'material-design-icons-iconfont/dist/material-design-icons.css';
+
+import router from './router';
+import store from './store';
+
+import App from './App.vue';
+import { ipcRenderer } from 'electron';
+import { EVENTS } from '@/constants';
+
+// Plugins
+import './plugins';
+
+// Styles
+import './scss/main.scss';
+
+// State
+import './class/CaptionManager';
+
+Vue.config.productionTip = false;
+
+const vm = new Vue({
+ components: { App },
+ vuetify,
+ store,
+ router,
+ template: ' '
+}).$mount('#app');
+
+ipcRenderer.on(EVENTS.NAVIGATE, (event, path) => router.push({ path }));
diff --git a/src/renderer/mixins/TimeStamp.js b/src/renderer/mixins/TimeStamp.js
new file mode 100644
index 0000000..c2f1057
--- /dev/null
+++ b/src/renderer/mixins/TimeStamp.js
@@ -0,0 +1,71 @@
+export default {
+ computed: {
+ minutes: {
+ /**
+ *
+ */
+ get: function() {
+ return this.padZero(Math.floor(this.time / (60 * 1000)).toFixed(0));
+ },
+ /**
+ *
+ * @param {*} e
+ */
+ set: function(e) {
+ this.setTime({ minutes: e });
+ }
+ },
+ seconds: {
+ /**
+ *
+ */
+ get: function() {
+ return this.padZero(
+ (((this.time % (60 * 1000)) / 1000) | 0).toString()
+ );
+ },
+ /**
+ *
+ * @param {*} e
+ */
+ set: function(e) {
+ this.setTime({ seconds: e });
+ }
+ },
+ milliseconds: {
+ /**
+ *
+ */
+ get: function() {
+ return this.padZero(((this.time % 1000) / 10).toFixed(0).slice(0, 2));
+ },
+ /**
+ *
+ * @param {*} e
+ */
+ set: function(e) {
+ this.setTime({ milliseconds: e });
+ }
+ }
+ },
+ methods: {
+ /**
+ *
+ * @param {*} s
+ */
+ padZero(s) {
+ return 2 > s.length ? `0${s}` : s;
+ },
+ /**
+ *
+ * @param {*} param0
+ */
+ setTime({
+ minutes = Number(this.minutes),
+ seconds = Number(this.seconds),
+ milliseconds = Number(this.milliseconds)
+ } = {}) {
+ this.time = minutes * 60 * 1000 + seconds * 1000 + milliseconds * 10;
+ }
+ }
+};
diff --git a/src/renderer/plugins/highlight.js b/src/renderer/plugins/highlight.js
new file mode 100644
index 0000000..cd15b71
--- /dev/null
+++ b/src/renderer/plugins/highlight.js
@@ -0,0 +1,4 @@
+import Vue from 'vue';
+import VueHighlightJS from 'vue-highlightjs';
+
+Vue.use(VueHighlightJS);
diff --git a/src/renderer/plugins/index.js b/src/renderer/plugins/index.js
new file mode 100644
index 0000000..ffb11ab
--- /dev/null
+++ b/src/renderer/plugins/index.js
@@ -0,0 +1,2 @@
+import './highlight';
+import './quill-editor';
diff --git a/src/renderer/plugins/quill-editor.js b/src/renderer/plugins/quill-editor.js
new file mode 100644
index 0000000..7c488cd
--- /dev/null
+++ b/src/renderer/plugins/quill-editor.js
@@ -0,0 +1,29 @@
+import Vue from 'vue';
+import Quill from 'quill';
+import VueQuillEditor from 'vue-quill-editor';
+
+// require styles
+import 'quill/dist/quill.core.css';
+import 'quill/dist/quill.snow.css';
+import 'quill/dist/quill.bubble.css';
+import './quill-editor.scss';
+const Size = Quill.import('attributors/style/size');
+
+Size.whitelist = ['10px', '14px', '16px', '18px', '21px', '23px', '32px'];
+
+Quill.register(Quill.import('attributors/style/align'), true);
+Quill.register(Quill.import('attributors/style/background'), true);
+Quill.register(Quill.import('attributors/style/color'), true);
+Quill.register(Quill.import('attributors/style/direction'), true);
+Quill.register(Quill.import('attributors/style/font'), true);
+Quill.register(Size, true);
+
+Vue.use(VueQuillEditor, {
+ placeholder: '',
+ modules: {
+ toolbar: '#toolbar'
+ },
+ formats: {
+ size: '20rem'
+ }
+});
diff --git a/src/renderer/plugins/quill-editor.scss b/src/renderer/plugins/quill-editor.scss
new file mode 100644
index 0000000..f15ae86
--- /dev/null
+++ b/src/renderer/plugins/quill-editor.scss
@@ -0,0 +1,51 @@
+@import "~@/renderer/scss/sizes";
+@import "~@/renderer/scss/colors";
+
+@mixin quill-label($label, $size) {
+ .ql-snow .ql-picker.ql-size {
+ & .ql-picker-item[data-value="#{$size}"]::before {
+ content: $label;
+ }
+
+ & .ql-picker-label[data-value="#{$size}"]::before {
+ content: $label;
+ }
+ }
+}
+
+@include quill-label("14px", "14px");
+@include quill-label("16px", "16px");
+@include quill-label("18px", "18px");
+@include quill-label("21px", "21px");
+
+.ql-toolbar.ql-snow {
+ border: none;
+ border-top-left-radius: $border-radius;
+ border-top-right-radius: $border-radius;
+}
+
+.ql-container.ql-snow {
+ border: none;
+}
+
+.ql-snow .ql-color-picker,
+.ql-snow .ql-icon-picker {
+ width: 31px;
+}
+
+.ql- {
+ &toolbar {
+ height: 5.6rem;
+ display: flex;
+ align-items: center;
+ background-color: $grey;
+ }
+
+ &editor {
+ background-color: $white-background-opacity;
+ }
+
+ &container {
+ height: 23.6rem !important;
+ }
+}
diff --git a/src/renderer/plugins/vuetify.js b/src/renderer/plugins/vuetify.js
new file mode 100644
index 0000000..11aa9fa
--- /dev/null
+++ b/src/renderer/plugins/vuetify.js
@@ -0,0 +1,22 @@
+import Vue from 'vue';
+import Vuetify from 'vuetify';
+import 'vuetify/dist/vuetify.min.css';
+
+
+Vue.use(Vuetify);
+
+export default new Vuetify({
+ theme: {
+ themes: {
+ light: {
+ primary: '#095B8F',
+ secondary: '#123550',
+ accent: '#0C7AC0',
+ error: '#FF5252',
+ info: '#2196F3',
+ success: '#4CAF50',
+ warning: '#FFC107'
+ }
+ }
+ }
+});
diff --git a/src/renderer/router/index.js b/src/renderer/router/index.js
new file mode 100644
index 0000000..a13d7ab
--- /dev/null
+++ b/src/renderer/router/index.js
@@ -0,0 +1,24 @@
+import Vue from 'vue';
+import Router from 'vue-router';
+
+Vue.use(Router);
+
+export default new Router({
+ routes: [
+ {
+ path: '/',
+ name: 'landing-page',
+ component: require('../components/pages/LandingPage').default
+ },
+ {
+ path: '/preview',
+ name: 'preview-page',
+ component: require('../components/pages/PreviewPage').default
+ },
+ {
+ path: '/caption-studio',
+ name: 'caption-studio',
+ component: require('../components/pages/CaptionStudio').default
+ }
+ ]
+});
\ No newline at end of file
diff --git a/src/renderer/scss/_base.scss b/src/renderer/scss/_base.scss
new file mode 100644
index 0000000..12013e7
--- /dev/null
+++ b/src/renderer/scss/_base.scss
@@ -0,0 +1,14 @@
+html {
+ font-size: 62.5%;
+ width: 100%;
+}
+
+body, .application, #app {
+ @extend .font;
+ @extend .font-16;
+ background-color: $white;
+ color: #293F4F;
+ width: 100%;
+}
+
+
diff --git a/src/renderer/scss/_button.scss b/src/renderer/scss/_button.scss
new file mode 100644
index 0000000..b7af36f
--- /dev/null
+++ b/src/renderer/scss/_button.scss
@@ -0,0 +1,12 @@
+@import './colors';
+.--capital {
+ .v-btn__content {
+ text-transform: capitalize !important;
+ }
+}
+
+.--accent {
+ .v-btn__content {
+ color: $accent;
+ }
+}
\ No newline at end of file
diff --git a/src/renderer/scss/_colors.scss b/src/renderer/scss/_colors.scss
new file mode 100644
index 0000000..01ae0ef
--- /dev/null
+++ b/src/renderer/scss/_colors.scss
@@ -0,0 +1,10 @@
+$primary: #095B8F;
+$secondary: #123550;
+$accent: #0C7AC0;
+$black: #293F4F;
+$white: #fff;
+$white-background: #F2F2F2;
+$white-background-opacity: rgba(242, 242, 242, 0.5);
+$selected: #979797;
+$grey: #D8D8D8;
+$light-label: rgba(0,0,0,0.54);
\ No newline at end of file
diff --git a/src/renderer/scss/_fonts.scss b/src/renderer/scss/_fonts.scss
new file mode 100644
index 0000000..9754479
--- /dev/null
+++ b/src/renderer/scss/_fonts.scss
@@ -0,0 +1,122 @@
+$font-path: '~@/renderer/assets/fonts/';
+$font-aleo: $font-path + 'Aleo-Regular.otf';
+$font-path-assistant: $font-path + 'Assistant/Assistant-';
+
+$aleo: 'Aleo';
+$assistant: 'Assistant';
+$body-font-family: $assistant;
+
+
+@font-face {
+ font-family: $aleo;
+ font-weight: normal;
+ src: url($font-aleo) format('opentype');
+}
+
+@font-face {
+ font-family: $assistant;
+ font-weight: normal;
+ src: url($font-path-assistant + 'Regular.ttf') format('truetype');
+}
+@font-face {
+ font-family: $assistant;
+ font-weight: 600;
+ src: url($font-path-assistant + 'SemiBold.ttf') format('truetype');
+}
+@font-face {
+ font-family: $assistant;
+ font-weight: 700;
+ src: url($font-path-assistant + 'Bold.ttf') format('truetype');
+}
+@import 'mixins';
+@font-face {
+ font-family: $assistant;
+ font-weight: 800;
+ src: url($font-path-assistant + 'ExtraBold.ttf') format('truetype');
+}
+@font-face {
+ font-family: $assistant;
+ font-weight: 300;
+ src: url($font-path-assistant + 'Light.ttf') format('truetype');
+}
+@font-face {
+ font-family: $assistant;
+ font-weight: 200;
+ src: url($font-path-assistant + 'ExtraLight.ttf') format('truetype');
+}
+
+.capitalize {
+ text-transform: capitalize;
+}
+
+.font {
+ font-family: $assistant;
+}
+
+.font-regular {
+ @extend .font;
+ font-weight: normal;
+}
+
+.font-semi-bold {
+ @extend .font;
+ font-weight: 600;
+}
+
+.font-bold {
+ @extend .font;
+ font-weight: 700;
+}
+
+.font-extra-bold {
+ @extend .font;
+ font-weight: 800;
+}
+
+.font-light {
+ @extend .font;
+ font-weight: 300;
+}
+
+.font-extra-light {
+ @extend .font;
+ font-weight: 200;
+}
+
+.font-aleo {
+ font-family: $aleo;
+ font-weight: normal;
+}
+
+.font-12 {
+ @include font-size(1.2, 1.6);
+}
+
+.font-14 {
+ @include font-size(1.4, 1.6);
+}
+
+.font-16 {
+ @include font-size();
+}
+
+.font-21 {
+ @include font-size(2.1, 2.7);
+}
+
+.font-28 {
+ @include font-size(2.8, 3.7);
+}
+
+.font-31 {
+ @include font-size(3.1, 4.1)
+}
+
+.font-66 {
+ @include font-size(6.6, 7.9);
+}
+
+
+
+
+
diff --git a/src/renderer/scss/_mixins.scss b/src/renderer/scss/_mixins.scss
new file mode 100644
index 0000000..1da2182
--- /dev/null
+++ b/src/renderer/scss/_mixins.scss
@@ -0,0 +1,33 @@
+@mixin font-size($size: 1.6, $height: 2.1) {
+ font-size: $size + rem !important;
+ line-height: ($height / $size) + em;
+}
+
+@mixin key($width: 4.4rem, $height: 4.4rem, $gap: 0.56rem ) {
+ height: $height;
+ width: $width;
+ border-radius: 0.4rem;
+ background-color: #f7f7f7;
+ box-shadow: 0 0 2px 0 rgba(0, 0, 0, 0.14), 0 2px 2px 0 rgba(0, 0, 0, 0.12),
+ 0 1px 3px 0 rgba(0, 0, 0, 0.2);
+ color: $accent;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ text-transform: capitalize;
+ margin: $gap;
+ transition: background-color 0.25s;
+
+ &.--medium {
+ width: ($width + $gap + 0.3) * 3;
+ }
+
+ &.--large {
+ width: ($width + $gap + 0.5) * 5;
+ }
+
+ &.--active {
+ background-color: $accent;
+ color: $white;
+ }
+}
\ No newline at end of file
diff --git a/src/renderer/scss/_sizes.scss b/src/renderer/scss/_sizes.scss
new file mode 100644
index 0000000..cd895f0
--- /dev/null
+++ b/src/renderer/scss/_sizes.scss
@@ -0,0 +1 @@
+$border-radius: 1rem;
\ No newline at end of file
diff --git a/src/renderer/scss/components/TimeStamp.scss b/src/renderer/scss/components/TimeStamp.scss
new file mode 100644
index 0000000..57ab97d
--- /dev/null
+++ b/src/renderer/scss/components/TimeStamp.scss
@@ -0,0 +1,10 @@
+.time-stamp {
+ @extend .font-21;
+ align-items: center;
+ background-color: $white;
+ color: $secondary;
+ display: flex;
+ height: 3.6rem;
+ justify-content: center;
+ width: 15.3rem;
+}
\ No newline at end of file
diff --git a/src/renderer/scss/components/_index.scss b/src/renderer/scss/components/_index.scss
new file mode 100644
index 0000000..09854e4
--- /dev/null
+++ b/src/renderer/scss/components/_index.scss
@@ -0,0 +1 @@
+@import 'TimeStamp';
\ No newline at end of file
diff --git a/src/renderer/scss/main.scss b/src/renderer/scss/main.scss
new file mode 100644
index 0000000..42450af
--- /dev/null
+++ b/src/renderer/scss/main.scss
@@ -0,0 +1,7 @@
+@import 'sizes';
+@import 'button';
+@import 'colors';
+@import 'mixins';
+@import 'fonts';
+@import 'base';
+@import './components/index';
\ No newline at end of file
diff --git a/src/renderer/store/index.js b/src/renderer/store/index.js
new file mode 100644
index 0000000..b831538
--- /dev/null
+++ b/src/renderer/store/index.js
@@ -0,0 +1,24 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+
+import persistentState from './storage/PersistentState';
+import projectInfo from './modules/ProjectInfo';
+import gamePreview from './modules/GamePreview';
+import captionInfo from './modules/CaptionInfo';
+
+Vue.use(Vuex);
+
+export default new Vuex.Store({
+ modules: {
+ projectInfo,
+ gamePreview,
+ captionInfo
+ },
+
+ plugins: [
+ persistentState({
+ name: 'studioConfig',
+ key: 'studio'
+ })
+ ]
+});
\ No newline at end of file
diff --git a/src/renderer/store/modules/CaptionInfo.js b/src/renderer/store/modules/CaptionInfo.js
new file mode 100644
index 0000000..4b344c0
--- /dev/null
+++ b/src/renderer/store/modules/CaptionInfo.js
@@ -0,0 +1,19 @@
+export default {
+ state: {
+ audioLocation: undefined,
+ captionLocation: undefined,
+ isUnsavedChanges: false,
+ },
+
+ mutations: {
+ audioLocation: (state, payload) => state.audioLocation = payload.audioLocation,
+ captionLocation: (state, payload) => state.captionLocation = payload.captionLocation,
+ isUnsavedChanges: (state, payload) => state.isUnsavedChanges = payload.isUnsavedChanges,
+ },
+
+ actions: {
+ setAudioLocation: (store, payload) => store.commit('audioLocation', payload),
+ setCaptionLocation: (store, payload) => store.commit('captionLocation', payload),
+ setIsUnsavedChanges: (store, payload) => store.commit('isUnsavedChanges', payload),
+ }
+};
\ No newline at end of file
diff --git a/src/renderer/store/modules/GamePreview.js b/src/renderer/store/modules/GamePreview.js
new file mode 100644
index 0000000..8fed28b
--- /dev/null
+++ b/src/renderer/store/modules/GamePreview.js
@@ -0,0 +1,16 @@
+export default {
+ state: {
+ previewTarget: undefined,
+ previewURL: undefined
+ },
+
+ mutations: {
+ previewTarget: (state, payload) => state.previewTarget = payload.previewTarget,
+ previewURL: (state, payload) => state.previewURL = payload.previewURL
+ },
+
+ actions: {
+ setPreviewTarget: (store, payload) => store.commit('previewTarget', payload),
+ setPreviewURL: (store, payload) => store.commit('previewURL', payload)
+ }
+};
\ No newline at end of file
diff --git a/src/renderer/store/modules/ProjectInfo.js b/src/renderer/store/modules/ProjectInfo.js
new file mode 100644
index 0000000..aa5bde1
--- /dev/null
+++ b/src/renderer/store/modules/ProjectInfo.js
@@ -0,0 +1,13 @@
+export default {
+ state: {
+ location: undefined
+ },
+
+ mutations: {
+ location: (state, payload) => state.location = payload.location
+ },
+
+ actions: {
+ setProjectLocation: (store, payload) => store.commit('location', payload)
+ }
+};
\ No newline at end of file
diff --git a/src/renderer/store/storage/PersistentState.js b/src/renderer/store/storage/PersistentState.js
new file mode 100644
index 0000000..cc27d84
--- /dev/null
+++ b/src/renderer/store/storage/PersistentState.js
@@ -0,0 +1,115 @@
+import Store from 'electron-store';
+import merge from 'deepmerge';
+import { ipcMain, ipcRenderer } from 'electron';
+
+/**
+ * Referenced from vuex-electron to allow for full configuration of electron-store.
+ * @class PersistentState
+ */
+class PersistentState {
+ /**
+ *
+ * @readonly
+ * @memberof PersistentState
+ */
+ get state() { return this.storage.get(this.key); }
+ /**
+ *
+ * @memberof PersistentState
+ */
+ set state(value) { this.storage.set(this.key, value); }
+
+ /**
+ * Creates an instance of PersistentState.
+ * @param {*} options
+ * @param {*} vuexStore
+ * @memberof PersistentState
+ */
+ constructor(options, vuexStore) {
+ this.key = options.key || 'state';
+
+ this.events = {
+ IPC_CONNECT: 'ipcBridgeConnect',
+ IPC_NOTIFY_MAIN: 'ipcNotifyMain',
+ IPC_NOTIFY_RENDERER: 'ipcNotifyRenderer'
+ };
+ this.connections = {};
+
+ this.storage = new Store(options);
+ this.vuexStore = vuexStore;
+
+ if (this.state) {
+ this.vuexStore.replaceState(merge(this.vuexStore.state, this.state));
+ }
+ this.state = this.vuexStore.state;
+
+ this.vuexStore.subscribe((mutation, state) => this.state = state);
+
+ // Setup main and renderer process bridging.
+ // This will sync up the main and renderer processes' store objects.
+ if (process.type === 'renderer') {
+ this.setupRendererProcessBridge();
+ }
+ else {
+ this.setupMainProcessBridge();
+ }
+ }
+
+ /**
+ *
+ * @memberof PersistentState
+ */
+ setupMainProcessBridge() {
+ // Listen for bridge connections.
+ ipcMain.on(this.events.IPC_CONNECT, (event) => {
+ const sender = event.sender;
+ const senderId = sender.id;
+
+ this.connections[senderId] = sender;
+
+ // Manage connection.
+ sender.on('destroyed', () => delete this.connections[senderId]);
+ });
+
+ // Listen for bridge notifications to the main procress.
+ ipcMain.on(this.events.IPC_NOTIFY_MAIN, (event, { type, payload }) => this.vuexStore.dispatch(type, payload));
+
+ // Anytime there is an update to the main process's store, notify the renderer process of that change.
+ this.vuexStore.subscribe((mutation) => {
+ const { type, payload } = mutation;
+
+ // Update each connection.
+ Object.keys(this.connections).forEach(id => {
+ this.connections[id].send(this.events.IPC_NOTIFY_RENDERER, { type, payload });
+ });
+ });
+ }
+
+ /**
+ *
+ * @memberof PersistentState
+ */
+ setupRendererProcessBridge() {
+ ipcRenderer.send(this.events.IPC_CONNECT);
+
+ // Cahce the original vuex commit.
+ const vuexCommit = this.vuexStore.commit;
+
+ // Warn about using commit in the renderer process. Main might update update correctly.
+ // NOTE: This should throw an error, however for testing we need to be able to change the
+ // state directly from the renderer process.
+ this.vuexStore.commit = (type, payload) => {
+ console.warn('You should not call commit in the renderer process. Use dispatch instead.');
+ vuexCommit(type, payload);
+ };
+ // Forward renderer process dispatches to the main process.
+ this.vuexStore.dispatch = (type, payload) => ipcRenderer.send(this.events.IPC_NOTIFY_MAIN, { type, payload });
+
+ // Listen for store changes from the main process and apply them.
+ ipcRenderer.on(this.events.IPC_NOTIFY_RENDERER, (event, { type, payload }) => {
+ vuexCommit(type, payload);
+ });
+ }
+}
+
+export default (options = {}) => vuexStore => new PersistentState(options, vuexStore);
\ No newline at end of file
diff --git a/tasks/aliases.js b/tasks/aliases.js
deleted file mode 100644
index 034701f..0000000
--- a/tasks/aliases.js
+++ /dev/null
@@ -1,87 +0,0 @@
-module.exports = function(grunt)
-{
- // Can build against a specific platform, for instance app:osx32
- // no platform will build for all platforms
- grunt.registerTask('app', 'Build the Application', function(platform)
- {
- grunt.task.run(
- 'jade:release',
- 'clean:main',
- 'jshint:main',
- 'uglify:app',
- 'clean:css',
- 'less:release',
- 'moduleAppTasks',
- 'clean:defaultTemplate',
- 'libs',
- 'copy:defaultTemplate',
- 'exec:appModules',
- 'nodewebkit:' + (platform || 'all')
- );
- });
-
- // Same as the app task except that the code is build in DEBUG mode
- grunt.registerTask('app-debug', 'Build the Application in debug mode', function(platform)
- {
- grunt.task.run(
- 'jade:debug',
- 'clean:main',
- 'jshint:main',
- 'concat:main',
- 'replace:app',
- 'clean:css',
- 'less:development',
- 'moduleAppTasksDebug',
- 'libs-debug',
- 'copy:defaultTemplate',
- 'exec:appModules',
- 'nodewebkit:' + (platform || 'all')
- );
- });
-
- // Package the app into the installers
- // which are user-friend packages for installing
- // on the user's platform
- grunt.registerTask('package', function(platform)
- {
- var tasks = [];
-
- // Package a single platform
- if (/win/.test(platform))
- {
- tasks.push('exec:package' + platform);
- }
- else if (/osx/.test(platform))
- {
- tasks.push('appdmg:' + platform);
- }
- // Package all platforms
- else
- {
- tasks.push(
- 'exec:packagewin32',
- 'exec:packagewin64',
- 'appdmg:osx32',
- 'appdmg:osx64'
- );
- }
- grunt.task.run(tasks);
- });
-
- // Open the application directly
- grunt.registerTask('open', 'Open the App', function(platform)
- {
- if (!platform)
- {
- grunt.fail.fatal("Open must have a platform, e.g.,'osx64'");
- return;
- }
- grunt.task.run('exec:open' + platform);
- });
-
- // Template copy
- grunt.registerTask('default-template', 'Copy the default template', [
- 'bower:install',
- 'copy:defaultTemplate'
- ]);
-};
\ No newline at end of file
diff --git a/tasks/appdmg.js b/tasks/appdmg.js
deleted file mode 100644
index f9b460b..0000000
--- a/tasks/appdmg.js
+++ /dev/null
@@ -1,47 +0,0 @@
-module.exports = {
- options: {
- "title": "<%= project.name %>",
- "icon": "<%= distFolder %>/assets/images/icon.icns",
- "background": "<%= installerDir %>/assets/background.png",
- "icon-size": 80
- },
- osx64: {
- options: {
- "contents": [
- {
- "x": 448,
- "y": 344,
- "type": "link",
- "path": "/Applications"
- },
- {
- "x": 192,
- "y": 344,
- "type": "file",
- "path": "<%= buildDir %>/<%= project.name %>/osx64/<%= project.name %>.app"
- }
- ]
- },
- dest: '<%= buildDir %>/<%= project.name %>-Setup-x64.dmg'
- },
- osx32: {
- options: {
- "contents": [
- {
- "x": 448,
- "y": 344,
- "type": "link",
- "path": "/Applications"
- },
- {
- "x": 192,
- "y": 344,
- "type": "file",
- "path": "<%= buildDir %>/<%= project.name %>/osx32/<%= project.name %>.app"
- }
- ]
- },
- dest: '<%= buildDir %>/<%= project.name %>-Setup-x32.dmg'
- }
-
-};
\ No newline at end of file
diff --git a/tasks/copy.js b/tasks/copy.js
deleted file mode 100644
index f02e8e1..0000000
--- a/tasks/copy.js
+++ /dev/null
@@ -1,8 +0,0 @@
-module.exports = {
- defaultTemplate: {
- expand: true,
- cwd: '<%= components %>/default/',
- src: '**',
- dest: '<%= distFolder %>/assets/templates/default/'
- }
-};
\ No newline at end of file
diff --git a/tasks/exec.js b/tasks/exec.js
deleted file mode 100644
index 046997b..0000000
--- a/tasks/exec.js
+++ /dev/null
@@ -1,26 +0,0 @@
-module.exports = {
- appModules : {
- command: 'npm install',
- cwd: '<%= distFolder %>',
- stdout: false,
- stderr: false
- },
- "packagewin32": {
- cmd: 'makensis <%= installerDir %>/win32.nsi'
- },
- "packagewin64": {
- cmd: 'makensis <%= installerDir %>/win64.nsi'
- },
- "openosx32" : {
- cmd: 'open <%= buildDir %>/<%= project.name %>/osx32/<%= project.name %>.app'
- },
- "openosx64" : {
- cmd: 'open <%= buildDir %>/<%= project.name %>/osx64/<%= project.name %>.app'
- },
- "openwin32" : {
- cmd: '<%= buildDir %>/<%= project.name %>/win32/<%= project.name %>.exe'
- },
- "openwin64" : {
- cmd: '<%= buildDir %>/<%= project.name %>/win64/<%= project.name %>.exe'
- }
-};
\ No newline at end of file
diff --git a/tasks/jade.js b/tasks/jade.js
deleted file mode 100644
index 871a858..0000000
--- a/tasks/jade.js
+++ /dev/null
@@ -1,33 +0,0 @@
-module.exports = {
- debug: {
- options: {
- pretty: true,
- data: {
- debug: true,
- name: "<%= project.name %>",
- version: "<%= project.version %>"
- }
- },
- files: {
- "<%= distFolder %>/captions.html": "src/jade/captions.jade",
- "<%= distFolder %>/index.html": "src/jade/index.jade",
- "<%= distFolder %>/new.html": "src/jade/new.jade",
- "<%= distFolder %>/preview.html": "src/jade/preview.jade",
- "<%= distFolder %>/preview-client.html": "src/jade/preview-client.jade",
- "<%= distFolder %>/remote.html": "src/jade/remote.jade",
- "<%= distFolder %>/tasks-terminal.html": "src/jade/tasks-terminal.jade",
- "<%= distFolder %>/tasks-test.html": "src/jade/tasks-test.jade",
- "<%= distFolder %>/tasks.html": "src/jade/tasks.jade"
- }
- },
- release: {
- options: {
- data: {
- debug: false,
- name: "<%= project.name %>",
- version: "<%= project.version %>"
- }
- },
- files: "<%= jade.debug.files %>"
- }
-};
\ No newline at end of file
diff --git a/tasks/nodewebkit.js b/tasks/nodewebkit.js
deleted file mode 100644
index f5a4021..0000000
--- a/tasks/nodewebkit.js
+++ /dev/null
@@ -1,44 +0,0 @@
-module.exports = {
- options: {
- version: '0.12.3',
- buildDir: '<%= buildDir %>',
- macIcns: '<%= distFolder %>/assets/images/icon.icns',
- winIco: '<%= distFolder %>/assets/images/icon.ico',
- macZip: true,
- macPlist: {
- "LSEnvironment": {
- "PATH": "/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin"
- }
- }
- },
- all: {
- options: {
- platforms: ['osx', 'win']
- },
- src: '<%= distFolder %>/**/*'
- },
- osx32: {
- options: {
- platforms: ['osx32']
- },
- src: '<%= nodewebkit.all.src %>'
- },
- osx64: {
- options: {
- platforms: ['osx64']
- },
- src: '<%= nodewebkit.all.src %>'
- },
- win32: {
- options: {
- platforms: ['win32']
- },
- src: '<%= nodewebkit.all.src %>'
- },
- win64: {
- options: {
- platforms: ['win64']
- },
- src: '<%= nodewebkit.all.src %>'
- }
-};
\ No newline at end of file
diff --git a/tasks/overrides/clean.js b/tasks/overrides/clean.js
deleted file mode 100644
index c2a9ea4..0000000
--- a/tasks/overrides/clean.js
+++ /dev/null
@@ -1,6 +0,0 @@
-module.exports = {
- defaultTemplate: [
- '<%= components %>/default/',
- '<%= distFolder %>/assets/templates/default/'
- ]
-};
\ No newline at end of file
diff --git a/tasks/overrides/replace.js b/tasks/overrides/replace.js
deleted file mode 100644
index 224ca54..0000000
--- a/tasks/overrides/replace.js
+++ /dev/null
@@ -1,36 +0,0 @@
-module.exports = {
- main: {
- src: '<%= jsFolder %>/main.js',
- overwrite: true,
- replacements: [{
- from: /\bDEBUG\b/g,
- to: "true"
- },{
- from: /\bRELEASE\b/g,
- to: "false"
- },{
- from: /\bWEB\b/g,
- to: "true"
- },{
- from: /\bAPP\b/g,
- to: "false"
- }]
- },
- app: {
- src: '<%= jsFolder %>/main.js',
- overwrite: true,
- replacements: [{
- from: /\bDEBUG\b/g,
- to: "true"
- },{
- from: /\bRELEASE\b/g,
- to: "false"
- },{
- from: /\bWEB\b/g,
- to: "false"
- },{
- from: /\bAPP\b/g,
- to: "true"
- }]
- }
-};
\ No newline at end of file
diff --git a/tasks/overrides/uglify.js b/tasks/overrides/uglify.js
deleted file mode 100644
index fe2818c..0000000
--- a/tasks/overrides/uglify.js
+++ /dev/null
@@ -1,34 +0,0 @@
-module.exports = {
- main: {
- files: {
- '<%= jsFolder %>/main.js': '<%= project.js.main %>'
- },
- options: {
- compress: {
- global_defs: {
- "DEBUG": false,
- "RELEASE": true,
- "WEB": true,
- "APP": false
- },
- dead_code: true,
- drop_console: true
- }
- }
- },
- app: {
- files: '<%= uglify.main.files %>',
- options: {
- compress: {
- global_defs: {
- "DEBUG": false,
- "RELEASE": true,
- "WEB": false,
- "APP": true
- },
- dead_code: true,
- drop_console: true
- }
- }
- }
-};
\ No newline at end of file
diff --git a/tasks/overrides/version.js b/tasks/overrides/version.js
deleted file mode 100644
index a6e605a..0000000
--- a/tasks/overrides/version.js
+++ /dev/null
@@ -1,19 +0,0 @@
-module.exports = {
- options: {
- 'deploy/package.json' : 'version',
- 'installer/win32.nsi' : nsiVersion,
- 'installer/win64.nsi' : nsiVersion
- }
-};
-
-function nsiVersion(contents, version)
-{
- // Strip off any "-alpha" "-beta" "-rc" etc
- var extra = version.lastIndexOf('-');
- var parts = (extra > -1 ? version.substr(0, extra) : version).split('.');
-
- // Replace in the file contents and return
- return contents.replace(/(\!define VERSIONMAJOR) [0-9]+/, "$1 " + parts[0])
- .replace(/(\!define VERSIONMINOR) [0-9]+/, "$1 " + parts[1])
- .replace(/(\!define VERSIONBUILD) [0-9]+/, "$1 " + parts[2]);
-}
\ No newline at end of file
diff --git a/tasks/overrides/watch.js b/tasks/overrides/watch.js
deleted file mode 100644
index e7df3be..0000000
--- a/tasks/overrides/watch.js
+++ /dev/null
@@ -1,12 +0,0 @@
-module.exports = {
- css: {
- files: [
- '<%= project.css.main %>',
- '<%= project.file %>',
- 'src/**/*.less'
- ],
- tasks: [
- 'less:development'
- ]
- }
-};
\ No newline at end of file
diff --git a/testing/jsconfig.spec.json b/testing/jsconfig.spec.json
new file mode 100644
index 0000000..a9ac636
--- /dev/null
+++ b/testing/jsconfig.spec.json
@@ -0,0 +1,18 @@
+{
+ "compilerOptions": {
+ "sourceMap": true,
+ "declaration": false,
+ "moduleResolution": "node",
+ "emitDecoratorMetadata": true,
+ "experimentalDecorators": true,
+ "lib": [
+ "es2016",
+ "dom"
+ ],
+ "baseUrl": ".",
+ "paths": {
+ "@renderer/*": ["app/*"],
+ "@constants/*": ["constants/*"],
+ },
+ },
+}
\ No newline at end of file
diff --git a/testing/unit/main/karma-main.config.js b/testing/unit/main/karma-main.config.js
new file mode 100644
index 0000000..d60d2dd
--- /dev/null
+++ b/testing/unit/main/karma-main.config.js
@@ -0,0 +1,58 @@
+const path = require('path');
+const baseConfig = require('../../webpack-testing.config');
+
+module.exports = (config) => {
+ config.set({
+ failOnEmptyTestSuite: false,
+ colors: true,
+ autoWatch: true,
+ singleRun: true,
+
+ files: ['specs/**/*.spec.js'],
+
+ client: {
+ useIframe: false
+ },
+
+ frameworks: ['mocha', 'chai'],
+ preprocessors: {
+ 'specs/**/*.spec.js': ['webpack', 'electron', 'sourcemap']
+ },
+
+ webpack: {
+ ...baseConfig,
+
+ resolve: {
+ alias: {
+ '@': path.resolve(__dirname, '../../../src/main')
+ }
+ },
+
+ target: 'electron-main'
+ },
+ webpackMiddleware: {
+ stats: 'errors-only'
+ },
+
+ colors: true,
+ autoWatch: true,
+ singleRun: true,
+
+ logLevel: config.LOG_INFO,
+ reporters: ['progress'],
+
+ customLaunchers: {
+ VisibleElectron: {
+ base: 'Electron',
+ browserWindowOptions: {
+ show: true,
+ webPreferences: {
+ nodeIntegration: true
+ }
+ }
+ }
+ },
+
+ browsers: ['VisibleElectron']
+ });
+};
diff --git a/testing/unit/renderer/karma-renderer.config.js b/testing/unit/renderer/karma-renderer.config.js
new file mode 100644
index 0000000..1c9b9b7
--- /dev/null
+++ b/testing/unit/renderer/karma-renderer.config.js
@@ -0,0 +1,55 @@
+const path = require('path');
+const baseConfig = require('../../webpack-testing.config');
+
+module.exports = (config) => {
+ config.set({
+ failOnEmptyTestSuite: false,
+ colors: true,
+ autoWatch: true,
+ singleRun: true,
+
+ files: ['specs/**/*.spec.js'],
+
+ client: {
+ useIframe: false
+ },
+
+ frameworks: ['mocha', 'chai'],
+ preprocessors: {
+ 'specs/**/*.spec.js': ['webpack']
+ },
+
+ webpack: {
+ ...baseConfig,
+
+ resolve: {
+ alias: {
+ '@': path.resolve(__dirname, '../../../src')
+ },
+ extensions: ['.js', '.vue']
+ },
+
+ target: 'electron-renderer'
+ },
+ webpackMiddleware: {
+ stats: 'errors-only'
+ },
+
+ logLevel: config.LOG_INFO,
+ reporters: ['progress'],
+
+ customLaunchers: {
+ VisibleElectron: {
+ base: 'Electron',
+ browserWindowOptions: {
+ show: true,
+ webPreferences: {
+ nodeIntegration: true
+ }
+ }
+ }
+ },
+
+ browsers: ['VisibleElectron']
+ });
+};
diff --git a/testing/unit/renderer/specs/caption-studio/CaptionPreview.spec.js b/testing/unit/renderer/specs/caption-studio/CaptionPreview.spec.js
new file mode 100644
index 0000000..1594fdd
--- /dev/null
+++ b/testing/unit/renderer/specs/caption-studio/CaptionPreview.spec.js
@@ -0,0 +1,84 @@
+import { createVue } from '../../utils/VueUtils';
+import CaptionPreview from '@/renderer/components/caption-studio/CaptionPreview.vue';
+import { EventBus } from '@/renderer/class/EventBus';
+import Sinon from 'sinon';
+
+describe('CaptionPreview.vue', () => {
+
+ it('setup() should create captionPlayer successfully', () => {
+ const wrapper = createVue(CaptionPreview);
+ expect(wrapper.vm.captionPlayer);
+ });
+
+ it('should recieve all events properly', async () => {
+ const setActiveCaption = CaptionPreview.methods.setActiveCaption;
+ const loadCaptionData = CaptionPreview.methods.loadCaptionData;
+ const onTimeChange = CaptionPreview.methods.onTimeChange;
+ const setup = CaptionPreview.methods.setup;
+
+ CaptionPreview.methods.setActiveCaption = Sinon.stub();
+ CaptionPreview.methods.loadCaptionData = Sinon.stub();
+ CaptionPreview.methods.onTimeChange = Sinon.stub();
+ CaptionPreview.methods.setup = Sinon.stub();
+
+ const wrapper = createVue(CaptionPreview);
+
+ EventBus.$emit('caption_changed');
+ EventBus.$emit('caption_data');
+ EventBus.$emit('time_current');
+ EventBus.$emit('caption_reset');
+
+ await wrapper.vm.$nextTick();
+
+ expect(CaptionPreview.methods.setActiveCaption.callCount).to.equal(1);
+ expect(CaptionPreview.methods.loadCaptionData.callCount).to.equal(1);
+ expect(CaptionPreview.methods.onTimeChange.callCount).to.equal(1);
+ expect(CaptionPreview.methods.setup.callCount).to.equal(2); //this is also called on Mounted.
+
+ CaptionPreview.methods.setActiveCaption = setActiveCaption;
+ CaptionPreview.methods.loadCaptionData = loadCaptionData;
+ CaptionPreview.methods.onTimeChange = onTimeChange;
+ CaptionPreview.methods.setup = setup;
+ });
+
+ it('computed properties should update as expected', async () => {
+ const wrapper = createVue(CaptionPreview);
+
+ expect(wrapper.vm.atEnd).to.be.true;
+ expect(wrapper.vm.atStart).to.be.true;
+
+ EventBus.$emit('caption_changed', { name: 'Test', index: 1, lastIndex: 2 });
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.vm.atEnd).to.be.false;
+ expect(wrapper.vm.atStart).to.be.false;
+
+ EventBus.$emit('caption_changed', { name: 'Test', index: 0, lastIndex: 2 });
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.vm.atEnd).to.be.false;
+ expect(wrapper.vm.atStart).to.be.true;
+
+ EventBus.$emit('caption_changed', { name: 'Test', index: 2, lastIndex: 2 });
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.vm.atEnd).to.be.true;
+ expect(wrapper.vm.atStart).to.be.false;
+ });
+
+ it('setActiveCaption() should properly set data value when recieving event', async () => {
+ const wrapper = createVue(CaptionPreview);
+
+ expect(wrapper.vm.name).to.equal('');
+ expect(wrapper.vm.index).to.equal(0);
+ expect(wrapper.vm.lastIndex).to.equal(0);
+
+ EventBus.$emit('caption_changed', { name: 'Test', index: 2, lastIndex: 1 });
+
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.vm.name).to.equal('Test');
+ expect(wrapper.vm.index).to.equal(2);
+ expect(wrapper.vm.lastIndex).to.equal(1);
+ });
+});
\ No newline at end of file
diff --git a/testing/unit/renderer/specs/caption-studio/FileDirectory.spec.js b/testing/unit/renderer/specs/caption-studio/FileDirectory.spec.js
new file mode 100644
index 0000000..8f8054f
--- /dev/null
+++ b/testing/unit/renderer/specs/caption-studio/FileDirectory.spec.js
@@ -0,0 +1,64 @@
+import { createVue } from '../../utils/VueUtils';
+import FileDirectory from '@/renderer/components/caption-studio/FileDirectory.vue';
+import { EventBus } from '@/renderer/class/EventBus';
+import Directory from '@/renderer/class/Directory';
+import Sinon from 'sinon';
+import { directory, active } from '../../utils/data';
+
+describe('FileDirectory.vue', () => {
+
+ it('should recieve all events properly', async () => {
+ const nextFile = FileDirectory.methods.nextFile;
+ const previousFile = FileDirectory.methods.previousFile;
+ const onFileCaptionChange = FileDirectory.methods.onFileCaptionChange;
+ const jsonEmit = FileDirectory.methods.jsonEmit;
+
+ FileDirectory.methods.nextFile = Sinon.stub();
+ FileDirectory.methods.previousFile = Sinon.stub();
+ FileDirectory.methods.onFileCaptionChange = Sinon.stub();
+ FileDirectory.methods.jsonEmit = Sinon.stub();
+
+ const wrapper = createVue(FileDirectory, {
+ propsData: {
+ directory,
+ active
+ }
+ });
+
+ EventBus.$emit('next_file');
+ EventBus.$emit('previous_file');
+ EventBus.$emit('file_captioned');
+ EventBus.$emit('json_file_selected');
+
+ await wrapper.vm.$nextTick();
+
+ expect(FileDirectory.methods.nextFile.callCount).to.equal(1);
+ expect(FileDirectory.methods.previousFile.callCount).to.equal(1);
+ expect(FileDirectory.methods.onFileCaptionChange.callCount).to.equal(1);
+ expect(FileDirectory.methods.jsonEmit.callCount).to.equal(1);
+
+ FileDirectory.methods.nextFile = nextFile;
+ FileDirectory.methods.previousFile = previousFile;
+ FileDirectory.methods.onFileCaptionChange = onFileCaptionChange;
+ FileDirectory.methods.jsonEmit = jsonEmit;
+ });
+
+ it('when a file is selected hasActive should update properly', async () => {
+ //directory__select
+ const wrapper = createVue(FileDirectory, {
+ propsData: {
+ directory,
+ active
+ }
+ });
+
+ expect(wrapper.vm.hasActive).to.be.false;
+
+ const input = wrapper.find('.directory__select');
+ input.element.checked = true;
+ input.trigger('change');
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.vm.hasActive).to.be.true;
+ });
+});
\ No newline at end of file
diff --git a/testing/unit/renderer/specs/caption-studio/FileExplorer.spec.js b/testing/unit/renderer/specs/caption-studio/FileExplorer.spec.js
new file mode 100644
index 0000000..4be88a1
--- /dev/null
+++ b/testing/unit/renderer/specs/caption-studio/FileExplorer.spec.js
@@ -0,0 +1,14 @@
+import { createVue } from '../../utils/VueUtils';
+import FileExplorer from '@/renderer/components/caption-studio/FileExplorer.vue';
+import { EventBus } from '@/renderer/class/EventBus';
+import Sinon from 'sinon';
+
+const file = {'name':'title.mp3','fullPath':'/Full/path/to/title.mp3','relativePath':'path/to/title.mp3','type':{'ext':'mp3','mime':'audio/mpeg'}};
+
+//TODO: sort out how to test this properly
+// describe('FileExplorer.js', () => {
+
+// it('setup() should create captionPlayer successfully', () => {
+// const wrapper = createVue(FileExplorer);
+// });
+// });
\ No newline at end of file
diff --git a/testing/unit/renderer/specs/caption-studio/JsonPreview.spec.js b/testing/unit/renderer/specs/caption-studio/JsonPreview.spec.js
new file mode 100644
index 0000000..bfeeac9
--- /dev/null
+++ b/testing/unit/renderer/specs/caption-studio/JsonPreview.spec.js
@@ -0,0 +1,126 @@
+import { createVue } from '../../utils/VueUtils';
+import JsonPreview from '@/renderer/components/caption-studio/JsonPreview.vue';
+import { EventBus } from '@/renderer/class/EventBus';
+import Sinon from 'sinon';
+import { files, active, badCaptions } from '../../utils/data';
+
+
+describe('JsonPreview.vue', () => {
+
+ it('Component should mount properly', () => {
+ const wrapper = createVue(JsonPreview);
+ wrapper.destroy();
+ });
+
+ it('should recieve all events properly', async () => {
+ const onUpdate = JsonPreview.methods.onUpdate;
+ const onCaptionChange = JsonPreview.methods.onCaptionChange;
+ const update = JsonPreview.methods.update;
+ const createFileNameMap = JsonPreview.methods.createFileNameMap;
+ const onSave = JsonPreview.methods.onSave;
+
+ JsonPreview.methods.onUpdate = Sinon.stub();
+ JsonPreview.methods.onCaptionChange = Sinon.stub();
+ JsonPreview.methods.update = Sinon.stub();
+ JsonPreview.methods.createFileNameMap = Sinon.stub();
+ JsonPreview.methods.onSave = Sinon.stub();
+
+ const wrapper = createVue(JsonPreview);
+
+ EventBus.$emit('caption_update');
+ EventBus.$emit('caption_changed');
+ EventBus.$emit('caption_data');
+ EventBus.$emit('file_list_generated');
+ EventBus.$emit('saveCaptionData');
+
+ await wrapper.vm.$nextTick();
+
+ expect(JsonPreview.methods.onUpdate.callCount).to.equal(1);
+ expect(JsonPreview.methods.onCaptionChange.callCount).to.equal(1);
+ expect(JsonPreview.methods.update.callCount).to.equal(1);
+ expect(JsonPreview.methods.createFileNameMap.callCount).to.equal(1);
+ expect(JsonPreview.methods.onSave.callCount).to.equal(1);
+
+ JsonPreview.methods.onUpdate = onUpdate;
+ JsonPreview.methods.onCaptionChange = onCaptionChange;
+ JsonPreview.methods.update = update;
+ JsonPreview.methods.createFileNameMap = createFileNameMap;
+ JsonPreview.methods.onSave = onSave;
+ wrapper.destroy();
+ });
+
+
+ it('onCaptionChange()', async () => {
+ const wrapper = createVue(JsonPreview);
+
+ expect(wrapper.vm.currentIndex).to.equal(0);
+ expect(wrapper.vm.activeFile).to.equal('');
+ expect(wrapper.vm.fileNameMap['title.mp3']).to.undefined;
+
+ EventBus.$emit('caption_changed', { name: active.name, file: active, index: 1});
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.vm.currentIndex).to.equal(1);
+ expect(wrapper.vm.activeFile).to.equal('title.mp3');
+ expect(wrapper.vm.fileNameMap['title.mp3'].fullPath).to.equal(active.fullPath);
+ wrapper.destroy();
+
+ });
+
+ it('update() should call all methods properly', async () => {
+ const checkErrors = JsonPreview.methods.checkErrors;
+ const cleanData = JsonPreview.methods.cleanData;
+ const createBlob = JsonPreview.methods.createBlob;
+
+ JsonPreview.methods.checkErrors = Sinon.stub();
+ JsonPreview.methods.cleanData = Sinon.stub().returnsArg(0);
+ JsonPreview.methods.createBlob = Sinon.stub();
+
+ const wrapper = createVue(JsonPreview);
+
+ //run with origin !== 'userOpen'
+ EventBus.$emit('caption_data', {data: { caption: 'caption' } }, 'test' );
+ await wrapper.vm.$nextTick();
+
+ expect(JsonPreview.methods.cleanData.callCount).to.equal(1);
+ expect(JsonPreview.methods.checkErrors.callCount).to.equal(1);
+ expect(JsonPreview.methods.createBlob.callCount).to.equal(2); //called once on mounted as well vb
+ expect(wrapper.vm.data.data.caption).to.equal('caption');
+
+ //run with origin !== 'userOpen'
+ EventBus.$emit('caption_data', {data: { caption: 'caption' } }, 'userOpen' );
+
+ await wrapper.vm.$nextTick();
+
+ expect(JsonPreview.methods.cleanData.callCount).to.equal(2);
+ expect(JsonPreview.methods.checkErrors.callCount).to.equal(3); //called an extra time when emitting because of origin === userOpen
+ expect(JsonPreview.methods.createBlob.callCount).to.equal(3); //called once on mounted as well vb
+ expect(wrapper.vm.data.data.caption).to.equal('caption');
+
+ JsonPreview.methods.checkErrors = checkErrors;
+ JsonPreview.methods.cleanData = cleanData;
+ JsonPreview.methods.createBlob = createBlob;
+ wrapper.destroy();
+ });
+
+ it('update() should clean and prepare data using checkErrors() and cleanData()', async () => {
+ const wrapper = createVue(JsonPreview);
+ EventBus.$emit('caption_data', badCaptions, 'test' );
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.vm.data['bad-caption']).to.be.undefined; // bad-caption should be removed
+ expect(wrapper.vm.data['good-caption'][0].content).to.equal('good-caption'); // confirm good-caption still exists
+ expect(wrapper.vm.jsonErrors).to.be.false; //clean data should remove any captions with errors
+ wrapper.destroy();
+ });
+
+ it('createFileNameMap()', async () => {
+ const wrapper = createVue(JsonPreview);
+
+ EventBus.$emit('file_list_generated', files);
+
+ await wrapper.vm.$nextTick();
+ expect(wrapper.vm.fileNameMap.title.name).to.equal(files[0].name); //title and title.mp3
+ wrapper.destroy();
+ });
+});
\ No newline at end of file
diff --git a/testing/unit/renderer/specs/caption-studio/TextEditor.spec.js b/testing/unit/renderer/specs/caption-studio/TextEditor.spec.js
new file mode 100644
index 0000000..4cdfd1d
--- /dev/null
+++ b/testing/unit/renderer/specs/caption-studio/TextEditor.spec.js
@@ -0,0 +1,66 @@
+import { createVue } from '../../utils/VueUtils';
+import { mount } from '@vue/test-utils';
+import TextEditor from '@/renderer/components/caption-studio/TextEditor.vue';
+import { EventBus } from '@/renderer/class/EventBus';
+import Sinon from 'sinon';
+
+
+describe('TextEditor.vue', () => {
+
+ it('Component should createVue properly', () => {
+ const wrapper = createVue(TextEditor);
+ });
+
+ it('should recieve all events properly', async () => {
+ const onUpdate = TextEditor.methods.onUpdate;
+ const reset = TextEditor.methods.reset;
+ const onJsonErrors = TextEditor.methods.onJsonErrors;
+
+ TextEditor.methods.onUpdate = Sinon.stub();
+ TextEditor.methods.reset = Sinon.stub();
+ TextEditor.methods.onJsonErrors = Sinon.stub();
+
+ const wrapper = createVue(TextEditor);
+
+ EventBus.$emit('caption_changed');
+ EventBus.$emit('caption_reset');
+ EventBus.$emit('json_errors');
+
+ await wrapper.vm.$nextTick();
+
+ expect(TextEditor.methods.onUpdate.callCount).to.equal(1);
+ expect(TextEditor.methods.reset.callCount).to.equal(1);
+ expect(TextEditor.methods.onJsonErrors.callCount).to.equal(1);
+
+ TextEditor.methods.onUpdate = onUpdate;
+ TextEditor.methods.reset = reset;
+ TextEditor.methods.onJsonErrors = onJsonErrors;
+ });
+
+ it('computed properites should all return as expected', async () => {
+
+ const wrapper = createVue(TextEditor);
+
+ expect(wrapper.vm.canAdd).to.be.false;
+ expect(wrapper.vm.canRemove).to.be.false;
+ expect(wrapper.vm.characterCount).to.equal(0);
+ expect(wrapper.vm.lineCount).to.equal(0);
+
+ EventBus.$emit('caption_changed', { data: { start: 10, end: 100, content: 'caption-content', edited: true}, index: 1, lastIndex: 1, name: 'test' }, 'test');
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.vm.canAdd).to.be.true;
+ expect(wrapper.vm.canRemove).to.be.false;
+ expect(wrapper.vm.characterCount).to.equal(15);
+ expect(wrapper.vm.lineCount).to.equal(0);
+
+ EventBus.$emit('caption_changed', { data: { start: 10, end: 100, content: 'caption-content line', edited: true}, index: 1, lastIndex: 2, name: 'test' }, 'test');
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.vm.canAdd).to.be.false;
+ expect(wrapper.vm.canRemove).to.be.true;
+ expect(wrapper.vm.characterCount).to.equal(19);
+ expect(wrapper.vm.lineCount).to.equal(1);
+ });
+
+});
\ No newline at end of file
diff --git a/testing/unit/renderer/specs/caption-studio/TimeStamp.spec.js b/testing/unit/renderer/specs/caption-studio/TimeStamp.spec.js
new file mode 100644
index 0000000..c6d3019
--- /dev/null
+++ b/testing/unit/renderer/specs/caption-studio/TimeStamp.spec.js
@@ -0,0 +1,10 @@
+import { createVue } from '../../utils/VueUtils';
+import TimeStamp from '@/renderer/components/caption-studio/TimeStamp.vue';
+
+describe('TimeStamp.vue', () => {
+
+ it('should mount successfully', () => {
+ const wrapper = createVue(TimeStamp);
+ });
+
+});
\ No newline at end of file
diff --git a/testing/unit/renderer/specs/caption-studio/TimeStampInput.spec.js b/testing/unit/renderer/specs/caption-studio/TimeStampInput.spec.js
new file mode 100644
index 0000000..8cbe003
--- /dev/null
+++ b/testing/unit/renderer/specs/caption-studio/TimeStampInput.spec.js
@@ -0,0 +1,10 @@
+import { createVue } from '../../utils/VueUtils';
+import TimeStampInput from '@/renderer/components/caption-studio/TimeStampInput.vue';
+
+describe('TimeStampInput.vue', () => {
+
+ it('should mount successfully', () => {
+ const wrapper = createVue(TimeStampInput);
+ });
+
+});
\ No newline at end of file
diff --git a/testing/unit/renderer/specs/caption-studio/WaveSurfer.spec.js b/testing/unit/renderer/specs/caption-studio/WaveSurfer.spec.js
new file mode 100644
index 0000000..290227d
--- /dev/null
+++ b/testing/unit/renderer/specs/caption-studio/WaveSurfer.spec.js
@@ -0,0 +1,16 @@
+import { createVue } from '../../utils/VueUtils';
+import WaveSurfer from '@/renderer/components/caption-studio/WaveSurfer.vue';
+
+describe('WaveSurfer.vue', () => {
+
+ //WaveSurfer.vue mostly acts as a wrapper for Wavesurfer.js.
+ it('should mount successfully', () => {
+ const wrapper = createVue(WaveSurfer);
+
+ expect(wrapper.vm.wave).to.not.be.null;
+ expect(wrapper.vm.isPlaying).to.be.false,
+ expect(wrapper.vm.hasFile).to.be.false,
+ expect(wrapper.vm.currentTime).to.equal(0);
+ });
+
+});
\ No newline at end of file
diff --git a/testing/unit/renderer/specs/class/Caption.spec.js b/testing/unit/renderer/specs/class/Caption.spec.js
new file mode 100644
index 0000000..7f4ccc0
--- /dev/null
+++ b/testing/unit/renderer/specs/class/Caption.spec.js
@@ -0,0 +1,33 @@
+import { Caption } from '../../../../../src/renderer/class/Caption';
+
+describe('Caption.js', () => {
+ it('Caption should still instantiate with only a name', () => {
+ const caption = new Caption('nameOnly');
+
+ const captionData = caption.getData();
+ expect(captionData.content).to.equal(' ');
+ expect(captionData.end).to.equal(0);
+ expect(captionData.start).to.equal(0);
+ });
+
+ it('Constructor should use updateContent() to set caption info', () => {
+ const caption = new Caption( 'testCaption', {
+ content: 'test-caption-line',
+ end: 10,
+ start: 0
+ });
+
+ let captionData = caption.getData();
+ expect(captionData.content).to.equal('test-caption-line');
+ expect(captionData.end).to.equal(10);
+ expect(captionData.start).to.equal(0);
+
+ caption.updateContent({start: 10, end: 20});
+
+ captionData = caption.getData();
+ expect(captionData.content).to.equal('test-caption-line');
+ expect(captionData.end).to.equal(20);
+ expect(captionData.start).to.equal(10);
+
+ });
+});
\ No newline at end of file
diff --git a/testing/unit/renderer/specs/class/CaptionManager.spec.js b/testing/unit/renderer/specs/class/CaptionManager.spec.js
new file mode 100644
index 0000000..3522451
--- /dev/null
+++ b/testing/unit/renderer/specs/class/CaptionManager.spec.js
@@ -0,0 +1,90 @@
+import CaptionManager from '../../../../../src/renderer/class/CaptionManager';
+import { EventBus } from '@/renderer/class/EventBus';
+import { active, captionData } from '../../utils/data';
+
+const activeName = active.name.replace(/.(ogg|mp3|mpeg)$/, '');
+
+const sleep = (millis) => {
+ return new Promise((resolve) => setTimeout(resolve, millis));
+};
+
+describe('CaptionManager.js', () => {
+
+ it('fileChanged', async () => {
+ EventBus.$emit('file_selected', {file: active});
+
+ await sleep(10);
+
+ expect(CaptionManager.file.name).to.equal(active.name);
+ expect(CaptionManager.activeCaption).to.equal(activeName);
+ });
+
+ it('onJsonUpdate', async () => {
+ EventBus.$emit('json_update', captionData);
+
+ await sleep(10);
+
+ //just confirm it was added properly
+ expect(CaptionManager.data.bongos[0].content).to.equal(captionData.bongos[0].content);
+ });
+
+ it('addCaption', async () => {
+ EventBus.$emit('add_caption');
+ await sleep(10);
+ expect(CaptionManager.activeCaption).to.equal(activeName);
+ expect(CaptionManager.currentCaptionIndex.content).to.equal(' ');
+ expect(CaptionManager.currentCaptionIndex.start).to.equal(0);
+ expect(CaptionManager.currentCaptionIndex.end).to.equal(0);
+ });
+
+ it('addIndex()', () => {
+ const previousIndex = CaptionManager.activeIndex;
+ EventBus.$emit('caption_add_index');
+ expect(previousIndex).to.be.lessThan(CaptionManager.activeIndex);
+ //addIndex inserts a blank template in as the new Caption
+ expect(CaptionManager.currentCaptionIndex.content).to.equal(' ');
+ expect(CaptionManager.currentCaptionIndex.start).to.equal(0);
+ expect(CaptionManager.currentCaptionIndex.end).to.equal(0);
+ });
+
+ it('updateActiveCaption()', () => {
+ EventBus.$emit('caption_update', { start: 10, end: 100, content: 'new-caption-content'});
+ expect(CaptionManager.currentCaptionIndex.content).to.equal('new-caption-content');
+ expect(CaptionManager.currentCaptionIndex.start).to.equal(10);
+ expect(CaptionManager.currentCaptionIndex.end).to.equal(100);
+ });
+
+ it('moveIndex()', () => {
+ let previousIndex = CaptionManager.activeIndex;
+ EventBus.$emit('caption_move_index', -1);
+
+ expect(CaptionManager.activeIndex).to.equal(previousIndex - 1);
+ previousIndex = CaptionManager.activeIndex;
+
+ EventBus.$emit('caption_move_index', -100);
+ expect(CaptionManager.activeIndex).to.equal(0);
+ previousIndex = CaptionManager.activeIndex;
+
+ EventBus.$emit('caption_move_index', 100);
+ expect(CaptionManager.activeIndex).to.equal(CaptionManager.lastIndex);
+ previousIndex = CaptionManager.activeIndex;
+ });
+
+ it('removeAtIndex()', () => {
+ const captionContent = CaptionManager.currentCaptionIndex.content;
+ const lastIndex = CaptionManager.lastIndex;
+ EventBus.$emit('caption_remove_index');
+
+ expect(CaptionManager.currentCaptionIndex.content).to.not.equal(captionContent);
+ expect(CaptionManager.lastIndex).to.equal(lastIndex - 1);
+
+ });
+
+ it('reset()', () => {
+ EventBus.$emit('caption_reset');
+
+ expect(Object.keys(CaptionManager.data).length).to.equal(0);
+ expect(CaptionManager.activeIndex).to.equal(0);
+ expect(CaptionManager.activeCaption).to.equal('');
+ });
+});
\ No newline at end of file
diff --git a/testing/unit/renderer/specs/class/Directory.spec.js b/testing/unit/renderer/specs/class/Directory.spec.js
new file mode 100644
index 0000000..abd520a
--- /dev/null
+++ b/testing/unit/renderer/specs/class/Directory.spec.js
@@ -0,0 +1,64 @@
+import Directory from '../../../../../src/renderer/class/Directory';
+import { EventBus } from '@/renderer/class/EventBus';
+import { files, addFile } from '../../utils/data';
+
+
+describe('Directory.js', () => {
+
+ it('if constructed with initial files it should sort them alphabetically', () => {
+ const directory = new Directory({
+ files
+ });
+
+ expect(directory.files[0].name).to.equal('acoustic-guitar.mp3');
+ expect(directory.files[1].name).to.equal('title.mp3');
+ });
+
+ it('addFile()', () => {
+ const directory = new Directory();
+
+ directory.addFile(addFile);
+ //addFile() will build out any additional directories required based on the relative path of the file.
+ expect(directory.files.length).to.equal(0);
+ expect(directory.dir.path.dir.to.files.length).to.equal(1);
+ });
+
+ it('selectByIndex', () => {
+ const directory = new Directory({
+ files
+ });
+
+ const file = directory.selectByIndex(0);
+ expect(file.name).to.equal('acoustic-guitar.mp3');
+ expect(directory.currentFile().name).to.equal('acoustic-guitar.mp3');
+
+ });
+ it('selectByFile', () => {
+ const fileToSelect = files[0];
+ const directory = new Directory({
+ files
+ });
+
+ const returnedFile = directory.selectByFile(fileToSelect);
+ expect(returnedFile.name).to.equal(files[0].name);
+ expect(directory.currentFile().name).to.equal(files[0].name);
+ });
+
+ it('switching files should return the new file and update the current file to match', () => {
+ const directory = new Directory({
+ files
+ });
+
+ //sets currentfile to first file alphabetically
+ expect(directory.currentFile().name).to.equal(files[1].name);
+
+ const nextFile = directory.nextFile();
+ expect(nextFile.name).to.equal(files[0].name);
+ expect(directory.currentFile().name).to.equal(files[0].name);
+
+ const prevFile = directory.previousFile();
+ expect(prevFile.name).to.equal(files[1].name);
+ expect(directory.currentFile().name).to.equal(files[1].name);
+
+ });
+});
\ No newline at end of file
diff --git a/testing/unit/renderer/specs/dialogs/PreviewTargetDialog.spec.js b/testing/unit/renderer/specs/dialogs/PreviewTargetDialog.spec.js
new file mode 100644
index 0000000..328ed53
--- /dev/null
+++ b/testing/unit/renderer/specs/dialogs/PreviewTargetDialog.spec.js
@@ -0,0 +1,67 @@
+import { createVue } from '../../utils/VueUtils';
+import PreviewTargetDialog from '@/renderer/components/dialogs/PreviewTargetDialog.vue';
+import Sinon from 'sinon';
+
+describe('PreviewTargetDialog.js', () => {
+
+ it('dialog should not be initially visible', () => {
+ const wrapper = createVue(PreviewTargetDialog);
+ expect(wrapper.find('.dialog').element.style.display).to.equal('none');
+ });
+
+ it('dialog should be visible after setting the "visible" property', async () => {
+ const wrapper = createVue(PreviewTargetDialog);
+ await wrapper.setProps({ visible: true });
+ expect(wrapper.find('.dialog').element.style.display).to.not.equal('none');
+ });
+
+ it('should set the preview types', () => {
+ const setPreviewType = PreviewTargetDialog.methods.setPreviewType;
+ PreviewTargetDialog.methods.setPreviewType = Sinon.stub();
+
+ const wrapper = createVue(PreviewTargetDialog);
+
+ wrapper.find('#deployOption').trigger('change');
+ expect(PreviewTargetDialog.methods.setPreviewType.calledWith('deploy')).to.equal(true);
+
+ wrapper.find('#urlOption').trigger('change');
+ expect(PreviewTargetDialog.methods.setPreviewType.calledWith('url')).to.equal(true);
+
+ expect(PreviewTargetDialog.methods.setPreviewType.callCount).to.equal(2);
+ PreviewTargetDialog.methods.setPreviewType = setPreviewType;
+ });
+
+ it('should call confirm and cancel callbacks', async () => {
+ const wrapper = createVue(PreviewTargetDialog);
+ const onConfirm = Sinon.fake();
+ const onCancel = Sinon.fake();
+
+ await wrapper.setProps({ onConfirm, onCancel });
+
+ wrapper.find('#confirmBtn').trigger('click');
+ wrapper.find('#cancelBtn').trigger('click');
+
+ expect(onConfirm.callCount).to.equal(1);
+ expect(onCancel.callCount).to.equal(1);
+ });
+
+ it('should call confirm with the correct results', async () => {
+ const wrapper = createVue(PreviewTargetDialog);
+ const onConfirm = Sinon.fake();
+
+ await wrapper.setProps({ onConfirm });
+ await wrapper.find('#urlOption').setChecked(true);
+
+ const input = await wrapper.find('#urlInput');
+ input.element.value = 'localhost:8080';
+
+ await wrapper.find('#confirmBtn').trigger('click');
+
+ expect(onConfirm.callCount).to.equal(1);
+
+ const results = onConfirm.args[0][0];
+
+ expect(results.type).to.equal('url');
+ expect(results.url).to.equal('localhost:8080');
+ });
+});
\ No newline at end of file
diff --git a/testing/unit/renderer/specs/dialogs/TemplateProjectDialog.spec.js b/testing/unit/renderer/specs/dialogs/TemplateProjectDialog.spec.js
new file mode 100644
index 0000000..b25c1a4
--- /dev/null
+++ b/testing/unit/renderer/specs/dialogs/TemplateProjectDialog.spec.js
@@ -0,0 +1,74 @@
+import { createVue } from '../../utils/VueUtils';
+import TemplateProjectDialog from '@/renderer/components/dialogs/TemplateProjectDialog.vue';
+import Sinon from 'sinon';
+import { EVENTS } from '@/constants';
+
+describe('TemplateProjectDialog.js', () => {
+
+ it('dialog should not be initially visible', () => {
+ const wrapper = createVue(TemplateProjectDialog);
+ expect(wrapper.find('.dialog').element.style.display).to.equal('none');
+ });
+
+ it('dialog should be visible after setting the "visible" property', async () => {
+ const wrapper = createVue(TemplateProjectDialog);
+ await wrapper.setProps({ visible: true });
+ expect(wrapper.find('.dialog').element.style.display).to.not.equal('none');
+ });
+
+ it('should set the template type', () => {
+ const setTemplateType = TemplateProjectDialog.methods.setTemplateType;
+ TemplateProjectDialog.methods.setTemplateType = Sinon.stub();
+
+ const wrapper = createVue(TemplateProjectDialog);
+
+ wrapper.find('#pixiOption').trigger('change');
+ expect(TemplateProjectDialog.methods.setTemplateType.calledWith('pixi')).to.equal(true);
+
+ wrapper.find('#phaserOption').trigger('change');
+ expect(TemplateProjectDialog.methods.setTemplateType.calledWith('phaser')).to.equal(true);
+
+ wrapper.find('#createjsOption').trigger('change');
+ expect(TemplateProjectDialog.methods.setTemplateType.calledWith('createjs')).to.equal(true);
+
+ expect(TemplateProjectDialog.methods.setTemplateType.callCount).to.equal(3);
+ TemplateProjectDialog.methods.setTemplateType = setTemplateType;
+ });
+
+ it('should call cancel callbacks', async () => {
+ const wrapper = createVue(TemplateProjectDialog);
+ const onCancel = Sinon.fake();
+
+ await wrapper.setProps({ onCancel });
+ wrapper.find('#cancelBtn').trigger('click');
+
+ expect(onCancel.callCount).to.equal(1);
+ });
+
+ it('should call confirm with the correct results', async () => {
+ const sendEvent = TemplateProjectDialog.methods.sendEvent;
+ TemplateProjectDialog.methods.sendEvent = Sinon.stub();
+
+ const wrapper = createVue(TemplateProjectDialog);
+
+ await wrapper.find('#pixiOption').setChecked(true);
+
+ const input = await wrapper.find('.urlInput');
+ input.element.value = 'localhost:8080';
+
+ wrapper.find('#confirmBtn').trigger('click');
+
+ expect(TemplateProjectDialog.methods.sendEvent.callCount).to.equal(1);
+
+ const args = TemplateProjectDialog.methods.sendEvent.args[0];
+ const event = args[0];
+ const results = args[1];
+
+ expect(event).to.equal(EVENTS.CREATE_PROJECT_TEMPLATE);
+ expect(results.type).to.equal('pixi');
+ expect(results.location).to.equal('localhost:8080\/New SpringRoll Game');
+
+ TemplateProjectDialog.methods.sendEvent = sendEvent;
+ });
+
+});
\ No newline at end of file
diff --git a/testing/unit/renderer/specs/pages/CaptionStudio.spec.js b/testing/unit/renderer/specs/pages/CaptionStudio.spec.js
new file mode 100644
index 0000000..579479f
--- /dev/null
+++ b/testing/unit/renderer/specs/pages/CaptionStudio.spec.js
@@ -0,0 +1,19 @@
+import { createVue } from '../../utils/VueUtils';
+import CaptionStudio from '@/renderer/components/pages/CaptionStudio.vue';
+
+describe('CaptionStudio.vue', () => {
+
+ it('should mount and render', () => {
+ const wrapper = createVue(CaptionStudio);
+ wrapper.vm.$store.commit('audioLocation', { audioLocation: 'this/is/also/a/test' });
+ expect(wrapper.vm.enabled).to.be.false;
+ });
+
+ it('hide button should properly set the state to hidden', () => {
+ const wrapper = createVue(CaptionStudio);
+
+ expect(wrapper.vm.explorerHidden).to.be.false;
+ wrapper.find('.caption__hide-sidebar').trigger('click');
+ expect(wrapper.vm.explorerHidden).to.be.true;
+ });
+});
\ No newline at end of file
diff --git a/testing/unit/renderer/specs/pages/LandingPage.spec.js b/testing/unit/renderer/specs/pages/LandingPage.spec.js
new file mode 100644
index 0000000..456c4e4
--- /dev/null
+++ b/testing/unit/renderer/specs/pages/LandingPage.spec.js
@@ -0,0 +1,67 @@
+import { createVue } from '../../utils/VueUtils';
+import { EVENTS, DIALOGS } from '@/constants';
+import LandingPage from '@/renderer/components/pages/LandingPage.vue';
+import Sinon from 'sinon';
+
+describe('LandingPage.js', () => {
+ const sendEvent = LandingPage.methods.sendEvent;
+
+ afterEach(() => {
+ LandingPage.methods.sendEvent = sendEvent;
+ });
+
+ it('should mount and render', () => {
+ const wrapper = createVue(LandingPage);
+ expect(wrapper.find('.name').text()).to.equal('SpringRoll Studio');
+ });
+
+ it('should dispatch EVENTS.OPEN_DIALOG with parameter DIALOGS.PROJECT_LOCATION_SETTER', () => {
+ LandingPage.methods.sendEvent = Sinon.stub();
+
+ const wrapper = createVue(LandingPage);
+ wrapper.find('.projectLocationBtn').trigger('click');
+
+ expect(LandingPage.methods.sendEvent.calledWith(EVENTS.OPEN_DIALOG, DIALOGS.PROJECT_LOCATION_SETTER)).to.equal(true);
+ });
+
+ it('preview game button should toggle the preview target dialog', () => {
+ const toggle = LandingPage.methods.togglePreviewTargetDialog;
+ LandingPage.methods.togglePreviewTargetDialog = Sinon.stub();
+
+ const wrapper = createVue(LandingPage);
+ const btn = wrapper.find('.previewGameBtn');
+
+ btn.element.disabled = false;
+ btn.trigger('click');
+
+ expect(LandingPage.methods.togglePreviewTargetDialog.callCount).to.equal(1);
+ expect(LandingPage.methods.togglePreviewTargetDialog.calledWith(true)).to.equal(true);
+
+ LandingPage.methods.togglePreviewTargetDialog = toggle;
+ });
+
+ it('project template button should toggle the project template dialog', () => {
+ const toggle = LandingPage.methods.toggleProjectTemplateDialog;
+ LandingPage.methods.toggleProjectTemplateDialog = Sinon.stub();
+
+ const wrapper = createVue(LandingPage);
+ const btn = wrapper.find('.projectTemplateBtn');
+
+ btn.element.disabled = false;
+ btn.trigger('click');
+
+ expect(LandingPage.methods.toggleProjectTemplateDialog.callCount).to.equal(1);
+ expect(LandingPage.methods.toggleProjectTemplateDialog.calledWith(true)).to.equal(true);
+
+ LandingPage.methods.togglePreviewTargetDialog = toggle;
+ });
+
+ it('should dispatch EVENTS.OPEN_CAPTION_STUDIO', () => {
+ LandingPage.methods.sendEvent = Sinon.stub();
+
+ const wrapper = createVue(LandingPage);
+ wrapper.find('.captionStudioBtn').trigger('click');
+
+ expect(LandingPage.methods.sendEvent.calledWith(EVENTS.OPEN_CAPTION_STUDIO)).to.equal(true);
+ });
+});
\ No newline at end of file
diff --git a/testing/unit/renderer/specs/store/PersistentState.spec.js b/testing/unit/renderer/specs/store/PersistentState.spec.js
new file mode 100644
index 0000000..7020e2a
--- /dev/null
+++ b/testing/unit/renderer/specs/store/PersistentState.spec.js
@@ -0,0 +1,56 @@
+import { createVue } from '../../utils/VueUtils';
+import { mount } from '@vue/test-utils';
+import LandingPage from '@/renderer/components/pages/LandingPage.vue';
+
+describe('PersistentState.js', () => {
+
+ it('should persist the project location', () => {
+ let wrapper = createVue(LandingPage);
+ wrapper.vm.$store.commit('location', { location: 'path/to/test' });
+
+ wrapper.destroy();
+ wrapper = createVue(LandingPage);
+
+ expect(wrapper.vm.$store.state.projectInfo.location).to.equal('path/to/test');
+ });
+
+ it('should persist the preview target', () => {
+ let wrapper = createVue(LandingPage);
+ wrapper.vm.$store.commit('previewTarget', { previewTarget: 'deploy' });
+
+ wrapper.destroy();
+ wrapper = createVue(LandingPage);
+
+ expect(wrapper.vm.$store.state.gamePreview.previewTarget).to.equal('deploy');
+ });
+
+ it('should persist the preview url', () => {
+ let wrapper = createVue(LandingPage);
+ wrapper.vm.$store.commit('previewURL', { previewURL: 'this/is/a/test' });
+
+ wrapper.destroy();
+ wrapper = createVue(LandingPage);
+
+ expect(wrapper.vm.$store.state.gamePreview.previewURL).to.equal('this/is/a/test');
+ });
+
+ it('should persist the audio file location', () => {
+ let wrapper = createVue(LandingPage);
+ wrapper.vm.$store.commit('audioLocation', { audioLocation: 'this/is/also/a/test' });
+
+ wrapper.destroy();
+ wrapper = createVue(LandingPage);
+
+ expect(wrapper.vm.$store.state.captionInfo.audioLocation).to.equal('this/is/also/a/test');
+ });
+
+ it('should persist the caption file location', () => {
+ let wrapper = createVue(LandingPage);
+ wrapper.vm.$store.commit('captionLocation', { captionLocation: 'this/is/another/test' });
+
+ wrapper.destroy();
+ wrapper = createVue(LandingPage);
+
+ expect(wrapper.vm.$store.state.captionInfo.captionLocation).to.equal('this/is/another/test');
+ });
+});
\ No newline at end of file
diff --git a/testing/unit/renderer/utils/VueUtils.js b/testing/unit/renderer/utils/VueUtils.js
new file mode 100644
index 0000000..ec10db8
--- /dev/null
+++ b/testing/unit/renderer/utils/VueUtils.js
@@ -0,0 +1,33 @@
+import { mount, createLocalVue } from '@vue/test-utils';
+import Vuex from 'vuex';
+
+import persistentState from '@/renderer/store/storage/PersistentState';
+import projectInfo from '@/renderer/store/modules/ProjectInfo';
+import gamePreview from '@/renderer/store/modules/GamePreview';
+import captionInfo from '@/renderer/store/modules/CaptionInfo';
+
+const localVue = createLocalVue();
+
+localVue.use(Vuex);
+
+export const createVue = (component, options = {}) => {
+ return mount(component, {
+ ...options,
+ localVue,
+
+ store: new Vuex.Store({
+ modules: {
+ projectInfo,
+ gamePreview,
+ captionInfo,
+ },
+
+ plugins: [
+ persistentState({
+ name: 'studioConfig',
+ key: 'studio'
+ })
+ ]
+ })
+ });
+};
\ No newline at end of file
diff --git a/testing/unit/renderer/utils/data.js b/testing/unit/renderer/utils/data.js
new file mode 100644
index 0000000..696dc58
--- /dev/null
+++ b/testing/unit/renderer/utils/data.js
@@ -0,0 +1,67 @@
+import Directory from '@/renderer/class/Directory';
+
+export const captionData = {
+ 'acoustic-guitar': [
+ {
+ 'content': 'Test',
+ 'start': 10,
+ 'end': 1190
+ },
+ {
+ 'content': ' test2',
+ 'start': 1190,
+ 'end': 1882
+ }
+ ],
+ 'bongos': [
+ {
+ 'content': 'Springroll rocks!!',
+ 'start': 0,
+ 'end': 643
+ }
+ ],
+ 'title-audio': [
+ {
+ 'content': ' title audio',
+ 'start': 0,
+ 'end': 2257
+ }
+ ],
+ 'bugle': [
+ {
+ 'content': ' test',
+ 'start': 0,
+ 'end': 665
+ }
+ ]
+};
+
+export const directory = new Directory({'name':'music','files':[{'name':'title.mp3','fullPath':'/Full/path/to/title.mp3','relativePath':'path/to/title.mp3','type':{'ext':'mp3','mime':'audio/mpeg'}},{'name':'title.ogg','fullPath':'/Full/path/to/title.ogg','relativePath':'path/to/title.ogg','type':{'ext':'ogg','mime':'audio/ogg'}}],'directories':{}});
+
+export const active = {'name':'title.mp3','fullPath':'/Full/path/to/title.mp3','relativePath':'path/to/title.mp3','type':{'ext':'mp3','mime':'audio/mpeg'}};
+
+export const files = [{'name':'title.mp3','fullPath':'/Full/path/to/title.mp3','relativePath':'path/to/title.mp3','type':{'ext':'mp3','mime':'audio/mpeg'}}, {'name':'acoustic-guitar.mp3','fullPath':'/Full/path/to/acoustic-guitar.mp3','relativePath':'path/to/acoustic-guitar.mp3','type':{'ext':'mp3','mime':'audio/mpeg'}}];
+export const addFile = {'name':'addFile.mp3','fullPath':'/Full/path/to/addFile.mp3','relativePath':'path/to/addFile.mp3','type':{'ext':'mp3','mime':'audio/mpeg'}};
+
+
+export const badCaptions = {
+ 'bad-caption': [
+ {
+ 'content': 'test',
+ 'start': 1120,
+ 'end': 0
+ },
+ {
+ 'content': '',
+ 'start': 1190,
+ 'end': 1882
+ }
+ ],
+ 'good-caption': [
+ {
+ 'content': 'good-caption',
+ 'start': 0,
+ 'end': 643
+ }
+ ]
+};
\ No newline at end of file
diff --git a/testing/webpack-testing.config.js b/testing/webpack-testing.config.js
new file mode 100644
index 0000000..13e1298
--- /dev/null
+++ b/testing/webpack-testing.config.js
@@ -0,0 +1,56 @@
+const { VueLoaderPlugin } = require('vue-loader');
+
+/**
+ * To be used for testing only.
+ */
+module.exports = {
+ // devtool: '#inline-source-map',
+
+ plugins: [
+ new VueLoaderPlugin()
+ ],
+
+ module: {
+ rules: [
+ {
+ test: /\.js$/,
+ loader: 'babel-loader'
+ },
+ {
+ test: /\.vue$/,
+ loader: 'vue-loader'
+ },
+ {
+ test: /\.scss$/,
+ use: ['vue-style-loader', 'css-loader', 'sass-loader']
+ },
+ {
+ test: /\.sass$/,
+ use: ['vue-style-loader', 'css-loader', 'sass-loader?indentedSyntax']
+ },
+ {
+ test: /\.less$/,
+ use: ['vue-style-loader', 'css-loader', 'less-loader']
+ },
+ {
+ test: /\.css$/,
+ use: ['vue-style-loader', 'css-loader']
+ },
+ {
+ test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
+ loader: 'url-loader'
+ },
+ {
+ test: /\.(ttf|eot|woff|woff2|otf)$/,
+ loader: 'file-loader'
+ }
+ ]
+ },
+
+ resolve: {
+ alias: {
+ 'vue$': 'vue/dist/vue.esm.js'
+ },
+ extensions: ['.js', '.vue', '.json', '.css', '.node']
+ }
+};
\ No newline at end of file
diff --git a/vue.config.js b/vue.config.js
new file mode 100644
index 0000000..a3c451d
--- /dev/null
+++ b/vue.config.js
@@ -0,0 +1,51 @@
+const path = require('path');
+
+module.exports = {
+ configureWebpack: {
+ entry: {
+ app: path.resolve(__dirname, 'src/renderer/main.js')
+ },
+
+ resolve: {
+ alias: {
+ vue$: 'vue/dist/vue.esm.js'
+ }
+ }
+ },
+
+ pluginOptions: {
+
+ electronBuilder: {
+ nodeIntegration: true,
+ webviewTag: true,
+ webSecurity: false,
+
+ mainProcessFile: 'src/main/index.js',
+
+ // Fix this. This will watch file creation and deletion as well.
+ mainProcessWatch: ['src/main/**/*.js'],
+
+ builderOptions: {
+ productName: 'SpringrollStudio',
+ appId: 'io.springroll.studio',
+ artifactName: '${productName}-${os}-${version}-Setup.${ext}',
+ directories: {
+ output: 'build'
+ },
+ extraResources: ['extraResources/**'],
+ dmg: {
+ contents: [
+ { x: 410, y: 150, type: 'link', path: '/Applications' },
+ { x: 130, y: 150, type: 'file' }
+ ]
+ },
+ mac: {
+ icon: 'build/icons/icon.icns'
+ },
+ win: {
+ icon: 'build/icons/icon.ico'
+ }
+ }
+ }
+ }
+};
\ No newline at end of file