<commit_before>'use strict';

var webdriver = require('selenium-webdriver');
var test = require('selenium-webdriver/testing');
var Page = require('./page');
var PageLaxMode = require('./pageLaxMode');
var TestOperations = require('./testOperations');

module.exports.todoMVCTest = function (frameworkName, baseUrl, speedMode, laxMode, browserName) {
	test.describe('TodoMVC - ' + frameworkName, function () {
		var TODO_ITEM_ONE = 'buy some cheese';
		var TODO_ITEM_TWO = 'feed the cat';
		var TODO_ITEM_THREE = 'book a doctors appointment';
		var browser, testOps, page;

		// a number of tests use this set of ToDo items.
		function createStandardItems() {
			page.enterItem(TODO_ITEM_ONE);
			page.enterItem(TODO_ITEM_TWO);
			page.enterItem(TODO_ITEM_THREE);
		}

		function launchBrowser() {

			if (process.env.SAUCE_USERNAME !== undefined) {
				browser = new webdriver.Builder()
				.usingServer(
					'http://' +
					process.env.SAUCE_USERNAME +
					':' +
					process.env.SAUCE_ACCESS_KEY +
					'@ondemand.saucelabs.com:80/wd/hub'
				)
				.withCapabilities({
					'tunnel-identifier': process.env.TRAVIS_JOB_NUMBER,
					build: process.env.TRAVIS_BUILD_NUMBER,
					browserName: browserName,
					username: process.env.SAUCE_USERNAME,
					accessKey: process.env.SAUCE_ACCESS_KEY
				})
				.build();
			} else {
				browser = new webdriver.Builder()
				.withCapabilities({
					browserName: browserName
				})
				.build();
			}

			browser.get(baseUrl);

			page = laxMode ? new PageLaxMode(browser) : new Page(browser);
			testOps = new TestOperations(page);

			// for apps that use require, we have to wait a while for the dependencies to
			// be loaded. There must be a more elegant solution than this!
			browser.sleep(200);
		}

		function closeBrowser() {
			browser.quit();
		}

		if (speedMode) {
			test.before(function () {
				launchBrowser();
			});
			test.after(function () {
				closeBrowser();
			});
			test.beforeEach(function () {
				page.getItemElements().then(function (items) {
					if (items.length > 0) {
						// find any items that are not complete
						page.getNonCompletedItemElements().then(function (nonCompleteItems) {
							if (nonCompleteItems.length > 0) {
								page.clickMarkAllCompletedCheckBox();
							}
							page.clickClearCompleteButton();
						});
					}
				});
			});
		} else {
			test.beforeEach(function () {
				launchBrowser();
				page.ensureAppIsVisible();
			});
			test.afterEach(function () {
				closeBrowser();
			});
		}

		test.describe('When page is initially opened', function () {
			test.it('should focus on the todo input field', function () {
				testOps.assertFocussedElementId('new-todo');
			});
		});

		test.describe('No Todos', function () {
			test.it('should hide #main and #footer', function () {
				testOps.assertItemCount(0);
				testOps.assertMainSectionIsHidden();
				testOps.assertFooterIsHidden();
			});
		});

		test.describe('New Todo', function () {
			test.it('should allow me to add todo items', function () {
				page.enterItem(TODO_ITEM_ONE);
				testOps.assertItems([TODO_ITEM_ONE]);

				page.enterItem(TODO_ITEM_TWO);
				testOps.assertItems([TODO_ITEM_ONE, TODO_ITEM_TWO]);
			});

			test.it('should clear text input field when an item is added', function () {
				page.enterItem(TODO_ITEM_ONE);
				testOps.assertItemInputFieldText('');
			});

			test.it('should append new items to the bottom of the list', function () {
				createStandardItems();
				testOps.assertItemCount(3);
				testOps.assertItemText(0, TODO_ITEM_ONE);
				testOps.assertItemText(1, TODO_ITEM_TWO);
				testOps.assertItemText(2, TODO_ITEM_THREE);
			});

			test.it('should trim text input', function () {
				page.enterItem('   ' + TODO_ITEM_ONE + '  ');
				testOps.assertItemText(0, TODO_ITEM_ONE);
			});

			test.it('should show #main and #footer when items added', function () {
				page.enterItem(TODO_ITEM_ONE);
				testOps.assertMainSectionIsVisible();
				testOps.assertFooterIsVisible();
			});
		});

		test.describe('Mark all as completed', function () {
			test.beforeEach(function () {
				createStandardItems();
			});

			test.it('should allow me to mark all items as completed', function () {
				page.clickMarkAllCompletedCheckBox();

				testOps.assertItemAtIndexIsCompleted(0);
				testOps.assertItemAtIndexIsCompleted(1);
				testOps.assertItemAtIndexIsCompleted(2);
			});

			test.it('should allow me to clear the completion state of all items', function () {
				page.clickMarkAllCompletedCheckBox();
				page.clickMarkAllCompletedCheckBox();

				testOps.assertItemAtIndexIsNotCompleted(0);
				testOps.assertItemAtIndexIsNotCompleted(1);
				testOps.assertItemAtIndexIsNotCompleted(2);
			});

			test.it('complete all checkbox should update state when items are completed / cleared', function () {
				page.clickMarkAllCompletedCheckBox();

				testOps.assertCompleteAllIsChecked();

				// all items are complete, now mark one as not-complete
				page.toggleItemAtIndex(0);
				testOps.assertCompleteAllIsClear();

				// now mark as complete, so that once again all items are completed
				page.toggleItemAtIndex(0);
				testOps.assertCompleteAllIsChecked();
			});
		});

		test.describe('Item', function () {
			test.it('should allow me to mark items as complete', function () {
				page.enterItem(TODO_ITEM_ONE);
				page.enterItem(TODO_ITEM_TWO);

				page.toggleItemAtIndex(0);
				testOps.assertItemAtIndexIsCompleted(0);
				testOps.assertItemAtIndexIsNotCompleted(1);

				page.toggleItemAtIndex(1);
				testOps.assertItemAtIndexIsCompleted(0);
				testOps.assertItemAtIndexIsCompleted(1);
			});

			test.it('should allow me to un-mark items as complete', function () {
				page.enterItem(TODO_ITEM_ONE);
				page.enterItem(TODO_ITEM_TWO);

				page.toggleItemAtIndex(0);
				testOps.assertItemAtIndexIsCompleted(0);
				testOps.assertItemAtIndexIsNotCompleted(1);

				page.toggleItemAtIndex(0);
				testOps.assertItemAtIndexIsNotCompleted(0);
				testOps.assertItemAtIndexIsNotCompleted(1);
			});

			test.it('should allow me to edit an item', function () {
				createStandardItems();
				page.doubleClickItemAtIndex(1);

				page.editItemAtIndex(1, 'buy some sausages' + webdriver.Key.ENTER);

				testOps.assertItems([TODO_ITEM_ONE, 'buy some sausages', TODO_ITEM_THREE]);
			});
		});

		test.describe('Editing', function () {
			test.it('should hide other controls when editing', function () {
				createStandardItems();
				page.doubleClickItemAtIndex(1);

				testOps.assertItemToggleIsHidden(1);
				testOps.assertItemLabelIsHidden(1);
			});

			test.it('should save edits on enter', function () {
				createStandardItems();
				page.doubleClickItemAtIndex(1);

				page.editItemAtIndex(1, 'buy some sausages' + webdriver.Key.ENTER);

				testOps.assertItems([TODO_ITEM_ONE, 'buy some sausages', TODO_ITEM_THREE]);
			});

			test.it('should save edits on blur', function () {
				createStandardItems();
				page.doubleClickItemAtIndex(1);

				page.editItemAtIndex(1, 'buy some sausages');

				// click a toggle button so that the blur() event is fired
				page.toggleItemAtIndex(0);

				testOps.assertItems([TODO_ITEM_ONE, 'buy some sausages', TODO_ITEM_THREE]);
			});

			test.it('should trim entered text', function () {
				createStandardItems();
				page.doubleClickItemAtIndex(1);

				page.editItemAtIndex(1, '    buy some sausages  ' + webdriver.Key.ENTER);

				testOps.assertItems([TODO_ITEM_ONE, 'buy some sausages', TODO_ITEM_THREE]);
			});

			test.it('should remove the item if an empty text string was entered', function () {
				createStandardItems();
				page.doubleClickItemAtIndex(1);

				page.editItemAtIndex(1, webdriver.Key.ENTER);

				testOps.assertItems([TODO_ITEM_ONE, TODO_ITEM_THREE]);
			});

			test.it('should cancel edits on escape', function () {
				createStandardItems();
				page.doubleClickItemAtIndex(1);

				page.editItemAtIndex(1, 'foo' + webdriver.Key.ESCAPE);

				testOps.assertItems([TODO_ITEM_ONE, TODO_ITEM_TWO, TODO_ITEM_THREE]);
			});
		});

		test.describe('Counter', function () {
			test.it('should display the current number of todo items', function () {
				page.enterItem(TODO_ITEM_ONE);
				testOps.assertItemCountText('1 item left');
				page.enterItem(TODO_ITEM_TWO);
				testOps.assertItemCountText('2 items left');
			});
		});

		test.describe('Clear completed button', function () {
			test.beforeEach(function () {
				createStandardItems();
			});

			test.it('should display the correct text', function () {
				page.toggleItemAtIndex(1);
				testOps.assertClearCompleteButtonText('Clear completed');
			});

			test.it('should remove completed items when clicked', function () {
				page.toggleItemAtIndex(1);
				page.clickClearCompleteButton();
				testOps.assertItemCount(2);
				testOps.assertItems([TODO_ITEM_ONE, TODO_ITEM_THREE]);
			});

			test.it('should be hidden when there are no items that are completed', function () {
				page.toggleItemAtIndex(1);
				testOps.assertClearCompleteButtonIsVisible();
				page.clickClearCompleteButton();
				testOps.assertClearCompleteButtonIsHidden();
			});
		});

		test.describe('Persistence', function () {
			test.it('should persist its data', function () {
				// set up state
				page.enterItem(TODO_ITEM_ONE);
				page.enterItem(TODO_ITEM_TWO);
				page.toggleItemAtIndex(1);

				function stateTest() {
					testOps.assertItems([TODO_ITEM_ONE, TODO_ITEM_TWO]);
					testOps.assertItemAtIndexIsCompleted(1);
					testOps.assertItemAtIndexIsNotCompleted(0);
				}

				// test it
				stateTest();

				// navigate away and back again
				browser.get('about:blank');
				browser.get(baseUrl);

				// repeat the state test
				stateTest();
			});
		});

		test.describe('Routing', function () {
			test.beforeEach(function () {
				createStandardItems();
			});
			test.it('should allow me to display active items', function () {
				page.toggleItemAtIndex(1);
				page.filterByActiveItems();

				testOps.assertItems([TODO_ITEM_ONE, TODO_ITEM_THREE]);
			});

			test.it('should respect the back button', function () {
				createStandardItems();
				page.toggleItemAtIndex(1);

				page.filterByActiveItems();
				page.filterByCompletedItems();

				// should show completed items
				testOps.assertItems([TODO_ITEM_TWO]);

				// then active items
				page.back();
				testOps.assertItems([TODO_ITEM_ONE, TODO_ITEM_THREE]);

				// then all items
				page.back();
				testOps.assertItems([TODO_ITEM_ONE, TODO_ITEM_TWO, TODO_ITEM_THREE]);
			});

			test.it('should allow me to display completed items', function () {
				page.toggleItemAtIndex(1);
				page.filterByCompletedItems();

				testOps.assertItems([TODO_ITEM_TWO]);
			});

			test.it('should allow me to display all items', function () {
				page.toggleItemAtIndex(1);

				// apply the other filters first, before returning to the 'all' state
				page.filterByActiveItems();
				page.filterByCompletedItems();
				page.filterByAllItems();

				testOps.assertItems([TODO_ITEM_ONE, TODO_ITEM_TWO, TODO_ITEM_THREE]);
			});

			test.it('should highlight the currently applied filter', function () {
				// initially 'all' should be selected
				testOps.assertFilterAtIndexIsSelected(0);

				page.filterByActiveItems();
				testOps.assertFilterAtIndexIsSelected(1);

				page.filterByCompletedItems();
				testOps.assertFilterAtIndexIsSelected(2);
			});
		});
	});
};
<commit_msg>:bug: Fix routing test<commit_after>'use strict';

