├── .gitignore
├── images
├── logo.jpg
├── download.png
├── icon19.png
├── icon48.png
├── loader.gif
└── background.png
├── background.html
├── README.md
├── tests
├── index.html
├── main.js
├── test.utils.js
├── test.domain.js
├── test.services.js
└── mocha.css
├── scripts
├── domain
│ ├── User.js
│ ├── Cue.js
│ └── CueCategory.js
├── config.js
├── utils
│ ├── Pageable.js
│ ├── mode.js
│ └── GA.js
├── core
│ └── Router.js
├── view
│ ├── View.js
│ ├── Badge.js
│ ├── Version.js
│ ├── TopNavigation.js
│ ├── Message.js
│ ├── CuesView.js
│ └── CategoriesView.js
├── background.js
├── popup.js
├── service
│ ├── UserCueCategoryService.js
│ ├── UserCueService.js
│ ├── UserService.js
│ └── CueCategoryService.js
└── require.js
├── manifest.json
├── popup.html
├── dev
└── build.php
└── styles
└── popup.css
/.gitignore:
--------------------------------------------------------------------------------
1 | /nbproject
2 | .idea
3 | scripts/popup-build-*.js
4 | scripts/background-build-*.js
5 |
--------------------------------------------------------------------------------
/images/logo.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DmitryVarennikov/cuenation-chrome-ext/master/images/logo.jpg
--------------------------------------------------------------------------------
/images/download.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DmitryVarennikov/cuenation-chrome-ext/master/images/download.png
--------------------------------------------------------------------------------
/images/icon19.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DmitryVarennikov/cuenation-chrome-ext/master/images/icon19.png
--------------------------------------------------------------------------------
/images/icon48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DmitryVarennikov/cuenation-chrome-ext/master/images/icon48.png
--------------------------------------------------------------------------------
/images/loader.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DmitryVarennikov/cuenation-chrome-ext/master/images/loader.gif
--------------------------------------------------------------------------------
/background.html:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/images/background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DmitryVarennikov/cuenation-chrome-ext/master/images/background.png
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | CueNation Google Chrome extension
2 | =============
3 |
4 | An implementation for [CueNation API](https://github.com/dVaffection/cuenation-api)
5 |
--------------------------------------------------------------------------------
/tests/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Tests
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/scripts/domain/User.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | define(function () {
4 | function User(token) {
5 | if (! (this instanceof User)) {
6 | throw new Error('`this` must be an instance of domain.User');
7 | }
8 |
9 | this.token = token;
10 | }
11 |
12 | return User;
13 | });
14 |
--------------------------------------------------------------------------------
/scripts/config.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var conf = {
4 | "dev": {
5 | "api-server-url": "http://localhost:8080"
6 | },
7 | "prod": {
8 | "api-server-url": "http://162.243.254.55:8080"
9 | }
10 | };
11 |
12 |
13 | define(['scripts/utils/mode'], function (mode) {
14 | return conf[mode];
15 | });
16 |
--------------------------------------------------------------------------------
/scripts/domain/Cue.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | define(function () {
4 | function Cue(id, title, link, createdAt) {
5 | if (! (this instanceof Cue)) {
6 | throw new Error('`this` must be an instance of domain.Cue');
7 | }
8 |
9 | this.id = id;
10 | this.title = title;
11 | this.link = link;
12 | this.createdAt = new Date(createdAt * 1000);
13 | }
14 |
15 | return Cue;
16 | });
17 |
--------------------------------------------------------------------------------
/scripts/domain/CueCategory.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | define(function () {
4 | function CueCategory(id, name, host, link) {
5 | if (! (this instanceof CueCategory)) {
6 | throw new Error('`this` must be an instance of domain.CueCategory');
7 | }
8 |
9 | this.id = id;
10 | this.name = name;
11 | this.host = host;
12 | this.link = link;
13 | }
14 |
15 | return CueCategory;
16 | });
17 |
--------------------------------------------------------------------------------
/tests/main.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | require.config({
4 | baseUrl: '../',
5 | urlArgs: 'bust=' + (new Date()).getTime()
6 | });
7 |
8 | require(['tests/mocha'], function () {
9 | // mocha is global
10 | mocha.setup('bdd');
11 | mocha.checkLeaks();
12 |
13 | require([
14 | 'tests/test.utils',
15 | 'tests/test.domain',
16 | 'tests/test.services'
17 | ],
18 | function () {
19 | mocha.run();
20 | });
21 | });
22 |
--------------------------------------------------------------------------------
/scripts/utils/Pageable.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | define(function () {
4 | function Pageable(number, totalElements, totalPages, size) {
5 | if (! (this instanceof Pageable)) {
6 | throw new Error('`this` must be an instance of utils.Pageable');
7 | }
8 |
9 | this.number = number;
10 | this.totalElements = totalElements;
11 | this.totalPages = totalPages;
12 | this.size = size || 10;
13 | }
14 |
15 | return Pageable;
16 | });
17 |
--------------------------------------------------------------------------------
/tests/test.utils.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | define(function (require) {
4 |
5 | var chai = require('tests/chai');
6 | var Pageable = require('scripts/utils/Pageable');
7 |
8 | describe('utils', function () {
9 | describe('Pageable', function () {
10 | it('correct object', function () {
11 | var pageable = new Pageable(0, 10, 5);
12 |
13 | chai.assert.equal(pageable.number, 0);
14 | chai.assert.equal(pageable.totalElements, 10);
15 | chai.assert.equal(pageable.totalPages, 5);
16 | chai.assert.equal(pageable.size, 10);
17 | });
18 | });
19 | });
20 |
21 | });
22 |
--------------------------------------------------------------------------------
/scripts/core/Router.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | define(function () {
4 | function Router(cuesView, categoriesView) {
5 | if (! (this instanceof Router)) {
6 | throw new Error('`this` must be an instance of core.Router');
7 | }
8 |
9 | var routes = {
10 | "cues": cuesView.render,
11 | "categories": categoriesView.render
12 | };
13 |
14 | /**
15 | * @param {String} route
16 | * @returns {Function}
17 | */
18 | this.getCallback = function (route) {
19 | if (! routes[route]) {
20 | throw new Error('Unrecognized route "' + route + '"');
21 | }
22 |
23 | return routes[route];
24 | }
25 | }
26 |
27 | return Router;
28 | });
29 |
--------------------------------------------------------------------------------
/scripts/view/View.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | define(function () {
4 | function View(container) {
5 | if (! (this instanceof View)) {
6 | throw new Error('`this` must be an instance of view.View');
7 | }
8 |
9 | this.renderLoader = function () {
10 | var loader = document.createElement('img');
11 | loader.setAttribute('src', 'images/loader.gif');
12 | loader.setAttribute('width', '32');
13 | loader.setAttribute('height', '32');
14 | loader.setAttribute('id', 'loader');
15 |
16 | container.innerHTML = '';
17 | container.appendChild(loader);
18 | }
19 |
20 | this.render = function () {
21 | throw new Error('Not implemented!');
22 | }
23 | }
24 |
25 | return View;
26 | });
27 |
--------------------------------------------------------------------------------
/scripts/view/Badge.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | define(function () {
4 | function Badge(userCueService) {
5 | if (!(this instanceof Badge)) {
6 | throw new Error('`this` must be an instance of view.Badge');
7 | }
8 |
9 | this.render = function (user) {
10 | userCueService.get(user.token, 0, function (err, cues, pageable) {
11 | var text;
12 | if (err) {
13 | text = 'err';
14 | } else {
15 | text = pageable.totalElements ? pageable.totalElements.toString() : '';
16 | }
17 |
18 | chrome.browserAction.setBadgeText({text: text});
19 | chrome.browserAction.setBadgeBackgroundColor({color: "#807A60"});
20 | });
21 | }
22 | }
23 |
24 | return Badge;
25 | });
26 |
--------------------------------------------------------------------------------
/scripts/utils/mode.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | function getMode() {
4 | var mode;
5 |
6 | if (!mode) {
7 | var xhr = new XMLHttpRequest();
8 | xhr.open("GET", chrome.runtime.getURL('manifest.json'), false);
9 | xhr.send();
10 |
11 | if (xhr.status === 200) {
12 | var json = JSON.parse(xhr.responseText);
13 | // Chrome Web Store adds `update_url` when you upload your extension.
14 | // `http://stackoverflow.com/a/12833511/407986`
15 | mode = 'update_url' in json ? "prod" : "dev";
16 | }
17 | }
18 |
19 | return mode;
20 | }
21 |
22 | define(function () {
23 | return getMode();
24 | });
25 |
26 | //define(function (callback) {
27 | // chrome.management.get(chrome.runtime.id, function (extensionInfo) {
28 | // var env = extensionInfo.installType === 'development' ? 'dev' : 'prod';
29 | //
30 | // callback(conf[env]);
31 | // });
32 | //});
--------------------------------------------------------------------------------
/scripts/utils/GA.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | function GA(mode) {
4 | if (!(this instanceof GA)) {
5 | throw new Error('`this` must be an instance of utils.GA');
6 | }
7 |
8 | if ('prod' === mode) {
9 | _gaq.push(['_setAccount', 'UA-10762441-2']);
10 | }
11 |
12 | this.trackPageview = function () {
13 | if ('prod' === mode) {
14 | _gaq.push(['_trackPageview']);
15 | }
16 | }
17 |
18 | this.trackClickEvent = function (name) {
19 | if ('prod' === mode) {
20 | _gaq.push(['_trackEvent', name, 'clicked']);
21 | }
22 | }
23 | }
24 |
25 |
26 | define(['google-analytics', 'scripts/utils/mode'], function (_, mode) {
27 |
28 | var ga;
29 |
30 | return {
31 | getInstance: function () {
32 | if (!(ga instanceof GA)) {
33 | ga = new GA(mode);
34 | }
35 |
36 | return ga;
37 | }
38 | };
39 |
40 | });
--------------------------------------------------------------------------------
/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "CueNation",
3 | "version": "0.3.1",
4 | "manifest_version": 2,
5 | "description": "Let you subscribe and track new cue files of your desired radio shows on CueNation.com.",
6 | "icons": {
7 | "48": "images/icon48.png"
8 | },
9 | "browser_action": {
10 | "default_icon": "images/icon19.png",
11 | "default_title": "CueNation",
12 | "default_popup": "popup.html"
13 | },
14 | "background": {
15 | "page": "background.html"
16 | },
17 | "permissions": [
18 | "storage",
19 | "http://localhost:8080/*",
20 | "http://162.243.254.55:8080/*"
21 | ],
22 | "content_security_policy": "script-src 'self' https://ssl.google-analytics.com; object-src 'self'",
23 | "web_accessible_resources": [
24 | "manifest.json"
25 | ]
26 | }
--------------------------------------------------------------------------------
/scripts/view/Version.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | define(function () {
4 | function Version() {
5 | if (!(this instanceof Version)) {
6 | throw new Error('`this` must be an instance of view.Version');
7 | }
8 |
9 | function getVersion(callback) {
10 | var req = new XMLHttpRequest();
11 | req.open("GET", chrome.runtime.getURL('manifest.json'), true);
12 | req.onreadystatechange = function () {
13 | if (4 === req.readyState) {
14 | if (200 === req.status) {
15 | var json = JSON.parse(req.responseText);
16 |
17 | callback(json.version);
18 | }
19 | }
20 | }
21 | req.send();
22 | }
23 |
24 | getVersion(function (version) {
25 | var container = document.getElementById('version');
26 | container.innerText = version;
27 | });
28 | }
29 |
30 | return Version;
31 | });
32 |
--------------------------------------------------------------------------------
/popup.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |

17 |
18 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/scripts/background.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | require.config({
4 | baseUrl: '..',
5 | paths: {
6 | "google-analytics": [
7 | 'https://ssl.google-analytics.com/ga'
8 | ]
9 | }
10 | });
11 |
12 | require([
13 | 'scripts/utils/GA',
14 | 'scripts/service/UserService',
15 | 'scripts/service/UserCueService',
16 | 'scripts/view/Badge'
17 | ],
18 | function (GA, UserService, UserCueService, Badge) {
19 | // var ga = GA.getInstance();
20 | // ga.trackPageview();
21 |
22 |
23 | function start(err, user) {
24 | var userCueService = new UserCueService(),
25 | badge = new Badge(userCueService);
26 |
27 | badge.render(user);
28 | // once a minute is enough
29 | setInterval(function (badge, user) {
30 | badge.render(user);
31 | }, 60000, badge, user);
32 | }
33 |
34 | // "8c9f8cf4-1689-48ab-bf53-ee071a377f60"
35 | var userService = new UserService();
36 | userService.init(chrome.storage.sync, start);
37 | });
38 |
--------------------------------------------------------------------------------
/scripts/view/TopNavigation.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | define(function () {
4 | function TopNavigation(router, ga) {
5 | if (!(this instanceof TopNavigation)) {
6 | throw new Error('`this` must be an instance of view.TopNavigation');
7 | }
8 |
9 | var forEach = Array.prototype.forEach;
10 | // listen to navigation links click event
11 | forEach.call(document.getElementById('menu').querySelectorAll('a.inner'), function (el) {
12 | el.addEventListener('click', function (e) {
13 | e.preventDefault();
14 |
15 | // track clicks with GA
16 | ga.trackClickEvent(this.getAttribute('href'));
17 |
18 |
19 | forEach.call(document.getElementById('menu').children, function (el) {
20 | el.removeAttribute('class');
21 | });
22 | el.parentNode.setAttribute('class', 'active');
23 |
24 | var callback = router.getCallback(this.getAttribute('href'));
25 | callback();
26 | });
27 | });
28 |
29 | window.addEventListener('scroll', function () {
30 | var menu = document.getElementById('menu');
31 |
32 | if (document.body.scrollTop >= 110) {
33 | menu.setAttribute('class', 'clear menu-fixed-to-top');
34 | } else {
35 | menu.setAttribute('class', 'clear ');
36 | }
37 | });
38 | }
39 |
40 | return TopNavigation;
41 | });
42 |
--------------------------------------------------------------------------------
/scripts/view/Message.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | define(function () {
4 | function Message() {
5 | if (! (this instanceof Message)) {
6 | throw new Error('`this` must be an instance of view.Message');
7 | }
8 |
9 | var WINDOW_WIDTH = 630;
10 |
11 | this.show = function (status, message) {
12 | var messageEl = document.getElementById('flash-message');
13 | messageEl.setAttribute('class', status);
14 | messageEl.innerText = message;
15 | messageEl.style.display = 'block';
16 |
17 | // center messageEl depending on its width
18 | var leftOffset = WINDOW_WIDTH / 2 - Math.round(messageEl.offsetWidth / 2);
19 | messageEl.style.left = leftOffset + 'px';
20 | // and always display on top regardless of the scroll position
21 | messageEl.style.top = document.body.scrollTop + 'px';
22 |
23 |
24 | setTimeout(function () {
25 | messageEl.innerText = '';
26 | messageEl.setAttribute('class', '');
27 | messageEl.style.display = 'none';
28 | }, 3000);
29 |
30 | window.addEventListener('scroll', function () {
31 | var messageEl = document.getElementById('flash-message');
32 | if (messageEl.style.display !== 'none') {
33 | messageEl.style.top = document.body.scrollTop + 'px';
34 | }
35 | });
36 | }
37 | }
38 |
39 | Message.status = {
40 | ERROR: 'error',
41 | INFO: 'info'
42 | };
43 |
44 | return Message;
45 | });
46 |
--------------------------------------------------------------------------------
/tests/test.domain.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | define(function (require) {
4 |
5 | var chai = require('tests/chai');
6 | var User = require('scripts/domain/User'),
7 | Cue = require('scripts/domain/Cue'),
8 | CueCategory = require('scripts/domain/CueCategory');
9 |
10 | describe('domain', function () {
11 | describe('User', function () {
12 | it('correct object', function () {
13 | var user = new User('8c9f8cf4-1689-48ab-bf53-ee071a377f60');
14 |
15 | chai.assert.equal(user.token, '8c9f8cf4-1689-48ab-bf53-ee071a377f60');
16 | });
17 | });
18 | describe('Cue', function () {
19 | it('correct object', function () {
20 | var id = '53ef0f3844ae8cebf6152396',
21 | title = 'Bryan Kearney - KEARNAGE 060 (2014-08-05)',
22 | link = 'http://cuenation.com?page=tracklist&folder=kearnage&filename=Bryan+Kearney+-+KEARNAGE+060.cue',
23 | createdAt = 1407353640;
24 |
25 | var cue = new Cue(id, title, link, createdAt);
26 |
27 | chai.assert.equal(cue.id, id);
28 | chai.assert.equal(cue.title, title);
29 | chai.assert.equal(cue.link, link);
30 | chai.assert.instanceOf(cue.createdAt, Date);
31 | // yes, in milliseconds
32 | chai.assert.equal(cue.createdAt.getTime(), createdAt * 1000);
33 | });
34 | });
35 | describe('CueCategory', function () {
36 | it('correct object', function () {
37 | var id = '53e5bc5b837125a9f6149e4b',
38 | name = '#goldrushRADIO',
39 | host = 'with Ben Gold',
40 | link = 'http://cuenation.com/?page=cues&folder=goldrushradio';
41 |
42 | var cueCategory = new CueCategory(id, name, host, link);
43 |
44 | chai.assert.equal(cueCategory.id, id);
45 | chai.assert.equal(cueCategory.name, name);
46 | chai.assert.equal(cueCategory.host, host);
47 | chai.assert.equal(cueCategory.link, link);
48 | });
49 | });
50 | });
51 |
52 | });
53 |
--------------------------------------------------------------------------------
/scripts/popup.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | require.config({
4 | baseUrl: '..',
5 | paths: {
6 | "google-analytics": [
7 | 'https://ssl.google-analytics.com/ga'
8 | ]
9 | }
10 | });
11 |
12 | require([
13 | 'scripts/utils/GA',
14 | 'scripts/domain/User',
15 | 'scripts/core/Router',
16 | 'scripts/view/Version',
17 | 'scripts/view/TopNavigation',
18 | 'scripts/view/Message',
19 | 'scripts/view/CuesView',
20 | 'scripts/view/CategoriesView',
21 | 'scripts/service/UserService',
22 | 'scripts/service/UserCueService',
23 | 'scripts/service/CueCategoryService',
24 | 'scripts/service/UserCueCategoryService'
25 | ],
26 | function (GA, User, Router, Version, TopNavigation, MessageView, CuesView, CategoriesView, UserService, UserCueService, CueCategoryService, UserCueCategoryService) {
27 | function start(err, user) {
28 | var ga = GA.getInstance();
29 | ga.trackPageview();
30 |
31 |
32 | var versionView = new Version(),
33 | messageView = new MessageView(),
34 | userCueService = new UserCueService(),
35 | cueCategoryService = new CueCategoryService(),
36 | userCueCategoryService = new UserCueCategoryService(),
37 | container = document.getElementsByClassName('page-content').item(0);
38 |
39 |
40 | var cuesView = new CuesView(container, messageView, user, userCueService, userCueCategoryService),
41 | categoriesView = new CategoriesView(container, messageView, user, userCueService, cueCategoryService, userCueCategoryService),
42 | router = new Router(cuesView, categoriesView),
43 | nav = new TopNavigation(router, ga);
44 |
45 |
46 | if (err) {
47 | messageView.show('error', err);
48 | } else {
49 | // manually click first page
50 | document.querySelector('#menu .landing').click();
51 | }
52 | }
53 |
54 |
55 | // "8c9f8cf4-1689-48ab-bf53-ee071a377f60"
56 | var userService = new UserService();
57 | userService.init(chrome.storage.sync, start);
58 | });
59 |
--------------------------------------------------------------------------------
/scripts/service/UserCueCategoryService.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | define(['scripts/config', 'scripts/domain/CueCategory'], function (config, CueCategory) {
4 | function UserCueCategoryService() {
5 | if (! (this instanceof UserCueCategoryService)) {
6 | throw new Error('`this` must be an instance of service.UserCueCategoryService');
7 | }
8 |
9 | var userCueCategoryService = this;
10 | var baseUrl = config['api-server-url'];
11 |
12 | /**
13 | * @param {String} token
14 | * @param {Function} callback({Error}, {domain.CueCategory[]})
15 | */
16 | this.get = function (token, callback) {
17 | var req = new XMLHttpRequest();
18 | req.open('GET', baseUrl + '/user-tokens/' + token + '/cue-categories', true);
19 | req.onreadystatechange = function () {
20 | if (4 === req.readyState) {
21 | if (200 === req.status) {
22 | var response = JSON.parse(req.response);
23 | var cueCategories = [];
24 |
25 | var cueCategoriesData = response._embedded && response._embedded.userCueCategories || [];
26 | for (var i = 0; i < cueCategoriesData.length; i ++) {
27 | cueCategories[i] = new CueCategory(
28 | cueCategoriesData[i].id,
29 | cueCategoriesData[i].name,
30 | cueCategoriesData[i].host,
31 | cueCategoriesData[i].link);
32 | }
33 |
34 | callback(null, cueCategories);
35 | } else {
36 | callback(Error(req.statusText));
37 | }
38 | }
39 | }
40 | req.send(null);
41 | }
42 |
43 | /**
44 | * @param {String} token
45 | * @param {String[]} ids
46 | * @param {Function} callback({Error})
47 | */
48 | this.put = function (token, ids, callback) {
49 | var req = new XMLHttpRequest();
50 | req.open('PUT', baseUrl + '/user-tokens/' + token + '/cue-categories', true);
51 | req.onreadystatechange = function () {
52 | if (4 === req.readyState) {
53 | if (200 === req.status) {
54 | callback(null);
55 | } else {
56 | callback(Error(req.statusText));
57 | }
58 | }
59 | }
60 | req.setRequestHeader('Content-Type', 'application/json');
61 | req.send(JSON.stringify({ids: ids}));
62 | };
63 | }
64 |
65 | return UserCueCategoryService;
66 | });
67 |
--------------------------------------------------------------------------------
/scripts/service/UserCueService.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | define(['scripts/config', 'scripts/domain/Cue', 'scripts/utils/Pageable'], function (config, Cue, Pageable) {
4 | function UserCueService() {
5 | if (! (this instanceof UserCueService)) {
6 | throw new Error('`this` must be an instance of service.UserCueService');
7 | }
8 |
9 | var userCueService = this;
10 | var baseUrl = config['api-server-url'];
11 |
12 | /**
13 | * @param {String} token
14 | * @param {Number} page
15 | * @param {Function} callback({Error}, {domain.Cue[]}, {utils.Pageable})
16 | */
17 | this.get = function (token, page, callback) {
18 | var req = new XMLHttpRequest();
19 | req.open('GET', baseUrl + '/user-tokens/' + token + '/cues?page=' + page + '&size=50', true);
20 | req.onreadystatechange = function () {
21 | if (4 === req.readyState) {
22 | if (200 === req.status) {
23 | var response = JSON.parse(req.response);
24 | var cues = [];
25 |
26 | var cuesData = response._embedded && response._embedded.userCues || [];
27 | for (var i = 0; i < cuesData.length; i ++) {
28 | cues[i] = new Cue(cuesData[i].id, cuesData[i].title, cuesData[i].link,
29 | cuesData[i].createdAt);
30 | }
31 |
32 | var pageable = new Pageable(response.page.number, response.page.totalElements,
33 | response.page.totalPages, response.page.size);
34 |
35 | callback(null, cues, pageable);
36 | } else {
37 | callback(Error(req.statusText));
38 | }
39 | }
40 | }
41 | req.send(null);
42 | }
43 |
44 | /**
45 | * @param {String} token
46 | * @param {String[]} ids
47 | * @param {Function} callback({Error})
48 | */
49 | this.put = function (token, ids, callback) {
50 | var req = new XMLHttpRequest();
51 | req.open('PUT', baseUrl + '/user-tokens/' + token + '/cues', true);
52 | req.onreadystatechange = function () {
53 | if (4 === req.readyState) {
54 | if (200 === req.status) {
55 | callback(null);
56 | } else {
57 | callback(Error(req.statusText));
58 | }
59 | }
60 | }
61 | req.setRequestHeader('Content-Type', 'application/json');
62 | req.send(JSON.stringify({ids: ids}));
63 | };
64 | }
65 |
66 | return UserCueService;
67 | });
68 |
--------------------------------------------------------------------------------
/scripts/service/UserService.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | define(['scripts/config', 'scripts/domain/User'], function (config, User) {
4 | function UserService() {
5 | if (! (this instanceof UserService)) {
6 | throw new Error('`this` must be an instance of service.UserService');
7 | }
8 |
9 | var userService = this;
10 | var baseUrl = config['api-server-url'];
11 |
12 | /**
13 | * @param {Function} callback({Error}, {domain.User})
14 | */
15 | this.post = function (callback) {
16 | var req = new XMLHttpRequest();
17 | req.open('POST', baseUrl + '/user-tokens', true);
18 | req.onreadystatechange = function () {
19 | if (4 === req.readyState) {
20 | if (201 === req.status) {
21 | var url = req.getResponseHeader('Location');
22 | var re = /([a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12})/;
23 | if (null === url.match(re)) {
24 | callback(Error("Can not find user token in the URL: " + url));
25 | } else {
26 | var token = url.match(re)[0];
27 | userService.get(token, callback);
28 | }
29 | } else {
30 | callback(Error(req.statusText));
31 | }
32 | }
33 | };
34 | req.send(null);
35 | }
36 |
37 | /**
38 | * @param {String} token
39 | * @param {Function} callback({Error}, {domain.User})
40 | */
41 | this.get = function (token, callback) {
42 | var req = new XMLHttpRequest();
43 | req.open('GET', baseUrl + '/user-tokens/' + token, true);
44 | req.onreadystatechange = function () {
45 | if (4 === req.readyState) {
46 | if (200 === req.status) {
47 | var response = JSON.parse(req.response);
48 | var user = new User(response.token);
49 | callback(null, user);
50 | } else {
51 | callback(Error(req.statusText));
52 | }
53 | }
54 | }
55 | req.send(null);
56 | }
57 |
58 | /**
59 | * @param {Object} storage - chrome.storage.sync (see https://developer.chrome.com/extensions/storage)
60 | * @param {Function} callback({Error}, {domain.User})
61 | */
62 | this.init = function (storage, callback) {
63 | var user;
64 |
65 | storage.get('token', function (obj) {
66 | if (obj.token) {
67 | user = new User(obj.token);
68 | callback(null, user);
69 | } else {
70 | userService.post(function (err, user) {
71 | if (err) {
72 | callback(err);
73 | } else {
74 | storage.set({"token": user.token}, function () {
75 | callback(null, user);
76 | });
77 | }
78 | });
79 | }
80 | });
81 | }
82 | }
83 |
84 | return UserService;
85 | });
86 |
87 |
--------------------------------------------------------------------------------
/dev/build.php:
--------------------------------------------------------------------------------
1 | ', $argv[0]), PHP_EOL;
11 | exit;
12 | }
13 |
14 | $outputDir = rtrim($argv[1], '/');
15 |
16 | createBuild();
17 | copyFiles($outputDir);
18 | alterContent($outputDir);
19 | removeBuild();
20 |
21 |
22 | function isRoot()
23 | {
24 | return file_exists(getcwd() . '/manifest.json');
25 | }
26 |
27 | function execute($cmd)
28 | {
29 | $handle = popen($cmd, 'r');
30 | if (false !== $handle) {
31 | while (false !== ($buffer = fgets($handle))) {
32 | echo $buffer;
33 | }
34 | $statusCode = pclose($handle);
35 | if (0 !== $statusCode) {
36 | echo 'Error while opening process file pointer, status code: ', $statusCode, PHP_EOL;
37 | exit(1);
38 | }
39 | } else {
40 | echo 'Failed to execute cmd: "', $cmd, '"', PHP_EOL;
41 | exit(1);
42 | }
43 | }
44 |
45 | function createBuild()
46 | {
47 | $cmd = sprintf('r.js -o baseUrl=. name=scripts/popup out=scripts/popup-build-%s.js paths.google-analytics=empty:', date('Y-m-d'));
48 | execute($cmd);
49 |
50 | $cmd = sprintf(
51 | 'r.js -o baseUrl=. cssIn=styles/popup.css out=styles/popup-build-%s.css optimizeCss=standard', date('Y-m-d')
52 | );
53 | execute($cmd);
54 |
55 | $cmd = sprintf('r.js -o baseUrl=. name=scripts/background out=scripts/background-build-%s.js paths.google-analytics=empty:', date('Y-m-d'));
56 | execute($cmd);
57 | }
58 |
59 | function copyFiles($outputDir)
60 | {
61 | // images
62 | $cmd = sprintf('rm -rf %s/*', $outputDir);
63 | execute($cmd);
64 |
65 | $cmd = 'cp -r images ' . $outputDir;
66 | execute($cmd);
67 |
68 | // styles
69 | $cmd = sprintf('mkdir %s/styles', $outputDir);
70 | execute($cmd);
71 |
72 | $cmd = sprintf('cp styles/popup-build-%s.css %s/styles/', date('Y-m-d'), $outputDir);
73 | execute($cmd);
74 |
75 | // scripts
76 | $cmd = sprintf('mkdir %s/scripts', $outputDir);
77 | execute($cmd);
78 |
79 | $cmd = sprintf('cp scripts/require.js %s/scripts/', $outputDir);
80 | execute($cmd);
81 |
82 | $cmd = sprintf('cp scripts/popup-build-%s.js %s/scripts/', date('Y-m-d'), $outputDir);
83 | execute($cmd);
84 |
85 | $cmd = sprintf('cp scripts/background-build-%s.js %s/scripts/', date('Y-m-d'), $outputDir);
86 | execute($cmd);
87 |
88 | // the rest
89 | $cmd = 'cp popup.html ' . $outputDir;
90 | execute($cmd);
91 |
92 | $cmd = 'cp background.html ' . $outputDir;
93 | execute($cmd);
94 |
95 | $cmd = 'cp manifest.json ' . $outputDir;
96 | execute($cmd);
97 | }
98 |
99 | function alterContent($outputDir)
100 | {
101 | $change = function ($filename, $searchContent, $replaceContent) {
102 | $content = file_get_contents($filename);
103 | $content = str_replace($searchContent, $replaceContent, $content);
104 | file_put_contents($filename, $content);
105 | };
106 |
107 |
108 | $change(
109 | $outputDir . '/popup.html', 'data-main="scripts/popup"',
110 | sprintf('data-main="scripts/popup-build-%s"', date('Y-m-d'))
111 | );
112 |
113 | $change(
114 | $outputDir . '/popup.html', 'href="styles/popup.css"',
115 | sprintf('href="styles/popup-build-%s.css"', date('Y-m-d'))
116 | );
117 |
118 | $change(
119 | $outputDir . '/background.html', 'data-main="scripts/background"',
120 | sprintf('data-main="scripts/background-build-%s"', date('Y-m-d'))
121 | );
122 |
123 | // let's put it aside as we can't properly test packed extensions locally
124 | // $change($outputDir . '/manifest.json', '"http://localhost:8080/*",', '');
125 | }
126 |
127 | function removeBuild()
128 | {
129 | $cmd = sprintf('rm scripts/popup-build-%s.js', date('Y-m-d'));
130 | execute($cmd);
131 |
132 | $cmd = sprintf('rm styles/popup-build-%s.css', date('Y-m-d'));
133 | execute($cmd);
134 |
135 | $cmd = sprintf('rm scripts/background-build-%s.js', date('Y-m-d'));
136 | execute($cmd);
137 | }
138 |
--------------------------------------------------------------------------------
/scripts/service/CueCategoryService.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | define(['scripts/config', 'scripts/domain/CueCategory'], function (config, CueCategory) {
4 | function CueCategoryService() {
5 | if (!(this instanceof CueCategoryService)) {
6 | throw new Error('`this` must be an instance of service.CueCategoryService');
7 | }
8 |
9 | var cueCategoryService = this;
10 | var baseUrl = config['api-server-url'];
11 |
12 | /**
13 | * @param {Function} callback({Error}, {domain.CueCategory[]})
14 | */
15 | this.get = function (callback) {
16 | var storage = chrome.storage.local;
17 |
18 | storage.get('cue-categories-e-tag', function (obj) {
19 | var eTag = '';
20 | if (obj['cue-categories-e-tag']) {
21 | eTag = obj['cue-categories-e-tag'];
22 | }
23 |
24 | get(eTag, function (err, cueCategories, eTag) {
25 | if (err) {
26 | callback(err);
27 | // categories changes, let's re-cache them along with the new ETag
28 | } else if (cueCategories.length) {
29 | if (eTag) {
30 | setCache(cueCategories, eTag, function () {
31 | callback(null, cueCategories);
32 | });
33 | } else {
34 | console.error('ETag was not set');
35 | callback(null, cueCategories);
36 | }
37 | } else {
38 | // if categories didn't come then they didn't change, let's fetch them from cache
39 | storage.get('cue-categories', function (obj) {
40 | if (obj['cue-categories']) {
41 | callback(null, obj['cue-categories']);
42 | } else {
43 | // though if for some reason they don't exist in our cache let's fetch them from the server
44 | get('', function (err, cueCategories) {
45 | if (err) {
46 | callback(err);
47 | } else {
48 | setCache(cueCategories, eTag, function () {
49 | callback(null, cueCategories);
50 | });
51 | }
52 | });
53 | }
54 | });
55 | }
56 | });
57 | });
58 |
59 |
60 | function setCache(cueCategories, eTag, callback) {
61 | storage.set({'cue-categories-e-tag': eTag}, function () {
62 | storage.set({'cue-categories': cueCategories}, function () {
63 | callback();
64 | });
65 | });
66 | }
67 |
68 | /**
69 | * @param {String} ifNonMatch
70 | * @param {Function} callback({Error}, {domain.CueCategory[]}, {String})
71 | */
72 | function get(ifNonMatch, callback) {
73 | var req = new XMLHttpRequest();
74 | req.open('GET', baseUrl + '/cue-categories', true);
75 | req.setRequestHeader('If-None-Match', ifNonMatch);
76 | req.onreadystatechange = function () {
77 | if (4 === req.readyState) {
78 | var cueCategories = [],
79 | eTag = null;
80 |
81 | if (200 === req.status) {
82 | eTag = req.getResponseHeader('ETag');
83 |
84 | var response = JSON.parse(req.response);
85 | var cueCategoriesData = response._embedded && response._embedded.cueCategories || [];
86 | for (var i = 0; i < cueCategoriesData.length; i++) {
87 | cueCategories[i] = new CueCategory(
88 | cueCategoriesData[i].id,
89 | cueCategoriesData[i].name,
90 | cueCategoriesData[i].host,
91 | cueCategoriesData[i].link);
92 | }
93 |
94 | callback(null, cueCategories, eTag);
95 | } else if (304 === req.status) {
96 | callback(null, cueCategories, eTag);
97 | } else {
98 | callback(Error(req.statusText));
99 | }
100 | }
101 | };
102 | req.send(null);
103 | }
104 | }
105 | }
106 |
107 | return CueCategoryService;
108 | });
109 |
--------------------------------------------------------------------------------
/tests/test.services.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | define(function (require) {
4 | // mocha.timeout(0);
5 |
6 | var chai = require('tests/chai');
7 | var User = require('scripts/domain/User'),
8 | Cue = require('scripts/domain/Cue'),
9 | CueCategory = require('scripts/domain/CueCategory'),
10 | UserService = require('scripts/service/UserService'),
11 | CueCategoryService = require('scripts/service/CueCategoryService'),
12 | UserCueCategoryService = require('scripts/service/UserCueCategoryService'),
13 | UserCueService = require('scripts/service/UserCueService');
14 |
15 | describe('service', function () {
16 | var token,
17 | cueCategoryIds = [];
18 |
19 | describe('CueCategoryService', function () {
20 | it('get', function (done) {
21 |
22 | var cueCategoryService = new CueCategoryService();
23 | cueCategoryService.get(function (err, cueCategories) {
24 | chai.assert.isNull(err, err);
25 | chai.assert.isArray(cueCategories);
26 |
27 | // after json serialization CueCategory object becomes a plain hash
28 | // chai.assert.instanceOf(cueCategories[0], CueCategory);
29 |
30 | for (var i = 0; i < 5; i++) {
31 | cueCategoryIds[i] = cueCategories[i].id;
32 | }
33 |
34 | done();
35 | });
36 | });
37 | });
38 |
39 | describe('UserService', function () {
40 | it('post and get', function (done) {
41 | var userService = new UserService();
42 | userService.post(function (err, user) {
43 | chai.assert.isNull(err, err);
44 | chai.assert.instanceOf(user, User);
45 |
46 | token = user.token;
47 |
48 | userService.get(user.token, function (err, user) {
49 | chai.assert.isNull(err, err);
50 | chai.assert.instanceOf(user, User);
51 |
52 | done();
53 | });
54 | });
55 | });
56 | });
57 |
58 | describe('UserCueCategoryService', function () {
59 | it('put and get', function (done) {
60 |
61 | var userCueCategoryService = new UserCueCategoryService();
62 |
63 | userCueCategoryService.put(token, cueCategoryIds, function (err) {
64 | chai.assert.isNull(err, err);
65 |
66 | userCueCategoryService.get(token, function (err, cueCategories) {
67 | chai.assert.isNull(err, err);
68 | chai.assert.isArray(cueCategories);
69 |
70 | var mustBeEmpty = cueCategoryIds;
71 | var index;
72 | for (var i = 0; i < cueCategories.length; i++) {
73 | index = mustBeEmpty.indexOf(cueCategories[i].id);
74 | if (-1 === index) {
75 | chai.assert.ok(false);
76 | } else {
77 | mustBeEmpty.splice(index, 1);
78 | }
79 | }
80 |
81 | chai.assert(0 === mustBeEmpty.length);
82 |
83 |
84 | done();
85 | });
86 | });
87 |
88 | });
89 | });
90 |
91 | describe('UserCueService', function () {
92 | it('get and put', function (done) {
93 |
94 | var userCueService = new UserCueService();
95 |
96 | userCueService.get(token, 0, function (err, cues) {
97 | chai.assert.isNull(err, err);
98 | chai.assert.isArray(cues);
99 |
100 | var initialNumberOfCues = cues.length;
101 | if (initialNumberOfCues < 2) {
102 | chai.assert(false, 'Can not test `userCueService.put` as we don\'t obtain enough cues ' +
103 | 'from the server in the first place');
104 | }
105 |
106 | var cueIds = [cues[0].id];
107 | userCueService.put(token, cueIds, function (err) {
108 | chai.assert.isNull(err, err);
109 |
110 | userCueService.get(token, 0, function (err, cues) {
111 | chai.assert.isNull(err, err);
112 |
113 | for (var i = 0; i < cues.length; i++) {
114 | if (cueIds.indexOf(cues[i].id) > -1) {
115 | chai.assert(false, 'We got a cue from server which we marked as viewed!');
116 | }
117 | }
118 |
119 | done();
120 | });
121 | });
122 | });
123 | });
124 | });
125 | });
126 |
127 | });
128 |
129 |
--------------------------------------------------------------------------------
/styles/popup.css:
--------------------------------------------------------------------------------
1 | .clear:after {
2 | content: " ";
3 | display: block;
4 | height: 0;
5 | clear: both;
6 | overflow: hidden;
7 | visibility: hidden;
8 | }
9 |
10 | body {
11 | background: #FDF5EA url("../images/background.png");
12 | font-family: Verdana, Sans-serif;
13 | font-size: 12px;
14 | color: #333333;
15 | }
16 |
17 | h1 {
18 | font-size: 18px;
19 | margin: 0;
20 | }
21 |
22 | ul {
23 | margin: 0;
24 | padding: 0;
25 | }
26 |
27 | #flash-message {
28 | position: absolute;
29 | padding: 5px;
30 | display: none;
31 | z-index: 1;
32 | }
33 |
34 | #flash-message.info {
35 | color: #C09853;
36 | background-color: #FCF8E3;
37 | border: 1px solid #FBEED5;
38 | }
39 |
40 | #flash-message.error {
41 | color: #c01711;
42 | background-color: #fcd3db;
43 | border: 1px solid #fb879b;
44 | }
45 |
46 | #wrapper {
47 | background-color: #FFFFFF;
48 | padding: 5px;
49 | width: 600px;
50 | box-shadow: 5px 5px 5px #888888;
51 | }
52 |
53 | #version {
54 | position: absolute;
55 | top: 20px;
56 | left: 570px;
57 | padding-right: 1px;
58 | font-weight: bold;
59 | color: #807A60;
60 | box-shadow: 2px 2px 5px #888888;
61 | }
62 |
63 | #logo {
64 | margin: 0;
65 | display: block;
66 | }
67 |
68 | #menu-wrapper {
69 | height: 44px;
70 | }
71 |
72 | #menu {
73 | display: block;
74 | margin: 0;
75 | padding: 5px 0;
76 | width: 600px;
77 | background-color: #FFFFFF;
78 | border-bottom: solid 1px #EADDBB;
79 | }
80 |
81 | #menu li {
82 | float: left;
83 | list-style: none;
84 | background-color: #B7AF9C;
85 | border: 1px solid #000;
86 | margin-right: 5px;
87 | }
88 |
89 | #menu li a {
90 | cursor: pointer;
91 | display: block;
92 | background-color: #807A60;
93 | padding: 7px 15px;
94 | margin: 1px;
95 | color: #EDE8D4;
96 | text-decoration: none;
97 | font-size: 12px;
98 | font-weight: bold;
99 | }
100 |
101 | #menu li.active {
102 | background-color: #807A60;
103 | }
104 |
105 | #menu li.active a {
106 | background-color: #f3eeda;
107 | color: #807A60;
108 | }
109 |
110 | .menu-fixed-to-top {
111 | position: fixed;
112 | top: 0;
113 | }
114 |
115 | .page-content {
116 | width: 588px;
117 | border: solid 1px #EADDBB;
118 | border-top: none;
119 | margin: 0 0 15px 0;
120 | padding: 5px 5px 5px 5px;
121 | background-color: #f3eeda;
122 | }
123 |
124 | #loader {
125 | margin: 30px 0 30px 269px;
126 | }
127 |
128 | #no-cues {
129 | text-align: center;
130 | margin: 30px auto;
131 | color: #807a60;
132 | text-shadow: white 0px 1px 0px;
133 | font-weight: bold;
134 | }
135 |
136 | .delete-all {
137 | color: #AAA488;
138 | font-weight: bold;
139 | font-size: 10px;
140 | text-decoration: underline;
141 | cursor: pointer;
142 | text-align: right;
143 | margin-bottom: 10px;
144 | }
145 |
146 | .delete-all:hover {
147 | color: #807A60;
148 | }
149 |
150 | #recent-cues {
151 | list-style: none inside url('../images/download.png');
152 | margin: 10px 0 0 0;
153 | }
154 |
155 | #recent-cues li {
156 | font-size: 12px;
157 | margin-bottom: 5px;
158 | }
159 |
160 | #recent-cues a {
161 | color: #AAA488;
162 | }
163 |
164 | #recent-cues a:hover {
165 | color: #807A60;
166 | }
167 |
168 | #recent-cues span.delete {
169 | color: #AAA488;
170 | font-weight: bold;
171 | font-size: 10px;
172 | text-decoration: underline;
173 | cursor: pointer;
174 | }
175 |
176 | #recent-cues span.delete:hover {
177 | color: #807A60;
178 | }
179 |
180 | #categories-navigation {
181 | display: block;
182 | float: left;
183 | }
184 |
185 | #categories-navigation li {
186 | float: left;
187 | list-style: none;
188 | margin-right: 2px;
189 | }
190 |
191 | #categories-navigation li span {
192 | cursor: pointer;
193 | color: #807A60;
194 | font-size: 14px;
195 | font-weight: bold;
196 | }
197 |
198 | #categories-navigation li span:hover {
199 | color: #AAA488;
200 | }
201 |
202 | #top-controls-container-wrapper {
203 | height: 60px;
204 | }
205 |
206 | .top-controls-container-fixed {
207 | position: fixed;
208 | top: 43px;
209 | width: 571px;
210 | }
211 |
212 | #checked-filter-container {
213 | float: left;
214 | font-style: italic;
215 | font-size: 10px;
216 | font-weight: bold;
217 | }
218 |
219 | #checked-filter-container input {
220 | vertical-align: middle;
221 | margin-left: 0;
222 | }
223 |
224 | button[name="save"] {
225 | display: block;
226 | float: right;
227 | margin: 0;
228 | border: 1px solid #306C90;
229 | padding: 3px 6px;
230 | }
231 |
232 | .letter-container {
233 | margin-bottom: 5px;
234 | border: solid 1px #EADDBB;
235 | padding: 5px 5px 5px 10px;
236 | background-color: #f7f7ef;
237 | }
238 |
239 | .letter-container .letter {
240 | font-size: 14px;
241 | font-weight: bold;
242 | }
243 |
244 | .letter-container .categories-list {
245 | padding: 10px 0 0 0;
246 | -webkit-column-count: 3;
247 | column-count: 3;
248 | }
249 |
250 | .letter-container .categories-list label {
251 | display: inline-block;
252 | padding-left: 15px;
253 | margin-bottom: 5px;
254 | color: #807A60;
255 | font-size: 11px;
256 | font-weight: bold;
257 | width: 100%;
258 | box-sizing: border-box;
259 | }
260 |
261 | .letter-container .categories-list label input {
262 | margin-left: -15px;
263 | vertical-align: middle;
264 | float: left;
265 | }
266 |
267 | .letter-container .categories-list label i {
268 | font-size: 10px;
269 | font-weight: normal;
270 | font-style: italic;
271 | color: #333333;
272 | }
273 |
--------------------------------------------------------------------------------
/tests/mocha.css:
--------------------------------------------------------------------------------
1 | @charset "utf-8";
2 |
3 | body {
4 | margin:0;
5 | }
6 |
7 | #mocha {
8 | font: 20px/1.5 "Helvetica Neue", Helvetica, Arial, sans-serif;
9 | margin: 60px 50px;
10 | }
11 |
12 | #mocha ul,
13 | #mocha li {
14 | margin: 0;
15 | padding: 0;
16 | }
17 |
18 | #mocha ul {
19 | list-style: none;
20 | }
21 |
22 | #mocha h1,
23 | #mocha h2 {
24 | margin: 0;
25 | }
26 |
27 | #mocha h1 {
28 | margin-top: 15px;
29 | font-size: 1em;
30 | font-weight: 200;
31 | }
32 |
33 | #mocha h1 a {
34 | text-decoration: none;
35 | color: inherit;
36 | }
37 |
38 | #mocha h1 a:hover {
39 | text-decoration: underline;
40 | }
41 |
42 | #mocha .suite .suite h1 {
43 | margin-top: 0;
44 | font-size: .8em;
45 | }
46 |
47 | #mocha .hidden {
48 | display: none;
49 | }
50 |
51 | #mocha h2 {
52 | font-size: 12px;
53 | font-weight: normal;
54 | cursor: pointer;
55 | }
56 |
57 | #mocha .suite {
58 | margin-left: 15px;
59 | }
60 |
61 | #mocha .test {
62 | margin-left: 15px;
63 | overflow: hidden;
64 | }
65 |
66 | #mocha .test.pending:hover h2::after {
67 | content: '(pending)';
68 | font-family: arial, sans-serif;
69 | }
70 |
71 | #mocha .test.pass.medium .duration {
72 | background: #c09853;
73 | }
74 |
75 | #mocha .test.pass.slow .duration {
76 | background: #b94a48;
77 | }
78 |
79 | #mocha .test.pass::before {
80 | content: '✓';
81 | font-size: 12px;
82 | display: block;
83 | float: left;
84 | margin-right: 5px;
85 | color: #00d6b2;
86 | }
87 |
88 | #mocha .test.pass .duration {
89 | font-size: 9px;
90 | margin-left: 5px;
91 | padding: 2px 5px;
92 | color: #fff;
93 | -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.2);
94 | -moz-box-shadow: inset 0 1px 1px rgba(0,0,0,.2);
95 | box-shadow: inset 0 1px 1px rgba(0,0,0,.2);
96 | -webkit-border-radius: 5px;
97 | -moz-border-radius: 5px;
98 | -ms-border-radius: 5px;
99 | -o-border-radius: 5px;
100 | border-radius: 5px;
101 | }
102 |
103 | #mocha .test.pass.fast .duration {
104 | display: none;
105 | }
106 |
107 | #mocha .test.pending {
108 | color: #0b97c4;
109 | }
110 |
111 | #mocha .test.pending::before {
112 | content: '◦';
113 | color: #0b97c4;
114 | }
115 |
116 | #mocha .test.fail {
117 | color: #c00;
118 | }
119 |
120 | #mocha .test.fail pre {
121 | color: black;
122 | }
123 |
124 | #mocha .test.fail::before {
125 | content: '✖';
126 | font-size: 12px;
127 | display: block;
128 | float: left;
129 | margin-right: 5px;
130 | color: #c00;
131 | }
132 |
133 | #mocha .test pre.error {
134 | color: #c00;
135 | max-height: 300px;
136 | overflow: auto;
137 | }
138 |
139 | /**
140 | * (1): approximate for browsers not supporting calc
141 | * (2): 42 = 2*15 + 2*10 + 2*1 (padding + margin + border)
142 | * ^^ seriously
143 | */
144 | #mocha .test pre {
145 | display: block;
146 | float: left;
147 | clear: left;
148 | font: 12px/1.5 monaco, monospace;
149 | margin: 5px;
150 | padding: 15px;
151 | border: 1px solid #eee;
152 | max-width: 85%; /*(1)*/
153 | max-width: calc(100% - 42px); /*(2)*/
154 | word-wrap: break-word;
155 | border-bottom-color: #ddd;
156 | -webkit-border-radius: 3px;
157 | -webkit-box-shadow: 0 1px 3px #eee;
158 | -moz-border-radius: 3px;
159 | -moz-box-shadow: 0 1px 3px #eee;
160 | border-radius: 3px;
161 | }
162 |
163 | #mocha .test h2 {
164 | position: relative;
165 | }
166 |
167 | #mocha .test a.replay {
168 | position: absolute;
169 | top: 3px;
170 | right: 0;
171 | text-decoration: none;
172 | vertical-align: middle;
173 | display: block;
174 | width: 15px;
175 | height: 15px;
176 | line-height: 15px;
177 | text-align: center;
178 | background: #eee;
179 | font-size: 15px;
180 | -moz-border-radius: 15px;
181 | border-radius: 15px;
182 | -webkit-transition: opacity 200ms;
183 | -moz-transition: opacity 200ms;
184 | transition: opacity 200ms;
185 | opacity: 0.3;
186 | color: #888;
187 | }
188 |
189 | #mocha .test:hover a.replay {
190 | opacity: 1;
191 | }
192 |
193 | #mocha-report.pass .test.fail {
194 | display: none;
195 | }
196 |
197 | #mocha-report.fail .test.pass {
198 | display: none;
199 | }
200 |
201 | #mocha-report.pending .test.pass,
202 | #mocha-report.pending .test.fail {
203 | display: none;
204 | }
205 | #mocha-report.pending .test.pass.pending {
206 | display: block;
207 | }
208 |
209 | #mocha-error {
210 | color: #c00;
211 | font-size: 1.5em;
212 | font-weight: 100;
213 | letter-spacing: 1px;
214 | }
215 |
216 | #mocha-stats {
217 | position: fixed;
218 | top: 15px;
219 | right: 10px;
220 | font-size: 12px;
221 | margin: 0;
222 | color: #888;
223 | z-index: 1;
224 | }
225 |
226 | #mocha-stats .progress {
227 | float: right;
228 | padding-top: 0;
229 | }
230 |
231 | #mocha-stats em {
232 | color: black;
233 | }
234 |
235 | #mocha-stats a {
236 | text-decoration: none;
237 | color: inherit;
238 | }
239 |
240 | #mocha-stats a:hover {
241 | border-bottom: 1px solid #eee;
242 | }
243 |
244 | #mocha-stats li {
245 | display: inline-block;
246 | margin: 0 5px;
247 | list-style: none;
248 | padding-top: 11px;
249 | }
250 |
251 | #mocha-stats canvas {
252 | width: 40px;
253 | height: 40px;
254 | }
255 |
256 | #mocha code .comment { color: #ddd; }
257 | #mocha code .init { color: #2f6fad; }
258 | #mocha code .string { color: #5890ad; }
259 | #mocha code .keyword { color: #8a6343; }
260 | #mocha code .number { color: #2f6fad; }
261 |
262 | @media screen and (max-device-width: 480px) {
263 | #mocha {
264 | margin: 60px 0px;
265 | }
266 |
267 | #mocha #stats {
268 | position: absolute;
269 | }
270 | }
271 |
--------------------------------------------------------------------------------
/scripts/view/CuesView.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | define([
4 | 'scripts/view/View',
5 | 'scripts/view/Message',
6 | 'scripts/view/Badge'
7 | ], function (View, Message, Badge) {
8 | function CuesView(container, messageView, user, userCueService, userCueCategoryService) {
9 | if (!(this instanceof CuesView)) {
10 | throw new Error('`this` must be an instance of view.CuesView');
11 | }
12 |
13 | View.call(this, container);
14 |
15 | var cuesView = this,
16 | badge = new Badge(userCueService);
17 |
18 | /**
19 | * @param {domain.Cue[]} cues
20 | * @param {domain.CueCategory[]} userCategories
21 | */
22 | function render(cues, userCategories) {
23 | var containerBody = document.createDocumentFragment(),
24 | dismissAllLink,
25 | cuesList,
26 | text;
27 |
28 | dismissAllLink = document.createElement('div');
29 | dismissAllLink.setAttribute('class', 'delete-all');
30 | dismissAllLink.innerText = '[Dismiss all]';
31 |
32 | cuesList = document.createElement('ul');
33 | cuesList.setAttribute('id', 'recent-cues');
34 |
35 | function createCuesListItem(cue) {
36 | var li,
37 | a,
38 | span,
39 | whitespace;
40 |
41 | a = document.createElement('a');
42 | a.setAttribute('href', cue.link);
43 | a.setAttribute('target', '_blank');
44 | a.innerText = cue.title;
45 |
46 | span = document.createElement('span');
47 | span.setAttribute('class', 'delete');
48 | span.dataset.id = cue.id;
49 | span.innerText = '[Dismiss]';
50 |
51 | whitespace = document.createTextNode(' ');
52 |
53 | li = document.createElement('li');
54 | li.appendChild(a);
55 | li.appendChild(whitespace);
56 | li.appendChild(span);
57 |
58 | return li;
59 | }
60 |
61 | for (var i = 0; i < cues.length; i++) {
62 | cuesList.appendChild(createCuesListItem(cues[i]));
63 | }
64 |
65 | containerBody.appendChild(dismissAllLink);
66 | containerBody.appendChild(cuesList);
67 |
68 |
69 | container.innerHTML = '';
70 |
71 | if (0 === cues.length) {
72 | text = document.createElement('div');
73 | text.setAttribute('id', 'no-cues');
74 |
75 | if (0 === userCategories.length) {
76 | text.innerText = 'You are not subscribed for any category yet!';
77 | } else {
78 | text.innerText = 'No new cues :(';
79 | }
80 | container.appendChild(text);
81 | } else {
82 | container.appendChild(containerBody);
83 | listenToDelete(userCategories);
84 | }
85 | }
86 |
87 | function listenToDelete(userCategories) {
88 | var forEach = Array.prototype.forEach;
89 |
90 | // delete individual
91 | forEach.call(container.querySelectorAll('.delete'), function (el) {
92 | el.addEventListener('click', function (e) {
93 | var ids = [this.dataset.id];
94 |
95 | // upon clicking "dismiss" there are 2 strategies:
96 | // - either we simply remove a link
97 | // - or we re-render the whole layout (actually only in order not to duplicate "No new cues" message)
98 | if (1 === container.querySelectorAll('.delete').length) {
99 | render([], userCategories);
100 | } else {
101 | this.parentElement.remove();
102 | }
103 |
104 | userCueService.put(user.token, ids, function (err) {
105 | if (err) {
106 | messageView.show(Message.status.ERROR, err);
107 | } else {
108 | badge.render(user);
109 | }
110 | });
111 | });
112 | });
113 |
114 | // delete all
115 | container.querySelector('.delete-all').addEventListener('click', function (e) {
116 | var ids = [];
117 |
118 | forEach.call(container.querySelectorAll('.delete'), function (el) {
119 | ids.push(el.dataset.id);
120 | });
121 |
122 | // re-render the whole layout (actually only in order not to duplicate "No new cues" message)
123 | render([], userCategories);
124 |
125 | userCueService.put(user.token, ids, function (err) {
126 | if (err) {
127 | messageView.show(Message.status.ERROR, err);
128 | } else {
129 | badge.render(user);
130 | }
131 | });
132 | });
133 | }
134 |
135 | this.render = function () {
136 | cuesView.renderLoader();
137 |
138 | var _cues,
139 | _userCategories;
140 |
141 | function caller() {
142 | if (typeof _cues != 'undefined' && typeof _userCategories != 'undefined') {
143 | render(_cues, _userCategories);
144 | }
145 | }
146 |
147 | userCueService.get(user.token, 0, function (err, cues, pageable) {
148 | if (err) {
149 | messageView.show('error', err);
150 | } else {
151 | _cues = cues;
152 | caller();
153 | }
154 | });
155 |
156 | userCueCategoryService.get(user.token, function (err, userCategories) {
157 | if (err) {
158 | messageView.show('error', err);
159 | } else {
160 | _userCategories = userCategories;
161 | caller();
162 | }
163 | });
164 | }
165 | }
166 |
167 | CuesView.prototype = Object.create(View.prototype);
168 | CuesView.prototype.constructor = CuesView;
169 |
170 | return CuesView;
171 | });
172 |
--------------------------------------------------------------------------------
/scripts/view/CategoriesView.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | define([
4 | 'scripts/view/View',
5 | 'scripts/view/Message',
6 | 'scripts/view/Badge'
7 | ], function (View, Message, Badge) {
8 | function CategoriesView(container, messageView, user, userCueService, cueCategoryService, userCueCategoryService) {
9 | if (!(this instanceof CategoriesView)) {
10 | throw new Error('`this` must be an instance of view.CategoriesView');
11 | }
12 |
13 | View.call(this, container);
14 |
15 | var categoriesView = this,
16 | badge = new Badge(userCueService);
17 |
18 | // @TODO: whn the page is scrolled down and user click "Show only selected" checkbox navigation panel disappears
19 |
20 | window.addEventListener('scroll', function () {
21 | var topControlsContainer = document.getElementById('top-controls-container'),
22 | scrollTop = document.getElementById('logo').offsetHeight
23 | + document.getElementById('menu').offsetHeight;
24 |
25 | if (!topControlsContainer) {
26 | return;
27 | }
28 |
29 | if (document.body.scrollTop >= scrollTop) {
30 | topControlsContainer.setAttribute('class', 'letter-container clear top-controls-container-fixed');
31 | } else {
32 | topControlsContainer.setAttribute('class', 'letter-container clear ');
33 | }
34 | });
35 |
36 |
37 | /**
38 | * Return categories which belong to the given letter
39 | *
40 | * @param {String} letter
41 | * @param {domain.CueCategory[]} categories
42 | * @returns {Array}
43 | */
44 | function pickCategoriesWhichBelongToLetter(letter, categories) {
45 | var sliced = [];
46 |
47 | function equals(char, name) {
48 | if (!name.length) {
49 | return false;
50 | }
51 |
52 | if ('#' === char) {
53 | var re = /[^a-z]/i;
54 | return re.test(name[0]);
55 | } else {
56 | // name may start with "the" article, leave it out
57 | name = name.toLocaleLowerCase();
58 | if ('the' === name.substr(0, 3)) {
59 | name = name.substr(3).trim();
60 | }
61 | return name[0].toUpperCase() === char;
62 | }
63 | }
64 |
65 | for (var i = 0, j = 0; i < categories.length; i++) {
66 | if (equals(letter, categories[i].name)) {
67 | sliced[j] = categories[i];
68 | j++;
69 | }
70 | }
71 |
72 | return sliced;
73 | }
74 |
75 | function createCategoriesNavigationElement(char) {
76 | var span = document.createElement('span');
77 | span.dataset.id = 'abc-' + char;
78 | span.innerText = char;
79 |
80 | var li = document.createElement('li');
81 | li.appendChild(span);
82 |
83 | return li;
84 | }
85 |
86 | /**
87 | * @param {String} char
88 | * @param {Array} categories
89 | * @param {Array} userCategories
90 | * @param {Boolean} checkedFilter
91 | * @returns {HTMLElement} only if it contains categories otherwise NULL
92 | */
93 | function createLetterContainer(char, categories, userCategories, checkedFilter) {
94 | var label, input, name, host;
95 |
96 | function isChecked(id) {
97 | for (var i = 0; i < userCategories.length; i++) {
98 | if (userCategories[i].id === id) {
99 | return true;
100 | }
101 | }
102 |
103 | return false;
104 | }
105 |
106 | function createInput(category) {
107 | var el = document.createElement('input');
108 | el.setAttribute('type', 'checkbox');
109 | el.setAttribute('name', 'categories');
110 | el.setAttribute('value', category.id);
111 | if (isChecked(category.id)) {
112 | el.setAttribute('checked', 'checked');
113 | }
114 |
115 | return el;
116 | }
117 |
118 | function createName(category) {
119 | return document.createTextNode(category.name);
120 | }
121 |
122 | function createHost(category) {
123 | var el = document.createElement('i');
124 | if (category.host) {
125 | el.innerText = category.host;
126 | } else {
127 | el.innerHTML = ' ';
128 | }
129 |
130 | return el;
131 | }
132 |
133 | function createLabel(input, name, host) {
134 | label = document.createElement('label');
135 | label.appendChild(input);
136 | label.appendChild(name);
137 | label.appendChild(document.createElement('br'));
138 | label.appendChild(host);
139 |
140 | return label;
141 | }
142 |
143 | var letterContainer = document.createElement('div');
144 | letterContainer.setAttribute('class', 'letter-container');
145 | letterContainer.setAttribute('id', 'abc-' + char);
146 |
147 | var letter = document.createElement('div');
148 | letter.setAttribute('class', char);
149 | letter.innerText = char;
150 |
151 | var categoriesList = document.createElement('div');
152 | categoriesList.setAttribute('class', 'categories-list');
153 |
154 | for (var i = 0; i < categories.length; i++) {
155 | if (checkedFilter && !isChecked(categories[i].id)) {
156 | continue;
157 | }
158 |
159 | input = createInput(categories[i]);
160 | name = createName(categories[i]);
161 | host = createHost(categories[i]);
162 | label = createLabel(input, name, host);
163 |
164 | categoriesList.appendChild(label);
165 | }
166 |
167 |
168 | letterContainer.appendChild(letter);
169 | letterContainer.appendChild(categoriesList);
170 |
171 |
172 | // return DOM element only if it contains categories
173 | return categoriesList.getElementsByTagName('label').length > 0 ? letterContainer : null;
174 | }
175 |
176 |
177 | function render(categories, userCategories, checkedFilter) {
178 | var containerBody = document.createDocumentFragment(),
179 | topControlsContainer,
180 | topControlsContainerRow,
181 | topControlsContainerWrapper,
182 | alphabet = [],
183 | categoriesNavigation,
184 | categoriesNavigationEl,
185 | letterContainers = document.createDocumentFragment(),
186 | letterContainer,
187 | button,
188 | checkedFilterContainer,
189 | checkedFilterInput;
190 |
191 |
192 | button = document.createElement('button');
193 | button.setAttribute('name', 'save');
194 | button.innerText = 'Save';
195 |
196 | categoriesNavigation = document.createElement('ul');
197 | categoriesNavigation.setAttribute('id', 'categories-navigation');
198 |
199 | topControlsContainerRow = document.createElement('div');
200 | topControlsContainerRow.setAttribute('class', 'clear');
201 | topControlsContainerRow.appendChild(button);
202 | topControlsContainerRow.appendChild(categoriesNavigation);
203 |
204 | checkedFilterInput = document.createElement('input');
205 | checkedFilterInput.setAttribute('type', 'checkbox');
206 | checkedFilterInput.setAttribute('name', 'checked-filter');
207 | if (checkedFilter) {
208 | checkedFilterInput.setAttribute('checked', 'checked');
209 | }
210 |
211 |
212 | checkedFilterContainer = document.createElement('label');
213 | checkedFilterContainer.setAttribute('id', 'checked-filter-container');
214 | checkedFilterContainer.appendChild(checkedFilterInput);
215 | checkedFilterContainer.appendChild(document.createTextNode('Show only selected'));
216 |
217 | topControlsContainer = document.createElement('div');
218 | topControlsContainer.setAttribute('id', 'top-controls-container');
219 | topControlsContainer.setAttribute('class', 'letter-container clear');
220 | topControlsContainer.appendChild(topControlsContainerRow);
221 | topControlsContainer.appendChild(checkedFilterContainer);
222 |
223 | topControlsContainerWrapper = document.createElement('div');
224 | topControlsContainerWrapper.setAttribute('id', 'top-controls-container-wrapper');
225 | topControlsContainerWrapper.appendChild(topControlsContainer);
226 |
227 |
228 | // first create alphabet
229 | alphabet.push('#');
230 | for (var i = 65; i < 91; i++) {
231 | alphabet.push(String.fromCharCode(i));
232 | }
233 | // then go through it and create DOM
234 | alphabet.forEach(function (char) {
235 | var slicedCategories = pickCategoriesWhichBelongToLetter(char, categories),
236 | slicedUserCategories = pickCategoriesWhichBelongToLetter(char, userCategories);
237 |
238 | if (slicedCategories.length > 0) {
239 | categoriesNavigationEl = createCategoriesNavigationElement(char);
240 | letterContainer = createLetterContainer(char, slicedCategories, slicedUserCategories, checkedFilter);
241 |
242 | if (letterContainer) {
243 | categoriesNavigation.appendChild(categoriesNavigationEl);
244 | letterContainers.appendChild(letterContainer);
245 | }
246 | }
247 | });
248 |
249 |
250 | containerBody.appendChild(topControlsContainerWrapper);
251 | containerBody.appendChild(letterContainers);
252 |
253 | container.innerHTML = '';
254 | container.appendChild(containerBody);
255 |
256 |
257 | listenToSave(categories, userCategories);
258 | listenToCategoriesNavigation();
259 | listenToCategoryTick(categories, userCategories);
260 | listenToCheckedFilter(categories, userCategories);
261 | }
262 |
263 | function listenToSave(categories, userCategories) {
264 | var forEach = Array.prototype.forEach;
265 |
266 | forEach.call(container.querySelectorAll('button[name="save"]'), function (button) {
267 | button.addEventListener('click', function (e) {
268 | var ids = [];
269 | forEach.call(container.querySelectorAll('input[name="categories"]:checked'), function (input) {
270 | ids.push(input.value);
271 | });
272 |
273 |
274 | messageView.show(Message.status.INFO, 'Saved');
275 | userCueCategoryService.put(user.token, ids, function (err) {
276 | if (err) {
277 | messageView.show(Message.status.ERROR, err);
278 | } else {
279 | badge.render(user);
280 |
281 |
282 | // if we are in "show only selected" mode. In this case I want to "commit" changes
283 | // and in order to do that let's re-render the page content
284 | var filter = document.getElementsByName('checked-filter')[0];
285 | if (filter.checked) {
286 | render(categories, userCategories, true);
287 | }
288 | }
289 | });
290 | });
291 | });
292 | }
293 |
294 | function listenToCategoriesNavigation() {
295 | var forEach = Array.prototype.forEach,
296 | navigationOffset = document.getElementById('menu').offsetHeight
297 | + document.getElementById('top-controls-container').offsetHeight;
298 |
299 | forEach.call(document.getElementById('categories-navigation').querySelectorAll('span'), function (el) {
300 | el.addEventListener('click', function (e) {
301 | var offset = document.getElementById(el.dataset.id).offsetTop - navigationOffset;
302 | window.scrollTo(0, offset);
303 | });
304 | });
305 | }
306 |
307 | function listenToCategoryTick(categories, userCategories) {
308 | var forEach = Array.prototype.forEach;
309 |
310 | forEach.call(container.querySelectorAll('input[name="categories"]'), function (el) {
311 | el.addEventListener('change', function (e) {
312 | var id = el.value;
313 | var i;
314 | if (el.checked) {
315 | for (i = 0; i < categories.length; i++) {
316 | if (categories[i].id === id) {
317 | userCategories.push(categories[i]);
318 | break;
319 | }
320 | }
321 | } else {
322 | for (i = 0; i < userCategories.length; i++) {
323 | if (userCategories[i].id === id) {
324 | userCategories.splice(i, 1);
325 | break;
326 | }
327 | }
328 | }
329 | });
330 | });
331 | }
332 |
333 | function listenToCheckedFilter(categories, userCategories) {
334 | var filter = document.getElementsByName('checked-filter')[0];
335 | filter.addEventListener('change', function (e) {
336 | if (this.checked) {
337 | render(categories, userCategories, true);
338 | } else {
339 | render(categories, userCategories, false);
340 | }
341 | });
342 | }
343 |
344 |
345 | this.render = function () {
346 | categoriesView.renderLoader();
347 |
348 |
349 | var _categories,
350 | _userCategories;
351 |
352 | function caller() {
353 | if (typeof _categories != 'undefined' && typeof _userCategories != 'undefined') {
354 | render(_categories, _userCategories, false);
355 | }
356 | }
357 |
358 | cueCategoryService.get(function (err, categories) {
359 | if (err) {
360 | messageView.show('error', err);
361 | } else {
362 | _categories = categories;
363 | caller();
364 | }
365 | });
366 |
367 | userCueCategoryService.get(user.token, function (err, userCategories) {
368 | if (err) {
369 | messageView.show('error', err);
370 | } else {
371 | _userCategories = userCategories;
372 | caller();
373 | }
374 | });
375 | }
376 | }
377 |
378 | CategoriesView.prototype = Object.create(View.prototype);
379 | CategoriesView.prototype.constructor = CategoriesView;
380 |
381 | return CategoriesView;
382 | });
383 |
--------------------------------------------------------------------------------
/scripts/require.js:
--------------------------------------------------------------------------------
1 | /*
2 | RequireJS 2.1.14 Copyright (c) 2010-2014, The Dojo Foundation All Rights Reserved.
3 | Available via the MIT or new BSD license.
4 | see: http://github.com/jrburke/requirejs for details
5 | */
6 | var requirejs,require,define;
7 | (function(ba){function G(b){return"[object Function]"===K.call(b)}function H(b){return"[object Array]"===K.call(b)}function v(b,c){if(b){var d;for(d=0;dthis.depCount&&!this.defined){if(G(l)){if(this.events.error&&this.map.isDefine||g.onError!==ca)try{f=i.execCb(c,l,b,f)}catch(d){a=d}else f=i.execCb(c,l,b,f);this.map.isDefine&&void 0===f&&((b=this.module)?f=b.exports:this.usingExports&&
19 | (f=this.exports));if(a)return a.requireMap=this.map,a.requireModules=this.map.isDefine?[this.map.id]:null,a.requireType=this.map.isDefine?"define":"require",w(this.error=a)}else f=l;this.exports=f;if(this.map.isDefine&&!this.ignore&&(r[c]=f,g.onResourceLoad))g.onResourceLoad(i,this.map,this.depMaps);y(c);this.defined=!0}this.defining=!1;this.defined&&!this.defineEmitted&&(this.defineEmitted=!0,this.emit("defined",this.exports),this.defineEmitComplete=!0)}}else this.fetch()}},callPlugin:function(){var a=
20 | this.map,b=a.id,d=p(a.prefix);this.depMaps.push(d);q(d,"defined",u(this,function(f){var l,d;d=m(aa,this.map.id);var e=this.map.name,P=this.map.parentMap?this.map.parentMap.name:null,n=i.makeRequire(a.parentMap,{enableBuildCallback:!0});if(this.map.unnormalized){if(f.normalize&&(e=f.normalize(e,function(a){return c(a,P,!0)})||""),f=p(a.prefix+"!"+e,this.map.parentMap),q(f,"defined",u(this,function(a){this.init([],function(){return a},null,{enabled:!0,ignore:!0})})),d=m(h,f.id)){this.depMaps.push(f);
21 | if(this.events.error)d.on("error",u(this,function(a){this.emit("error",a)}));d.enable()}}else d?(this.map.url=i.nameToUrl(d),this.load()):(l=u(this,function(a){this.init([],function(){return a},null,{enabled:!0})}),l.error=u(this,function(a){this.inited=!0;this.error=a;a.requireModules=[b];B(h,function(a){0===a.map.id.indexOf(b+"_unnormalized")&&y(a.map.id)});w(a)}),l.fromText=u(this,function(f,c){var d=a.name,e=p(d),P=M;c&&(f=c);P&&(M=!1);s(e);t(j.config,b)&&(j.config[d]=j.config[b]);try{g.exec(f)}catch(h){return w(C("fromtexteval",
22 | "fromText eval for "+b+" failed: "+h,h,[b]))}P&&(M=!0);this.depMaps.push(e);i.completeLoad(d);n([d],l)}),f.load(a.name,n,l,j))}));i.enable(d,this);this.pluginMaps[d.id]=d},enable:function(){V[this.map.id]=this;this.enabling=this.enabled=!0;v(this.depMaps,u(this,function(a,b){var c,f;if("string"===typeof a){a=p(a,this.map.isDefine?this.map:this.map.parentMap,!1,!this.skipMap);this.depMaps[b]=a;if(c=m(L,a.id)){this.depExports[b]=c(this);return}this.depCount+=1;q(a,"defined",u(this,function(a){this.defineDep(b,
23 | a);this.check()}));this.errback&&q(a,"error",u(this,this.errback))}c=a.id;f=h[c];!t(L,c)&&(f&&!f.enabled)&&i.enable(a,this)}));B(this.pluginMaps,u(this,function(a){var b=m(h,a.id);b&&!b.enabled&&i.enable(a,this)}));this.enabling=!1;this.check()},on:function(a,b){var c=this.events[a];c||(c=this.events[a]=[]);c.push(b)},emit:function(a,b){v(this.events[a],function(a){a(b)});"error"===a&&delete this.events[a]}};i={config:j,contextName:b,registry:h,defined:r,urlFetched:S,defQueue:A,Module:Z,makeModuleMap:p,
24 | nextTick:g.nextTick,onError:w,configure:function(a){a.baseUrl&&"/"!==a.baseUrl.charAt(a.baseUrl.length-1)&&(a.baseUrl+="/");var b=j.shim,c={paths:!0,bundles:!0,config:!0,map:!0};B(a,function(a,b){c[b]?(j[b]||(j[b]={}),U(j[b],a,!0,!0)):j[b]=a});a.bundles&&B(a.bundles,function(a,b){v(a,function(a){a!==b&&(aa[a]=b)})});a.shim&&(B(a.shim,function(a,c){H(a)&&(a={deps:a});if((a.exports||a.init)&&!a.exportsFn)a.exportsFn=i.makeShimExports(a);b[c]=a}),j.shim=b);a.packages&&v(a.packages,function(a){var b,
25 | a="string"===typeof a?{name:a}:a;b=a.name;a.location&&(j.paths[b]=a.location);j.pkgs[b]=a.name+"/"+(a.main||"main").replace(ia,"").replace(Q,"")});B(h,function(a,b){!a.inited&&!a.map.unnormalized&&(a.map=p(b))});if(a.deps||a.callback)i.require(a.deps||[],a.callback)},makeShimExports:function(a){return function(){var b;a.init&&(b=a.init.apply(ba,arguments));return b||a.exports&&da(a.exports)}},makeRequire:function(a,e){function j(c,d,m){var n,q;e.enableBuildCallback&&(d&&G(d))&&(d.__requireJsBuild=
26 | !0);if("string"===typeof c){if(G(d))return w(C("requireargs","Invalid require call"),m);if(a&&t(L,c))return L[c](h[a.id]);if(g.get)return g.get(i,c,a,j);n=p(c,a,!1,!0);n=n.id;return!t(r,n)?w(C("notloaded",'Module name "'+n+'" has not been loaded yet for context: '+b+(a?"":". Use require([])"))):r[n]}J();i.nextTick(function(){J();q=s(p(null,a));q.skipMap=e.skipMap;q.init(c,d,m,{enabled:!0});D()});return j}e=e||{};U(j,{isBrowser:z,toUrl:function(b){var d,e=b.lastIndexOf("."),k=b.split("/")[0];if(-1!==
27 | e&&(!("."===k||".."===k)||1e.attachEvent.toString().indexOf("[native code"))&&!Y?(M=!0,e.attachEvent("onreadystatechange",b.onScriptLoad)):
34 | (e.addEventListener("load",b.onScriptLoad,!1),e.addEventListener("error",b.onScriptError,!1)),e.src=d,J=e,D?y.insertBefore(e,D):y.appendChild(e),J=null,e;if(ea)try{importScripts(d),b.completeLoad(c)}catch(m){b.onError(C("importscripts","importScripts failed for "+c+" at "+d,m,[c]))}};z&&!q.skipDataMain&&T(document.getElementsByTagName("script"),function(b){y||(y=b.parentNode);if(I=b.getAttribute("data-main"))return s=I,q.baseUrl||(E=s.split("/"),s=E.pop(),O=E.length?E.join("/")+"/":"./",q.baseUrl=
35 | O),s=s.replace(Q,""),g.jsExtRegExp.test(s)&&(s=I),q.deps=q.deps?q.deps.concat(s):[s],!0});define=function(b,c,d){var e,g;"string"!==typeof b&&(d=c,c=b,b=null);H(c)||(d=c,c=null);!c&&G(d)&&(c=[],d.length&&(d.toString().replace(ka,"").replace(la,function(b,d){c.push(d)}),c=(1===d.length?["require"]:["require","exports","module"]).concat(c)));if(M){if(!(e=J))N&&"interactive"===N.readyState||T(document.getElementsByTagName("script"),function(b){if("interactive"===b.readyState)return N=b}),e=N;e&&(b||
36 | (b=e.getAttribute("data-requiremodule")),g=F[e.getAttribute("data-requirecontext")])}(g?g.defQueue:R).push([b,c,d])};define.amd={jQuery:!0};g.exec=function(b){return eval(b)};g(q)}})(this);
37 |
--------------------------------------------------------------------------------