var webdriver = require('selenium-webdriver');
var test = require('selenium-webdriver/testing');
var Page = require('./page');
var PageLaxMode = require('./pageLaxMode');
var TestOperations = require('./testOperations');

module.exports.todoMVCTest = function (frameworkName, baseUrl, speedMode, laxMode, browserName) {
	test.describe('TodoMVC - ' + frameworkName, function () {
		var TODO_ITEM_ONE = 'buy some cheese';
		var TODO_ITEM_TWO = 'feed the cat';
		var TODO_ITEM_THREE = 'book a doctors appointment';
		var browser, testOps, page;

		// a number of tests use this set of ToDo items.
		function createStandardItems() {
			page.enterItem(TODO_ITEM_ONE);
			page.enterItem(TODO_ITEM_TWO);
			page.enterItem(TODO_ITEM_THREE);
		}

		function launchBrowser() {

			if (process.env.SAUCE_USERNAME !== undefined) {
				browser = new webdriver.Builder()
				.usingServer(
					'http://' +
					process.env.SAUCE_USERNAME +
					':' +
					process.env.SAUCE_ACCESS_KEY +
					'@ondemand.saucelabs.com:80/wd/hub'
				)
				.withCapabilities({
					'tunnel-identifier': process.env.TRAVIS_JOB_NUMBER,
					build: process.env.TRAVIS_BUILD_NUMBER,
					browserName: browserName,
					username: process.env.SAUCE_USERNAME,
					accessKey: process.env.SAUCE_ACCESS_KEY
				})
				.build();
			} else {
				browser = new webdriver.Builder()
				.withCapabilities({
					browserName: browserName
				})
				.build();
			}

			browser.get(baseUrl);

			page = laxMode ? new PageLaxMode(browser) : new Page(browser);
			testOps = new TestOperations(page);

			// for apps that use require, we have to wait a while for the dependencies to
			// be loaded. There must be a more elegant solution than this!
			browser.sleep(200);
		}

		function closeBrowser() {
			browser.quit();
		}

		if (speedMode) {
			test.before(function () {
				launchBrowser();
			});
			test.after(function () {
				closeBrowser();
			});
			test.beforeEach(function () {
				page.getItemElements().then(function (items) {
					if (items.length > 0) {
						// find any items that are not complete
						page.getNonCompletedItemElements().then(function (nonCompleteItems) {
							if (nonCompleteItems.length > 0) {
								page.clickMarkAllCompletedCheckBox();
							}
							page.clickClearCompleteButton();
						});
					}
				});
			});
		} else {
			test.beforeEach(function () {
				launchBrowser();
				page.ensureAppIsVisible();
			});
			test.afterEach(function () {
				closeBrowser();
			});
		}

		test.describe('When page is initially opened', function () {
			test.it('should focus on the todo input field', function () {
				testOps.assertFocussedElementId('new-todo');
			});
		});

		test.describe('No Todos', function () {
			test.it('should hide #main and #footer', function () {
				testOps.assertItemCount(0);
				testOps.assertMainSectionIsHidden();
				testOps.assertFooterIsHidden();
			});
		});

		test.describe('New Todo', function () {
			test.it('should allow me to add todo items', function () {
				page.enterItem(TODO_ITEM_ONE);
				testOps.assertItems([TODO_ITEM_ONE]);

				page.enterItem(TODO_ITEM_TWO);
				testOps.assertItems([TODO_ITEM_ONE, TODO_ITEM_TWO]);
			});

			test.it('should clear text input field when an item is added', function () {
				page.enterItem(TODO_ITEM_ONE);
				testOps.assertItemInputFieldText('');
			});

			test.it('should append new items to the bottom of the list', function () {
				createStandardItems();
				testOps.assertItemCount(3);
				testOps.assertItemText(0, TODO_ITEM_ONE);
				testOps.assertItemText(1, TODO_ITEM_TWO);
				testOps.assertItemText(2, TODO_ITEM_THREE);
			});

			test.it('should trim text input', function () {
				page.enterItem('   ' + TODO_ITEM_ONE + '  ');
				testOps.assertItemText(0, TODO_ITEM_ONE);
			});

			test.it('should show #main and #footer when items added', function () {
				page.enterItem(TODO_ITEM_ONE);
				testOps.assertMainSectionIsVisible();
				testOps.assertFooterIsVisible();
			});
		});

		test.describe('Mark all as completed', function () {
			test.beforeEach(function () {
				createStandardItems();
			});

			test.it('should allow me to mark all items as completed', function () {
				page.clickMarkAllCompletedCheckBox();

				testOps.assertItemAtIndexIsCompleted(0);
				testOps.assertItemAtIndexIsCompleted(1);
				testOps.assertItemAtIndexIsCompleted(2);
			});

			test.it('should allow me to clear the completion state of all items', function () {
				page.clickMarkAllCompletedCheckBox();
				page.clickMarkAllCompletedCheckBox();

				testOps.assertItemAtIndexIsNotCompleted(0);
				testOps.assertItemAtIndexIsNotCompleted(1);
				testOps.assertItemAtIndexIsNotCompleted(2);
			});

			test.it('complete all checkbox should update state when items are completed / cleared', function () {
				page.clickMarkAllCompletedCheckBox();

				testOps.assertCompleteAllIsChecked();

				// all items are complete, now mark one as not-complete
				page.toggleItemAtIndex(0);
				testOps.assertCompleteAllIsClear();

				// now mark as complete, so that once again all items are completed
				page.toggleItemAtIndex(0);
				testOps.assertCompleteAllIsChecked();
			});
		});

		test.describe('Item', function () {
			test.it('should allow me to mark items as complete', function () {
				page.enterItem(TODO_ITEM_ONE);
				page.enterItem(TODO_ITEM_TWO);

				page.toggleItemAtIndex(0);
				testOps.assertItemAtIndexIsCompleted(0);
				testOps.assertItemAtIndexIsNotCompleted(1);

				page.toggleItemAtIndex(1);
				testOps.assertItemAtIndexIsCompleted(0);
				testOps.assertItemAtIndexIsCompleted(1);
			});

			test.it('should allow me to un-mark items as complete', function () {
				page.enterItem(TODO_ITEM_ONE);
				page.enterItem(TODO_ITEM_TWO);

				page.toggleItemAtIndex(0);
				testOps.assertItemAtIndexIsCompleted(0);
				testOps.assertItemAtIndexIsNotCompleted(1);

				page.toggleItemAtIndex(0);
				testOps.assertItemAtIndexIsNotCompleted(0);
				testOps.assertItemAtIndexIsNotCompleted(1);
			});

			test.it('should allow me to edit an item', function () {
				createStandardItems();
				page.doubleClickItemAtIndex(1);

				page.editItemAtIndex(1, 'buy some sausages' + webdriver.Key.ENTER);

				testOps.assertItems([TODO_ITEM_ONE, 'buy some sausages', TODO_ITEM_THREE]);
			});
		});

		test.describe('Editing', function () {
			test.it('should hide other controls when editing', function () {
				createStandardItems();
				page.doubleClickItemAtIndex(1);

				testOps.assertItemToggleIsHidden(1);
				testOps.assertItemLabelIsHidden(1);
			});

			test.it('should save edits on enter', function () {
				createStandardItems();
				page.doubleClickItemAtIndex(1);

				page.editItemAtIndex(1, 'buy some sausages' + webdriver.Key.ENTER);

				testOps.assertItems([TODO_ITEM_ONE, 'buy some sausages', TODO_ITEM_THREE]);
			});

			test.it('should save edits on blur', function () {
				createStandardItems();
				page.doubleClickItemAtIndex(1);

				page.editItemAtIndex(1, 'buy some sausages');

				// click a toggle button so that the blur() event is fired
				page.toggleItemAtIndex(0);

				testOps.assertItems([TODO_ITEM_ONE, 'buy some sausages', TODO_ITEM_THREE]);
			});

			test.it('should trim entered text', function () {
				createStandardItems();
				page.doubleClickItemAtIndex(1);

				page.editItemAtIndex(1, '    buy some sausages  ' + webdriver.Key.ENTER);

				testOps.assertItems([TODO_ITEM_ONE, 'buy some sausages', TODO_ITEM_THREE]);
			});

			test.it('should remove the item if an empty text string was entered', function () {
				createStandardItems();
				page.doubleClickItemAtIndex(1);

				page.editItemAtIndex(1, webdriver.Key.ENTER);

				testOps.assertItems([TODO_ITEM_ONE, TODO_ITEM_THREE]);
			});

			test.it('should cancel edits on escape', function () {
				createStandardItems();
				page.doubleClickItemAtIndex(1);

				page.editItemAtIndex(1, 'foo' + webdriver.Key.ESCAPE);

				testOps.assertItems([TODO_ITEM_ONE, TODO_ITEM_TWO, TODO_ITEM_THREE]);
			});
		});

		test.describe('Counter', function () {
			test.it('should display the current number of todo items', function () {
				page.enterItem(TODO_ITEM_ONE);
				testOps.assertItemCountText('1 item left');
				page.enterItem(TODO_ITEM_TWO);
				testOps.assertItemCountText('2 items left');
			});
		});

		test.describe('Clear completed button', function () {
			test.beforeEach(function () {
				createStandardItems();
			});

			test.it('should display the correct text', function () {
				page.toggleItemAtIndex(1);
				testOps.assertClearCompleteButtonText('Clear completed');
			});

			test.it('should remove completed items when clicked', function () {
				page.toggleItemAtIndex(1);
				page.clickClearCompleteButton();
				testOps.assertItemCount(2);
				testOps.assertItems([TODO_ITEM_ONE, TODO_ITEM_THREE]);
			});

			test.it('should be hidden when there are no items that are completed', function () {
				page.toggleItemAtIndex(1);
				testOps.assertClearCompleteButtonIsVisible();
				page.clickClearCompleteButton();
				testOps.assertClearCompleteButtonIsHidden();
			});
		});

		test.describe('Persistence', function () {
			test.it('should persist its data', function () {
				// set up state
				page.enterItem(TODO_ITEM_ONE);
				page.enterItem(TODO_ITEM_TWO);
				page.toggleItemAtIndex(1);

				function stateTest() {
					testOps.assertItems([TODO_ITEM_ONE, TODO_ITEM_TWO]);
					testOps.assertItemAtIndexIsCompleted(1);
					testOps.assertItemAtIndexIsNotCompleted(0);
				}

				// test it
				stateTest();

				// navigate away and back again
				browser.get('about:blank');
				browser.get(baseUrl);

				// repeat the state test
				stateTest();
			});
		});

		test.describe('Routing', function () {
			test.beforeEach(function () {
				createStandardItems();
			});
			test.it('should allow me to display active items', function () {
				page.toggleItemAtIndex(1);
				page.filterByActiveItems();

				testOps.assertItems([TODO_ITEM_ONE, TODO_ITEM_THREE]);
			});

			test.it('should respect the back button', function () {
				page.toggleItemAtIndex(1);

				page.filterByActiveItems();
				page.filterByCompletedItems();

				// should show completed items
				testOps.assertItems([TODO_ITEM_TWO]);

				// then active items
				page.back();
				testOps.assertItems([TODO_ITEM_ONE, TODO_ITEM_THREE]);

				// then all items
				page.back();
				testOps.assertItems([TODO_ITEM_ONE, TODO_ITEM_TWO, TODO_ITEM_THREE]);
			});

			test.it('should allow me to display completed items', function () {
				page.toggleItemAtIndex(1);
				page.filterByCompletedItems();

				testOps.assertItems([TODO_ITEM_TWO]);
			});

			test.it('should allow me to display all items', function () {
				page.toggleItemAtIndex(1);

				// apply the other filters first, before returning to the 'all' state
				page.filterByActiveItems();
				page.filterByCompletedItems();
				page.filterByAllItems();

				testOps.assertItems([TODO_ITEM_ONE, TODO_ITEM_TWO, TODO_ITEM_THREE]);
			});

			test.it('should highlight the currently applied filter', function () {
				// initially 'all' should be selected
				testOps.assertFilterAtIndexIsSelected(0);

				page.filterByActiveItems();
				testOps.assertFilterAtIndexIsSelected(1);

				page.filterByCompletedItems();
				testOps.assertFilterAtIndexIsSelected(2);
			});
		});
	});
};
<|endoftext|>


<commit_before>var jsonData = { "versions": [
                   { "major": 0, "minor": 4, "patch": 1, "day": 20, "month": 5, "year": 2015, "type": 1,
                     "platforms": [
                       { "name": "Windows", "supported": "Windows 7 and later",
                         "files": [
                           { "extension": ".exe", "bitness": 32 }, { "extension": ".zip", "bitness": 32 },
                           { "extension": ".exe", "bitness": 64 }, { "extension": ".zip", "bitness": 64 }
                         ]
                       },
                       { "name": "Linux", "supported": "Ubuntu 14.04 LTS (Trusty Tahr) and later",
                         "files": [
                           { "extension": ".tar.gz", "bitness": 32 },
                           { "extension": ".tar.gz", "bitness": 64 }
                         ]
                       },
                       { "name": "OS X", "supported": "Mac OS X 10.7 (Lion) and later",
                         "files": [
                           { "extension": ".dmg" }, { "extension": ".zip" }
                         ]
                       }
                     ]
                   },
                   { "major": 0, "minor": 0, "patch": 0, "day": 0, "month": 0, "year": 0, "type": 2,
                     "platforms": [
                       { "name": "Windows", "supported": "Windows 7 and later",
                         "files": [
                           { "extension": ".exe", "bitness": 32 }, { "extension": ".zip", "bitness": 32 },
                           { "extension": ".exe", "bitness": 64 }, { "extension": ".zip", "bitness": 64 }
                         ]
                       },
                       { "name": "Linux", "supported": "Ubuntu 14.04 LTS (Trusty Tahr) and later",
                         "files": [
                           { "extension": ".tar.gz", "bitness": 32 },
                           { "extension": ".tar.gz", "bitness": 64 }
                         ]
                       },
                       { "name": "OS X", "supported": "Mac OS X 10.7 (Lion) and later",
                         "files": [
                           { "extension": ".dmg" }, { "extension": ".zip" }
                         ]
                       }
                     ],
                     "changes": [
                       { "change": "CellML support: don't distinguish between CellML 1.0 and CellML 1.1 anymore when creating a new CellML file (see issue #827)." },
                       { "change": "SED-ML support: made it possible to create a new SED-ML file (see issue #826)." },
                       { "change": "Raw CellML view: fixed a problem with reformatting unescaping HTML entities (see issue #822)." },
                       { "change": "Raw SED-ML view: this view has been added so that we can quickly edit and validate SED-ML files (see issue #682)." },
                       { "change": "Single Cell (simulation) view: disabled the development mode when using a read-only file (see issue #817). Fixed a problem with the X axis sometimes not lining up when adding a new graph panel (see issue #820)." },
                       { "change": "Third-party libraries: upgraded libSBML to version 5.12.0 (see issue #812) and libgit2 to version 0.23.4 (see issue #816)." }
                     ],
                     "information": "Note: this snapshot is for those who want/need access to OpenCOR's latest features, meaning that it may be unstable."
                   },
                   { "major": 0, "minor": 4, "patch": 0, "day": 29, "month": 4, "year": 2015, "type": 0,
                     "platforms": [
                       { "name": "Windows", "supported": "Windows 7 and later",
                         "files": [
                           { "extension": ".exe", "bitness": 32 }, { "extension": ".zip", "bitness": 32 },
                           { "extension": ".exe", "bitness": 64 }, { "extension": ".zip", "bitness": 64 }
                         ]
                       },
                       { "name": "Linux", "supported": "Ubuntu 14.04 LTS (Trusty Tahr) and later",
                         "files": [
                           { "extension": ".tar.gz", "bitness": 32 },
                           { "extension": ".tar.gz", "bitness": 64 }
                         ]
                       },
                       { "name": "OS X", "supported": "Mac OS X 10.7 (Lion) and later",
                         "files": [
                           { "extension": ".dmg" }, { "extension": ".zip" }
                         ]
                       }
                     ]
                   },
                   { "major": 0, "minor": 3, "patch": 0, "day": 19, "month": 8, "year": 2014, "type": 0,
                     "platforms": [
                       { "name": "Windows", "supported": "Windows XP and later",
                         "files": [
                           { "extension": ".exe" }, { "extension": ".zip" }
                         ]
                       },
                       { "name": "Linux", "supported": "Ubuntu 14.04 LTS (Trusty Tahr) and later",
                         "files": [
                           { "extension": ".tar.gz", "bitness": 32 },
                           { "extension": ".tar.gz", "bitness": 64 }
                         ]
                       },
                       { "name": "OS X", "supported": "Mac OS X 10.7 (Lion) and later",
                         "files": [
                           { "extension": ".dmg" }, { "extension": ".zip" }
                         ]
                       }
                     ]
                   },
                   { "major": 0, "minor": 2, "patch": 0, "day": 19, "month": 11, "year": 2013, "type": 0,
                     "platforms": [
                       { "name": "Windows", "supported": "Windows XP and later",
                         "files": [
                           { "extension": ".exe" }, { "extension": ".zip" }
                         ]
                       },
                       { "name": "Linux", "supported": "Ubuntu 12.04 LTS (Precise Pangolin) and later",
                         "files": [
                           { "extension": ".tar.gz", "bitness": 32 },
                           { "extension": ".tar.gz", "bitness": 64 }
                         ]
                       },
                       { "name": "OS X", "supported": "Mac OS X 10.7 (Lion) and later",
                         "files": [
                           { "extension": ".dmg" }, { "extension": ".zip" }
                         ]
                       }
                     ]
                   },
                   { "major": 0, "minor": 1, "patch": 2, "day": 29, "month": 5, "year": 2013, "type": 0,
                     "platforms": [
                       { "name": "Windows", "supported": "Windows XP and later",
                         "files": [
                           { "extension": ".exe" }, { "extension": ".zip" }
                         ]
                       },
                       { "name": "Linux", "supported": "Ubuntu 12.04 LTS (Precise Pangolin) and later",
                         "files": [
                           { "extension": ".tar.gz", "bitness": 32 },
                           { "extension": ".tar.gz", "bitness": 64 }
                         ]
                       },
                       { "name": "OS X", "supported": "OS X 10.8 (Mountain Lion) and later",
                         "files": [
                           { "extension": ".dmg" }, { "extension": ".zip" }
                         ]
                       }
                     ]
                   },
                   { "major": 0, "minor": 1, "patch": 1, "day": 17, "month": 4, "year": 2013, "type": 0,
                     "platforms": [
                       { "name": "Windows", "supported": "Windows XP and later",
                         "files": [
                           { "extension": ".exe" }, { "extension": ".zip" }
                         ]
                       },
                       { "name": "Linux", "supported": "Ubuntu 12.04 LTS (Precise Pangolin) and later",
                         "files": [
                           { "extension": ".tar.gz", "bitness": 32 },
                           { "extension": ".tar.gz", "bitness": 64 }
                         ]
                       },
                       { "name": "OS X", "supported": "OS X 10.8 (Mountain Lion) and later",
                         "files": [
                           { "extension": ".dmg" }, { "extension": ".zip" }
                         ]
                       }
                     ]
                   },
                   { "major": 0, "minor": 1, "patch": 0, "day": 1, "month": 4, "year": 2013, "type": 0,
                     "platforms": [
                       { "name": "Windows", "supported": "Windows XP and later",
                         "files": [
                           { "extension": ".exe" }, { "extension": ".zip" }
                         ]
                       },
                       { "name": "Linux", "supported": "Ubuntu 12.04 LTS (Precise Pangolin) and later",
                         "files": [
                           { "extension": ".tar.gz", "bitness": 32 },
                           { "extension": ".tar.gz", "bitness": 64 }
                         ]
                       },
                       { "name": "OS X", "supported": "OS X 10.8 (Mountain Lion) and later",
                         "files": [
                           { "extension": ".dmg" }, { "extension": ".zip" }
                         ]
                       }
                     ]
                   }
                 ]
               };
<commit_msg>Slight update to the downloads page [ci skip].<commit_after>var jsonData = { "versions": [
                   { "major": 0, "minor": 4, "patch": 1, "day": 20, "month": 5, "year": 2015, "type": 1,
                     "platforms": [
                       { "name": "Windows", "supported": "Windows 7 and later",
                         "files": [
                           { "extension": ".exe", "bitness": 32 }, { "extension": ".zip", "bitness": 32 },
                           { "extension": ".exe", "bitness": 64 }, { "extension": ".zip", "bitness": 64 }
                         ]
                       },
                       { "name": "Linux", "supported": "Ubuntu 14.04 LTS (Trusty Tahr) and later",
                         "files": [
                           { "extension": ".tar.gz", "bitness": 32 },
                           { "extension": ".tar.gz", "bitness": 64 }
                         ]
                       },
                       { "name": "OS X", "supported": "Mac OS X 10.7 (Lion) and later",
                         "files": [
                           { "extension": ".dmg" }, { "extension": ".zip" }
                         ]
                       }
                     ]
                   },
                   { "major": 0, "minor": 0, "patch": 0, "day": 0, "month": 0, "year": 0, "type": 2,
                     "platforms": [
                       { "name": "Windows", "supported": "Windows 7 and later",
                         "files": [
                           { "extension": ".exe", "bitness": 32 }, { "extension": ".zip", "bitness": 32 },
                           { "extension": ".exe", "bitness": 64 }, { "extension": ".zip", "bitness": 64 }
                         ]
                       },
                       { "name": "Linux", "supported": "Ubuntu 14.04 LTS (Trusty Tahr) and later",
                         "files": [
                           { "extension": ".tar.gz", "bitness": 32 },
                           { "extension": ".tar.gz", "bitness": 64 }
                         ]
                       },
                       { "name": "OS X", "supported": "Mac OS X 10.7 (Lion) and later",
                         "files": [
                           { "extension": ".dmg" }, { "extension": ".zip" }
                         ]
                       }
                     ],
                     "changes": [
                       { "change": "CellML support: don't distinguish between CellML 1.0 and CellML 1.1 anymore when creating a new CellML file (see issue #827)." },
                       { "change": "SED-ML support: made it possible to create a new SED-ML file (see issue #826)." },
                       { "change": "CellML Text view: hyphens and periods are now allowed in cmeta:id's (see issue #830)." },
                       { "change": "Raw CellML view: fixed a problem with reformatting unescaping HTML entities (see issue #822)." },
                       { "change": "Raw SED-ML view: this view has been added so that we can quickly edit and validate SED-ML files (see issue #682)." },
                       { "change": "Single Cell (simulation) view: disabled the development mode when using a read-only file (see issue #817). Fixed a problem with the X axis sometimes not lining up when adding a new graph panel (see issue #820)." },
                       { "change": "Third-party libraries: upgraded libSBML to version 5.12.0 (see issue #812) and libgit2 to version 0.23.4 (see issue #816)." }
                     ],
                     "information": "Note: this snapshot is for those who want/need access to OpenCOR's latest features, meaning that it may be unstable."
                   },
                   { "major": 0, "minor": 4, "patch": 0, "day": 29, "month": 4, "year": 2015, "type": 0,
                     "platforms": [
                       { "name": "Windows", "supported": "Windows 7 and later",
                         "files": [
                           { "extension": ".exe", "bitness": 32 }, { "extension": ".zip", "bitness": 32 },
                           { "extension": ".exe", "bitness": 64 }, { "extension": ".zip", "bitness": 64 }
                         ]
                       },
                       { "name": "Linux", "supported": "Ubuntu 14.04 LTS (Trusty Tahr) and later",
                         "files": [
                           { "extension": ".tar.gz", "bitness": 32 },
                           { "extension": ".tar.gz", "bitness": 64 }
                         ]
                       },
                       { "name": "OS X", "supported": "Mac OS X 10.7 (Lion) and later",
                         "files": [
                           { "extension": ".dmg" }, { "extension": ".zip" }
                         ]
                       }
                     ]
                   },
                   { "major": 0, "minor": 3, "patch": 0, "day": 19, "month": 8, "year": 2014, "type": 0,
                     "platforms": [
                       { "name": "Windows", "supported": "Windows XP and later",
                         "files": [
                           { "extension": ".exe" }, { "extension": ".zip" }
                         ]
                       },
                       { "name": "Linux", "supported": "Ubuntu 14.04 LTS (Trusty Tahr) and later",
                         "files": [
                           { "extension": ".tar.gz", "bitness": 32 },
                           { "extension": ".tar.gz", "bitness": 64 }
                         ]
                       },
                       { "name": "OS X", "supported": "Mac OS X 10.7 (Lion) and later",
                         "files": [
                           { "extension": ".dmg" }, { "extension": ".zip" }
                         ]
                       }
                     ]
                   },
                   { "major": 0, "minor": 2, "patch": 0, "day": 19, "month": 11, "year": 2013, "type": 0,
                     "platforms": [
                       { "name": "Windows", "supported": "Windows XP and later",
                         "files": [
                           { "extension": ".exe" }, { "extension": ".zip" }
                         ]
                       },
                       { "name": "Linux", "supported": "Ubuntu 12.04 LTS (Precise Pangolin) and later",
                         "files": [
                           { "extension": ".tar.gz", "bitness": 32 },
                           { "extension": ".tar.gz", "bitness": 64 }
                         ]
                       },
                       { "name": "OS X", "supported": "Mac OS X 10.7 (Lion) and later",
                         "files": [
                           { "extension": ".dmg" }, { "extension": ".zip" }
                         ]
                       }
                     ]
                   },
                   { "major": 0, "minor": 1, "patch": 2, "day": 29, "month": 5, "year": 2013, "type": 0,
                     "platforms": [
                       { "name": "Windows", "supported": "Windows XP and later",
                         "files": [
                           { "extension": ".exe" }, { "extension": ".zip" }
                         ]
                       },
                       { "name": "Linux", "supported": "Ubuntu 12.04 LTS (Precise Pangolin) and later",
                         "files": [
                           { "extension": ".tar.gz", "bitness": 32 },
                           { "extension": ".tar.gz", "bitness": 64 }
                         ]
                       },
                       { "name": "OS X", "supported": "OS X 10.8 (Mountain Lion) and later",
                         "files": [
                           { "extension": ".dmg" }, { "extension": ".zip" }
                         ]
                       }
                     ]
                   },
                   { "major": 0, "minor": 1, "patch": 1, "day": 17, "month": 4, "year": 2013, "type": 0,
                     "platforms": [
                       { "name": "Windows", "supported": "Windows XP and later",
                         "files": [
                           { "extension": ".exe" }, { "extension": ".zip" }
                         ]
                       },
                       { "name": "Linux", "supported": "Ubuntu 12.04 LTS (Precise Pangolin) and later",
                         "files": [
                           { "extension": ".tar.gz", "bitness": 32 },
                           { "extension": ".tar.gz", "bitness": 64 }
                         ]
                       },
                       { "name": "OS X", "supported": "OS X 10.8 (Mountain Lion) and later",
                         "files": [
                           { "extension": ".dmg" }, { "extension": ".zip" }
                         ]
                       }
                     ]
                   },
                   { "major": 0, "minor": 1, "patch": 0, "day": 1, "month": 4, "year": 2013, "type": 0,
                     "platforms": [
                       { "name": "Windows", "supported": "Windows XP and later",
                         "files": [
                           { "extension": ".exe" }, { "extension": ".zip" }
                         ]
                       },
                       { "name": "Linux", "supported": "Ubuntu 12.04 LTS (Precise Pangolin) and later",
                         "files": [
                           { "extension": ".tar.gz", "bitness": 32 },
                           { "extension": ".tar.gz", "bitness": 64 }
                         ]
                       },
                       { "name": "OS X", "supported": "OS X 10.8 (Mountain Lion) and later",
                         "files": [
                           { "extension": ".dmg" }, { "extension": ".zip" }
                         ]
                       }
                     ]
                   }
                 ]
               };
<|endoftext|>


<commit_before>// Define [global] variables
/*jslint browser: true, white */
/*global window, videos, fullVersion, YT */
var localPlayer = false;
var currentVideo;
var videoTitle;
var player;
var playerConfig;
var videoJustChanged;
var totalDuration = 0;
var progressArray = [];

// Inspired from https://stackoverflow.com/a/11582513/185053 , modified for JSLint
function getURLParameter(name) {
  "use strict";
  // This function will return null if this specific parameter is not found
  var parameterValue = null;
  // Perform a regex match to find the value of the parameter from the query string
  var parameterRegex = new RegExp("[?|&]" + name + "=" + "([^&;]+?)(&|#|;|$)").exec(location.search);
  if (parameterRegex) {
    // If the regex found a match, replace any occurrence of + by %20
    parameterValue = parameterRegex[1].replace(/\+/g, "%20");
    // and perform proper decoding of the URI
    parameterValue = decodeURIComponent(parameterValue);
  }
  // Returns either null or the value of that parameter
  return parameterValue;
}

// From https://stackoverflow.com/a/1830844/185053
function isNumeric(n) {
  "use strict";
  if (n) {
    n = n.replace(/,/, ".");
  }
  return !isNaN(parseFloat(n)) && isFinite(n);
}

function determineCurrentVideoParameter() {
  "use strict";
  currentVideo = getURLParameter("p");
  if (!isNumeric(currentVideo)) {
    window.location = "?p=1" + (localPlayer ? "&local=1" : "");
  }
  currentVideo -= 1; //URL Parameter/visible ID is 1-based, while array is 0-based
  // currentVideo == -1 means playing the complete video
  if (currentVideo < -1) {
    window.location = "?p=1" + (localPlayer ? "&local=1" : "");
  }
  if (currentVideo > videos.length - 1) {
    window.location = "?p=" + videos.length + (localPlayer ? "&local=1" : "");
  }
}

function updateUI(updateURL) {
  "use strict";
  if (updateURL) {
    // Change the URL, so that if the user refreshes the page he gets back to this specific part
    var newURL = "?p=" + (currentVideo + 1) + (localPlayer ? "&local=1" : "");
    window.history.pushState({}, document.title, newURL);
  }

  // Show the name of the current video
  var updatedTitle = videoTitle + ((currentVideo === -1) ? ", versión completa" : ", Parte " + (currentVideo + 1));
  document.title = updatedTitle;
  document.getElementById("nameCurrent").innerHTML = updatedTitle;

  // Set the current video as active in the navigation bar
  // First check that the element has child nodes
  var ul = document.getElementById("navigation");
  if (ul.hasChildNodes()) {
    // First node is a text node, start with 1 and use i-1 to compare to currentVideo
    ul.childNodes.forEach(function (child, i) {
      child.className = (i - 1 === currentVideo) ? "active" : "";
    });
  }

  // Show the appropriate buttons
  document.getElementById("previousButton").style.visibility = (currentVideo < 1) ? "hidden" : "visible";
  document.getElementById("nextButton").style.visibility = (currentVideo > videos.length - 2) ? "hidden" : "visible";
  document.getElementById("versionCompleta").style.display = (currentVideo === -1) ? "none" : "inline";

  // Update the progress bar
  var pBar = document.getElementById("progress");
  pBar.max = totalDuration;
  pBar.value = (currentVideo === -1) ? 0 : progressArray[currentVideo];
  pBar.getElementsByTagName("span")[0].innerHTML = (currentVideo === -1) ? 0 : Math.floor((100 / totalDuration) * progressArray[currentVideo]);
}

function getVideoValues() {
  "use strict";
  var startSeconds;
  var endSeconds;
  var localVideoURL;
  var videoId = (currentVideo === -1) ? fullVersion : videos[currentVideo].id;
  if (currentVideo > -1) {
    startSeconds = videos[currentVideo].start;
    endSeconds = videos[currentVideo].end;
  }

  if (localPlayer) {
    // The local videos are available at:
    // videos/<folder with the same name as current file>/<YouTube ID>.mp4
    // Get the name of the current file, i.e. after last / and before .html
    var localVideoFolderName = window.location.href.split("/").slice(-1)[0].split(".")[0];
    localVideoURL = "videos/" + localVideoFolderName + "/" + videoId + ".mp4";
  }

  return{"videoId": videoId, "localVideoURL": localVideoURL, "startSeconds": startSeconds, "endSeconds": endSeconds};
}

function isSameLocalVideo(currentVideo, newVideo) {
  "use strict";
  var shortURL = currentVideo.substring(currentVideo.length - newVideo.length, currentVideo.length);
  return (shortURL === newVideo);
}

function changeVideo(updateURL) {
  "use strict";
  updateUI(updateURL);

  var vidValues = getVideoValues();
  if (currentVideo === -1) {
    if (!localPlayer) {
      player.loadVideoById({videoId: fullVersion});
    } else {
      if (!isSameLocalVideo(player.src, vidValues.localVideoURL)) {
        player.src = vidValues.localVideoURL;
      }
      player.startSeconds = null;
      player.endSeconds = null;
    }
  } else {
    if (!localPlayer) {
      player.loadVideoById({
        videoId: vidValues.videoId,
        startSeconds: vidValues.startSeconds,
        endSeconds: vidValues.endSeconds
      });
    } else {
      if (!isSameLocalVideo(player.src, vidValues.localVideoURL)) {
        player.src = vidValues.localVideoURL;
      }
      // Set the player at the timestamp we want to start on
      player.currentTime = vidValues.startSeconds;
      // Keep the start and end timestamps on the video object to check from the timeupdate event
      player.startSeconds = vidValues.startSeconds;
      player.endSeconds = vidValues.endSeconds;
    }
  }
  if (localPlayer && player.paused) {
    // The local player needs a little push when the full version played to the end and stopped.
    player.play();
  }
}

function changeVideoEvent(evt) {
  "use strict";
  currentVideo = evt.target.num;
  changeVideo(true);
}

function playFollowing() {
  "use strict";
  // Replays the same video or advances to the next, depending on the checkbox
  if (!document.getElementById("repeatVideo").checked) {
    currentVideo = (currentVideo < videos.length - 1) ? currentVideo + 1 : 0;
    changeVideo(true);
  } else {
    changeVideo(false);
  }
}

// Reload the video when onStateChange=YT.PlayerState.ENDED)
function onStateChange(state) {
  "use strict";
  var doSomething = false;
  if (!localPlayer) {
    if (state.data === YT.PlayerState.ENDED && !videoJustChanged) {
      videoJustChanged = true;
      doSomething = true;
      // Reset videoJustChanged after one second to prevent this block being called twice in succession (messes with the logic to advance video if selected)
      setTimeout(function () {
        videoJustChanged = false;
      }, 1000);
    }
  } else {
    if (player.endSeconds && (player.currentTime > player.endSeconds)) {
      doSomething = true;
    }
  }

  if (doSomething) {
    playFollowing();
  }
}

function playPauseLocalVideo() {
  "use strict";
  if (!player.paused) {
    player.pause();
  } else {
    player.play();
  }
}

function setUpLocalVideoPlayer() {
  "use strict";
  // Playing the videos via local HTML5 video
  var vidValues = getVideoValues();
  player = document.createElement("video");
  player.setAttribute("id", "localVideoPlayer");
  player.setAttribute("height", "96%");
  player.setAttribute("width", "99%");
  player.controls = "controls";
  player.autoplay = "autoplay";
  player.src = vidValues.localVideoURL;
  if (vidValues.startSeconds) {
    // Set the player at the timestamp we want to start on
    player.currentTime = vidValues.startSeconds;
    // Keep that start timestamp on the video object to check from the timeupdate event
    player.startSeconds = vidValues.startSeconds;
  }
  if (vidValues.endSeconds) {
    // Keep that end timestamp on the video object to check from the timeupdate event
    player.endSeconds = vidValues.endSeconds;
  }
  document.getElementById("videoPlayer").appendChild(player);

  // Use the timeupdate event to determine when playing this part is over
  player.addEventListener("timeupdate", onStateChange, false);
  // Listen to ended to identify when playing the full version is done
  player.addEventListener("ended", playFollowing, false);
  // Clicking on the video will pause or play it
  player.addEventListener("click", playPauseLocalVideo, false);
}

function setUpYouTubeVideoPlayer() {
  "use strict";
  // Load the YouTube JavaScript, per https://stackoverflow.com/a/3973468/185053
  var script = document.createElement('script');
  script.async = "async";
  script.type = "text/javascript";
  script.src = "https://www.youtube.com/iframe_api";
  document.body.appendChild(script);

  var vidValues = getVideoValues();
  // Based on https://webapps.stackexchange.com/a/103450/161341
  playerConfig = {
    height: "96%",
    width: "99%",
    videoId: vidValues.videoId,
    playerVars: {
      // https://developers.google.com/youtube/player_parameters
      autoplay: 1,            // Auto-play the video on load
      controls: 1,            // Show pause/play buttons in player
      showinfo: 0,            // Hide the video title
      rel: 0,                 // Hide related videos when pausing video
      //modestbranding: 1,      // Hide the Youtube Logo
      fs: 1,                  // Show the full screen button
      cc_load_policy: 0,      // Hide closed captions
      iv_load_policy: 3,      // Hide the Video Annotations
      start: vidValues.startSeconds,
      end: vidValues.endSeconds,
      autohide: 0            // Hide video controls when playing
      //enablejsapi: 1,
      //origin: "https://vallenato.fr"
    },
    events: {
      "onStateChange": onStateChange
    }
  };
}

function determineLocalOrYouTubePlayer() {
  "use strict";
  localPlayer = (null !== getURLParameter("local"));
}

function onYouTubePlayerAPIReady() {
  "use strict";
  if (!localPlayer) {
    // Play the first video in div
    player = new YT.Player("videoPlayer", playerConfig);
  }
}

// From https://stackoverflow.com/a/3733257/185053
function str_pad_left(string,pad,length) {
  "use strict";
  return (new Array(length+1).join(pad)+string).slice(-length);
}

function populateProgressArray() {
  "use strict";
  // Construct the array used for the progress bar
  videos.forEach(function (video) {
    var duration = video.end - video.start;
    progressArray.push(totalDuration);
    totalDuration += duration;
  });
  // Use the following to output the total duration and number of parts, used to update the index.html when adding a new song.
  //console.log("totalDuration: " + totalDuration + "s - " + str_pad_left(Math.floor(totalDuration / 60),'0',2) + 'm' + str_pad_left(totalDuration % 60,'0',2) + "s en " +  videos.length + " partes");
}

function previousVideo() {
  "use strict";
  // The button can't go lower than 0 (by design), but with the "p" shortcut we want to be able to go to the Full Version
  currentVideo = (currentVideo > -1) ? currentVideo - 1 : -1;
  changeVideo(true);
}

function nextVideo() {
  "use strict";
  currentVideo = (currentVideo < videos.length - 2) ? currentVideo + 1 : videos.length - 1;
  changeVideo(true);
}

function progressbarClicked(evt) {
  "use strict";
  var desiredDuration = Math.floor(evt.offsetX * totalDuration / evt.target.offsetWidth);
  var i = 0;
  while (desiredDuration > progressArray[i]) {
    if (i > videos.length) {
      break;
    } // Safety check
    i += 1;
  }
  currentVideo = i - 1; // The video that contains the timestamp clicked on
  changeVideo(true);
}

function versionCompletaClicked() {
  "use strict";
  currentVideo = -1;
  changeVideo(true);
}

function keyPressed(evt) {
  "use strict";
  var code;
  //Find which key is pressed
  if (evt.keyCode) {
    code = evt.keyCode;
  } else if (evt.which) {
    code = evt.which;
  }
  var character = String.fromCharCode(code).toLowerCase();

  // Previous video: "p"
  if ("p" === character) {
    previousVideo();
  }

  // Next video: "n" or "s" (siguiente)
  if ("n" === character || "s" === character) {
    nextVideo();
  }

  // Toggle Repeat: "r"
  if ("r" === character) {
    var repeatVideo = document.getElementById("repeatVideo");
    repeatVideo.checked = !repeatVideo.checked;
  }

  // Support some default Youtube embed player shortcuts that are made available if the focus in not in the player
  // Pause/Play: <space> (code 32)
  if (32 === code) {
    if (!localPlayer) {
      if (player.getPlayerState() === YT.PlayerState.PLAYING) {
        player.pauseVideo();
      } else {
        player.playVideo();
      }
    } else {
      playPauseLocalVideo();
    }
    //Stop the event
    // If there are too many parts, pressing <Space> would also perform a Page Down
    // IE
    evt.cancelBubble = true;
    evt.returnValue = false;
    if (evt.stopPropagation) {
      evt.stopPropagation();
      evt.preventDefault();
    }
  }

  // These shortcuts need enablejsapi and origin, disabled for now as it makes local testing more complex
  /*
  // Mute: "m"
  if ("m" === character) {
    console.log("Mute");
    if (player.isMuted()) {
      console.log("unmute");
      player.unMute();
    } else {
      console.log("mute");
      player.unMute();
    }
  }
  */
}

function createUI() {
  "use strict";
  var ul = document.getElementById("navigation");
  videos.forEach(function (ignore, i) {
    var li = document.createElement("li");
    li.appendChild(document.createTextNode("Parte " + (i + 1)));
    li.addEventListener("click", changeVideoEvent, false);
    li.num = i;
    ul.appendChild(li);
  });

  // Save the initial Title, as it contains the name of the current song
  videoTitle = document.title;

  document.getElementById("previousButton").addEventListener("click", previousVideo, false);
  document.getElementById("nextButton").addEventListener("click", nextVideo, false);
  document.getElementById("progress").addEventListener("click", progressbarClicked, false);
  document.getElementById("versionCompleta").addEventListener("click", versionCompletaClicked, false);
  document.addEventListener("keydown", keyPressed, false); // Keyboard shortcuts
  updateUI(false);
}

// Perform some initial setup/calculations
determineLocalOrYouTubePlayer();
determineCurrentVideoParameter();
populateProgressArray();
window.onload = function () {
  "use strict";
  createUI();
  if (!localPlayer) {
    setUpYouTubeVideoPlayer();
  } else {
    setUpLocalVideoPlayer();
  }
};

// Detect Back or Forward button
window.onpopstate = function () {
  "use strict";
  // Get the p parameter directly from the URL
  determineCurrentVideoParameter();
  changeVideo(false);
};
<commit_msg>Shorcut key <home> to restart the current fragment<commit_after>// Define [global] variables
/*jslint browser: true, white */
/*global window, videos, fullVersion, YT */
var localPlayer = false;
var currentVideo;
var videoTitle;
var player;
var playerConfig;
var videoJustChanged;
var totalDuration = 0;
var progressArray = [];

// Inspired from https://stackoverflow.com/a/11582513/185053 , modified for JSLint
function getURLParameter(name) {
  "use strict";
  // This function will return null if this specific parameter is not found
  var parameterValue = null;
  // Perform a regex match to find the value of the parameter from the query string
  var parameterRegex = new RegExp("[?|&]" + name + "=" + "([^&;]+?)(&|#|;|$)").exec(location.search);
  if (parameterRegex) {
    // If the regex found a match, replace any occurrence of + by %20
    parameterValue = parameterRegex[1].replace(/\+/g, "%20");
    // and perform proper decoding of the URI
    parameterValue = decodeURIComponent(parameterValue);
  }
  // Returns either null or the value of that parameter
  return parameterValue;
}

// From https://stackoverflow.com/a/1830844/185053
function isNumeric(n) {
  "use strict";
  if (n) {
    n = n.replace(/,/, ".");
  }
  return !isNaN(parseFloat(n)) && isFinite(n);
}

function determineCurrentVideoParameter() {
  "use strict";
  currentVideo = getURLParameter("p");
  if (!isNumeric(currentVideo)) {
    window.location = "?p=1" + (localPlayer ? "&local=1" : "");
  }
  currentVideo -= 1; //URL Parameter/visible ID is 1-based, while array is 0-based
  // currentVideo == -1 means playing the complete video
  if (currentVideo < -1) {
    window.location = "?p=1" + (localPlayer ? "&local=1" : "");
  }
  if (currentVideo > videos.length - 1) {
    window.location = "?p=" + videos.length + (localPlayer ? "&local=1" : "");
  }
}

function updateUI(updateURL) {
  "use strict";
  if (updateURL) {
    // Change the URL, so that if the user refreshes the page he gets back to this specific part
    var newURL = "?p=" + (currentVideo + 1) + (localPlayer ? "&local=1" : "");
    window.history.pushState({}, document.title, newURL);
  }

  // Show the name of the current video
  var updatedTitle = videoTitle + ((currentVideo === -1) ? ", versión completa" : ", Parte " + (currentVideo + 1));
  document.title = updatedTitle;
  document.getElementById("nameCurrent").innerHTML = updatedTitle;

  // Set the current video as active in the navigation bar
  // First check that the element has child nodes
  var ul = document.getElementById("navigation");
  if (ul.hasChildNodes()) {
    // First node is a text node, start with 1 and use i-1 to compare to currentVideo
    ul.childNodes.forEach(function (child, i) {
      child.className = (i - 1 === currentVideo) ? "active" : "";
    });
  }

  // Show the appropriate buttons
  document.getElementById("previousButton").style.visibility = (currentVideo < 1) ? "hidden" : "visible";
  document.getElementById("nextButton").style.visibility = (currentVideo > videos.length - 2) ? "hidden" : "visible";
  document.getElementById("versionCompleta").style.display = (currentVideo === -1) ? "none" : "inline";

  // Update the progress bar
  var pBar = document.getElementById("progress");
  pBar.max = totalDuration;
  pBar.value = (currentVideo === -1) ? 0 : progressArray[currentVideo];
  pBar.getElementsByTagName("span")[0].innerHTML = (currentVideo === -1) ? 0 : Math.floor((100 / totalDuration) * progressArray[currentVideo]);
}

function getVideoValues() {
  "use strict";
  var startSeconds;
  var endSeconds;
  var localVideoURL;
  var videoId = (currentVideo === -1) ? fullVersion : videos[currentVideo].id;
  if (currentVideo > -1) {
    startSeconds = videos[currentVideo].start;
    endSeconds = videos[currentVideo].end;
  }

  if (localPlayer) {
    // The local videos are available at:
    // videos/<folder with the same name as current file>/<YouTube ID>.mp4
    // Get the name of the current file, i.e. after last / and before .html
    var localVideoFolderName = window.location.href.split("/").slice(-1)[0].split(".")[0];
    localVideoURL = "videos/" + localVideoFolderName + "/" + videoId + ".mp4";
  }

  return{"videoId": videoId, "localVideoURL": localVideoURL, "startSeconds": startSeconds, "endSeconds": endSeconds};
}

function isSameLocalVideo(currentVideo, newVideo) {
  "use strict";
  var shortURL = currentVideo.substring(currentVideo.length - newVideo.length, currentVideo.length);
  return (shortURL === newVideo);
}

function changeVideo(updateURL) {
  "use strict";
  updateUI(updateURL);

  var vidValues = getVideoValues();
  if (currentVideo === -1) {
    if (!localPlayer) {
      player.loadVideoById({videoId: fullVersion});
    } else {
      if (!isSameLocalVideo(player.src, vidValues.localVideoURL)) {
        player.src = vidValues.localVideoURL;
      }
      player.startSeconds = null;
      player.endSeconds = null;
    }
  } else {
    if (!localPlayer) {
      player.loadVideoById({
        videoId: vidValues.videoId,
        startSeconds: vidValues.startSeconds,
        endSeconds: vidValues.endSeconds
      });
    } else {
      if (!isSameLocalVideo(player.src, vidValues.localVideoURL)) {
        player.src = vidValues.localVideoURL;
      }
      // Set the player at the timestamp we want to start on
      player.currentTime = vidValues.startSeconds;
      // Keep the start and end timestamps on the video object to check from the timeupdate event
      player.startSeconds = vidValues.startSeconds;
      player.endSeconds = vidValues.endSeconds;
    }
  }
  if (localPlayer && player.paused) {
    // The local player needs a little push when the full version played to the end and stopped.
    player.play();
  }
}

function changeVideoEvent(evt) {
  "use strict";
  currentVideo = evt.target.num;
  changeVideo(true);
}

function playFollowing() {
  "use strict";
  // Replays the same video or advances to the next, depending on the checkbox
  if (!document.getElementById("repeatVideo").checked) {
    currentVideo = (currentVideo < videos.length - 1) ? currentVideo + 1 : 0;
    changeVideo(true);
  } else {
    changeVideo(false);
  }
}

// Reload the video when onStateChange=YT.PlayerState.ENDED)
function onStateChange(state) {
  "use strict";
  var doSomething = false;
  if (!localPlayer) {
    if (state.data === YT.PlayerState.ENDED && !videoJustChanged) {
      videoJustChanged = true;
      doSomething = true;
      // Reset videoJustChanged after one second to prevent this block being called twice in succession (messes with the logic to advance video if selected)
      setTimeout(function () {
        videoJustChanged = false;
      }, 1000);
    }
  } else {
    if (player.endSeconds && (player.currentTime > player.endSeconds)) {
      doSomething = true;
    }
  }

  if (doSomething) {
    playFollowing();
  }
}

function playPauseLocalVideo() {
  "use strict";
  if (!player.paused) {
    player.pause();
  } else {
    player.play();
  }
}

function setUpLocalVideoPlayer() {
  "use strict";
  // Playing the videos via local HTML5 video
  var vidValues = getVideoValues();
  player = document.createElement("video");
  player.setAttribute("id", "localVideoPlayer");
  player.setAttribute("height", "96%");
  player.setAttribute("width", "99%");
  player.controls = "controls";
  player.autoplay = "autoplay";
  player.src = vidValues.localVideoURL;
  if (vidValues.startSeconds) {
    // Set the player at the timestamp we want to start on
    player.currentTime = vidValues.startSeconds;
    // Keep that start timestamp on the video object to check from the timeupdate event
    player.startSeconds = vidValues.startSeconds;
  }
  if (vidValues.endSeconds) {
    // Keep that end timestamp on the video object to check from the timeupdate event
    player.endSeconds = vidValues.endSeconds;
  }
  document.getElementById("videoPlayer").appendChild(player);

  // Use the timeupdate event to determine when playing this part is over
  player.addEventListener("timeupdate", onStateChange, false);
  // Listen to ended to identify when playing the full version is done
  player.addEventListener("ended", playFollowing, false);
  // Clicking on the video will pause or play it
  player.addEventListener("click", playPauseLocalVideo, false);
}

function setUpYouTubeVideoPlayer() {
  "use strict";
  // Load the YouTube JavaScript, per https://stackoverflow.com/a/3973468/185053
  var script = document.createElement('script');
  script.async = "async";
  script.type = "text/javascript";
  script.src = "https://www.youtube.com/iframe_api";
  document.body.appendChild(script);

  var vidValues = getVideoValues();
  // Based on https://webapps.stackexchange.com/a/103450/161341
  playerConfig = {
    height: "96%",
    width: "99%",
    videoId: vidValues.videoId,
    playerVars: {
      // https://developers.google.com/youtube/player_parameters
      autoplay: 1,            // Auto-play the video on load
      controls: 1,            // Show pause/play buttons in player
      showinfo: 0,            // Hide the video title
      rel: 0,                 // Hide related videos when pausing video
      //modestbranding: 1,      // Hide the Youtube Logo
      fs: 1,                  // Show the full screen button
      cc_load_policy: 0,      // Hide closed captions
      iv_load_policy: 3,      // Hide the Video Annotations
      start: vidValues.startSeconds,
      end: vidValues.endSeconds,
      autohide: 0            // Hide video controls when playing
      //enablejsapi: 1,
      //origin: "https://vallenato.fr"
    },
    events: {
      "onStateChange": onStateChange
    }
  };
}

function determineLocalOrYouTubePlayer() {
  "use strict";
  localPlayer = (null !== getURLParameter("local"));
}

function onYouTubePlayerAPIReady() {
  "use strict";
  if (!localPlayer) {
    // Play the first video in div
    player = new YT.Player("videoPlayer", playerConfig);
  }
}

// From https://stackoverflow.com/a/3733257/185053
function str_pad_left(string,pad,length) {
  "use strict";
  return (new Array(length+1).join(pad)+string).slice(-length);
}

function populateProgressArray() {
  "use strict";
  // Construct the array used for the progress bar
  videos.forEach(function (video) {
    var duration = video.end - video.start;
    progressArray.push(totalDuration);
    totalDuration += duration;
  });
  // Use the following to output the total duration and number of parts, used to update the index.html when adding a new song.
  //console.log("totalDuration: " + totalDuration + "s - " + str_pad_left(Math.floor(totalDuration / 60),'0',2) + 'm' + str_pad_left(totalDuration % 60,'0',2) + "s en " +  videos.length + " partes");
}

function previousVideo() {
  "use strict";
  // The button can't go lower than 0 (by design), but with the "p" shortcut we want to be able to go to the Full Version
  currentVideo = (currentVideo > -1) ? currentVideo - 1 : -1;
  changeVideo(true);
}

function nextVideo() {
  "use strict";
  currentVideo = (currentVideo < videos.length - 2) ? currentVideo + 1 : videos.length - 1;
  changeVideo(true);
}

function progressbarClicked(evt) {
  "use strict";
  var desiredDuration = Math.floor(evt.offsetX * totalDuration / evt.target.offsetWidth);
  var i = 0;
  while (desiredDuration > progressArray[i]) {
    if (i > videos.length) {
      break;
    } // Safety check
    i += 1;
  }
  currentVideo = i - 1; // The video that contains the timestamp clicked on
  changeVideo(true);
}

function versionCompletaClicked() {
  "use strict";
  currentVideo = -1;
  changeVideo(true);
}

function keyPressed(evt) {
  "use strict";
  var code;
  //Find which key is pressed
  if (evt.keyCode) {
    code = evt.keyCode;
  } else if (evt.which) {
    code = evt.which;
  }
  var character = String.fromCharCode(code).toLowerCase();

  // Previous video: "p"
  if ("p" === character) {
    previousVideo();
  }

  // Next video: "n" or "s" (siguiente)
  if ("n" === character || "s" === character) {
    nextVideo();
  }

  // Restart this fragment: <home> (code 36)
  if (36 === code) {
    changeVideo(false); // No need to change the URL
  }

  // Toggle Repeat: "r"
  if ("r" === character) {
    var repeatVideo = document.getElementById("repeatVideo");
    repeatVideo.checked = !repeatVideo.checked;
  }

  // Support some default Youtube embed player shortcuts that are made available if the focus in not in the player
  // Pause/Play: <space> (code 32)
  if (32 === code) {
    if (!localPlayer) {
      if (player.getPlayerState() === YT.PlayerState.PLAYING) {
        player.pauseVideo();
      } else {
        player.playVideo();
      }
    } else {
      playPauseLocalVideo();
    }
    //Stop the event
    // If there are too many parts, pressing <Space> would also perform a Page Down
    // IE
    evt.cancelBubble = true;
    evt.returnValue = false;
    if (evt.stopPropagation) {
      evt.stopPropagation();
      evt.preventDefault();
    }
  }

  // These shortcuts need enablejsapi and origin, disabled for now as it makes local testing more complex
  /*
  // Mute: "m"
  if ("m" === character) {
    console.log("Mute");
    if (player.isMuted()) {
      console.log("unmute");
      player.unMute();
    } else {
      console.log("mute");
      player.unMute();
    }
  }
  */
}

function createUI() {
  "use strict";
  var ul = document.getElementById("navigation");
  videos.forEach(function (ignore, i) {
    var li = document.createElement("li");
    li.appendChild(document.createTextNode("Parte " + (i + 1)));
    li.addEventListener("click", changeVideoEvent, false);
    li.num = i;
    ul.appendChild(li);
  });

  // Save the initial Title, as it contains the name of the current song
  videoTitle = document.title;

  document.getElementById("previousButton").addEventListener("click", previousVideo, false);
  document.getElementById("nextButton").addEventListener("click", nextVideo, false);
  document.getElementById("progress").addEventListener("click", progressbarClicked, false);
  document.getElementById("versionCompleta").addEventListener("click", versionCompletaClicked, false);
  document.addEventListener("keydown", keyPressed, false); // Keyboard shortcuts
  updateUI(false);
}

// Perform some initial setup/calculations
determineLocalOrYouTubePlayer();
determineCurrentVideoParameter();
populateProgressArray();
window.onload = function () {
  "use strict";
  createUI();
  if (!localPlayer) {
    setUpYouTubeVideoPlayer();
  } else {
    setUpLocalVideoPlayer();
  }
};

// Detect Back or Forward button
window.onpopstate = function () {
  "use strict";
  // Get the p parameter directly from the URL
  determineCurrentVideoParameter();
  changeVideo(false);
};
<|endoftext|>


<commit_before>/*
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 */

/*
 * Functions detect the context of the menu.
 *
 * The main function is detectContext.
 * The context is detected with detector functions.
 * Detector functions takes a node and returns boolean.
 * Detector functions are applied to the node in sequence and
 * the associated context name of the detector function returning true first
 * is returned.
 * Typical detector function uses XPath returning boolean.
 */

"use strict";

/**
 * @return {function(Node): boolean} A function takes node and returns true iff
 *     the given XPath evalutes true
 * @param {string} xpath A XPath expression to be evaluated.
 */
function xPathDetector(xpath) {
    return function(node) {
        var document = node.ownerDocument || node;
        var documentElement = document.documentElement;
        var namespaceResolver =
            document.createNSResolver(documentElement);

        return document.evaluate(xpath,
                                 node,
                                 namespaceResolver,
                                 XPathResult.BOOLEAN_TYPE,
                                 null).booleanValue;
    };
}

/**
 * @return {boolean} true iff the selection is not empty
 */
function isSelected(node) {
    var document = node.ownerDocument;
    var selection = document.getSelection();

    return !selection.isCollapsed;
}

/**
 * @return {boolean} true iff the given node is in a frame
 */
function isInFrame(node) {
    var document = node.ownerDocument;
    var window = document.defaultView;

    return window.frameElement !== null;
}

/**
 * An array of context definitions.
 *
 * Context definition consists of detector function and context name.
 *
 * @type {Array.<{detector: function(Node): boolean, context: string}>}
 */
var contextDetectors = [
    {detector: isSelected, context: "selection"},
    {detector: xPathDetector("self::img and (ancestor::a[@href] or ancestor::area[@href])"), context: "imageLink"},
    {detector: xPathDetector("ancestor-or-self::a[@href] or ancestor-or-self::area[@href]"), context: "link"},
    {detector: xPathDetector("self::img"), context: "image"},
    {detector: xPathDetector("self::audio"), context: "audio"},
    {detector: xPathDetector("self::video"), context: "video"},
    {detector: xPathDetector("self::input[@type = 'text' or @type = 'password'] or self::textarea"), context: "text"},
    {detector: isInFrame, context: "frame"}
];

/**
 * @return {string} The context of the node.
 * @param {Node} node A node on which the menu opens.
 */
function detectContext(node) {
    for (let detector of contextDetectors) {
        if (detector.detector(node)) {
            return detector.context;
        }
    }

    return "page";
}
<commit_msg>Lowered the priority of the "selection" context.<commit_after>/*
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 */

/*
 * Functions detect the context of the menu.
 *
 * The main function is detectContext.
 * The context is detected with detector functions.
 * Detector functions takes a node and returns boolean.
 * Detector functions are applied to the node in sequence and
 * the associated context name of the detector function returning true first
 * is returned.
 * Typical detector function uses XPath returning boolean.
 */

"use strict";

/**
 * @return {function(Node): boolean} A function takes node and returns true iff
 *     the given XPath evalutes true
 * @param {string} xpath A XPath expression to be evaluated.
 */
function xPathDetector(xpath) {
    return function(node) {
        var document = node.ownerDocument || node;
        var documentElement = document.documentElement;
        var namespaceResolver =
            document.createNSResolver(documentElement);

        return document.evaluate(xpath,
                                 node,
                                 namespaceResolver,
                                 XPathResult.BOOLEAN_TYPE,
                                 null).booleanValue;
    };
}

/**
 * @return {boolean} true iff the selection is not empty
 */
function isSelected(node) {
    var document = node.ownerDocument;
    var selection = document.getSelection();

    return !selection.isCollapsed;
}

/**
 * @return {boolean} true iff the given node is in a frame
 */
function isInFrame(node) {
    var document = node.ownerDocument;
    var window = document.defaultView;

    return window.frameElement !== null;
}

/**
 * An array of context definitions.
 *
 * Context definition consists of detector function and context name.
 *
 * @type {Array.<{detector: function(Node): boolean, context: string}>}
 */
var contextDetectors = [
    {detector: xPathDetector("self::img and (ancestor::a[@href] or ancestor::area[@href])"), context: "imageLink"},
    {detector: xPathDetector("ancestor-or-self::a[@href] or ancestor-or-self::area[@href]"), context: "link"},
    {detector: xPathDetector("self::img"), context: "image"},
    {detector: xPathDetector("self::audio"), context: "audio"},
    {detector: xPathDetector("self::video"), context: "video"},
    {detector: xPathDetector("self::input[@type = 'text' or @type = 'password'] or self::textarea"), context: "text"},
    {detector: isSelected, context: "selection"},
    {detector: isInFrame, context: "frame"}
];

/**
 * @return {string} The context of the node.
 * @param {Node} node A node on which the menu opens.
 */
function detectContext(node) {
    for (let detector of contextDetectors) {
        if (detector.detector(node)) {
            return detector.context;
        }
    }

    return "page";
}
<|endoftext|>


<commit_before>// ==UserScript==
// @name        yakcd
// @namespace   asannou
// @description Yet Another Kindle Cloud Downloader
// @include     https://read.amazon.com/
// @include     https://read.amazon.co.jp/
// @grant       none
// @run-at      document-end
// @license     MIT License
// ==/UserScript==

(function() {

var Indicator = (function() {
  var originalTitle = document.title;
  var value = 0;
  var maximum = 1;
  var getDialog = function() {
    var iframeWindow = $("#KindleLibraryIFrame").get(0).contentWindow;
    return iframeWindow.KindleLibraryProgressDialog;
  };
  var findProgressMessage = function() {
    var iframeDocument = $("#KindleLibraryIFrame").contents();
    return iframeDocument.find("#kindleLibrary_dialog_progressMessage");
  };
  return {
    setMaximum: function(m) {
      maximum = m;
    },
    increment: function() {
      value++;
    },
    display: function() {
      var percent = Math.round(value * 100 / maximum);
      document.title = "[" + percent + "%] " + originalTitle;
      if (!value) {
        this.openDialog();
      }
      getDialog().updateValue(percent);
    },
    incrementAndDisplay: function() {
      this.increment();
      this.display();
    },
    clear: function() {
      value = 0;
      document.title = originalTitle;
      this.closeDialog();
    },
    openDialog: function() {
      getDialog().open(function() {
        location.reload();
      });
      findProgressMessage().hide();
    },
    closeDialog: function() {
      findProgressMessage().show();
      getDialog().close();
    }
  };
})();

var Zipper = (function() {
  var javascript = (function() {
    importScripts("https://asannou.github.io/yakcd/jszip/dist/jszip.min.js");
    var zip = new JSZip();
    this.addEventListener("message", function(e) {
      var id = e.data[0];
      var name = e.data[1];
      var r = zip[name].apply(zip, e.data[2]);
      this.postMessage([id, r instanceof JSZip ? null : r]);
    });
  })
  .toString()
  .match(/{([\d\D]*)}/)[1];
  var blob = new Blob([ javascript ], { type: "text/javascript" });
  var blobURL = window.URL.createObjectURL(blob);
  var worker = new Worker(blobURL);
  var post = function(name) {
    return function() {
      var d = $.Deferred();
      var id = Math.random().toString(36).substr(2);
      var listener = function(e) {
        if (e.data[0] == id) {
          worker.removeEventListener("message", listener);
          return d.resolve(e.data[1]);
        }
      };
      worker.addEventListener("message", listener);
      worker.postMessage([id, name, $.makeArray(arguments)]);
      return d;
    };
  };
  return {
    file: post("file"),
    generate: post("generate")
  };
})();

var Retriable = function(dd) {
  var d = $.Deferred();
  (function n() {
    dd().pipe(d.resolve, function() {
      if (window.confirm("retry? " + this.url)) {
        n();
      } else {
        d.reject();
      }
    });
  })();
  return d;
};

var concurrency = (function() {
  var query = $("<a>", { href: $("script").last().attr("src") })[0].search;
  return query ? query.substr(1) : 6;
})();

var getFileSaver = function() {
  if (typeof saveAs == "undefined") {
    return $.getScript(
      "https://asannou.github.io/yakcd/jszip/vendor/FileSaver.js"
    );
  } else {
    return $.Deferred().resolve();
  }
};

var Yakcd = function(asin) {

var serviceClient = KindleModuleManager
.getModuleSync(KindleModuleManager.SERVICE_CLIENT);

return $.Deferred().resolve()
.pipe(function() {
  Indicator.display();
  return Retriable(function() {
    return serviceClient.startReading({
      asin: asin,
      clientVersion: KindleVersion.getVersionNumber()
    });
  });
})
.pipe(function(book) {
  return Retriable(function() {
    return $.ajax({
      url: book.manifestUrl,
      dataType: "jsonp",
      jsonp: false,
      jsonpCallback: "loadManifest",
      cache: true,
      timeout: 30000
    });
  })
  .pipe(function(manifest) {
    return $.Deferred().resolve(book, manifest);
  });
})
.pipe(function(book, manifest) {
  var ids = $.map(manifest.resourceManifest, function(m, i) {
    var type = m.type.split("/")[0];
    if (type == "image") {
      return i;
    } else {
      return null;
    }
  });
  Indicator.setMaximum(ids.length);
  var slicedIds = [];
  for (var i = 0; i < ids.length / concurrency; i++) {
    slicedIds.push(ids.slice(i * concurrency, (i + 1) * concurrency));
  }
  var d = $.Deferred().resolve();
  $.each(slicedIds, function(i, ids) {
    d = d
    .pipe(function() {
      return Retriable(function() {
        return serviceClient.getFileUrl({
          asin: asin,
          contentVersion: book.contentVersion,
          formatVersion: book.formatVersion,
          kindleSessionId: book.kindleSessionId,
          resourceIds: ids
        });
      });
    })
    .pipe(function(url) {
      return $.when.apply($, $.map(url.resourceUrls, function(u) {
        return Retriable(function() {
          return $.ajax({
            url: u.signedUrl,
            dataType: "jsonp",
            jsonp: false,
            jsonpCallback: "loadResource" + u.id,
            cache: true,
            timeout: 30000
          });
        })
        .pipe(function(resource) {
          var id = ("000" + resource.metadata.id).substr(-4);
          var type = resource.metadata.type.split("/")[1];
          var data = resource.data.split(",")[1];
          return Zipper.file(
            "resource" + id + "." + type,
            data,
            { base64: true }
          ).pipe(function() {
            Indicator.incrementAndDisplay();
            return $.Deferred().resolve();
          });
        });
      }));
    });
  });
  return d;
})
.pipe(getFileSaver)
.pipe(function() {
  return Zipper.generate({ type: "blob" });
})
.done(function(content) {
  saveAs(content, asin + ".zip");
  Indicator.clear();
});

};

var YakcdOffline = function(asin) {

var bookInfoDB = KindleModuleManager
.getModuleSync(KindleModuleManager.DB_CLIENT)
.getBookDb()
.BookInfoDB(asin);

return $.Deferred().resolve()
.pipe(function() {
  Indicator.display();
  return bookInfoDB.getResourceIds();
})
.pipe(function(ids) {
  return bookInfoDB.getResources(ids);
})
.pipe(function(resources) {
  resources = $.map(resources, function(r) {
    var type = r ? r.metadata.type.split("/") : [];
    if (type[0] == "image") {
      var id = ("000" + r.metadata.id).substr(-4);
      r.file = "resource" + id + "." + type[1];
      r.data = r.data.split(",")[1];
      return r;
    } else {
      return null;
    }
  });
  Indicator.setMaximum(resources.length);
  var d = $.Deferred().resolve();
  $.each(resources, function(i, r) {
    d = d
    .pipe(function() {
      return Zipper.file(
        r.file,
        r.data,
        { base64: true }
      ).pipe(function() {
        Indicator.incrementAndDisplay();
        return $.Deferred().resolve();
      });
    });
  });
  return d;
})
.pipe(getFileSaver)
.pipe(function() {
  return Zipper.generate({ type: "blob" });
})
.done(function(content) {
  saveAs(content, asin + ".zip");
  Indicator.clear();
});

};

var appendCssTo = function(head) {
  $("<link/>")
  .attr({
    rel: "stylesheet",
    type: "text/css",
    href: "https://asannou.github.io/yakcd/yakcd.css"
  })
  .appendTo(head);
};

var appendButtonTo = function(container) {
  var asin = container.attr("id");
  var bookImage = container.find(".book_image");
  var offset = bookImage.offset();
  offset.left += bookImage.width() - 16;
  offset.top -= 16;
  var button = $("<div/>")
  .appendTo(container)
  .attr("class", "yakcdButton")
  .css("position", "absolute")
  .offset(offset);
  if (
    container.hasClass("book_is_cached") ||
    container.hasClass("book_is_pinned")
  ) {
    button
    .append($("<div/>").attr("class", "cloudDown"))
    .append($("<div/>").attr("class", "cloudDownArrow offline"))
    .click(function() { YakcdOffline(asin); });
  } else {
    button
    .append($("<div/>").attr("class", "cloudDown"))
    .append($("<div/>").attr("class", "cloudDownArrow"))
    .click(function() { Yakcd(asin); });
  }
};

$.Deferred().resolve()
.pipe(function() {
  var d = $.Deferred();
  var iframe = $("#KindleLibraryIFrame");
  if (iframe.length) {
    d.resolve(iframe);
  } else {
    new MutationObserver(function(mutations) {
      this.disconnect();
      var iframeWindow = mutations[0].addedNodes[0];
      d.resolve($(iframeWindow));
    })
    .observe(
      $("#KindleLibraryContainer")[0],
      { childList: true }
    );
  }
  return d;
})
.pipe(function(iframe) {
  var d = $.Deferred();
  var iframeDocument = iframe.contents();
  if (iframeDocument.find("#titles_inner_wrapper").length) {
    d.resolve(iframeDocument);
  } else {
    iframe.on("load", function() {
      d.resolve(iframe.contents());
    });
  }
  return d;
})
.done(function(iframeDocument) {
  appendCssTo(iframeDocument.find("head"));
  new MutationObserver(function(mutations) {
    mutations.forEach(function(mutation) {
      $.makeArray(mutation.addedNodes).forEach(function(div) {
        appendButtonTo($(div));
      });
    });
  })
  .observe(
    iframeDocument.find("#titles_inner_wrapper")[0],
    { childList: true }
  );
  iframeDocument
  .find(".book_container")
  .each(function() {
    appendButtonTo($(this));
  });
});

})();

<commit_msg>Chrome の UserScript に対応<commit_after>// ==UserScript==
// @name        yakcd
// @namespace   asannou
// @description Yet Another Kindle Cloud Downloader
// @include     https://read.amazon.com/
// @include     https://read.amazon.co.jp/
// @grant       none
// @run-at      document-end
// @license     MIT License
// ==/UserScript==

(function(callback) {
  if (typeof jQuery == "undefined") {
    var script = document.createElement("script");
    script.setAttribute("src", "https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js");
    script.addEventListener("load", function() {
      var script = document.createElement("script");
      script.textContent = "(" + callback.toString() + ")(jQuery.noConflict(true));";
      document.body.appendChild(script);
    }, false);
    document.body.appendChild(script);
  } else {
    callback($);
  }
})(function($) {

var Indicator = (function() {
  var originalTitle = document.title;
  var value = 0;
  var maximum = 1;
  var getDialog = function() {
    var iframeWindow = $("#KindleLibraryIFrame").get(0).contentWindow;
    return iframeWindow.KindleLibraryProgressDialog;
  };
  var findProgressMessage = function() {
    var iframeDocument = $("#KindleLibraryIFrame").contents();
    return iframeDocument.find("#kindleLibrary_dialog_progressMessage");
  };
  return {
    setMaximum: function(m) {
      maximum = m;
    },
    increment: function() {
      value++;
    },
    display: function() {
      var percent = Math.round(value * 100 / maximum);
      document.title = "[" + percent + "%] " + originalTitle;
      if (!value) {
        this.openDialog();
      }
      getDialog().updateValue(percent);
    },
    incrementAndDisplay: function() {
      this.increment();
      this.display();
    },
    clear: function() {
      value = 0;
      document.title = originalTitle;
      this.closeDialog();
    },
    openDialog: function() {
      getDialog().open(function() {
        location.reload();
      });
      findProgressMessage().hide();
    },
    closeDialog: function() {
      findProgressMessage().show();
      getDialog().close();
    }
  };
})();

var Zipper = (function() {
  var javascript = (function() {
    importScripts("https://asannou.github.io/yakcd/jszip/dist/jszip.min.js");
    var zip = new JSZip();
    this.addEventListener("message", function(e) {
      var id = e.data[0];
      var name = e.data[1];
      var r = zip[name].apply(zip, e.data[2]);
      this.postMessage([id, r instanceof JSZip ? null : r]);
    });
  })
  .toString()
  .match(/{([\d\D]*)}/)[1];
  var blob = new Blob([ javascript ], { type: "text/javascript" });
  var blobURL = window.URL.createObjectURL(blob);
  var worker = new Worker(blobURL);
  var post = function(name) {
    return function() {
      var d = $.Deferred();
      var id = Math.random().toString(36).substr(2);
      var listener = function(e) {
        if (e.data[0] == id) {
          worker.removeEventListener("message", listener);
          return d.resolve(e.data[1]);
        }
      };
      worker.addEventListener("message", listener);
      worker.postMessage([id, name, $.makeArray(arguments)]);
      return d;
    };
  };
  return {
    file: post("file"),
    generate: post("generate")
  };
})();

var Retriable = function(dd) {
  var d = $.Deferred();
  (function n() {
    dd().pipe(d.resolve, function() {
      if (window.confirm("retry? " + this.url)) {
        n();
      } else {
        d.reject();
      }
    });
  })();
  return d;
};

var concurrency = (function() {
  var query = $("<a>", { href: $("script").last().attr("src") })[0].search;
  return query ? query.substr(1) : 6;
})();

var getFileSaver = function() {
  if (typeof saveAs == "undefined") {
    return $.getScript(
      "https://asannou.github.io/yakcd/jszip/vendor/FileSaver.js"
    );
  } else {
    return $.Deferred().resolve();
  }
};

var Yakcd = function(asin) {

var serviceClient = KindleModuleManager
.getModuleSync(KindleModuleManager.SERVICE_CLIENT);

return $.Deferred().resolve()
.pipe(function() {
  Indicator.display();
  return Retriable(function() {
    return serviceClient.startReading({
      asin: asin,
      clientVersion: KindleVersion.getVersionNumber()
    });
  });
})
.pipe(function(book) {
  return Retriable(function() {
    return $.ajax({
      url: book.manifestUrl,
      dataType: "jsonp",
      jsonp: false,
      jsonpCallback: "loadManifest",
      cache: true,
      timeout: 30000
    });
  })
  .pipe(function(manifest) {
    return $.Deferred().resolve(book, manifest);
  });
})
.pipe(function(book, manifest) {
  var ids = $.map(manifest.resourceManifest, function(m, i) {
    var type = m.type.split("/")[0];
    if (type == "image") {
      return i;
    } else {
      return null;
    }
  });
  Indicator.setMaximum(ids.length);
  var slicedIds = [];
  for (var i = 0; i < ids.length / concurrency; i++) {
    slicedIds.push(ids.slice(i * concurrency, (i + 1) * concurrency));
  }
  var d = $.Deferred().resolve();
  $.each(slicedIds, function(i, ids) {
    d = d
    .pipe(function() {
      return Retriable(function() {
        return serviceClient.getFileUrl({
          asin: asin,
          contentVersion: book.contentVersion,
          formatVersion: book.formatVersion,
          kindleSessionId: book.kindleSessionId,
          resourceIds: ids
        });
      });
    })
    .pipe(function(url) {
      return $.when.apply($, $.map(url.resourceUrls, function(u) {
        return Retriable(function() {
          return $.ajax({
            url: u.signedUrl,
            dataType: "jsonp",
            jsonp: false,
            jsonpCallback: "loadResource" + u.id,
            cache: true,
            timeout: 30000
          });
        })
        .pipe(function(resource) {
          var id = ("000" + resource.metadata.id).substr(-4);
          var type = resource.metadata.type.split("/")[1];
          var data = resource.data.split(",")[1];
          return Zipper.file(
            "resource" + id + "." + type,
            data,
            { base64: true }
          ).pipe(function() {
            Indicator.incrementAndDisplay();
            return $.Deferred().resolve();
          });
        });
      }));
    });
  });
  return d;
})
.pipe(getFileSaver)
.pipe(function() {
  return Zipper.generate({ type: "blob" });
})
.done(function(content) {
  saveAs(content, asin + ".zip");
  Indicator.clear();
});

};

var YakcdOffline = function(asin) {

var bookInfoDB = KindleModuleManager
.getModuleSync(KindleModuleManager.DB_CLIENT)
.getBookDb()
.BookInfoDB(asin);

return $.Deferred().resolve()
.pipe(function() {
  Indicator.display();
  return bookInfoDB.getResourceIds();
})
.pipe(function(ids) {
  return bookInfoDB.getResources(ids);
})
.pipe(function(resources) {
  resources = $.map(resources, function(r) {
    var type = r ? r.metadata.type.split("/") : [];
    if (type[0] == "image") {
      var id = ("000" + r.metadata.id).substr(-4);
      r.file = "resource" + id + "." + type[1];
      r.data = r.data.split(",")[1];
      return r;
    } else {
      return null;
    }
  });
  Indicator.setMaximum(resources.length);
  var d = $.Deferred().resolve();
  $.each(resources, function(i, r) {
    d = d
    .pipe(function() {
      return Zipper.file(
        r.file,
        r.data,
        { base64: true }
      ).pipe(function() {
        Indicator.incrementAndDisplay();
        return $.Deferred().resolve();
      });
    });
  });
  return d;
})
.pipe(getFileSaver)
.pipe(function() {
  return Zipper.generate({ type: "blob" });
})
.done(function(content) {
  saveAs(content, asin + ".zip");
  Indicator.clear();
});

};

var appendCssTo = function(head) {
  $("<link/>")
  .attr({
    rel: "stylesheet",
    type: "text/css",
    href: "https://asannou.github.io/yakcd/yakcd.css"
  })
  .appendTo(head);
};

var appendButtonTo = function(container) {
  var asin = container.attr("id");
  var bookImage = container.find(".book_image");
  var button = $("<div/>")
  .appendTo(container)
  .attr("class", "yakcdButton")
  .css({
    position: "absolute",
    left: "70%",
    top: "-6px"
  });
  if (
    container.hasClass("book_is_cached") ||
    container.hasClass("book_is_pinned")
  ) {
    button
    .append($("<div/>").attr("class", "cloudDown"))
    .append($("<div/>").attr("class", "cloudDownArrow offline"))
    .click(function() { YakcdOffline(asin); });
  } else {
    button
    .append($("<div/>").attr("class", "cloudDown"))
    .append($("<div/>").attr("class", "cloudDownArrow"))
    .click(function() { Yakcd(asin); });
  }
};

$.Deferred().resolve()
.pipe(function() {
  var d = $.Deferred();
  var iframe = $("#KindleLibraryIFrame");
  if (iframe.length) {
    d.resolve(iframe);
  } else {
    new MutationObserver(function(mutations) {
      this.disconnect();
      var iframeWindow = mutations[0].addedNodes[0];
      d.resolve($(iframeWindow));
    })
    .observe(
      $("#KindleLibraryContainer")[0],
      { childList: true }
    );
  }
  return d;
})
.pipe(function(iframe) {
  var d = $.Deferred();
  var iframeDocument = iframe.contents();
  if (iframeDocument.find("#titles_inner_wrapper").length) {
    d.resolve(iframeDocument);
  } else {
    iframe.on("load", function() {
      d.resolve(iframe.contents());
    });
  }
  return d;
})
.done(function(iframeDocument) {
  appendCssTo(iframeDocument.find("head"));
  new MutationObserver(function(mutations) {
    mutations.forEach(function(mutation) {
      $.makeArray(mutation.addedNodes).forEach(function(div) {
        appendButtonTo($(div));
      });
    });
  })
  .observe(
    iframeDocument.find("#titles_inner_wrapper")[0],
    { childList: true }
  );
  iframeDocument
  .find(".book_container")
  .each(function() {
    appendButtonTo($(this));
  });
});

});

<|endoftext|>