diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..389310f --- /dev/null +++ b/.babelrc @@ -0,0 +1,7 @@ +{ + "exclude": "node_modules/**", + "presets": [ + "@babel/preset-flow" + ], + "sourceMap": false +} \ No newline at end of file diff --git a/.eslintrc.js b/.eslintrc.js index 8b530b8..638b134 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -27,12 +27,6 @@ module.exports = { "always" ] }, - "globals": { - "angular": true, - "jQuery": true, - "$": true, - "GM": true - }, "settings": { "flowtype": { "onlyFilesWithFlowAnnotation": false diff --git a/Gruntfile.js b/Gruntfile.js index 29327a4..9ebeabb 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -20,6 +20,7 @@ const resolve = require('rollup-plugin-node-resolve'); const commonJs = require('rollup-plugin-commonjs'); const virtual = require('rollup-plugin-virtual'); +const babel = require('rollup-plugin-babel'); const licenseBanner = require('./licenseBanner'); const userScriptBanner = require('./userScriptBanner'); @@ -29,20 +30,6 @@ const baseFileName = 'qc-ext'; module.exports = function (grunt) { 'use strict'; - grunt.registerTask('flowRemoveTypes', 'Runs flow-remove-types.', function () { - const flowRemoveTypes = require('flow-remove-types'); - - const files = grunt.file.expand('assets/js/**/*.js'); - for (var i = 0, len = files.length; i < len; i++) { - const inputFile = files[i]; - const outputFileName = inputFile.substring('assets/js/'.length); - - const input = grunt.file.read(inputFile); - const output = flowRemoveTypes(input); - grunt.file.write('assets/removeFlow/' + outputFileName, output.toString()); - } - }); - // Project configuration. grunt.initConfig({ pkg: grunt.file.readJSON('package.json'), @@ -167,6 +154,7 @@ module.exports = function (grunt) { options: { pureExternalImports: true, plugins: [ + babel(), virtual({ 'jquery': 'export default jQuery', 'angular': 'export default angular', @@ -180,7 +168,7 @@ module.exports = function (grunt) { }, main: { files: { - 'assets/generated/rollup.js': 'assets/removeFlow/app.js' + 'assets/generated/rollup.js': 'assets/js/app.js' } } }, @@ -189,6 +177,16 @@ module.exports = function (grunt) { server: false }, files: ['assets/js/**/*.js'] + }, + babel: { + options: { + sourceMap: true + }, + dist: { + files: { + 'assets/generated/babel.js': 'assets/js/app.js' + } + } } }); @@ -198,13 +196,12 @@ module.exports = function (grunt) { // Register the tasks. grunt.registerTask('default', ['build']); grunt.registerTask('build', [ - 'flow', // Type-checking - 'flowRemoveTypes', // Removes flow annotations - 'eslint', // Check for lint 'compass', // Compile CSS 'htmlmin', // Minify HTML templates 'filesToJavascript', // Convert HTML templates to JS variables 'concat:variables', // Create finished variable.pass2.js file + 'flow', // Type-checking + 'eslint', // Check for lint 'rollup:main', // Rollup all the javascript files into one 'concat:source', // Add banner to rollup result 'uglify', // Minify the javascript diff --git a/assets/js/app.js b/assets/js/app.js index c34efcc..b937b73 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -28,7 +28,7 @@ import { setup as setupAngular } from './modules/angular-app'; (async () => { await settings.loadSettings(); - let domModifier = new DomModifier(); + const domModifier = new DomModifier(); domModifier.modify(); setupAngular(); diff --git a/assets/js/constants.js b/assets/js/constants.js index ffc5dd5..972d20c 100644 --- a/assets/js/constants.js +++ b/assets/js/constants.js @@ -39,7 +39,7 @@ function getWebserviceBaseUrl() { const comicDataUrl = getWebserviceBaseUrl() + 'comicdata/'; const itemDataUrl = getWebserviceBaseUrl() + 'itemdata/'; -let constants = { +const constants = { settingsKey: 'settings', developmentMode, @@ -58,7 +58,6 @@ let constants = { setGuestComicUrl: comicDataUrl + 'setguest', setNonCanonUrl: comicDataUrl + 'setnoncanon', - itemFriendDataUrl: itemDataUrl + 'friends/', itemLocationDataUrl: itemDataUrl + 'locations/', setItemDataPropertyUrl: itemDataUrl + 'setproperty', diff --git a/assets/js/modules/angular-app.js b/assets/js/modules/angular-app.js index de6c0ab..c137971 100644 --- a/assets/js/modules/angular-app.js +++ b/assets/js/modules/angular-app.js @@ -1,3 +1,4 @@ +// @flow /* * Copyright (C) 2016-2018 Alexander Krivács Schrøder * diff --git a/assets/js/modules/angular/api/comicData.js b/assets/js/modules/angular/api/comicData.js new file mode 100644 index 0000000..9c24eee --- /dev/null +++ b/assets/js/modules/angular/api/comicData.js @@ -0,0 +1,75 @@ +// @flow +/* + * Copyright (C) 2016-2018 Alexander Krivács Schrøder + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import type { ItemType } from './itemData'; + +export type ComicEditorDataMissing = { + first: ?number; + previous: ?number; + next: ?number; + last: ?number; + any: boolean; +} + +export type ComicEditorData = { + missing: { + cast: ComicEditorDataMissing; + location: ComicEditorDataMissing; + storyline: ComicEditorDataMissing; + title: ComicEditorDataMissing; + tagline: ComicEditorDataMissing; + any: boolean; + } +}; + +export type ComicItem = { + first: ?number; + previous: ?number; + first: ?number; + last: ?number; + id: number; + shortName: string; + name: string; + type: ItemType; + color: string; +}; + +export type ComicData = { + comic: number; + hasData: boolean; + publishDate: ?string; + isAccuratePublishDate: ?boolean; + title: ?string; + tagline: ?string; + isGuestComic: ?boolean; + isNonCanon: ?boolean; + news: ?string; + previous: ?number; + next: ?number; + editorData?: ComicEditorData; + items: Array; + allItems?: Array; +}; + +export type ComicItemRepository = { + [string]: ComicItem[]; + + cast?: ComicItem[]; + location?: ComicItem[]; + storyline?: ComicItem[]; +} diff --git a/assets/js/modules/angular/api/itemData.js b/assets/js/modules/angular/api/itemData.js new file mode 100644 index 0000000..95814e6 --- /dev/null +++ b/assets/js/modules/angular/api/itemData.js @@ -0,0 +1,50 @@ +// @flow +/* + * Copyright (C) 2016-2018 Alexander Krivács Schrøder + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +export type ItemType = 'cast' | 'location' | 'storyline'; + +export type ItemBaseData = { + id: number; + shortName: string; + name: string; + type: ItemType; +} + +export type ItemBaseDataWithColor = ItemBaseData & { + color: string; +} + +export type ItemData = ItemBaseDataWithColor & { + first: number; + last: number; + appearances: number; + totalComics: number; + presence: number; + hasImage: boolean; +}; + +export type ItemRelationData = ItemBaseDataWithColor & { + count: number; + percentage: number; +}; + +export type DecoratedItemData = ItemData & { + highlightColor: string; + locations: ItemRelationData[]; + friends: ItemRelationData[]; +}; diff --git a/assets/js/modules/angular/assemble.js b/assets/js/modules/angular/assemble.js index 9f2e057..a88a7e6 100644 --- a/assets/js/modules/angular/assemble.js +++ b/assets/js/modules/angular/assemble.js @@ -1,3 +1,4 @@ +// @flow /* * Copyright (C) 2016-2018 Alexander Krivács Schrøder * @@ -15,6 +16,8 @@ * along with this program. If not, see . */ +import type { AngularModule } from 'angular'; + import config from './config'; import run from './run'; @@ -25,6 +28,7 @@ import titleController from './controllers/titleController'; import colorService from './services/colorService'; import comicService from './services/comicService'; import eventFactory from './services/eventFactory'; +import eventService from './services/eventService'; import messageReportingService from './services/messageReportingService'; import styleService from './services/styleService'; @@ -47,7 +51,7 @@ import qcSetTaglineDirective from './directives/qcSetTaglineDirective'; import qcSettingsDirective from './directives/qcSettingsDirective'; import qcSetTitleDirective from './directives/qcSetTitleDirective'; -export default function (app) { +export default function (app: AngularModule) { config(app); run(app); @@ -58,6 +62,7 @@ export default function (app) { colorService(app); comicService(app); eventFactory(app); + eventService(app); messageReportingService(app); styleService(app); diff --git a/assets/js/modules/angular/config.js b/assets/js/modules/angular/config.js index b378271..15da11d 100644 --- a/assets/js/modules/angular/config.js +++ b/assets/js/modules/angular/config.js @@ -1,3 +1,4 @@ +// @flow /* * Copyright (C) 2016-2018 Alexander Krivács Schrøder * @@ -15,17 +16,23 @@ * along with this program. If not, see . */ +import GM from 'greasemonkey'; +import angular from 'angular'; +import type { AngularModule } from 'angular'; + import settings from './../settings'; import decorateHttpService from './decorateHttpService'; +import decorateScope from './decorateScope'; -export default function (app) { +export default function (app: AngularModule) { // Set up routing and do other configuration app.config(['$stateProvider', '$urlRouterProvider', '$locationProvider', '$provide', '$logProvider', function ($stateProvider, $urlRouterProvider, $locationProvider, $provide, $logProvider) { decorateHttpService($provide); + decorateScope($provide); $stateProvider.state('homepage', { url: '^/$', @@ -44,6 +51,6 @@ export default function (app) { $locationProvider.html5Mode(true); - $logProvider.debugEnabled(settings.showDebugLogs); + $logProvider.debugEnabled(settings.values.showDebugLogs); }]); } diff --git a/assets/js/modules/angular/controllers/ControllerBases.js b/assets/js/modules/angular/controllers/ControllerBases.js new file mode 100644 index 0000000..f3da709 --- /dev/null +++ b/assets/js/modules/angular/controllers/ControllerBases.js @@ -0,0 +1,103 @@ +// @flow +/* + * Copyright (C) 2016-2018 Alexander Krivács Schrøder + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import type { $DecoratedScope } from '../decorateScope'; +import type { ComicService } from '../services/comicService'; +import type { EventService } from '../services/eventService'; +import type { ComicData } from '../api/comicData'; + +export class ComicDataControllerBase { + $scope: $DecoratedScope; + eventService: EventService; + + constructor( + $scope: $DecoratedScope, + eventService: EventService + ) { + this.$scope = $scope; + this.eventService = eventService; + + eventService.comicDataLoadingEvent.subscribe($scope, + (event, comic) => { + $scope.safeApply(() => { + this._comicDataLoading(comic); + }); + }); + + eventService.comicDataLoadedEvent.subscribe($scope, + (event, comicData) => { + $scope.safeApply(() => { + this._comicDataLoaded(comicData); + }); + }); + + eventService.comicDataErrorEvent.subscribe($scope, + (event, error) => { + $scope.safeApply(() => { + this._comicDataError(error); + }); + }); + + eventService.itemsChangedEvent.subscribe($scope, + (event, data) => { + $scope.safeApply(() => { + this._itemsChanged(); + }); + }); + } + + _comicDataLoading(comic: number) { + } + + _comicDataLoaded(comicData: ComicData) { + } + + _comicDataError(error: any) { + } + + _itemsChanged() { + } +} + +export class SetValueControllerBase extends ComicDataControllerBase { + comicService: ComicService; + + unique: string; + + constructor( + $scope: $DecoratedScope, + comicService: ComicService, + eventService: EventService + ) { + super($scope, eventService); + + this.comicService = comicService; + + this.unique = Math.random().toString(36).slice(-5); + } + + _updateValue() { + } + + keyPress(event: KeyboardEvent) { + if (event.keyCode === 13) { + // ENTER key + this._updateValue(); + } + } +} diff --git a/assets/js/modules/angular/controllers/bodyController.js b/assets/js/modules/angular/controllers/bodyController.js index bd8ba5f..ff705df 100644 --- a/assets/js/modules/angular/controllers/bodyController.js +++ b/assets/js/modules/angular/controllers/bodyController.js @@ -1,3 +1,4 @@ +// @flow /* * Copyright (C) 2016-2018 Alexander Krivács Schrøder * @@ -15,14 +16,19 @@ * along with this program. If not, see . */ +import type { AngularModule, $Log, $Scope } from 'angular'; + import settings from '../../settings'; -export default function (app) { +import type { $DecoratedScope } from '../decorateScope'; +import type { ComicService } from '../services/comicService'; + +export default function (app: AngularModule) { app.controller('bodyController', ['$log', '$scope', 'comicService', - function ($log, $scope, comicService) { + function ($log: $Log, $scope: $DecoratedScope, comicService: ComicService) { $log.debug('START bodyController()'); - var isStupidFox = + const isStupidFox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1; function previous() { @@ -37,14 +43,14 @@ export default function (app) { }); } - var shortcut = + const shortcut = window.eval('window.shortcut'); // Firefox balks at me trying to use the "shortcut" object from // my user script. Works just fine in Chrome. I can't be bothered // to cater to one browser's stupidity. if (isStupidFox) { - var shortcutRemove = + const shortcutRemove = window.eval('window.shortcut.remove').bind(shortcut); shortcutRemove('Left'); shortcutRemove('Right'); @@ -82,7 +88,7 @@ export default function (app) { shortcut.add('Ctrl+Right', next); shortcut.add('Q', function () { - if (settings.editMode) { + if (settings.values.editMode) { $('input[id^="addItem"]').focus(); } }, { disable_in_input: true }); diff --git a/assets/js/modules/angular/controllers/comicController.js b/assets/js/modules/angular/controllers/comicController.js index 4655508..22b51ce 100644 --- a/assets/js/modules/angular/controllers/comicController.js +++ b/assets/js/modules/angular/controllers/comicController.js @@ -1,3 +1,4 @@ +// @flow /* * Copyright (C) 2016-2018 Alexander Krivács Schrøder * @@ -15,9 +16,12 @@ * along with this program. If not, see . */ -export default function (app) { +import type { AngularModule, $Log } from 'angular'; +import type { ComicService } from '../services/comicService'; + +export default function (app: AngularModule) { app.controller('comicController', ['$log', 'comicService', - function ($log, comicService) { + function ($log: $Log, comicService: ComicService) { $log.debug('START comicController()'); this.comicService = comicService; $log.debug('END comicController()'); diff --git a/assets/js/modules/angular/controllers/titleController.js b/assets/js/modules/angular/controllers/titleController.js index 1b5bc8d..836a7be 100644 --- a/assets/js/modules/angular/controllers/titleController.js +++ b/assets/js/modules/angular/controllers/titleController.js @@ -1,3 +1,4 @@ +// @flow /* * Copyright (C) 2016-2018 Alexander Krivács Schrøder * @@ -15,52 +16,54 @@ * along with this program. If not, see . */ +import type { AngularModule, $Log } from 'angular'; + import constants from '../../../constants'; -export default function (app) { - app.controller('titleController', ['$log', '$scope', 'eventFactory', - function ($log, $scope, Event) { - $log.debug('START titleController()'); - - this.title = 'Loading Questionable Content Extension...'; - - var comicDataLoadingEvent = - new Event(constants.comicdataLoadingEvent); - var comicDataLoadedEvent = - new Event(constants.comicdataLoadedEvent); - - $scope.safeApply = function (fn) { - var phase = this.$root.$$phase; - if (phase === '$apply' || phase === '$digest') { - if (fn && typeof fn === 'function') { - fn(); - } - } else { - this.$apply(fn); - } - }; - - var self = this; - - comicDataLoadingEvent.subscribe($scope, function (event, comic) { - $scope.safeApply(function () { - self.title = 'Loading #' + comic + - ' — Questionable Content'; - }); - }); - - comicDataLoadedEvent.subscribe($scope, function (event, comicData) { - $scope.safeApply(function () { - if (comicData.hasData && comicData.title) { - self.title = '#' + comicData.comic + ': ' + - comicData.title + ' — Questionable Content'; - } else { - self.title = '#' + comicData.comic + - ' — Questionable Content'; - } - }); - }); - - $log.debug('END titleController()'); - }]); +import { ComicDataControllerBase } from './ControllerBases'; + +import type { $DecoratedScope } from '../decorateScope'; +import type { EventService } from '../services/eventService'; +import type { ComicData } from '../api/comicData'; + +export class TitleController extends ComicDataControllerBase { + static $inject: string[]; + + $log: $Log; + + title: string; + + constructor( + $scope: $DecoratedScope, + $log: $Log, + eventService: EventService + ) { + $log.debug('START TitleController'); + + super($scope, eventService); + + this.$log = $log; + + this.title = 'Loading Questionable Content Extension...'; + + $log.debug('END TitleController'); + } + + _comicDataLoading(comic: number) { + this.title = `Loading #${comic} — Questionable Content`; + } + + _comicDataLoaded(comicData: ComicData) { + if (comicData.hasData && comicData.title) { + this.title = `#${comicData.comic}: ${comicData.title} — Questionable Content`; + } else { + this.title = `#${comicData.comic} — Questionable Content`; + } + } + +} +TitleController.$inject = ['$scope', '$log', 'eventService']; + +export default function (app: AngularModule) { + app.controller('titleController', TitleController); } diff --git a/assets/js/modules/angular/decorateHttpService.js b/assets/js/modules/angular/decorateHttpService.js index 8a5a3b7..9bc5dac 100644 --- a/assets/js/modules/angular/decorateHttpService.js +++ b/assets/js/modules/angular/decorateHttpService.js @@ -1,3 +1,4 @@ +// @flow /* * Copyright (C) 2016-2018 Alexander Krivács Schrøder * @@ -15,13 +16,14 @@ * along with this program. If not, see . */ +import GM from 'greasemonkey'; import angular from 'angular'; // TODO: Since we're not using the original service at all, we might // as well completely replace it rather than decorate it... // http://www.bennadel.com/blog/ // 2927-overriding-core-and-custom-services-in-angularjs.htm -export default function ($provide) { +export default function ($provide: any) { // Let's take over $http and make it use Greasemonkey's cross-domain // XMLHTTPRequests instead of the browser's. @@ -29,16 +31,16 @@ export default function ($provide) { // START Code bits borrowed from angular // (see angular's license for details) - var APPLICATION_JSON = 'application/json'; - var JSON_START = /^\[|^\{(?!\{)/; - var JSON_ENDS = { + const APPLICATION_JSON = 'application/json'; + const JSON_START = /^\[|^\{(?!\{)/; + const JSON_ENDS = { '[': /]$/, '{': /}$/ }; - var JSON_PROTECTION_PREFIX = /^\)\]\}',?\n/; + const JSON_PROTECTION_PREFIX = /^\)\]\}',?\n/; function isJsonLike(str) { - var jsonStart = str.match(JSON_START); + const jsonStart = str.match(JSON_START); return jsonStart && JSON_ENDS[jsonStart[0]].test(str); } @@ -58,14 +60,14 @@ export default function ($provide) { // Strip json vulnerability protection prefix // and trim whitespace - var tempData = data.replace(JSON_PROTECTION_PREFIX, '') + const tempData = data.replace(JSON_PROTECTION_PREFIX, '') .trim(); if (!tempData) { return data; } - var contentType = headers('Content-Type'); + const contentType = headers('Content-Type'); if (contentType && contentType.indexOf(APPLICATION_JSON) === 0 || @@ -79,10 +81,10 @@ export default function ($provide) { // END Code bits borrowed from angular function getHeaderFunction(headers) { - var keyedHeaders = {}; + const keyedHeaders = {}; angular.forEach(headers, function (value) { - var splitValue = value.trim().split(':', 2); + const splitValue = value.trim().split(':', 2); if (splitValue.length < 2) { return; @@ -97,9 +99,9 @@ export default function ($provide) { }; } - var injector = angular.injector(['ng']); - var $q = injector.get('$q'); - var ourHttp = { + const injector = (angular: any).injector(['ng']); + const $q = injector.get('$q'); + const ourHttp = { get: function (url, config) { return $q(function (resolve, reject) { GM.xmlHttpRequest({ @@ -109,14 +111,14 @@ export default function ($provide) { Accept: APPLICATION_JSON }, onload: function (gmResponse) { - var headers = getHeaderFunction( + const headers = getHeaderFunction( gmResponse.responseHeaders .split('\n')); - var responseData = gmResponse.response; + let responseData = gmResponse.response; responseData = defaultHttpResponseTransform( responseData, headers); - var response = { + const response = { data: responseData, status: gmResponse.status, headers: headers, @@ -127,13 +129,13 @@ export default function ($provide) { resolve(response); }, onerror: function (gmResponse) { - var headers = getHeaderFunction(gmResponse + const headers = getHeaderFunction(gmResponse .responseHeaders.split('\n')); - var responseData = gmResponse.response; + let responseData = gmResponse.response; responseData = defaultHttpResponseTransform( responseData, headers); - var response = { + const response = { data: responseData, status: gmResponse.status, headers: headers, @@ -157,14 +159,14 @@ export default function ($provide) { Accept: APPLICATION_JSON }, onload: function (gmResponse) { - var headers = getHeaderFunction( + const headers = getHeaderFunction( gmResponse.responseHeaders .split('\n')); - var responseData = gmResponse.response; + let responseData = gmResponse.response; responseData = defaultHttpResponseTransform( responseData, headers); - var response = { + const response = { data: responseData, status: gmResponse.status, headers: headers, @@ -175,13 +177,13 @@ export default function ($provide) { resolve(response); }, onerror: function (gmResponse) { - var headers = getHeaderFunction(gmResponse + const headers = getHeaderFunction(gmResponse .responseHeaders.split('\n')); - var responseData = gmResponse.response; + let responseData = gmResponse.response; responseData = defaultHttpResponseTransform( responseData, headers); - var response = { + const response = { data: responseData, status: gmResponse.status, headers: headers, diff --git a/assets/js/modules/angular/decorateScope.js b/assets/js/modules/angular/decorateScope.js new file mode 100644 index 0000000..80f2dc3 --- /dev/null +++ b/assets/js/modules/angular/decorateScope.js @@ -0,0 +1,43 @@ +// @flow +/* + * Copyright (C) 2016-2018 Alexander Krivács Schrøder + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import GM from 'greasemonkey'; + +import type { $Scope } from 'angular'; + +export type $DecoratedScope = $Scope & { + safeApply(fn: Function): void; +}; + +export default function ($provide: any) { + $provide.decorator('$rootScope', ['$delegate', function ($delegate) { + const $rootScopePrototype = Object.getPrototypeOf($delegate); + $rootScopePrototype.safeApply = function (fn) { + const phase = this.$root.$$phase; + if (phase === '$apply' || phase === '$digest') { + if (fn && typeof fn === 'function') { + fn(); + } + } else { + this.$apply(fn); + } + }; + + return $delegate; + }]); +} diff --git a/assets/js/modules/angular/directives/donutDirective.js b/assets/js/modules/angular/directives/donutDirective.js index 14c4d7c..df47374 100644 --- a/assets/js/modules/angular/directives/donutDirective.js +++ b/assets/js/modules/angular/directives/donutDirective.js @@ -1,3 +1,4 @@ +// @flow /* * Copyright (C) 2016-2018 Alexander Krivács Schrøder * @@ -15,10 +16,12 @@ * along with this program. If not, see . */ +import type { AngularModule } from 'angular'; + import variables from '../../../../generated/variables.pass2'; -export default function (app) { - app.directive('donut', [ +export default function (app: AngularModule) { + app.directive('donut', function () { return { restrict: 'E', @@ -62,5 +65,5 @@ export default function (app) { ], template: variables.html.donut }; - }]); + }); } diff --git a/assets/js/modules/angular/directives/onErrorDirective.js b/assets/js/modules/angular/directives/onErrorDirective.js index 29f4e7e..53d5de8 100644 --- a/assets/js/modules/angular/directives/onErrorDirective.js +++ b/assets/js/modules/angular/directives/onErrorDirective.js @@ -1,3 +1,4 @@ +// @flow /* * Copyright (C) 2016-2018 Alexander Krivács Schrøder * @@ -15,7 +16,9 @@ * along with this program. If not, see . */ -export default function (app) { +import type { AngularModule } from 'angular'; + +export default function (app: AngularModule) { app.directive('onError', function () { return { restrict: 'A', diff --git a/assets/js/modules/angular/directives/qcAddItemDirective.js b/assets/js/modules/angular/directives/qcAddItemDirective.js index 73b0b3e..23ec401 100644 --- a/assets/js/modules/angular/directives/qcAddItemDirective.js +++ b/assets/js/modules/angular/directives/qcAddItemDirective.js @@ -1,3 +1,4 @@ +// @flow /* * Copyright (C) 2016-2018 Alexander Krivács Schrøder * @@ -15,212 +16,227 @@ * along with this program. If not, see . */ +import type { AngularModule, $Log, $Timeout, $Http, $Filter } from 'angular'; + import constants from '../../../constants'; import variables from '../../../../generated/variables.pass2'; -export default function (app) { +import { SetValueControllerBase } from '../controllers/ControllerBases'; + +import type { $DecoratedScope } from '../decorateScope'; +import type { ComicService } from '../services/comicService'; +import type { EventService } from '../services/eventService'; +import type { ItemBaseData, ItemData } from '../api/itemData'; +import type { ComicItem } from '../api/comicData'; + +const addCastTemplate = 'Add new cast member'; +const addCastItem: ItemBaseData = { + id: -1, + type: 'cast', + shortName: `${addCastTemplate} ''`, + name: '' +}; +const addStorylineTemplate = 'Add new storyline'; +const addStorylineItem: ItemBaseData = { + id: -1, + type: 'storyline', + shortName: `${addStorylineTemplate} ''`, + name: '' +}; +const addLocationTemplate = 'Add new location'; +const addLocationItem: ItemBaseData = { + id: -1, + type: 'location', + shortName: `${addLocationTemplate} ''`, + name: '' +}; + +function escapeRegExp(s) { + return s.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&'); +} + +let triggeredFocus = false; +let dropdownOpen = false; +let firstRun = true; + +export class AddItemController extends SetValueControllerBase { + static $inject: string[]; + + $log: $Log; + $http: $Http; + $timeout: $Timeout; + $filter: $Filter; + + searchFieldId: string; + dropdownId: string; + dropdownButtonId: string; + + itemFilterText: string; + items: ItemBaseData[]; + + constructor( + $scope: $DecoratedScope, + $log: $Log, + comicService: ComicService, + eventService: EventService, + $http: $Http, + $timeout: $Timeout, + $filter: $Filter + ) { + $log.debug('START AddItemController'); + + super($scope, comicService, eventService); + + this.$log = $log; + this.$http = $http; + this.$timeout = $timeout; + this.$filter = $filter; + + this.searchFieldId = `#addItem_${this.unique}_search`; + this.dropdownId = `#addItem_${this.unique}_dropdown`; + this.dropdownButtonId = `#addItem_${this.unique}_dropdownButton`; + + this.items = []; + this.itemFilterText = ''; + + (this: any).itemFilter = this.itemFilter.bind(this); + + this._loadItemData(); + + $log.debug('END AddItemController'); + } + + _loadItemData() { + this.$http.get(constants.itemDataUrl) + .then((response) => { + let itemData: ItemBaseData[] = []; + if (response.status === 200) { + itemData = response.data; + } + + itemData.push(addCastItem); + itemData.push(addStorylineItem); + itemData.push(addLocationItem); + + this.$scope.safeApply(() => { + this.items = itemData; + }); + }); + } + + _updateValue() { + // Add the top item in the list + const filteredList = this.$filter('filter')(this.items, + this.itemFilter); + const chosenItem = filteredList[0]; + this.addItem(chosenItem); + } + + _itemsChanged() { + this._loadItemData(); + } + + searchChanged() { + let filterText = this.itemFilterText; + + if (filterText.charAt(0) === '!') { + filterText = filterText.substr(1); + } else if (filterText.charAt(0) === '@') { + filterText = filterText.substr(1); + } else if (filterText.charAt(0) === '#') { + filterText = filterText.substr(1); + } + + addCastItem.shortName = `${addCastTemplate} '${filterText}'`; + addCastItem.name = filterText; + addStorylineItem.shortName = `${addStorylineTemplate} '${filterText}'`; + addStorylineItem.name = filterText; + addLocationItem.shortName = `${addLocationTemplate} '${filterText}'`; + addLocationItem.name = filterText; + } + + itemFilter(value: ItemBaseData) { + let filterText = this.itemFilterText; + + let result = true; + if (filterText.charAt(0) === '!') { + result = value.type === 'cast'; + filterText = filterText.substr(1); + } else if (filterText.charAt(0) === '@') { + result = value.type === 'location'; + filterText = filterText.substr(1); + } else if (filterText.charAt(0) === '#') { + result = value.type === 'storyline'; + filterText = filterText.substr(1); + } + + const searchRegex = new RegExp(escapeRegExp(filterText), 'i'); + result = result && + value.shortName.match(searchRegex) !== null; + + return result; + } + + focusSearch() { + this.$log.debug('AddItemController::focusSearch(): #1 Search focused'); + if (firstRun) { + $(this.dropdownId).on('shown.bs.dropdown', () => { + dropdownOpen = true; + this.$log.debug('AddItemController::focusSearch(): #4 Dropdown opened'); + + // Opening the dropdown makes the search field + // lose focus. So set it again. + $(this.searchFieldId).focus(); + triggeredFocus = false; + + $(this.dropdownId + ' .dropdown-menu').width($( + this.dropdownId).width()); + }); + $(this.dropdownId).on('hidden.bs.dropdown', () => { + this.$log.debug('AddItemController::focusSearch(): #5 Dropdown closed'); + dropdownOpen = false; + }); + + firstRun = false; + } + + if (!dropdownOpen && !triggeredFocus) { + this.$log.debug( + 'AddItemController::focusSearch(): #2 Focus was user-initiated'); + triggeredFocus = true; + this.$timeout(() => { + if (!dropdownOpen) { + this.$log.debug( + 'AddItemController::focusSearch(): #3 Toggle dropdown'); + ($(this.dropdownButtonId): any).dropdown('toggle'); + } + }, 150); + } + } + + addItem(item: ComicItem) { + this.comicService.addItem(item).then((response) => { + if (response.status === 200) { + this.eventService.itemsChangedEvent.publish(); + this.$scope.safeApply(() => { + this.itemFilterText = ''; + }); + } + }); + } +} +AddItemController.$inject = [ + '$scope', '$log', 'comicService', 'eventService', + '$http', '$timeout' +]; + +export default function (app: AngularModule) { app.directive('qcAddItem', function () { return { restrict: 'E', replace: true, scope: {}, - controller: ['$http', '$scope', '$log', '$timeout', '$filter', - 'comicService', 'eventFactory', - function ($http, $scope, $log, $timeout, $filter, - comicService, Event) { - $log.debug('START qcAddItem()'); - - var self = this; - - this.unique = Math.random().toString(36).slice(-5); - - var itemsChangedEvent = - new Event(constants.itemsChangedEvent); - - $scope.safeApply = function (fn) { - var phase = this.$root.$$phase; - if (phase === '$apply' || phase === '$digest') { - if (fn && typeof fn === 'function') { - fn(); - } - } else { - this.$apply(fn); - } - }; - - var searchFieldId = '#addItem_' + this.unique + '_search'; - var dropdownId = '#addItem_' + this.unique + '_dropdown'; - var dropdownButtonId = '#addItem_' + this.unique + - '_dropdownButton'; - this.items = []; - - var addCastTemplate = 'Add new cast member '; - var addCastItem = { - id: -1, - type: 'cast', - shortName: addCastTemplate + '\'\'', - name: '' - }; - var addStorylineTemplate = 'Add new storyline '; - var addStorylineItem = { - id: -1, - type: 'storyline', - shortName: addStorylineTemplate + '\'\'', - name: '' - }; - var addLocationTemplate = 'Add new location '; - var addLocationItem = { - id: -1, - type: 'location', - shortName: addLocationTemplate + '\'\'', - name: '' - }; - - function loadItemData() { - $http.get(constants.itemDataUrl) - .then(function (response) { - var itemData = []; - if (response.status === 200) { - itemData = response.data; - } - - itemData.push(addCastItem); - itemData.push(addStorylineItem); - itemData.push(addLocationItem); - - $scope.safeApply(function () { - self.items = itemData; - }); - }); - } - loadItemData(); - - function escapeRegExp(s) { - return s.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&'); - } - - this.searchChanged = function () { - var filterText = self.itemFilterText; - - if (filterText.charAt(0) === '!') { - filterText = filterText.substr(1); - } else if (filterText.charAt(0) === '@') { - filterText = filterText.substr(1); - } else if (filterText.charAt(0) === '#') { - filterText = filterText.substr(1); - } - - addCastItem.shortName = addCastTemplate + - '\'' + filterText + '\''; - addCastItem.name = filterText; - addStorylineItem.shortName = addStorylineTemplate + - '\'' + filterText + '\''; - addStorylineItem.name = filterText; - addLocationItem.shortName = addLocationTemplate + - '\'' + filterText + '\''; - addLocationItem.name = filterText; - }; - - var triggeredFocus = false; - var dropdownOpen = false; - var firstRun = true; - - this.itemFilterText = ''; - this.itemFilter = function (value) { - var filterText = self.itemFilterText; - - var result = true; - if (filterText.charAt(0) === '!') { - result = value.type === 'cast'; - filterText = filterText.substr(1); - } else if (filterText.charAt(0) === '@') { - result = value.type === 'location'; - filterText = filterText.substr(1); - } else if (filterText.charAt(0) === '#') { - result = value.type === 'storyline'; - filterText = filterText.substr(1); - } - - var searchRegex = new RegExp( - escapeRegExp(filterText), 'i'); - result = result && - value.shortName.match(searchRegex) !== null; - - return result; - }; - // {shortName: ''}; - this.focusSearch = function () { - $log.debug('qcAdditem(): #1 Search focused'); - if (firstRun) { - $(dropdownId).on('shown.bs.dropdown', function () { - dropdownOpen = true; - $log.debug('qcAdditem(): #4 Dropdown opened'); - - // Opening the dropdown makes the search field - // lose focus. So set it again. - $(searchFieldId).focus(); - triggeredFocus = false; - - $(dropdownId + ' .dropdown-menu').width($( - dropdownId).width()); - }); - $(dropdownId).on('hidden.bs.dropdown', function () { - $log.debug('qcAdditem(): #5 Dropdown closed'); - dropdownOpen = false; - }); - - firstRun = false; - } - - if (!dropdownOpen && !triggeredFocus) { - $log.debug( - 'qcAdditem(): #2 Focus was user-initiated'); - triggeredFocus = true; - $timeout(function () { - if (!dropdownOpen) { - $log.debug( - 'qcAdditem(): #3 Toggle dropdown'); - $(dropdownButtonId).dropdown('toggle'); - } - }, 150); - } - }; - - this.keyPress = function (event) { - if (event.keyCode === 13) { - // ENTER key - - // Add the top item in the list - var filteredList = $filter('filter')(self.items, - self.itemFilter); - var chosenItem = filteredList[0]; - self.addItem(chosenItem); - } - }; - - /*#this.blurSearch = function() { - if (!intendedFocus) { - $log.debug('addItem(): blur!'); - } - };*/ - - this.addItem = function (item) { - comicService.addItem(item).then(function (response) { - if (response.status === 200) { - itemsChangedEvent.notify(); - $scope.safeApply(function () { - self.itemFilterText = ''; - }); - } - }); - }; - - itemsChangedEvent.subscribe($scope, function () { - loadItemData(); - }); - - $log.debug('END qcAddItem()'); - }], + controller: AddItemController, controllerAs: 'a', template: variables.html.addItem }; diff --git a/assets/js/modules/angular/directives/qcChangeLogDirective.js b/assets/js/modules/angular/directives/qcChangeLogDirective.js index c73a432..bfa2adf 100644 --- a/assets/js/modules/angular/directives/qcChangeLogDirective.js +++ b/assets/js/modules/angular/directives/qcChangeLogDirective.js @@ -1,3 +1,4 @@ +// @flow /* * Copyright (C) 2016-2018 Alexander Krivács Schrøder * @@ -15,61 +16,85 @@ * along with this program. If not, see . */ +import type { AngularModule, $Log } from 'angular'; + +import GM from 'greasemonkey'; + import constants from '../../../constants'; import settings from '../../settings'; import variables from '../../../../generated/variables.pass2'; -export default function (app) { +import { ComicDataControllerBase } from '../controllers/ControllerBases'; + +import type { $DecoratedScope } from '../decorateScope'; +import type { EventService } from '../services/eventService'; +import type { ComicData } from '../api/comicData'; + +export class ChangeLogController extends ComicDataControllerBase { + static $inject: string[]; + + $log: $Log; + + versionUpdated: boolean; + currentVersion: string; + previousVersion: ?string; + + constructor( + $scope: $DecoratedScope, + $log: $Log, + eventService: EventService + ) { + $log.debug('START ChangeLogController'); + + super($scope, eventService); + + this.$log = $log; + + this.versionUpdated = false; + this.currentVersion = GM.info.script.version; + this.previousVersion = null; + + $('#changeLogDialog').on('hide.bs.modal', () => { + this.$log.debug('Saving settings...'); + settings.values.version = this.currentVersion; + settings.saveSettings().then(() => { + this.$log.debug('Settings saved.'); + }); + }); + + $log.debug('END ChangeLogController'); + } + + _comicDataLoaded(comicData: ComicData) { + if (!settings.values.version) { + // Version is undefined. We're a new user! + this.$log.debug('ChangeLogController::_comicDataLoaded(): Version undefined!'); + } else if (settings.values.version !== + this.currentVersion) { + // Version is changed. Script has been updated! + // Show the change log dialog. + this.previousVersion = settings.values.version; + this.$log.debug('ChangeLogController::_comicDataLoaded(): Version different!'); + } else { + return; + } + this.versionUpdated = true; + } + + close() { + ($('#changeLogDialog'): any).modal('hide'); + } + +} +ChangeLogController.$inject = ['$scope', '$log', 'eventService']; + +export default function (app: AngularModule) { app.directive('qcChangeLog', function () { return { restrict: 'E', replace: true, scope: {}, - controller: ['$log', 'eventFactory', '$scope', - function ($log, Event, $scope) { - $log.debug('START qcChangeLog()'); - - $log.debug(); - - this.versionUpdated = false; - this.currentVersion = GM.info.script.version; - this.previousVersion = null; - var self = this; - - var comicDataLoadedEvent = - new Event(constants.comicdataLoadedEvent); - - comicDataLoadedEvent.subscribe($scope, - function () { - if (settings.version === undefined) { - // Version is undefined. We're a new user! - $log.debug('qcChangeLog(): Version undefined!'); - } else if (settings.version !== - self.currentVersion) { - // Version is changed. Script has been updated! - // Show the change log dialog. - self.previousVersion = settings.version; - $log.debug('qcChangeLog(): Version different!'); - } else { - return; - } - self.versionUpdated = true; - }); - - $('#changeLogDialog').on('hide.bs.modal', function () { - $log.debug('Saving settings...'); - settings.version = self.currentVersion; - settings.saveSettings().then(() => { - $log.debug('Settings saved.'); - }); - }); - - this.close = function () { - $('#changeLogDialog').modal('hide'); - }; - - $log.debug('END qcChangeLog()'); - }], + controller: ChangeLogController, controllerAs: 'clvm', template: variables.html.changeLog }; diff --git a/assets/js/modules/angular/directives/qcComicDirective.js b/assets/js/modules/angular/directives/qcComicDirective.js index 3460424..ba33258 100644 --- a/assets/js/modules/angular/directives/qcComicDirective.js +++ b/assets/js/modules/angular/directives/qcComicDirective.js @@ -1,3 +1,4 @@ +// @flow /* * Copyright (C) 2016-2018 Alexander Krivács Schrøder * @@ -15,7 +16,10 @@ * along with this program. If not, see . */ -export default function (app) { +import type { AngularModule } from 'angular'; + + +export default function (app: AngularModule) { app.directive('qcComic', function () { return { restrict: 'E', diff --git a/assets/js/modules/angular/directives/qcComicNavDirective.js b/assets/js/modules/angular/directives/qcComicNavDirective.js index 6ad1824..66ab020 100644 --- a/assets/js/modules/angular/directives/qcComicNavDirective.js +++ b/assets/js/modules/angular/directives/qcComicNavDirective.js @@ -1,3 +1,4 @@ +// @flow /* * Copyright (C) 2016-2018 Alexander Krivács Schrøder * @@ -15,54 +16,80 @@ * along with this program. If not, see . */ +import type { AngularModule, $Log } from 'angular'; + import constants from '../../../constants'; import variables from '../../../../generated/variables.pass2'; -export default function (app) { +import { ComicDataControllerBase } from '../controllers/ControllerBases'; + +import type { $DecoratedScope } from '../decorateScope'; +import type { ComicService } from '../services/comicService'; +import type { EventService } from '../services/eventService'; +import type { ComicData } from '../api/comicData'; + +export class ComicNavController extends ComicDataControllerBase { + static $inject: string[]; + + $log: $Log; + comicService: ComicService; + latestComic: number; + + currentComic: ?number; + + constructor( + $scope: $DecoratedScope, + $log: $Log, + comicService: ComicService, + eventService: EventService, + latestComic: number + ) { + $log.debug('START ComicNavController'); + + super($scope, eventService); + + this.$log = $log; + this.comicService = comicService; + this.latestComic = latestComic; + + this.currentComic = null; + + $log.debug('END ComicNavController'); + } + + _comicDataLoaded(comicData: ComicData) { + this.currentComic = comicData.comic; + } + + go() { + this.$log.debug(`ComicNavController::go(): ${this.currentComic ? this.currentComic : 'NONE'}`); + if (!this.currentComic) { + this.currentComic = this.latestComic; + } else if (this.currentComic < 1) { + this.currentComic = 1; + } else if (this.currentComic > this.latestComic) { + this.currentComic = this.latestComic; + } + this.comicService.gotoComic(this.currentComic); + } + + keyPress(event: KeyboardEvent) { + if (event.keyCode === 13) { + // ENTER key + this.go(); + } + } + +} +ComicNavController.$inject = ['$scope', '$log', 'comicService', 'eventService', 'latestComic']; + +export default function (app: AngularModule) { app.directive('qcComicNav', function () { return { restrict: 'E', replace: true, scope: {}, - controller: ['$log', 'comicService', 'latestComic', 'eventFactory', - '$scope', - function ($log, comicService, latestComic, Event, $scope) { - $log.debug('START qcComicNav()'); - - var comicDataLoadedEvent = - new Event(constants.comicdataLoadedEvent); - - this.currentComic = null; - this.latestComic = latestComic; - var self = this; - - this.go = function () { - $log.debug('qcComicNav.go(): ' + self.currentComic); - if (self.currentComic === undefined || - self.currentComic === null) { - self.currentComic = latestComic; - } else if (self.currentComic < 1) { - self.currentComic = 1; - } else if (self.currentComic > latestComic) { - self.currentComic = latestComic; - } - comicService.gotoComic(self.currentComic); - }; - - this.keyPress = function (event) { - if (event.keyCode === 13) { - // ENTER key - self.go(); - } - }; - - comicDataLoadedEvent.subscribe($scope, - function (event, comicData) { - self.currentComic = comicData.comic; - }); - - $log.debug('END qcComicNav()'); - }], + controller: ComicNavController, controllerAs: 'cn', template: variables.html.comicNav }; diff --git a/assets/js/modules/angular/directives/qcDateDirective.js b/assets/js/modules/angular/directives/qcDateDirective.js index 92049a7..6db425a 100644 --- a/assets/js/modules/angular/directives/qcDateDirective.js +++ b/assets/js/modules/angular/directives/qcDateDirective.js @@ -1,3 +1,4 @@ +// @flow /* * Copyright (C) 2016-2018 Alexander Krivács Schrøder * @@ -15,66 +16,74 @@ * along with this program. If not, see . */ +import type { AngularModule, $Log } from 'angular'; + import constants from '../../../constants'; -import settings from '../../settings'; +import settings, { Settings } from '../../settings'; import variables from '../../../../generated/variables.pass2'; -export default function (app) { +import { ComicDataControllerBase } from '../controllers/ControllerBases'; + +import type { $DecoratedScope } from '../decorateScope'; +import type { EventService } from '../services/eventService'; +import type { ComicData } from '../api/comicData'; + +export class DateController extends ComicDataControllerBase { + static $inject: string[]; + + $log: $Log; + + settings: Settings; + + date: ?Date; + approximateDate: boolean; + + constructor( + $scope: $DecoratedScope, + $log: $Log, + eventService: EventService + ) { + $log.debug('START DateController'); + + super($scope, eventService); + + this.$log = $log; + + this.settings = settings; + this.date = null; + this.approximateDate = false; + + $log.debug('END DateController'); + } + + _comicDataLoading(comic: number) { + self.date = null; + self.approximateDate = false; + } + + _comicDataLoaded(comicData: ComicData) { + this.approximateDate = !comicData.isAccuratePublishDate; + const publishDate = comicData.publishDate; + this.$log.debug('DateController::_comicDataLoaded(): ', publishDate); + if (publishDate !== null && + publishDate !== undefined) { + const date = new Date(publishDate); + this.date = date; + } else { + this.date = null; + } + } + +} +DateController.$inject = ['$scope', '$log', 'eventService']; + +export default function (app: AngularModule) { app.directive('qcDate', function () { return { restrict: 'E', replace: true, scope: {}, - controller: ['$scope', '$log', 'eventFactory', - function ($scope, $log, Event) { - var comicDataLoadingEvent = - new Event(constants.comicdataLoadingEvent); - var comicDataLoadedEvent = - new Event(constants.comicdataLoadedEvent); - - $log.debug('START qcDate()'); - - $scope.safeApply = function (fn) { - var phase = this.$root.$$phase; - - if (phase === '$apply' || phase === '$digest') { - if (fn && typeof fn === 'function') { - fn(); - } - } else { - this.$apply(fn); - } - }; - - var self = this; - this.settings = settings; - this.date = null; - this.approximateDate = false; - comicDataLoadingEvent.subscribe($scope, - function () { - $scope.safeApply(function () { - self.date = null; - self.approximateDate = false; - }); - }); - comicDataLoadedEvent.subscribe($scope, - function (event, comicData) { - $scope.safeApply(function () { - self.approximateDate = !comicData.isAccuratePublishDate; - var publishDate = comicData.publishDate; - $log.debug('qcDate(): ', publishDate); - if (publishDate !== null && - publishDate !== undefined) { - var date = new Date(publishDate); - self.date = date; - } else { - self.date = null; - } - }); - }); - - $log.debug('END qcDate()'); - }], + controller: DateController, controllerAs: 'd', template: variables.html.date }; diff --git a/assets/js/modules/angular/directives/qcEditComicDataDirective.js b/assets/js/modules/angular/directives/qcEditComicDataDirective.js index c000266..a2ed06d 100644 --- a/assets/js/modules/angular/directives/qcEditComicDataDirective.js +++ b/assets/js/modules/angular/directives/qcEditComicDataDirective.js @@ -1,3 +1,4 @@ +// @flow /* * Copyright (C) 2016-2018 Alexander Krivács Schrøder * @@ -15,85 +16,100 @@ * along with this program. If not, see . */ +import type { AngularModule, $Log } from 'angular'; + import angular from 'angular'; import constants from '../../../constants'; import variables from '../../../../generated/variables.pass2'; -export default function (app) { +import { ComicDataControllerBase } from '../controllers/ControllerBases'; + +import type { $DecoratedScope } from '../decorateScope'; +import type { ComicService } from '../services/comicService'; +import type { EventService } from '../services/eventService'; +import type { ComicData, ComicItem } from '../api/comicData'; +import type { ItemType } from '../api/itemData'; + +export class EditComicDataController extends ComicDataControllerBase { + static $inject: string[]; + + $log: $Log; + comicService: ComicService; + + editData: any; // TODO: Make properly strongly typed + + constructor( + $scope: $DecoratedScope, + $log: $Log, + eventService: EventService, + comicService: ComicService + ) { + $log.debug('START EditComicDataController'); + + super($scope, eventService); + + this.$log = $log; + this.comicService = comicService; + + $('#editComicDataDialog').on('show.bs.modal', () => { + // If something needs to be done, do it here. + }); + + $log.debug('END EditComicDataController'); + } + + _comicDataLoaded(comicData: ComicData) { + const editData: { comicData: ComicData, [ItemType]: { [number]: ComicItem } } = { comicData: comicData }; + + if (comicData.hasData) { + angular.forEach(comicData.items, + (item) => { + let editDataType: { [number]: ComicItem }; + if (!editData[item.type]) { + editDataType = editData[item.type] = {}; + } else { + editDataType = editData[item.type]; + } + + editDataType[item.id] = item; + }); + } + + this.editData = editData; + } + + remove(item: ComicItem) { + this.comicService.removeItem(item).then((response) => { + if (response.status === 200) { + this.eventService.itemsChangedEvent.publish(); + } + }); + } + + changeGuestComic() { + this.comicService.setGuestComic( + this.editData.comicData.isGuestComic); + } + + changeNonCanon() { + this.comicService.setNonCanon( + this.editData.comicData.isNonCanon); + } + + close() { + ($('#editComicDataDialog'): any).modal('hide'); + } +} +EditComicDataController.$inject = ['$scope', '$log', 'eventService', 'comicService']; + +export default function (app: AngularModule) { app.directive('qcEditComicData', function () { return { restrict: 'E', replace: true, scope: {}, - controller: ['$scope', '$log', 'eventFactory', 'comicService', - function ($scope, $log, Event, comicService) { - var comicDataLoadedEvent = - new Event(constants.comicdataLoadedEvent); - var itemsChangedEvent = - new Event(constants.itemsChangedEvent); - - $scope.safeApply = function (fn) { - var phase = this.$root.$$phase; - if (phase === '$apply' || phase === '$digest') { - if (fn && typeof fn === 'function') { - fn(); - } - } else { - this.$apply(fn); - } - }; - - var self = this; - - comicDataLoadedEvent - .subscribe($scope, function (event, comicData) { - $scope.safeApply(function () { - var editData = { comicData: comicData }; - - if (comicData.hasData) { - angular.forEach(comicData.items, - function (value) { - - if (!(value.type in editData)) { - editData[value.type] = {}; - } - - editData[value.type][value.id] = - value; - }); - } - - self.editData = editData; - }); - }); - - $('#editComicDataDialog').on('show.bs.modal', function () { - // If something needs to be done, do it here. - }); - - this.remove = function (item) { - comicService.removeItem(item).then(function (response) { - if (response.status === 200) { - itemsChangedEvent.notify(); - } - }); - }; - - this.changeGuestComic = function () { - comicService.setGuestComic( - self.editData.comicData.isGuestComic); - }; - - this.changeNonCanon = function () { - comicService.setNonCanon( - self.editData.comicData.isNonCanon); - }; - - this.close = function () { - $('#editComicDataDialog').modal('hide'); - }; - }], + controller: EditComicDataController, controllerAs: 'ecdvm', template: variables.html.editComicData }; diff --git a/assets/js/modules/angular/directives/qcExtraDirective.js b/assets/js/modules/angular/directives/qcExtraDirective.js index ea4063f..8c9586a 100644 --- a/assets/js/modules/angular/directives/qcExtraDirective.js +++ b/assets/js/modules/angular/directives/qcExtraDirective.js @@ -1,3 +1,4 @@ +// @flow /* * Copyright (C) 2016-2018 Alexander Krivács Schrøder * @@ -15,251 +16,284 @@ * along with this program. If not, see . */ +import type { AngularModule, $Log, $Http, $Sce } from 'angular'; + +import GM from 'greasemonkey'; import angular from 'angular'; import constants from '../../../constants'; -import settings from '../../settings'; +import settings, { Settings } from '../../settings'; import variables from '../../../../generated/variables.pass2'; -export default function (app) { - function Controller($scope, $log, comicService, latestComic, Event, $sce) { - var comicDataLoadingEvent = new Event(constants.comicdataLoadingEvent); - var comicDataLoadedEvent = new Event(constants.comicdataLoadedEvent); - var comicDataErrorEvent = new Event(constants.comicdataErrorEvent); - - $scope.safeApply = function (fn) { - var phase = this.$root.$$phase; - - if (phase === '$apply' || phase === '$digest') { - if (fn && - typeof fn === 'function') { - fn(); - } - } else { - this.$apply(fn); - } - }; - - var self = this; - +import { ComicDataControllerBase } from '../controllers/ControllerBases'; + +import type { $DecoratedScope } from '../decorateScope'; +import type { ColorService } from '../services/colorService'; +import type { ComicService } from '../services/comicService'; +import type { EventService } from '../services/eventService'; +import type { MessageReportingService } from '../services/messageReportingService'; +import type { StyleService } from '../services/styleService'; +import type { ItemData } from '../api/itemData'; +import type { ComicItemRepository, ComicData, ComicEditorData, ComicItem } from '../api/comicData'; + +export class ExtraController extends ComicDataControllerBase { + static $inject: string[]; + + $log: $Log; + $sce: $Sce; + comicService: ComicService; + + settings: Settings; + constants: *; + + messages: string[]; + missingDataInfo: string[]; + showWelcomeMessage: boolean; + showUpdateMessage: boolean; + isLoading: boolean; + hasError: boolean; + hasWarning: boolean; + + items: ComicItemRepository; + allItems: ComicItemRepository; + + editorData: ComicEditorData; + + constructor( + $scope: $DecoratedScope, + $log: $Log, + $sce: $Sce, + comicService: ComicService, + eventService: EventService, + latestComic: number + ) { + $log.debug('START ExtraController'); + + super($scope, eventService); + + this.$log = $log; + this.$sce = $sce; this.comicService = comicService; + this.settings = settings; this.constants = constants; this.items = {}; this.allItems = {}; - this.editorData = {}; + this.editorData = (({}: any): ComicEditorData); this.messages = []; this.missingDataInfo = []; - function reset() { - self.isLoading = false; - self.items = {}; - self.allItems = {}; - self.editorData = {}; - self.messages.length = 0; - self.missingDataInfo.length = 0; - self.hasError = false; - self.hasWarning = false; + $log.debug('END ExtraController'); + } + + _comicDataLoading(comic: number) { + this._loading(); + } + + _comicDataLoaded(comicData: ComicData) { + this._reset(); + + if (this.settings.values.editMode && + comicData.editorData) { + this.editorData = comicData.editorData; + this.editorData.missing.cast.any = + this.editorData.missing.cast.first !== null; + this.editorData.missing.location.any = + this.editorData.missing.location.first !== null; + this.editorData.missing.storyline.any = + this.editorData.missing.storyline.first !== null; + this.editorData.missing.title.any = + this.editorData.missing.title.first !== null; + this.editorData.missing.tagline.any = + this.editorData.missing.tagline.first !== null; + this.editorData.missing.any = + this.editorData.missing.cast.any || + this.editorData.missing.location.any || + this.editorData.missing.storyline.any || + this.editorData.missing.title.any || + this.editorData.missing.tagline.any; + + if (this.editorData.missing.cast.first == this.comicService.comic) { + this.editorData.missing.cast.first = null; + } + if (this.editorData.missing.cast.last == this.comicService.comic) { + this.editorData.missing.cast.last = null; + } + + if (this.editorData.missing.location.first == this.comicService.comic) { + this.editorData.missing.location.first = null; + } + if (this.editorData.missing.location.last == this.comicService.comic) { + this.editorData.missing.location.last = null; + } + + if (this.editorData.missing.storyline.first == this.comicService.comic) { + this.editorData.missing.storyline.first = null; + } + if (this.editorData.missing.storyline.last == this.comicService.comic) { + this.editorData.missing.storyline.last = null; + } } - function loading() { - reset(); - self.isLoading = true; - self.messages.push('Loading...'); + const self = this; + function processItem(item: ComicItem) { + let items: ComicItem[]; + if (!self.items[item.type]) { + items = self.items[item.type] = []; + } else { + items = self.items[item.type]; + } + items.push(item); } - comicDataLoadingEvent.subscribe($scope, function () { - $scope.safeApply(loading); - }); - - comicDataLoadedEvent.subscribe($scope, - function (event, comicData) { - $scope.safeApply(function () { - reset(); - - if (self.settings.editMode && - comicData.editorData) { - self.editorData = comicData.editorData; - self.editorData.missing.cast.any = - self.editorData.missing.cast.first !== null; - self.editorData.missing.location.any = - self.editorData.missing.location.first !== null; - self.editorData.missing.storyline.any = - self.editorData.missing.storyline.first !== null; - self.editorData.missing.title.any = - self.editorData.missing.title.first !== null; - self.editorData.missing.tagline.any = - self.editorData.missing.tagline.first !== null; - self.editorData.missing.any = - self.editorData.missing.cast.any || - self.editorData.missing.location.any || - self.editorData.missing.storyline.any || - self.editorData.missing.title.any || - self.editorData.missing.tagline.any; - - if (self.editorData.missing.cast.first == comicService.comic) { - self.editorData.missing.cast.first = null; - } - if (self.editorData.missing.cast.last == comicService.comic) { - self.editorData.missing.cast.last = null; - } - - if (self.editorData.missing.location.first == comicService.comic) { - self.editorData.missing.location.first = null; - } - if (self.editorData.missing.location.last == comicService.comic) { - self.editorData.missing.location.last = null; - } - - if (self.editorData.missing.storyline.first == comicService.comic) { - self.editorData.missing.storyline.first = null; - } - if (self.editorData.missing.storyline.last == comicService.comic) { - self.editorData.missing.storyline.last = null; - } - } - - function processItem(item) { - if (!(item.type in self.items)) { - self.items[item.type] = []; - } - self.items[item.type].push(item); - } - - function processAllItem(item) { - if (!(item.type in self.allItems)) { - self.allItems[item.type] = []; - } - self.allItems[item.type].push(item); - } - - if (!comicData.hasData) { - self.messages.push( - 'This strip has no navigation data yet' - ); - self.hasWarning = true; - - if (settings.showAllMembers) { - angular.forEach(comicData.allItems, processAllItem); - } - return; - } - - var hasCast = false; - var hasLocation = false; - //var hasStoryline = false; - angular.forEach(comicData.items, - function (item) { - processItem(item); - - if (item.type === 'cast') { - hasCast = true; - } else if (item.type === 'location') { - hasLocation = true; - } else if (item.type === 'storyline') { - //hasStoryline = true; - } - } - ); - if (settings.showAllMembers) { - angular.forEach(comicData.allItems, processAllItem); - } - - if (!hasCast) { - self.missingDataInfo.push('cast members'); - } - if (!hasLocation) { - self.missingDataInfo.push('a location'); - } - /* #if (!hasStoryline) { - self.missingDataInfo.push('a storyline'); - }*/ - if (!comicData.title) { - self.missingDataInfo.push('a title'); - } - if (!comicData.tagline && - comicService.comic > constants.taglineThreshold) { - self.missingDataInfo.push('a tagline'); - } - - var currentVersion = GM.info.script.version; - if (settings.version === undefined) { - // Version is undefined. We're a new user! - $log.debug('qcExtra(): Version undefined!'); - self.showWelcomeMessage = true; - } else if (settings.version !== - currentVersion) { - // Version is changed. Script has been updated! - self.showUpdateMessage = true; - } - }); - }); - - comicDataErrorEvent.subscribe($scope, - function (event, data) { - $scope.safeApply(reset); - $scope.safeApply(function () { - if (data.status !== 503) { - self.messages.push('Error communicating with server'); - self.hasError = true; - } else { - self.messages.push(constants.messages.maintenance); - } - }); - }); - - this.getTypeDescription = function (type) { - switch (type) { - case 'cast': - return 'Cast Members'; - case 'storyline': - return 'Storylines'; - case 'location': - return 'Locations'; - - case 'all-cast': - return $sce.trustAsHtml('Cast Members
' + - '(Non-Present)'); - case 'all-storyline': - return $sce.trustAsHtml('Storylines
' + - '(Non-Present)'); - case 'all-location': - return $sce.trustAsHtml('Locations
' + - '(Non-Present)'); + function processAllItem(item: ComicItem) { + let items: ComicItem[]; + if (!self.allItems[item.type]) { + items = self.allItems[item.type] = []; + } else { + items = self.allItems[item.type]; } - }; + items.push(item); + } - this.openSettings = function () { - $('#settingsDialog').modal('show'); - }; + if (!comicData.hasData) { + this.messages.push( + 'This strip has no navigation data yet' + ); + this.hasWarning = true; - this.editComicData = function () { - $('#editComicDataDialog').modal('show'); - }; + if (settings.values.showAllMembers && comicData.allItems) { + angular.forEach(comicData.allItems, processAllItem); + } + return; + } - this.showDetailsFor = function (item) { - $('#itemDetailsDialog').data('itemId', item.id); - $('#itemDetailsDialog').modal('show'); - }; + let hasCast = false; + let hasLocation = false; + //let hasStoryline = false; + angular.forEach(comicData.items, + function (item) { + processItem(item); + + if (item.type === 'cast') { + hasCast = true; + } else if (item.type === 'location') { + hasLocation = true; + } else if (item.type === 'storyline') { + //hasStoryline = true; + } + } + ); + if (settings.values.showAllMembers && comicData.allItems) { + angular.forEach(comicData.allItems, processAllItem); + } - this.enableTagModeFor = function (item) { - $log.debug(item); - }; + if (!hasCast) { + this.missingDataInfo.push('cast members'); + } + if (!hasLocation) { + this.missingDataInfo.push('a location'); + } + /* #if (!hasStoryline) { + self.missingDataInfo.push('a storyline'); + }*/ + if (!comicData.title) { + this.missingDataInfo.push('a title'); + } + if (!comicData.tagline && + this.comicService.comic > constants.taglineThreshold) { + this.missingDataInfo.push('a tagline'); + } - this.showChangeLog = function () { - self.showWelcomeMessage = false; - self.showUpdateMessage = false; - $('#changeLogDialog').modal('show'); - }; + const currentVersion = GM.info.script.version; + if (!settings.values.version) { + // Version is undefined. We're a new user! + this.$log.debug('qcExtra(): Version undefined!'); + this.showWelcomeMessage = true; + } else if (settings.values.version !== + currentVersion) { + // Version is changed. Script has been updated! + this.$log.debug(`qcExtra(): Version is ${settings.values.version}!`); + this.showUpdateMessage = true; + } + } + + _comicDataError(error: any) { + this._reset(); + if (error.status !== 503) { + this.messages.push('Error communicating with server'); + this.hasError = true; + } else { + this.messages.push(constants.messages.maintenance); + } } + _reset() { + this.isLoading = false; + this.items = {}; + this.allItems = {}; + this.editorData = (({}: any): ComicEditorData); + this.messages.length = 0; + this.missingDataInfo.length = 0; + this.hasError = false; + this.hasWarning = false; + } + + _loading() { + this._reset(); + this.isLoading = true; + this.messages.push('Loading...'); + } + + getTypeDescription(type: string) { + switch (type) { + case 'cast': + return 'Cast Members'; + case 'storyline': + return 'Storylines'; + case 'location': + return 'Locations'; + + case 'all-cast': + return this.$sce.trustAsHtml('Cast Members
' + + '(Non-Present)'); + case 'all-storyline': + return this.$sce.trustAsHtml('Storylines
' + + '(Non-Present)'); + case 'all-location': + return this.$sce.trustAsHtml('Locations
' + + '(Non-Present)'); + } + } + + openSettings() { + ($('#settingsDialog'): any).modal('show'); + } + + editComicData() { + ($('#editComicDataDialog'): any).modal('show'); + } + + showDetailsFor(item: ItemData) { + $('#itemDetailsDialog').data('itemId', item.id); + ($('#itemDetailsDialog'): any).modal('show'); + } + + showChangeLog() { + this.showWelcomeMessage = false; + this.showUpdateMessage = false; + ($('#changeLogDialog'): any).modal('show'); + } +} +ExtraController.$inject = ['$scope', '$log', '$sce', 'comicService', 'eventService', 'latestComic']; + +export default function (app: AngularModule) { app.directive('qcExtra', function () { return { restrict: 'E', replace: true, scope: {}, - controller: ['$scope', '$log', 'comicService', 'latestComic', - 'eventFactory', '$sce', Controller], + controller: ExtraController, controllerAs: 'e', template: variables.html.extra }; diff --git a/assets/js/modules/angular/directives/qcExtraNavDirective.js b/assets/js/modules/angular/directives/qcExtraNavDirective.js index 05abccf..93c25aa 100644 --- a/assets/js/modules/angular/directives/qcExtraNavDirective.js +++ b/assets/js/modules/angular/directives/qcExtraNavDirective.js @@ -1,3 +1,4 @@ +// @flow /* * Copyright (C) 2016-2018 Alexander Krivács Schrøder * @@ -15,9 +16,11 @@ * along with this program. If not, see . */ +import type { AngularModule } from 'angular'; + import variables from '../../../../generated/variables.pass2'; -export default function (app) { +export default function (app: AngularModule) { app.directive('qcExtraNav', function () { return { restrict: 'E', diff --git a/assets/js/modules/angular/directives/qcItemDetailsDirective.js b/assets/js/modules/angular/directives/qcItemDetailsDirective.js index fdbf24b..4620141 100644 --- a/assets/js/modules/angular/directives/qcItemDetailsDirective.js +++ b/assets/js/modules/angular/directives/qcItemDetailsDirective.js @@ -1,3 +1,4 @@ +// @flow /* * Copyright (C) 2016-2018 Alexander Krivács Schrøder * @@ -15,184 +16,213 @@ * along with this program. If not, see . */ +import type { AngularModule, $Log, $Http } from 'angular'; + import constants from '../../../constants'; -import settings from '../../settings'; +import settings, { Settings } from '../../settings'; import variables from '../../../../generated/variables.pass2'; -export default function (app) { +import type { $DecoratedScope } from '../decorateScope'; +import type { ColorService } from '../services/colorService'; +import type { ComicService } from '../services/comicService'; +import type { MessageReportingService } from '../services/messageReportingService'; +import type { StyleService } from '../services/styleService'; +import type { DecoratedItemData, ItemRelationData } from '../api/itemData'; + +export class ItemDetailsController { + static $inject: string[]; + + $log: $Log; + $http: $Http; + $scope: $DecoratedScope; + colorService: ColorService; + comicService: ComicService; + messageReportingService: MessageReportingService; + styleService: StyleService; + + isLoading: boolean; + settings: Settings; + itemData: DecoratedItemData; + + constructor( + $log: $Log, + $http: $Http, + $scope: $DecoratedScope, + colorService: ColorService, + comicService: ComicService, + messageReportingService: MessageReportingService, + styleService: StyleService + ) { + $log.debug('START ItemDetailsController'); + + this.$log = $log; + this.$http = $http; + this.$scope = $scope; + this.colorService = colorService; + this.comicService = comicService; + this.messageReportingService = messageReportingService; + this.styleService = styleService; + + this.isLoading = true; + this.settings = settings; + + $('#itemDetailsDialog').on('show.bs.modal', () => this._getItemDetails()); + + $log.debug('END ItemDetailsController'); + } + + _getItemDetails() { + const self = this; + const itemId = $('#itemDetailsDialog').data('itemId'); + this.$log.debug('ItemDetailsController::showModal() - item id:', + itemId); + + function handleRelationData(response): ?ItemRelationData[] { + if (response.status === 200) { + const relationData = (response.data: ItemRelationData[]); + + $.each(relationData, (_: number, relation) => { + relation.percentage = relation.count / + self.itemData.appearances * 100; + }); + + return relationData; + } + return null; + } + + function handleItemFriendsData(response) { + let friends = handleRelationData(response); + self.$scope.safeApply(() => { + self.itemData.friends = friends || []; + }); + } + + function handleItemLocationsData(response) { + let locations = handleRelationData(response); + self.$scope.safeApply(() => { + self.itemData.locations = locations || []; + }); + } + + function handleItemData(response) { + if (response.status === 200) { + const itemData = response.data; + + itemData.highlightColor = self.colorService + .createTintOrShade(itemData.color); + + if (itemData.hasImage) { + itemData.imagePath = + constants.characterImageBaseUrl + + itemData.id + '.' + + constants.characterImageExtension; + } + + self.$log.debug('qcItemDetails::showModal() - ' + + 'item data:', itemData); + + // If the color changes, also update the + // highlight color + self.$scope.safeApply(() => { + self.itemData = itemData; + self.isLoading = false; + + self.$scope.$watch(() => { + return self.itemData.color; + }, function () { + self.itemData.highlightColor = + self.colorService + .createTintOrShade( + itemData.color); + }); + }); + + self.$http.get(constants.itemFriendDataUrl + itemId) + .then(handleItemFriendsData); + self.$http.get(constants.itemLocationDataUrl + + itemId).then(handleItemLocationsData); + } else { + self.messageReportingService.reportError( + response.data); + } + } + + this.itemData = (({}: any): DecoratedItemData); + this.isLoading = true; + this.$http.get(constants.itemDataUrl + itemId) + .then(response => handleItemData(response)); + } + + _onErrorLog(response: any) { + this.messageReportingService.reportError(response.data); + return response; + } + + _onSuccessRefreshElseErrorLog(response: any) { + if (response.status === 200) { + this.comicService.refreshComicData(); + } else { + this._onErrorLog(response); + } + return response; + } + + showInfoFor(id: number) { + $('#itemDetailsDialog').data('itemId', id); + this._getItemDetails(); + } + + keypress(event: KeyboardEvent, property: string) { + if (event.keyCode === 13) { + // ENTER key + this.update(property); + } + } + + update(property: string) { + const self = this; + function updateItemColor(response) { + if (response.status === 200) { + if (property === 'color') { + self.$log.debug('ItemDetailsController::update() - ' + + 'update item color'); + self.styleService.removeItemStyle( + self.itemData.id); + } + } + return self._onSuccessRefreshElseErrorLog(response); + } + + const data = { + token: settings.values.editModeToken, + item: this.itemData.id, + property: property, + value: this.itemData[property] + }; + this.$http.post(constants.setItemDataPropertyUrl, data) + .then(r => updateItemColor(r)).catch(r => this._onErrorLog(r)); + } + + goToComic(comic: number) { + this.comicService.gotoComic(comic); + this.close(); + } + + close() { + ($('#itemDetailsDialog'): any).modal('hide'); + } +} +ItemDetailsController.$inject = ['$log', '$http', '$scope', 'colorService', + 'comicService', 'messageReportingService', 'styleService']; + +export default function (app: AngularModule) { app.directive('qcItemDetails', function () { return { restrict: 'E', replace: true, scope: {}, - controller: ['$log', '$http', '$scope', 'colorService', - 'comicService', 'messageReportingService', 'styleService', - function ($log, $http, $scope, colorService, - comicService, messageReportingService, styleService) { - var self = this; - - this.isLoading = true; - this.settings = settings; - - $scope.safeApply = function (fn) { - var phase = this.$root.$$phase; - if (phase === '$apply' || phase === '$digest') { - if (fn && typeof fn === 'function') { - fn(); - } - } else { - this.$apply(fn); - } - }; - - function getItemDetails() { - var itemId = $('#itemDetailsDialog').data('itemId'); - $log.debug('qcItemDetails::showModal() - item id:', - itemId); - - function handleRelationData(response) { - if (response.status === 200) { - var relationData = response.data; - - $.each(relationData, function (_, relation) { - relation.percentage = relation.count / - self.itemData.appearances * 100; - }); - - return relationData; - } - return null; - } - - function handleItemFriendsData(response) { - var friends = handleRelationData(response); - if (friends === null) { - friends = []; - } - $scope.safeApply(function () { - self.itemData.friends = friends; - }); - } - - function handleItemLocationsData(response) { - var locations = handleRelationData(response); - if (locations === null) { - locations = []; - } - $scope.safeApply(function () { - self.itemData.locations = locations; - }); - } - - function handleItemData(response) { - if (response.status === 200) { - var itemData = response.data; - - itemData.highlightColor = colorService - .createTintOrShade(itemData.color); - - if (itemData.hasImage) { - itemData.imagePath = - constants.characterImageBaseUrl + - itemData.id + '.' + - constants.characterImageExtension; - } - - $log.debug('qcItemDetails::showModal() - ' + - 'item data:', itemData); - - // If the color changes, also update the - // highlight color - $scope.safeApply(function () { - self.itemData = itemData; - self.isLoading = false; - - $scope.$watch(function () { - return self.itemData.color; - }, function () { - itemData.highlightColor = - colorService - .createTintOrShade( - itemData.color); - }); - }); - - $http.get(constants.itemFriendDataUrl + itemId) - .then(handleItemFriendsData); - $http.get(constants.itemLocationDataUrl + - itemId).then(handleItemLocationsData); - } else { - messageReportingService.reportError( - response.data); - } - } - - self.itemData = {}; - self.isLoading = true; - $http.get(constants.itemDataUrl + itemId) - .then(handleItemData); - } - - $('#itemDetailsDialog').on('show.bs.modal', getItemDetails); - - this.showInfoFor = function (id) { - $('#itemDetailsDialog').data('itemId', id); - getItemDetails(); - }; - - this.keypress = function (event, property) { - if (event.keyCode === 13) { - // ENTER key - self.update(property); - } - }; - - function onErrorLog(response) { - messageReportingService.reportError(response.data); - return response; - } - - function onSuccessRefreshElseErrorLog(response) { - if (response.status === 200) { - comicService.refreshComicData(); - } else { - onErrorLog(response); - } - return response; - } - - this.update = function (property) { - function updateItemColor(response) { - if (response.status === 200) { - if (property === 'color') { - $log.debug('qcItemDetails::update() - ' + - 'update item color'); - styleService.removeItemStyle( - self.itemData.id); - } - } - return onSuccessRefreshElseErrorLog(response); - } - - var data = { - token: settings.editModeToken, - item: self.itemData.id, - property: property, - value: self.itemData[property] - }; - $http.post(constants.setItemDataPropertyUrl, data) - .then(updateItemColor, onErrorLog); - }; - - this.goToComic = function (comic) { - comicService.gotoComic(comic); - self.close(); - }; - - this.close = function () { - $('#itemDetailsDialog').modal('hide'); - }; - }], + controller: ItemDetailsController, controllerAs: 'idvm', template: variables.html.itemDetails }; diff --git a/assets/js/modules/angular/directives/qcNavDirective.js b/assets/js/modules/angular/directives/qcNavDirective.js index 01155a5..92657c1 100644 --- a/assets/js/modules/angular/directives/qcNavDirective.js +++ b/assets/js/modules/angular/directives/qcNavDirective.js @@ -1,3 +1,4 @@ +// @flow /* * Copyright (C) 2016-2018 Alexander Krivács Schrøder * @@ -15,56 +16,78 @@ * along with this program. If not, see . */ +import type { AngularModule, $Log } from 'angular'; + import variables from '../../../../generated/variables.pass2'; -export default function (app) { - app.directive('qcNav', function () { - return { - restrict: 'E', - scope: { randomComic: '=' }, - controller: ['$scope', 'comicService', 'latestComic', - function ($scope, comicService, latestComic) { - this.comicService = comicService; +import type { $DecoratedScope } from '../decorateScope'; +import type { ComicService } from '../services/comicService'; + +export class NavController { + static $inject: string[]; + + $scope: $DecoratedScope; + comicService: ComicService; + latestComic: number; + randomComic: number; - function updateRandomComic() { - $scope.randomComic = Math.floor(Math.random() * - (parseInt(latestComic) + 1)); - } + constructor( + $scope: $DecoratedScope, + comicService: ComicService, + latestComic: number + ) { + this.$scope = $scope; + this.comicService = comicService; + this.latestComic = latestComic; - updateRandomComic(); + this._updateRandomComic(); + } - this.first = function (event) { - event.preventDefault(); - event.stopPropagation(); - comicService.first(); - }; + _updateRandomComic() { + this.$scope.randomComic = Math.floor(Math.random() * + (parseInt(this.latestComic) + 1)); + } - this.previous = function (event) { - event.preventDefault(); - event.stopPropagation(); - comicService.previous(); - }; + first(event: UIEvent) { + event.preventDefault(); + event.stopPropagation(); + this.comicService.first(); + } - this.next = function (event) { - event.preventDefault(); - event.stopPropagation(); - comicService.next(); - }; + previous(event: UIEvent) { + event.preventDefault(); + event.stopPropagation(); + this.comicService.previous(); + } - this.last = function (event) { - event.preventDefault(); - event.stopPropagation(); - comicService.last(); - }; + next(event: UIEvent) { + event.preventDefault(); + event.stopPropagation(); + this.comicService.next(); + } - this.random = function (event) { - event.preventDefault(); - event.stopPropagation(); + last(event: UIEvent) { + event.preventDefault(); + event.stopPropagation(); + this.comicService.last(); + } - comicService.gotoComic($scope.randomComic); - updateRandomComic(); - }; - }], + random(event: UIEvent) { + event.preventDefault(); + event.stopPropagation(); + + this.comicService.gotoComic(this.$scope.randomComic); + this._updateRandomComic(); + } +} +NavController.$inject = ['$scope', 'comicService', 'latestComic']; + +export default function (app: AngularModule) { + app.directive('qcNav', function () { + return { + restrict: 'E', + scope: { randomComic: '=' }, + controller: NavController, controllerAs: 'n', template: variables.html.navigation }; diff --git a/assets/js/modules/angular/directives/qcNewsDirective.js b/assets/js/modules/angular/directives/qcNewsDirective.js index 0e29dff..5dddc95 100644 --- a/assets/js/modules/angular/directives/qcNewsDirective.js +++ b/assets/js/modules/angular/directives/qcNewsDirective.js @@ -1,3 +1,4 @@ +// @flow /* * Copyright (C) 2016-2018 Alexander Krivács Schrøder * @@ -15,70 +16,65 @@ * along with this program. If not, see . */ +import type { AngularModule, $Log, $Sce } from 'angular'; + import constants from '../../../constants'; +import { nl2br, angularizeLinks } from '../util'; -export default function (app) { - app.directive('qcNews', function () { - return { - restrict: 'E', - replace: true, - scope: {}, - controller: ['$scope', '$sce', '$compile', 'eventFactory', - function ($scope, $sce, $compile, Event) { - var comicDataLoadingEvent = - new Event(constants.comicdataLoadingEvent); - var comicDataLoadedEvent = - new Event(constants.comicdataLoadedEvent); +import { ComicDataControllerBase } from '../controllers/ControllerBases'; + +import type { $DecoratedScope } from '../decorateScope'; +import type { EventService } from '../services/eventService'; +import type { ComicData } from '../api/comicData'; + +export class NewsController extends ComicDataControllerBase { + static $inject: string[]; - $scope.safeApply = function (fn) { - var phase = this.$root.$$phase; + $sce: $Sce; - if (phase === '$apply' || phase === '$digest') { - if (fn && typeof fn === 'function') { - fn(); - } - } else { - this.$apply(fn); - } - }; + news: string; - function nl2br(str, isXhtml) { - var breakTag = isXhtml || - typeof isXhtml === 'undefined' ? - '
' : '
'; + constructor( + $scope: $DecoratedScope, + $log: $Log, + eventService: EventService, + $sce: $Sce + ) { + $log.debug('START NewsController'); - return String(str).replace(/([^>\r\n]?)(\r\n|\n\r|\r|\n)/g, - '$1' + breakTag + '$2'); - } + super($scope, eventService); + this.$sce = $sce; - function angularizeLinks(str) { - var comicLinkRegexp = - /]*href=(?:"|')(?:http:\/\/(?:www\.)?questionablecontent.net\/)?view\.php\?comic=(\d+)(?:"|')[^>]*>/; + this.news = $sce.trustAsHtml('Loading...'); - return String(str).replace(comicLinkRegexp, - ''); - } + $log.debug('END NewsController'); + } - var self = this; + _comicDataLoading(comic: number) { + this.news = this.$sce.trustAsHtml('Loading...'); + } - this.news = $sce.trustAsHtml('Loading...'); - comicDataLoadingEvent.subscribe($scope, - function () { - $scope.safeApply(function () { - self.news = $sce.trustAsHtml('Loading...'); - }); - }); - comicDataLoadedEvent.subscribe($scope, - function (event, comicData) { - $scope.safeApply(function () { - self.news = $sce.trustAsHtml(nl2br( - angularizeLinks(comicData.news), false)); - }); - }); - }], + _comicDataLoaded(comicData: ComicData) { + const news = comicData.news; + if (news == null) { + this.news = ''; + } else { + this.news = this.$sce.trustAsHtml(nl2br(angularizeLinks(news), false)); + } + } + +} +NewsController.$inject = ['$scope', '$log', 'eventService', '$sce']; + +export default function (app: AngularModule) { + app.directive('qcNews', function () { + return { + restrict: 'E', + replace: true, + scope: {}, + controller: NewsController, controllerAs: 'n', - template: - '
' + template: '
' }; }); } diff --git a/assets/js/modules/angular/directives/qcRibbonDirective.js b/assets/js/modules/angular/directives/qcRibbonDirective.js index e095bec..7e01036 100644 --- a/assets/js/modules/angular/directives/qcRibbonDirective.js +++ b/assets/js/modules/angular/directives/qcRibbonDirective.js @@ -1,3 +1,4 @@ +// @flow /* * Copyright (C) 2016-2018 Alexander Krivács Schrøder * @@ -15,47 +16,58 @@ * along with this program. If not, see . */ +import type { AngularModule, $Log } from 'angular'; + import constants from '../../../constants'; -import settings from '../../settings'; +import settings, { Settings } from '../../settings'; import variables from '../../../../generated/variables.pass2'; -export default function (app) { +import { ComicDataControllerBase } from '../controllers/ControllerBases'; + +import type { $DecoratedScope } from '../decorateScope'; +import type { EventService } from '../services/eventService'; +import type { ComicData } from '../api/comicData'; + +export class RibbonController extends ComicDataControllerBase { + static $inject: string[]; + + settings: Settings; + isNonCanon: ?boolean; + isGuestComic: ?boolean; + isSmall: boolean; + + constructor( + $scope: $DecoratedScope, + $log: $Log, + eventService: EventService + ) { + $log.debug('START RibbonController'); + + super($scope, eventService); + + this.settings = settings; + this.isNonCanon = false; + this.isGuestComic = false; + this.isSmall = settings.values.showSmallRibbonByDefault; + + $log.debug('END RibbonController'); + } + + _comicDataLoaded(comicData: ComicData) { + this.isNonCanon = comicData.isNonCanon; + this.isGuestComic = comicData.isGuestComic; + } + +} +RibbonController.$inject = ['$scope', '$log', 'eventService']; + +export default function (app: AngularModule) { app.directive('qcRibbon', function () { return { restrict: 'E', replace: true, scope: {}, - controller: ['$scope', 'eventFactory', - function ($scope, Event) { - var comicDataLoadedEvent = - new Event(constants.comicdataLoadedEvent); - - $scope.safeApply = function (fn) { - var phase = this.$root.$$phase; - - if (phase === '$apply' || phase === '$digest') { - if (fn && typeof fn === 'function') { - fn(); - } - } else { - this.$apply(fn); - } - }; - - this.settings = settings; - this.isNonCanon = false; - this.isGuestComic = false; - this.isSmall = settings.showSmallRibbonByDefault; - - var self = this; - comicDataLoadedEvent.subscribe($scope, - function (event, comicData) { - $scope.safeApply(function () { - self.isNonCanon = comicData.isNonCanon; - self.isGuestComic = comicData.isGuestComic; - }); - }); - }], + controller: RibbonController, controllerAs: 'r', template: variables.html.ribbon }; diff --git a/assets/js/modules/angular/directives/qcSetPublishDateDirective.js b/assets/js/modules/angular/directives/qcSetPublishDateDirective.js index 30141de..6c93cc0 100644 --- a/assets/js/modules/angular/directives/qcSetPublishDateDirective.js +++ b/assets/js/modules/angular/directives/qcSetPublishDateDirective.js @@ -1,3 +1,5 @@ +// @flow + /* * Copyright (C) 2016-2018 Alexander Krivács Schrøder * @@ -15,77 +17,81 @@ * along with this program. If not, see . */ +import type { AngularModule, $Log } from 'angular'; + import constants from '../../../constants'; import variables from '../../../../generated/variables.pass2'; -export default function (app) { +import { SetValueControllerBase } from '../controllers/ControllerBases'; + +import type { $DecoratedScope } from '../decorateScope'; +import type { ComicService } from '../services/comicService'; +import type { EventService } from '../services/eventService'; +import type { MessageReportingService } from '../services/messageReportingService'; +import type { ComicData } from '../api/comicData'; + +export class SetPublishDateController extends SetValueControllerBase { + static $inject: string[]; + + $log: $Log; + + messageReportingService: MessageReportingService; + + publishDate: ?Date; + isAccuratePublishDate: ?boolean; + + constructor( + $scope: $DecoratedScope, + $log: $Log, + comicService: ComicService, + eventService: EventService, + messageReportingService: MessageReportingService + ) { + $log.debug('START SetPublishDateController'); + + super($scope, comicService, eventService); + this.$log = $log; + this.messageReportingService = messageReportingService; + + this.publishDate = new Date(); + + $log.debug('END SetPublishDateController'); + } + + _comicDataLoaded(comicData: ComicData) { + if (comicData.publishDate != null) { + this.publishDate = new Date(comicData.publishDate); + } else { + this.publishDate = null; + } + this.isAccuratePublishDate = + comicData.isAccuratePublishDate; + } + + _updateValue() { + this.setPublishDate(); + } + + setPublishDate() { + if (this.publishDate == null) { + // Error + this.messageReportingService.reportWarning( + 'The date entered is not valid!'); + return; + } + this.comicService.setPublishDate(this.publishDate, + this.isAccuratePublishDate != null ? this.isAccuratePublishDate : false); + } +} +SetPublishDateController.$inject = ['$scope', '$log', 'comicService', 'eventService', 'messageReportingService']; + +export default function (app: AngularModule) { app.directive('qcSetPublishDate', function () { return { restrict: 'E', replace: true, scope: {}, - controller: ['$scope', '$log', 'comicService', 'eventFactory', - 'messageReportingService', - function ($scope, $log, comicService, Event, - messageReportingService) { - $log.debug('START qcSetPublishDate()'); - - var self = this; - - this.unique = Math.random().toString(36).slice(-5); - - var comicDataLoadedEvent = - new Event(constants.comicdataLoadedEvent); - - $scope.safeApply = function (fn) { - var phase = this.$root.$$phase; - if (phase === '$apply' || phase === '$digest') { - if (fn && typeof fn === 'function') { - fn(); - } - } else { - this.$apply(fn); - } - }; - - this.keyPress = function (event) { - if (event.keyCode === 13) { - // ENTER key - self.setPublishDate(); - } - }; - - this.setPublishDate = function () { - if (self.publishDate === null || self.publishDate === - undefined) { - // Error - messageReportingService.reportWarning( - 'The date entered is not valid!'); - return; - } - comicService.setPublishDate(self.publishDate, - self.isAccuratePublishDate); - }; - - this.publishDate = ''; - comicDataLoadedEvent.subscribe($scope, - function (event, comicData) { - $scope.safeApply(function () { - var publishDate = comicData.publishDate; - if (publishDate !== null && - publishDate !== undefined) { - var date = new Date(publishDate); - self.publishDate = date; - } else { - self.publishDate = null; - } - self.isAccuratePublishDate = - comicData.isAccuratePublishDate; - }); - }); - - $log.debug('END qcSetPublishDate()'); - }], + controller: SetPublishDateController, controllerAs: 's', template: variables.html.setPublishDate }; diff --git a/assets/js/modules/angular/directives/qcSetTaglineDirective.js b/assets/js/modules/angular/directives/qcSetTaglineDirective.js index c53e736..e1d693e 100644 --- a/assets/js/modules/angular/directives/qcSetTaglineDirective.js +++ b/assets/js/modules/angular/directives/qcSetTaglineDirective.js @@ -1,3 +1,4 @@ +// @flow /* * Copyright (C) 2016-2018 Alexander Krivács Schrøder * @@ -15,58 +16,62 @@ * along with this program. If not, see . */ +import type { AngularModule, $Log } from 'angular'; + import constants from '../../../constants'; import variables from '../../../../generated/variables.pass2'; -export default function (app) { - app.directive('qcSetTagline', function () { - return { - restrict: 'E', - replace: true, - scope: {}, - controller: ['$scope', '$log', 'comicService', 'eventFactory', - function ($scope, $log, comicService, Event) { - $log.debug('START qcSetTagline()'); +import { SetValueControllerBase } from '../controllers/ControllerBases'; + +import type { $DecoratedScope } from '../decorateScope'; +import type { ComicService } from '../services/comicService'; +import type { EventService } from '../services/eventService'; +import type { ComicData } from '../api/comicData'; + +export class SetTaglineController extends SetValueControllerBase { + static $inject: string[]; - var self = this; + $log: $Log; - this.unique = Math.random().toString(36).slice(-5); + tagline: ?string; - var comicDataLoadedEvent = - new Event(constants.comicdataLoadedEvent); + constructor( + $scope: $DecoratedScope, + $log: $Log, + comicService: ComicService, + eventService: EventService + ) { + $log.debug('START SetTaglineController'); - $scope.safeApply = function (fn) { - var phase = this.$root.$$phase; - if (phase === '$apply' || phase === '$digest') { - if (fn && typeof fn === 'function') { - fn(); - } - } else { - this.$apply(fn); - } - }; + super($scope, comicService, eventService); + this.$log = $log; - this.keyPress = function (event) { - if (event.keyCode === 13) { - // ENTER key - self.setTagline(); - } - }; + this.tagline = ''; - this.setTagline = function () { - comicService.setTagline(self.tagline); - }; + $log.debug('END SetTaglineController'); + } - this.tagline = ''; - comicDataLoadedEvent.subscribe($scope, - function (event, comicData) { - $scope.safeApply(function () { - self.tagline = comicData.tagline; - }); - }); + _comicDataLoaded(comicData: ComicData) { + this.tagline = comicData.tagline; + } - $log.debug('END qcSetTagline()'); - }], + _updateValue() { + this.setTagline(); + } + + setTagline() { + this.comicService.setTagline(this.tagline ? this.tagline : ''); + } +} +SetTaglineController.$inject = ['$scope', '$log', 'comicService', 'eventService']; + +export default function (app: AngularModule) { + app.directive('qcSetTagline', function () { + return { + restrict: 'E', + replace: true, + scope: {}, + controller: SetTaglineController, controllerAs: 's', template: variables.html.setTagline }; diff --git a/assets/js/modules/angular/directives/qcSetTitleDirective.js b/assets/js/modules/angular/directives/qcSetTitleDirective.js index 61369ce..1a0bca3 100644 --- a/assets/js/modules/angular/directives/qcSetTitleDirective.js +++ b/assets/js/modules/angular/directives/qcSetTitleDirective.js @@ -1,3 +1,4 @@ +// @flow /* * Copyright (C) 2016-2018 Alexander Krivács Schrøder * @@ -15,58 +16,62 @@ * along with this program. If not, see . */ +import type { AngularModule, $Log } from 'angular'; + import constants from '../../../constants'; import variables from '../../../../generated/variables.pass2'; -export default function (app) { - app.directive('qcSetTitle', function () { - return { - restrict: 'E', - replace: true, - scope: {}, - controller: ['$scope', '$log', 'comicService', 'eventFactory', - function ($scope, $log, comicService, Event) { - $log.debug('START qcSetTitle()'); +import { SetValueControllerBase } from '../controllers/ControllerBases'; + +import type { $DecoratedScope } from '../decorateScope'; +import type { ComicService } from '../services/comicService'; +import type { EventService } from '../services/eventService'; +import type { ComicData } from '../api/comicData'; + +export class SetTitleController extends SetValueControllerBase { + static $inject: string[]; - var self = this; + $log: $Log; - this.unique = Math.random().toString(36).slice(-5); + title: ?string; - var comicDataLoadedEvent = - new Event(constants.comicdataLoadedEvent); + constructor( + $scope: $DecoratedScope, + $log: $Log, + comicService: ComicService, + eventService: EventService + ) { + $log.debug('START SetTitleController'); - $scope.safeApply = function (fn) { - var phase = this.$root.$$phase; - if (phase === '$apply' || phase === '$digest') { - if (fn && typeof fn === 'function') { - fn(); - } - } else { - this.$apply(fn); - } - }; + super($scope, comicService, eventService); + this.$log = $log; - this.keyPress = function (event) { - if (event.keyCode === 13) { - // ENTER key - self.setTitle(); - } - }; + this.title = ''; - this.setTitle = function () { - comicService.setTitle(self.title); - }; + $log.debug('END SetTitleController'); + } - this.title = ''; - comicDataLoadedEvent.subscribe($scope, - function (event, comicData) { - $scope.safeApply(function () { - self.title = comicData.title; - }); - }); + _comicDataLoaded(comicData: ComicData) { + this.title = comicData.title; + } - $log.debug('END qcSetTitle()'); - }], + _updateValue() { + this.setTitle(); + } + + setTitle() { + this.comicService.setTitle(this.title ? this.title : ''); + }; +} +SetTitleController.$inject = ['$scope', '$log', 'comicService', 'eventService']; + +export default function (app: AngularModule) { + app.directive('qcSetTitle', function () { + return { + restrict: 'E', + replace: true, + scope: {}, + controller: SetTitleController, controllerAs: 's', template: variables.html.setTitle }; diff --git a/assets/js/modules/angular/directives/qcSettingsDirective.js b/assets/js/modules/angular/directives/qcSettingsDirective.js index 8b9df9c..fd514dc 100644 --- a/assets/js/modules/angular/directives/qcSettingsDirective.js +++ b/assets/js/modules/angular/directives/qcSettingsDirective.js @@ -1,3 +1,4 @@ +// @flow /* * Copyright (C) 2016-2018 Alexander Krivács Schrøder * @@ -15,39 +16,62 @@ * along with this program. If not, see . */ -import settings from '../../settings'; +import type { AngularModule, $Log } from 'angular'; + +import settings, { Settings } from '../../settings'; import variables from '../../../../generated/variables.pass2'; -export default function (app) { +import type { $DecoratedScope } from '../decorateScope'; +import type { ComicService } from '../services/comicService'; + +export class SettingsController { + static $inject: string[]; + + $scope: $DecoratedScope; + $log: $Log; + comicService: ComicService; + + settings: Settings; + + constructor($scope: $DecoratedScope, $log: $Log, comicService: ComicService) { + $log.debug('START SettingsController'); + + this.$scope = $scope; + this.$log = $log; + this.comicService = comicService; + + this.settings = settings; + + $scope.$watchGroup([() => { + return this.settings.values.showAllMembers; + }, () => { + return this.settings.values.editMode; + }], () => { + this.comicService.refreshComicData(); + }); + + $('#settingsDialog').on('hide.bs.modal', () => { + $log.debug('Saving settings...'); + this.settings.saveSettings().then(() => { + $log.debug('Settings saved.'); + }); + }); + + $log.debug('END SettingsController'); + } + + close() { + ($('#settingsDialog'): any).modal('hide'); + } +} + +export default function (app: AngularModule) { app.directive('qcSettings', function () { return { restrict: 'E', replace: true, scope: {}, - controller: ['$scope', 'comicService', '$log', - function ($scope, comicService, $log) { - var self = this; - this.settings = settings; - - $scope.$watchGroup([function () { - return self.settings.showAllMembers; - }, function () { - return self.settings.editMode; - }], function () { - comicService.refreshComicData(); - }); - - $('#settingsDialog').on('hide.bs.modal', function () { - $log.debug('Saving settings...'); - settings.saveSettings().then(() => { - $log.debug('Settings saved.'); - }); - }); - - this.close = function () { - $('#settingsDialog').modal('hide'); - }; - }], + controller: SettingsController, controllerAs: 'svm', template: variables.html.settings }; diff --git a/assets/js/modules/angular/run.js b/assets/js/modules/angular/run.js index 512658e..d1f6475 100644 --- a/assets/js/modules/angular/run.js +++ b/assets/js/modules/angular/run.js @@ -1,3 +1,4 @@ +// @flow /* * Copyright (C) 2016-2018 Alexander Krivács Schrøder * @@ -15,9 +16,11 @@ * along with this program. If not, see . */ +import type { AngularModule } from 'angular'; + import settings from './../settings'; -export default function (app) { +export default function (app: AngularModule) { app.run(['$rootScope', 'comicService', 'startComic', function ($rootScope, comicService, startComic) { $rootScope.settings = settings; diff --git a/assets/js/modules/angular/scopes/rootScope.js b/assets/js/modules/angular/scopes/rootScope.js new file mode 100644 index 0000000..07e2b0b --- /dev/null +++ b/assets/js/modules/angular/scopes/rootScope.js @@ -0,0 +1,21 @@ +// @flow +/* + * Copyright (C) 2016-2018 Alexander Krivács Schrøder + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +export type RootScope = { + +}; diff --git a/assets/js/modules/angular/services/colorService.js b/assets/js/modules/angular/services/colorService.js index 1ba1fa1..e908ac0 100644 --- a/assets/js/modules/angular/services/colorService.js +++ b/assets/js/modules/angular/services/colorService.js @@ -1,3 +1,4 @@ +// @flow /* * Copyright (C) 2016-2018 Alexander Krivács Schrøder * @@ -15,281 +16,297 @@ * along with this program. If not, see . */ +import type { AngularModule, $Log } from 'angular'; + // Parts based on code from: // http://axonflux.com/handy-rgb-to-hsl-and-rgb-to-hsv-color-model-c -export default function (app) { - app.service('colorService', ['$log', - function ($log) { - $log.debug('START colorService()'); +function hue2rgb(p, q, t) { + if (t < 0) { + t += 1; + } - function hue2rgb(p, q, t) { - if (t < 0) { - t += 1; - } + if (t > 1) { + t -= 1; + } - if (t > 1) { - t -= 1; - } + if (t < 1 / 6) { + return p + (q - p) * 6 * t; + } - if (t < 1 / 6) { - return p + (q - p) * 6 * t; - } + if (t < 1 / 2) { + return q; + } - if (t < 1 / 2) { - return q; - } + if (t < 2 / 3) { + return p + (q - p) * (2 / 3 - t) * 6; + } - if (t < 2 / 3) { - return p + (q - p) * (2 / 3 - t) * 6; - } + return p; +} - return p; +type HSLValue = [number, number, number]; +type RGBValue = [number, number, number]; +type HSVValue = [number, number, number]; + +export class ColorService { + $log: $Log; + + constructor($log: $Log) { + this.$log = $log; + } + + /** + * Converts an RGB color value to HSL. Conversion formula + * adapted from http://en.wikipedia.org/wiki/HSL_color_space. + * Assumes r, g, and b are contained in the set [0, 255] and + * returns h, s, and l in the set [0, 1]. + * + * @param {number} r The red color value + * @param {number} g The green color value + * @param {number} b The blue color value + * @return {HSLValue} The HSL representation + */ + rgbToHsl(r: number, g: number, b: number): HSLValue { + r /= 255; + g /= 255; + b /= 255; + const max = Math.max(r, g, b); + const min = Math.min(r, g, b); + let h = 0; + let s; + const l = (max + min) / 2; + + if (max === min) { + h = s = 0; // Achromatic + } else { + const d = max - min; + + s = l > 0.5 ? d / (2 - max - min) : d / (max + min); + + switch (max) { + case r: + h = (g - b) / d + (g < b ? 6 : 0); + break; + + case g: + h = (b - r) / d + 2; + break; + + case b: + h = (r - g) / d + 4; + break; + } + h /= 6; + } + + return [h, s, l]; + } + + /** + * Converts an HSL color value to RGB. Conversion formula + * adapted from http://en.wikipedia.org/wiki/HSL_color_space. + * Assumes h, s, and l are contained in the set [0, 1] and + * returns r, g, and b in the set [0, 255]. + * + * @param {number} h The hue + * @param {number} s The saturation + * @param {number} l The lightness + * @return {RGBValue} The RGB representation + */ + hslToRgb(h: number, s: number, l: number): RGBValue { + let r; + let g; + let b; + + if (s === 0) { + r = g = b = l; // Achromatic + } else { + const q = l < 0.5 ? l * (1 + s) : l + s - l * s; + const p = 2 * l - q; + + r = hue2rgb(p, q, h + 1 / 3); + g = hue2rgb(p, q, h); + b = hue2rgb(p, q, h - 1 / 3); + } + + return [ + Math.round(r * 255), + Math.round(g * 255), + Math.round(b * 255) + ]; + } + + + /** + * Converts an RGB color value to HSV. Conversion formula + * adapted from http://en.wikipedia.org/wiki/HSV_color_space. + * Assumes r, g, and b are contained in the set [0, 255] and + * returns h, s, and v in the set [0, 1]. + * + * @param {number} r The red color value + * @param {number} g The green color value + * @param {number} b The blue color value + * @return {HSVValue} The HSV representation + */ + rgbToHsv(r: number, g: number, b: number): HSVValue { + r = r / 255; + g = g / 255; + b = b / 255; + const max = Math.max(r, g, b); + const min = Math.min(r, g, b); + let h = 0; + let s; + const v = max; + + const d = max - min; + + s = max === 0 ? 0 : d / max; + + if (max === min) { + h = 0; // Achromatic + } else { + switch (max) { + case r: + h = (g - b) / d + (g < b ? 6 : 0); + break; + + case g: + h = (b - r) / d + 2; + break; + + case b: + h = (r - g) / d + 4; + break; } + h /= 6; + } + + return [h, s, v]; + } + + /** + * Converts an HSV color value to RGB. Conversion formula + * adapted from http://en.wikipedia.org/wiki/HSV_color_space. + * Assumes h, s, and v are contained in the set [0, 1] and + * returns r, g, and b in the set [0, 255]. + * + * @param {number} h The hue + * @param {number} s The saturation + * @param {number} v The value + * @return {RGBValue} The RGB representation + */ + hsvToRgb(h: number, s: number, v: number) { + let r = 0; + let g = 0; + let b = 0; + + const i = Math.floor(h * 6); + const f = h * 6 - i; + const p = v * (1 - s); + const q = v * (1 - f * s); + const t = v * (1 - (1 - f) * s); + + switch (i % 6) { + case 0: + r = v; + g = t; + b = p; + break; + + case 1: + r = q; + g = v; + b = p; + break; + + case 2: + r = p; + g = v; + b = t; + break; + + case 3: + r = p; + g = q; + b = v; + break; + + case 4: + r = t; + g = p; + b = v; + break; + + case 5: + r = v; + g = p; + b = q; + break; + } + + return [ + Math.round(r * 255), + Math.round(g * 255), + Math.round(b * 255) + ]; + } + + hexColorToRgb(hexColor: string): RGBValue { + if (hexColor.charAt(0) === '#') { + hexColor = hexColor.substring(1); // Strip # + } + const rgb = parseInt(hexColor, 16); // Convert rrggbb to decimal + const r = rgb >> 16 & 0xff; // Extract red + const g = rgb >> 8 & 0xff; // Extract green + const b = rgb & 0xff; // Extract blue + return [r, g, b]; + } + + rgbToHexColor(r: number, g: number, b: number): string { + return '#' + ((1 << 24) + (r << 16) + (g << 8) + b) + .toString(16).slice(1); + } + + getRgbRelativeLuminance(r: number, g: number, b: number): number { + return 0.2126 * r + 0.7152 * g + 0.0722 * b; // Per ITU-R BT.709 + } + + createTintOrShade(hexColor: string, iterations?: number): string { + if (typeof iterations === 'undefined') { + iterations = 1; + } + + let rgb = this.hexColorToRgb(hexColor); + const hsl = this.rgbToHsl(rgb[0], rgb[1], rgb[2]); + + const tint = hsl[2] < 0.5; + + for (let i = iterations; i > 0; i--) { + // If it's a dark color, make it lighter + // and vice versa. + if (tint) { + // Increase the lightness by + // 50% (tint) + hsl[2] = (hsl[2] + 1) / 2; + } else { + // Decrease the lightness by + // 50% (shade) + hsl[2] /= 2; + } + } - /** - * Converts an RGB color value to HSL. Conversion formula - * adapted from http://en.wikipedia.org/wiki/HSL_color_space. - * Assumes r, g, and b are contained in the set [0, 255] and - * returns h, s, and l in the set [0, 1]. - * - * @param {number} r The red color value - * @param {number} g The green color value - * @param {number} b The blue color value - * @return Array The HSL representation - */ - this.rgbToHsl = function (r, g, b) { - r /= 255; - g /= 255; - b /= 255; - var max = Math.max(r, g, b); - var min = Math.min(r, g, b); - var h; - var s; - var l = (max + min) / 2; - - if (max === min) { - h = s = 0; // Achromatic - } else { - var d = max - min; - - s = l > 0.5 ? d / (2 - max - min) : d / (max + min); - - switch (max) { - case r: - h = (g - b) / d + (g < b ? 6 : 0); - break; - - case g: - h = (b - r) / d + 2; - break; - - case b: - h = (r - g) / d + 4; - break; - } - h /= 6; - } - - return [h, s, l]; - }; - - /** - * Converts an HSL color value to RGB. Conversion formula - * adapted from http://en.wikipedia.org/wiki/HSL_color_space. - * Assumes h, s, and l are contained in the set [0, 1] and - * returns r, g, and b in the set [0, 255]. - * - * @param {number} h The hue - * @param {number} s The saturation - * @param {number} l The lightness - * @return Array The RGB representation - */ - this.hslToRgb = function (h, s, l) { - var r; - var g; - var b; - - if (s === 0) { - r = g = b = l; // Achromatic - } else { - var q = l < 0.5 ? l * (1 + s) : l + s - l * s; - var p = 2 * l - q; - - r = hue2rgb(p, q, h + 1 / 3); - g = hue2rgb(p, q, h); - b = hue2rgb(p, q, h - 1 / 3); - } - - return [ - Math.round(r * 255), - Math.round(g * 255), - Math.round(b * 255) - ]; - }; - - /** - * Converts an RGB color value to HSV. Conversion formula - * adapted from http://en.wikipedia.org/wiki/HSV_color_space. - * Assumes r, g, and b are contained in the set [0, 255] and - * returns h, s, and v in the set [0, 1]. - * - * @param {number} r The red color value - * @param {number} g The green color value - * @param {number} b The blue color value - * @return Array The HSV representation - */ - this.rgbToHsv = function (r, g, b) { - r = r / 255; - g = g / 255; - b = b / 255; - var max = Math.max(r, g, b); - var min = Math.min(r, g, b); - var h; - var s; - var v = max; - - var d = max - min; - - s = max === 0 ? 0 : d / max; - - if (max === min) { - h = 0; // Achromatic - } else { - switch (max) { - case r: - h = (g - b) / d + (g < b ? 6 : 0); - break; - - case g: - h = (b - r) / d + 2; - break; - - case b: - h = (r - g) / d + 4; - break; - } - h /= 6; - } - - return [h, s, v]; - }; - - /** - * Converts an HSV color value to RGB. Conversion formula - * adapted from http://en.wikipedia.org/wiki/HSV_color_space. - * Assumes h, s, and v are contained in the set [0, 1] and - * returns r, g, and b in the set [0, 255]. - * - * @param {number} h The hue - * @param {number} s The saturation - * @param {number} v The value - * @return Array The RGB representation - */ - this.hsvToRgb = function (h, s, v) { - var r; - var g; - var b; - - var i = Math.floor(h * 6); - var f = h * 6 - i; - var p = v * (1 - s); - var q = v * (1 - f * s); - var t = v * (1 - (1 - f) * s); - - switch (i % 6) { - case 0: - r = v; - g = t; - b = p; - break; - - case 1: - r = q; - g = v; - b = p; - break; - - case 2: - r = p; - g = v; - b = t; - break; - - case 3: - r = p; - g = q; - b = v; - break; - - case 4: - r = t; - g = p; - b = v; - break; - - case 5: - r = v; - g = p; - b = q; - break; - } - - return [ - Math.round(r * 255), - Math.round(g * 255), - Math.round(b * 255) - ]; - }; - - this.hexColorToRgb = function (hexColor) { - if (hexColor.charAt(0) === '#') { - hexColor = hexColor.substring(1); // Strip # - } - var rgb = parseInt(hexColor, 16); // Convert rrggbb to decimal - var r = rgb >> 16 & 0xff; // Extract red - var g = rgb >> 8 & 0xff; // Extract green - var b = rgb & 0xff; // Extract blue - return [r, g, b]; - }; - - this.rgbToHexColor = function (r, g, b) { - return '#' + ((1 << 24) + (r << 16) + (g << 8) + b) - .toString(16).slice(1); - }; - - this.getRgbRelativeLuminance = function (r, g, b) { - return 0.2126 * r + 0.7152 * g + 0.0722 * b; // Per ITU-R BT.709 - }; - - this.createTintOrShade = function (hexColor, iterations) { - if (typeof iterations === 'undefined') { - iterations = 1; - } - - var rgb = this.hexColorToRgb(hexColor); - var hsl = this.rgbToHsl(rgb[0], rgb[1], rgb[2]); - - var tint = hsl[2] < 0.5; - - for (var i = iterations; i > 0; i--) { - // If it's a dark color, make it lighter - // and vice versa. - if (tint) { - // Increase the lightness by - // 50% (tint) - hsl[2] = (hsl[2] + 1) / 2; - } else { - // Decrease the lightness by - // 50% (shade) - hsl[2] /= 2; - } - } - - rgb = this.hslToRgb(hsl[0], hsl[1], hsl[2]); - return this.rgbToHexColor(rgb[0], rgb[1], rgb[2]); - }; + rgb = this.hslToRgb(hsl[0], hsl[1], hsl[2]); + return this.rgbToHexColor(rgb[0], rgb[1], rgb[2]); + } +} +export default function (app: AngularModule) { + app.service('colorService', ['$log', + function ($log: $Log) { + $log.debug('START colorService()'); + const colorService = new ColorService($log); $log.debug('END colorService()'); + return colorService; }]); } diff --git a/assets/js/modules/angular/services/comicService.js b/assets/js/modules/angular/services/comicService.js index 080bb9a..3ff8c52 100644 --- a/assets/js/modules/angular/services/comicService.js +++ b/assets/js/modules/angular/services/comicService.js @@ -1,3 +1,4 @@ +// @flow /* * Copyright (C) 2016-2018 Alexander Krivács Schrøder * @@ -15,275 +16,314 @@ * along with this program. If not, see . */ +import $ from 'jquery'; import angular from 'angular'; +import type { AngularModule, $Log, $Scope, AngularHttpService, $Location } from 'angular'; +import type { $StateParams } from 'angular-ui'; import constants from '../../../constants'; - import settings from '../../settings'; -export default function (app) { - app.service('comicService', ['$log', '$stateParams', '$location', - '$rootScope', '$http', 'latestComic', 'eventFactory', 'colorService', - 'styleService', 'messageReportingService', - function ($log, $stateParams, $location, - $scope, $http, latestComic, Event, colorService, - styleService, messageReportingService) { - $log.debug('START comicService()'); - var comicDataLoadingEvent = - new Event(constants.comicdataLoadingEvent); - var comicDataLoadedEvent = - new Event(constants.comicdataLoadedEvent); - var comicDataErrorEvent = new Event(constants.comicdataErrorEvent); - - var self = this; - - var comicExtensionIndex = 0; - - function updateComic() { - $log.debug('comicService:updateComic()'); - var comic; - - if (typeof $stateParams.comic === 'string') { - comic = Number($stateParams.comic); - } else { - comic = latestComic; +import type { EventService } from './eventService'; +import type { ColorService } from './colorService'; +import type { StyleService } from './styleService'; +import type { MessageReportingService } from './messageReportingService'; +import type { ComicData, ComicItem } from '../api/comicData.js'; + +export class ComicService { + $log: $Log; + $stateParams: $StateParams; + $location: $Location; + $scope: $Scope; + $http: AngularHttpService; + latestComic: number; + eventService: EventService; + colorService: ColorService; + styleService: StyleService; + messageReportingService: MessageReportingService; + + comic: number; + nextComic: number; + previousComic: number; + comicExtensionIndex: number; + comicExtension: string; + comicData: ComicData; + + constructor($log: $Log, $stateParams: $StateParams, $location: $Location, + $scope: $Scope, $http: AngularHttpService, latestComic: number, eventService: EventService, + colorService: ColorService, styleService: StyleService, + messageReportingService: MessageReportingService) { + this.$log = $log; + this.$stateParams = $stateParams; + this.$location = $location; + this.$scope = $scope; + this.$http = $http; + this.latestComic = latestComic; + this.eventService = eventService; + this.colorService = colorService; + this.styleService = styleService; + this.messageReportingService = messageReportingService; + + $scope.$on('$stateChangeSuccess', () => { + this._updateComic(); + this.refreshComicData(); + }); + } + + _updateComic() { + let comic; + if (typeof this.$stateParams.comic === 'string') { + comic = Number(this.$stateParams.comic); + } else { + comic = this.latestComic; + } + + this.$log.debug('ComicService:_updateComic(): Comic is', comic); + + this.comic = comic; + this.nextComic = this.comic + 1 > this.latestComic ? + this.latestComic : this.comic + 1; + this.previousComic = this.comic - 1 < 1 ? 1 : this.comic - 1; + this.comicExtensionIndex = 0; + this.comicExtension = + constants.comicExtensions[this.comicExtensionIndex]; + + if (settings.values.scrollToTop) { + $(window).scrollTop(0); + } + } + + // TODO: Add proper response type + _onErrorLog(response: any) { + if (response.status !== 503) { + this.messageReportingService.reportError(response.data); + } else { + this.messageReportingService.reportError( + constants.messages.maintenance); + } + return response; + } + + // TODO: Add proper response type + _onSuccessRefreshElseErrorLog(response: any) { + if (response.status === 200) { + this.refreshComicData(); + } else { + this._onErrorLog(response); + } + return response; + } + + _fixItem(item: ComicItem) { + if (item.first == this.comic) { + item.first = null; + } + + if (item.last == this.comic) { + item.last = null; + } + + this.styleService.addItemStyle(item.id, + item.color); + } + + refreshComicData() { + if (typeof this.comic === 'undefined') { + this.$log.debug('comicService::refreshComicData() called ' + + 'before the comicService was properly initialized. ' + + 'Ignored.'); + return; + } + + this.eventService.comicDataLoadingEvent.publish(this.comic); + + let comicDataUrl = constants.comicDataUrl + this.comic; + const urlParameters = {}; + if (settings.values.editMode) { + urlParameters.token = settings.values.editModeToken; + } + if (settings.values.skipGuest) { + urlParameters.exclude = 'guest'; + } else if (settings.values.skipNonCanon) { + urlParameters.exclude = 'non-canon'; + } + if (settings.values.showAllMembers) { + urlParameters.include = 'all'; + } + const urlQuery = $.param(urlParameters); + if (urlQuery) { + comicDataUrl += '?' + urlQuery; + } + + this.$log.debug('comicService:refreshComicData(): URL is', comicDataUrl); + this.$http.get(comicDataUrl) + .then((response) => { + if (response.status === 503) { + this.eventService.comicDataErrorEvent.publish(response); + return; } - - $log.debug('comicService:updateComic(): Comic is', comic); - - self.comic = comic; - self.nextComic = self.comic + 1 > latestComic ? - latestComic : self.comic + 1; - self.previousComic = self.comic - 1 < 1 ? 1 : self.comic - 1; - self.latestComic = latestComic; - comicExtensionIndex = 0; - self.comicExtension = - constants.comicExtensions[comicExtensionIndex]; - - if (settings.scrollToTop) { - $(window).scrollTop(0); + if (response.status !== 200) { + this._onErrorLog(response); + this.eventService.comicDataErrorEvent.publish(response); + return; } - } - - $scope.$on('$stateChangeSuccess', function () { - updateComic(); - self.refreshComicData(); - }); - function onErrorLog(response) { - if (response.status !== 503) { - messageReportingService.reportError(response.data); + const comicData = response.data; + if (comicData.hasData) { + if (comicData.next !== null) { + this.nextComic = comicData.next; + } else { + this.nextComic = this.comic + 1 > this.latestComic ? + this.latestComic : this.comic + 1; + } + if (comicData.previous !== null) { + this.previousComic = comicData.previous; + } else { + this.previousComic = this.comic - 1 < 1 ? 1 : + this.comic - 1; + } + + angular.forEach(comicData.items, item => this._fixItem(item)); + if (settings.values.showAllMembers) { + angular.forEach(comicData.allItems, item => this._fixItem(item)); + } } else { - messageReportingService.reportError( - constants.messages.maintenance); - } - return response; - } - - this.refreshComicData = function () { - if (typeof self.comic === 'undefined') { - $log.debug('comicService::refreshComicData() called ' + - 'before the comicService was properly initialized. ' + - 'Ignored.'); - return; + this.nextComic = this.comic + 1 > this.latestComic ? + this.latestComic : this.comic + 1; + this.previousComic = this.comic - 1 < 1 ? 1 : + this.comic - 1; + + if (settings.values.showAllMembers) { + angular.forEach(comicData.allItems, item => this._fixItem(item)); + } } - comicDataLoadingEvent.notify(self.comic); - var comicDataUrl = constants.comicDataUrl + self.comic; - - var urlParameters = {}; - if (settings.editMode) { - urlParameters.token = settings.editModeToken; - } - if (settings.skipGuest) { - urlParameters.exclude = 'guest'; - } else if (settings.skipNonCanon) { - urlParameters.exclude = 'non-canon'; - } - if (settings.showAllMembers) { - urlParameters.include = 'all'; - } - var urlQuery = $.param(urlParameters); - if (urlQuery) { - comicDataUrl += '?' + urlQuery; - } + comicData.comic = this.comic; + this.comicData = comicData; + this.eventService.comicDataLoadedEvent.publish(this.comicData); + }).catch((errorResponse) => { + this._onErrorLog(errorResponse); + this.eventService.comicDataErrorEvent.publish(errorResponse); + }); + } + + addItem(item: ComicItem) { + const data = { + token: settings.values.editModeToken, + comic: this.comic, + item: item + }; + return this.$http.post(constants.addItemToComicUrl, data) + .then(r => this._onSuccessRefreshElseErrorLog(r)).catch(r => this._onErrorLog(r)); + } + + removeItem(item: ComicItem) { + const data = { + token: settings.values.editModeToken, + comic: this.comic, + item: item + }; + return this.$http.post(constants.removeItemFromComicUrl, data) + .then(r => this._onSuccessRefreshElseErrorLog(r)).catch(r => this._onErrorLog(r)); + } + + setTitle(title: string) { + const data = { + token: settings.values.editModeToken, + comic: this.comic, + title: title + }; + return this.$http.post(constants.setComicTitleUrl, data) + .then(r => this._onSuccessRefreshElseErrorLog(r)).catch(r => this._onErrorLog(r)); + } + + setTagline(tagline: string) { + const data = { + token: settings.values.editModeToken, + comic: this.comic, + tagline: tagline + }; + return this.$http.post(constants.setComicTaglineUrl, data) + .then(r => this._onSuccessRefreshElseErrorLog(r)).catch(r => this._onErrorLog(r)); + } + + setPublishDate(publishDate: Date, isAccurate: boolean) { + const data = { + token: settings.values.editModeToken, + comic: this.comic, + publishDate: publishDate, + isAccuratePublishDate: isAccurate + }; + return this.$http.post(constants.setPublishDateUrl, data) + .then(r => this._onSuccessRefreshElseErrorLog(r)).catch(r => this._onErrorLog(r)); + } + + setGuestComic(value: boolean) { + const data = { + token: settings.values.editModeToken, + comic: this.comic, + value: value + }; + return this.$http.post(constants.setGuestComicUrl, data) + .then(r => this._onSuccessRefreshElseErrorLog(r)).catch(r => this._onErrorLog(r)); + } + + setNonCanon(value: boolean) { + const data = { + token: settings.values.editModeToken, + comic: this.comic, + value: value + }; + return this.$http.post(constants.setNonCanonUrl, data) + .then(r => this._onSuccessRefreshElseErrorLog(r)).catch(r => this._onErrorLog(r)); + } + + gotoComic(comicNo: number) { + this.$location.url('/view.php?comic=' + comicNo); + } + + canFallback() { + return this.comicExtensionIndex < + constants.comicExtensions.length - 1; + } + + tryFallback() { + this.comicExtensionIndex++; + this.comicExtension = constants + .comicExtensions[this.comicExtensionIndex]; + } + + first() { + this.gotoComic(1); + }; + + previous() { + this.gotoComic(this.previousComic); + }; + + next() { + this.gotoComic(this.nextComic); + }; + + last() { + this.gotoComic(this.latestComic); + }; +} - $log.debug('comicService:refreshComicData(): URL is', comicDataUrl); - $http.get(comicDataUrl) - .then(function (response) { - if (response.status === 503) { - comicDataErrorEvent.notify(response); - return; - } - if (response.status !== 200) { - onErrorLog(response); - comicDataErrorEvent.notify(response); - return; - } - - function fixItem(item) { - if (item.first == self.comic) { - item.first = null; - } - - if (item.last == self.comic) { - item.last = null; - } - - styleService.addItemStyle(item.id, - item.color); - } - - var comicData = response.data; - if (comicData.hasData) { - if (comicData.next !== null) { - self.nextComic = comicData.next; - } else { - self.nextComic = self.comic + 1 > latestComic ? - latestComic : self.comic + 1; - } - if (comicData.previous !== null) { - self.previousComic = comicData.previous; - } else { - self.previousComic = self.comic - 1 < 1 ? 1 : - self.comic - 1; - } - - angular.forEach(comicData.items, fixItem); - if (settings.showAllMembers) { - angular.forEach(comicData.allItems, fixItem); - } - } else { - self.nextComic = self.comic + 1 > latestComic ? - latestComic : self.comic + 1; - self.previousComic = self.comic - 1 < 1 ? 1 : - self.comic - 1; - - if (settings.showAllMembers) { - angular.forEach(comicData.allItems, fixItem); - } - } - - comicData.comic = self.comic; - self.comicData = comicData; - comicDataLoadedEvent.notify(self.comicData); - }, function (errorResponse) { - onErrorLog(errorResponse); - comicDataErrorEvent.notify(errorResponse); - }); - }; - - function onSuccessRefreshElseErrorLog(response) { - if (response.status === 200) { - self.refreshComicData(); - } else { - onErrorLog(response); - } - return response; - } - - this.addItem = function (item) { - var data = { - token: settings.editModeToken, - comic: self.comic, - item: item - }; - return $http.post(constants.addItemToComicUrl, data) - .then(onSuccessRefreshElseErrorLog, onErrorLog); - }; - - this.removeItem = function (item) { - var data = { - token: settings.editModeToken, - comic: self.comic, - item: item - }; - return $http.post(constants.removeItemFromComicUrl, data) - .then(onSuccessRefreshElseErrorLog, onErrorLog); - }; - - this.setTitle = function (title) { - var data = { - token: settings.editModeToken, - comic: self.comic, - title: title - }; - return $http.post(constants.setComicTitleUrl, data) - .then(onSuccessRefreshElseErrorLog, onErrorLog); - }; - - this.setTagline = function (tagline) { - var data = { - token: settings.editModeToken, - comic: self.comic, - tagline: tagline - }; - return $http.post(constants.setComicTaglineUrl, data) - .then(onSuccessRefreshElseErrorLog, onErrorLog); - }; - - this.setPublishDate = function (publishDate, isAccurate) { - var data = { - token: settings.editModeToken, - comic: self.comic, - publishDate: publishDate, - isAccuratePublishDate: isAccurate - }; - return $http.post(constants.setPublishDateUrl, data) - .then(onSuccessRefreshElseErrorLog, onErrorLog); - }; - - this.setGuestComic = function (value) { - var data = { - token: settings.editModeToken, - comic: self.comic, - value: value - }; - return $http.post(constants.setGuestComicUrl, data) - .then(onSuccessRefreshElseErrorLog, onErrorLog); - }; - - this.setNonCanon = function (value) { - var data = { - token: settings.editModeToken, - comic: self.comic, - value: value - }; - return $http.post(constants.setNonCanonUrl, data) - .then(onSuccessRefreshElseErrorLog, onErrorLog); - }; - - this.gotoComic = function (comicNo) { - $location.url('/view.php?comic=' + comicNo); - }; - - this.canFallback = function () { - return comicExtensionIndex < - constants.comicExtensions.length - 1; - }; - - this.tryFallback = function () { - comicExtensionIndex++; - self.comicExtension = constants - .comicExtensions[comicExtensionIndex]; - }; - - this.first = function () { - self.gotoComic(1); - }; - - this.previous = function () { - self.gotoComic(self.previousComic); - }; - - this.next = function () { - self.gotoComic(self.nextComic); - }; - - this.last = function () { - self.gotoComic(latestComic); - }; +export default function (app: AngularModule) { + app.service('comicService', ['$log', '$stateParams', '$location', + '$rootScope', '$http', 'latestComic', 'eventService', 'colorService', + 'styleService', 'messageReportingService', + function ($log: $Log, $stateParams: $StateParams, $location: $Location, + $scope: $Scope, $http: AngularHttpService, latestComic: number, eventService: EventService, + colorService: ColorService, styleService: StyleService, + messageReportingService: MessageReportingService) { + $log.debug('START comicService()'); + const comicService = new ComicService($log, $stateParams, $location, $scope, + $http, latestComic, eventService, colorService, styleService, + messageReportingService); $log.debug('END comicService()'); + return comicService; }]); } diff --git a/assets/js/modules/angular/services/eventFactory.js b/assets/js/modules/angular/services/eventFactory.js index 844e508..86a8bdf 100644 --- a/assets/js/modules/angular/services/eventFactory.js +++ b/assets/js/modules/angular/services/eventFactory.js @@ -1,3 +1,4 @@ +// @flow /* * Copyright (C) 2016-2018 Alexander Krivács Schrøder * @@ -15,30 +16,48 @@ * along with this program. If not, see . */ -export default function (app) { - app.factory('eventFactory', ['$rootScope', '$log', - function ($rootScope, $log) { - var eventFactory = function (eventName) { - this.eventName = eventName; - }; +import type { AngularModule, $Log, $Scope } from 'angular'; + +import type { RootScope } from '../scopes/rootScope'; + +export class Event { + static $log: $Log; + static $rootScope: $Scope; + + eventName: string; - eventFactory.prototype.subscribe = function (scope, callback) { - var handle = $rootScope.$on(this.eventName, callback); + constructor(eventName: string) { + this.eventName = eventName; + } - scope.$on('$destroy', handle); - }; + //$FlowFixMe when Flow properly supports generics + subscribe(scope: $Scope<*>, callback: (event: string, eventData?: T) => void) { + const handle = Event.$rootScope.$on(this.eventName, callback); + scope.$on('$destroy', handle); + } - eventFactory.prototype.notify = function (data) { - var eventData = [this.eventName]; + publish(data?: T) { + const eventData: [string, ?T] = [this.eventName, null]; - if (typeof data !== 'undefined') { - eventData = eventData.concat(data); - } + if (data != null) { + eventData[1] = data; + } - $log.debug('Event data: ', eventData); - $rootScope.$emit.apply($rootScope, eventData); - }; + Event.$log.debug('Event data: ', eventData); + Event.$rootScope.$emit.apply(Event.$rootScope, eventData); + } +} + +export type EventFactory = (eventName: string) => Event; + +export default function (app: AngularModule) { + app.factory('eventFactory', ['$rootScope', '$log', + function ($rootScope: $Scope, $log: $Log) { + $log.debug('START eventFactory()'); + Event.$rootScope = $rootScope; + Event.$log = $log; - return eventFactory; + $log.debug('END eventFactory()'); + return (eventName: string) => new Event(eventName); }]); } diff --git a/assets/js/modules/angular/services/eventService.js b/assets/js/modules/angular/services/eventService.js new file mode 100644 index 0000000..6e326fe --- /dev/null +++ b/assets/js/modules/angular/services/eventService.js @@ -0,0 +1,52 @@ +// @flow +/* + * Copyright (C) 2016-2018 Alexander Krivács Schrøder + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import type { AngularModule, $Log } from 'angular'; + +import constants from '../../../constants'; + +import type { Event, EventFactory } from './eventFactory'; +import type { ComicData } from '../api/comicData'; + +export class EventService { + $log: $Log; + + comicDataLoadingEvent: Event; + comicDataLoadedEvent: Event; + comicDataErrorEvent: Event; + itemsChangedEvent: Event; + + constructor($log: $Log, eventFactory: EventFactory) { + this.$log = $log; + + this.comicDataLoadingEvent = eventFactory(constants.comicdataLoadingEvent); + this.comicDataLoadedEvent = eventFactory(constants.comicdataLoadedEvent); + this.comicDataErrorEvent = eventFactory(constants.comicdataErrorEvent); // TODO: Figure out this type + this.itemsChangedEvent = eventFactory(constants.itemsChangedEvent); + } +} + +export default function (app: AngularModule) { + app.service('eventService', ['$log', 'eventFactory', + function($log: $Log, eventFactory: EventFactory) { + $log.debug('START eventService()'); + const eventService = new EventService($log, eventFactory); + $log.debug('END eventService()'); + return eventService; + }]); +} diff --git a/assets/js/modules/angular/services/messageReportingService.js b/assets/js/modules/angular/services/messageReportingService.js index 0a62786..2195c5c 100644 --- a/assets/js/modules/angular/services/messageReportingService.js +++ b/assets/js/modules/angular/services/messageReportingService.js @@ -1,3 +1,4 @@ +// @flow /* * Copyright (C) 2016-2018 Alexander Krivács Schrøder * @@ -14,80 +15,100 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ +import $ from 'jquery'; -export default function (app) { - app.service('messageReportingService', ['$log', '$timeout', - function ($log, $timeout) { - $log.debug('START messageReportingService()'); +import type { AngularModule, $Log, $Timeout } from 'angular'; - var messageQueue = []; - var isProcessing = false; - - function escapeHtml(text) { - return text.replace(/["&'/<>]/g, function (a) { - return { - '"': '"', - '&': '&', - '\'': ''', - '/': '/', - '<': '<', - '>': '>' - }[a]; - }); - } - - function processMessages() { - isProcessing = true; - - var nextMessage = messageQueue.shift(); - if (typeof nextMessage === 'undefined') { - isProcessing = false; - return; - } - - var unique = Math.random().toString(36).slice(-5); - - var messageHtml = '
' + - escapeHtml(nextMessage.message) + - '
'; - - $('#messageSeat').append(messageHtml); - var messageElement = $('#' + unique); - messageElement.slideDown(); - - function removeMessage() { - messageElement.slideUp( - function () { - messageElement.remove(); - processMessages(); - }); - } - - var timeoutHandle = $timeout(removeMessage, 5000, false); - - messageElement.click(function () { - $timeout.cancel(timeoutHandle); - removeMessage(); +function escapeHtml(text: string): string { + return text.replace(/["&'/<>]/g, function (a) { + return { + '"': '"', + '&': '&', + '\'': ''', + '/': '/', + '<': '<', + '>': '>' + }[a]; + }); +} + +type MessageType = 'danger' | 'warning'; +type Message = { type: MessageType, message: string }; + +export class MessageReportingService { + $log: $Log; + $timeout: $Timeout; + + messageQueue: Message[]; + isProcessing: boolean; + + constructor($log: $Log, $timeout: $Timeout) { + this.$log = $log; + this.$timeout = $timeout; + + this.messageQueue = []; + this.isProcessing = false; + } + + processMessages() { + this.isProcessing = true; + + let nextMessage = this.messageQueue.shift(); + if (typeof nextMessage === 'undefined') { + this.isProcessing = false; + return; + } + + const unique = Math.random().toString(36).slice(-5); + + const messageHtml = '
' + + escapeHtml(nextMessage.message) + + '
'; + + $('#messageSeat').append(messageHtml); + const messageElement = $('#' + unique); + messageElement.slideDown(); + + function removeMessage() { + messageElement.slideUp( + function () { + messageElement.remove(); + this.processMessages(); }); - } + } - function reportMessage(type, message) { - messageQueue.push({ type: type, message: message }); - if (!isProcessing) { processMessages(); } - } + const timeoutHandle = this.$timeout(removeMessage, 5000, false); - this.reportError = function (message) { - reportMessage('danger', message); - }; + messageElement.click(function () { + this.$timeout.cancel(timeoutHandle); + removeMessage(); + }); + } - this.reportWarning = function (message) { - reportMessage('warning', message); - }; + reportMessage(type: MessageType, message: string) { + this.messageQueue.push({ type: type, message: message }); + if (!this.isProcessing) { this.processMessages(); } + } + reportError(message: string) { + this.reportMessage('danger', message); + } + + reportWarning(message: string) { + this.reportMessage('warning', message); + } +} + +export default function (app: AngularModule) { + app.service('messageReportingService', ['$log', '$timeout', + function ($log: $Log, $timeout: $Timeout) { + $log.debug('START messageReportingService()'); + const messageReportingService = new MessageReportingService($log, $timeout); $log.debug('END messageReportingService()'); + return messageReportingService; }]); } diff --git a/assets/js/modules/angular/services/styleService.js b/assets/js/modules/angular/services/styleService.js index 576c165..fb3d290 100644 --- a/assets/js/modules/angular/services/styleService.js +++ b/assets/js/modules/angular/services/styleService.js @@ -1,3 +1,4 @@ +// @flow /* * Copyright (C) 2016-2018 Alexander Krivács Schrøder * @@ -15,87 +16,105 @@ * along with this program. If not, see . */ -export default function (app) { - app.service('styleService', ['$log', 'colorService', - function ($log, colorService) { - $log.debug('START styleService()'); +import $ from 'jquery'; +import type { AngularModule, $Log } from 'angular'; + +import type { ColorService } from './colorService'; + +function addStyle(style: string) { + const styleElement = + $(''); + $('head').append(styleElement); + return styleElement; +} + +export class StyleService { + $log: $Log; + colorService: ColorService; + + customStyles: {}; + customStyleElements: {}; + + constructor($log: $Log, colorService: ColorService) { + this.$log = $log; + this.colorService = colorService; + + this.customStyles = {}; + this.customStyleElements = {}; + } + + _hasCustomStyle(key: string) { + return key in this.customStyles; + } + + _addCustomStyle(key: string, style: string) { + if (this._hasCustomStyle(key)) { + return; + } - function addStyle(style) { - var styleElement = - $(''); - $('head').append(styleElement); - return styleElement; - } - - var customStyles = {}; - var customStyleElements = {}; - - this.addCustomStyle = function (key, style) { - if (this.hasCustomStyle(key)) { - return; - } - - var styleElement = addStyle(style); - customStyles[key] = style; - customStyleElements[key] = styleElement; - }; - - this.removeCustomStyle = function (key) { - delete customStyles[key]; - customStyleElements[key].remove(); - delete customStyleElements[key]; - }; - - this.hasCustomStyle = function (key) { - return key in customStyles; - }; - - this.addItemStyle = function (id, color) { - var itemId = 'item_' + id; - if (!this.hasCustomStyle(itemId)) { - var qcNavItem = '#qcnav_item_' + id + ' > table'; - var qcNavItemWithColor = qcNavItem + '.with_color'; - - var backgroundColor = color; - var foregroundColor = colorService.createTintOrShade(color); - var hoverFocusColor = colorService - .createTintOrShade(color, 2); - - var itemStyle = - qcNavItemWithColor + '{' + - 'background-color:' + backgroundColor + ';' + - '}' + - qcNavItemWithColor + ',' + - qcNavItemWithColor + ' a.qcnav_name_link,' + - qcNavItemWithColor + ' a:link,' + - qcNavItemWithColor + ' a:visited{' + - 'color:' + foregroundColor + ';' + - '}' + - qcNavItem + ' a.qcnav_name_link{' + - 'cursor: pointer;' + - 'text-decoration: none;' + - '}' + - qcNavItemWithColor + ' a:hover,' + - qcNavItemWithColor + ' a:focus{' + - 'color: ' + hoverFocusColor + ';' + - '}'; - - this.addCustomStyle(itemId, itemStyle); - } - }; - - this.removeItemStyle = function (id) { - var itemId = 'item_' + id; - if (this.hasCustomStyle(itemId)) { - this.removeCustomStyle(itemId); - } - }; - - this.hasItemStyle = function (id) { - var itemId = 'item_' + id; - return this.hasCustomStyle(itemId); - }; + const styleElement = addStyle(style); + this.customStyles[key] = style; + this.customStyleElements[key] = styleElement; + } + _removeCustomStyle(key: string) { + delete this.customStyles[key]; + this.customStyleElements[key].remove(); + delete this.customStyleElements[key]; + } + + addItemStyle(id: number, color: string) { + const itemId = 'item_' + id; + if (!this._hasCustomStyle(itemId)) { + const qcNavItem = '#qcnav_item_' + id + ' > table'; + const qcNavItemWithColor = qcNavItem + '.with_color'; + + const backgroundColor = color; + const foregroundColor = this.colorService.createTintOrShade(color); + const hoverFocusColor = this.colorService.createTintOrShade(color, 2); + + const itemStyle = + qcNavItemWithColor + '{' + + 'background-color:' + backgroundColor + ';' + + '}' + + qcNavItemWithColor + ',' + + qcNavItemWithColor + ' a.qcnav_name_link,' + + qcNavItemWithColor + ' a:link,' + + qcNavItemWithColor + ' a:visited{' + + 'color:' + foregroundColor + ';' + + '}' + + qcNavItem + ' a.qcnav_name_link{' + + 'cursor: pointer;' + + 'text-decoration: none;' + + '}' + + qcNavItemWithColor + ' a:hover,' + + qcNavItemWithColor + ' a:focus{' + + 'color: ' + hoverFocusColor + ';' + + '}'; + + this._addCustomStyle(itemId, itemStyle); + } + } + + removeItemStyle(id: number) { + const itemId = 'item_' + id; + if (this._hasCustomStyle(itemId)) { + this._removeCustomStyle(itemId); + } + } + + hasItemStyle(id: number) { + const itemId = 'item_' + id; + return this._hasCustomStyle(itemId); + } +} + +export default function (app: AngularModule) { + app.service('styleService', ['$log', 'colorService', + function ($log: $Log, colorService: ColorService) { + $log.debug('START styleService()'); + const styleService = new StyleService($log, colorService); $log.debug('END styleService()'); + return styleService; }]); } diff --git a/assets/js/modules/angular/util.js b/assets/js/modules/angular/util.js new file mode 100644 index 0000000..b245ce5 --- /dev/null +++ b/assets/js/modules/angular/util.js @@ -0,0 +1,33 @@ +// @flow +/* + * Copyright (C) 2016-2018 Alexander Krivács Schrøder + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +export function nl2br(str: string, isXhtml: boolean) { + const breakTag = isXhtml || + typeof isXhtml === 'undefined' ? + '
' : '
'; + + return String(str).replace(/([^>\r\n]?)(\r\n|\n\r|\r|\n)/g, + '$1' + breakTag + '$2'); +} + +const comicLinkRegexp = +/]*href=(?:"|')(?:http:\/\/(?:www\.)?questionablecontent.net\/)?view\.php\?comic=(\d+)(?:"|')[^>]*>/; +export function angularizeLinks(str: string) { + return String(str).replace(comicLinkRegexp, + ''); +} diff --git a/assets/js/modules/dom-modifier.js b/assets/js/modules/dom-modifier.js index 3d8f449..cc4b804 100644 --- a/assets/js/modules/dom-modifier.js +++ b/assets/js/modules/dom-modifier.js @@ -1,3 +1,4 @@ +// @flow /* * Copyright (C) 2016-2018 Alexander Krivács Schrøder * @@ -15,6 +16,9 @@ * along with this program. If not, see . */ +import GM from 'greasemonkey'; +import $ from 'jquery'; + import constants from '../constants'; import settings from './settings'; import variables from '../../generated/variables.pass2'; @@ -25,7 +29,7 @@ import angularApp from './angular-app'; * * @param {string} href - URL to the CSS document */ -function addCss(href) { +function addCss(href: string) { $('head').prepend( '' ); @@ -36,7 +40,7 @@ function addCss(href) { * * @param {string} style - The inline CSS document */ -function addStyle(style) { +function addStyle(style: string) { $('head').append($('')); } @@ -66,8 +70,8 @@ export default class DomModifier { // For some reason, Jeph didn't use id="strip" on the comic on // the front page. Whyyy???? // (In other words, we have to use this method instead of just '#strip'.) - var comicImg = $('img[src*="/comics/"]'); - var comicAnchor = comicImg.parent('a'); + const comicImg = $('img[src*="/comics/"]'); + let comicAnchor = comicImg.parent('a'); if (comicAnchor.length !== 1) { comicImg.wrap($('')); @@ -79,11 +83,11 @@ export default class DomModifier { // To avoid triggering a flash of the comic "reloading", do in-place DOM // manipulation instead of replacing the whole thing with a template. // Fixes issue #13 - var comicDirective = $(''); + const comicDirective = $(''); comicAnchor.before(comicDirective); comicAnchor.detach().appendTo(comicDirective); comicAnchor.attr('ng-href', 'view.php?comic={{c.comicService.nextComic}}'); - comicImg.attr('ng-src', 'http://questionablecontent.net/comics/' + + comicImg.attr('ng-src', '//questionablecontent.net/comics/' + '{{c.comicService.comic}}.{{c.comicService.comicExtension}}'); comicImg.attr('ng-click', 'c.next($event)'); comicImg.attr('on-error', 'c.comicService.canFallback() ' + @@ -92,23 +96,23 @@ export default class DomModifier { // #comicDirective.attr('id', 'comic-anchor'); comicDirective.append($('')); - var comicImage = comicImg.get(0); - var comicLinkUrl = comicImage.src; + const comicImage = comicImg.get(0); + let comicLinkUrl = comicImage.src; comicLinkUrl = comicLinkUrl.split('/'); - var comic = parseInt(comicLinkUrl[comicLinkUrl.length - 1].split('.')[0]); + const comic = parseInt(comicLinkUrl[comicLinkUrl.length - 1].split('.')[0]); angularApp.constant('startComic', comic); // Figure out what the latest comic # is based on the URL in the // "Latest/Last" navigation button. - var latestUrl = $('#comicnav a').get(3).href; - var latestComic = parseInt(latestUrl.split('=')[1]); + const latestUrl = $('#comicnav a').get(3).href; + let latestComic = parseInt(latestUrl.split('=')[1]); if (isNaN(latestComic)) { latestComic = comic; } - if (settings.showDebugLogs) { + if (settings.values.showDebugLogs) { console.debug('Running QC Extensions v' + GM.info.script.version); // eslint-disable-line no-console console.debug('Latest URL:', latestUrl, 'Latest Comic:', latestComic); // eslint-disable-line no-console } diff --git a/assets/js/modules/jQuery.changeElementType.js b/assets/js/modules/jQuery.changeElementType.js index dbb5084..a9af7e2 100644 --- a/assets/js/modules/jQuery.changeElementType.js +++ b/assets/js/modules/jQuery.changeElementType.js @@ -1,21 +1,21 @@ +// @flow // Found at: https://gist.github.com/etienned/2934516 /*! License unknown */ -(function($) { - 'use strict'; - $.fn.changeElementType = function(newType) { - var newElements = []; - this.each(function() { - var attrs = {}; - $.each(this.attributes, function(idx, attr) { - attrs[attr.nodeName] = attr.nodeValue; - }); - $(this).replaceWith(function() { - var newElement = $('<' + newType + '/>', attrs); - newElements.push(newElement.get()[0]); - return newElement.append($(this).contents()); - }); +import $ from 'jquery'; + +$.fn.changeElementType = function (newType) { + const newElements = []; + this.each(function () { + const attrs = {}; + $.each(this.attributes, function (idx, attr) { + attrs[attr.nodeName] = attr.nodeValue; + }); + $(this).replaceWith(function () { + const newElement = $('<' + newType + '/>', attrs); + newElements.push(newElement.get()[0]); + return newElement.append($(this).contents()); }); - return $(newElements); - }; -})(jQuery); + }); + return $(newElements); +}; diff --git a/assets/js/modules/settings.js b/assets/js/modules/settings.js index 32cd9ea..9f10670 100644 --- a/assets/js/modules/settings.js +++ b/assets/js/modules/settings.js @@ -20,30 +20,7 @@ import GM from 'greasemonkey'; import constants from '../constants'; -/** - * Because we used a shim for GM4 temporarily, we should - * load our shimmed settings when migrating, to give the - * user a better UX. - */ -function loadFromGM4Shim() { - let storagePrefix = GM.info.script.name.replace(/[^A-Z]*/g, '') + '-'; - function shimGetValue(aKey, aDefault) { - let aValue = localStorage.getItem(storagePrefix + aKey); - if (null === aValue && 'undefined' !== typeof aDefault) { return aDefault; } - return aValue; - } - function shimDeleteValue(aKey) { - localStorage.removeItem(storagePrefix + aKey); - } - - let shimSettings = shimGetValue(constants.settingsKey); - if (shimSettings) { - shimDeleteValue(constants.settingsKey); - } - return shimSettings; -} - -type SettingValues = { +export type SettingValues = { showDebugLogs: boolean, scrollToTop: boolean, @@ -61,12 +38,37 @@ type SettingValues = { showIndicatorRibbon: boolean, showSmallRibbonByDefault: boolean, - useCorrectTimeFormat: boolean + useCorrectTimeFormat: boolean, + + version: ?string }; -class Settings { +/** + * Because we used a shim for GM4 temporarily, we should + * load our shimmed settings when migrating, to give the + * user a better UX. + */ +function loadFromGM4Shim(): ?string { + const storagePrefix = GM.info.script.name.replace(/[^A-Z]*/g, '') + '-'; + function shimGetValue(aKey: string, aDefault?: string): ?string { + const aValue = localStorage.getItem(storagePrefix + aKey); + if (null === aValue && 'undefined' !== typeof aDefault) { return aDefault; } + return aValue; + } + function shimDeleteValue(aKey: string): void { + localStorage.removeItem(storagePrefix + aKey); + } + + const shimSettings = shimGetValue(constants.settingsKey); + if (shimSettings) { + shimDeleteValue(constants.settingsKey); + } + return shimSettings; +} + +export class Settings { defaults: SettingValues; - settings: SettingValues; + values: SettingValues; constructor() { this.defaults = { @@ -87,7 +89,9 @@ class Settings { showIndicatorRibbon: true, showSmallRibbonByDefault: false, - useCorrectTimeFormat: true + useCorrectTimeFormat: true, + + version: null }; // This proxy makes it so you can access properties within the settings object @@ -96,13 +100,13 @@ class Settings { return new Proxy(this, { get(target, prop) { if (!(prop in target)) { - return target.settings[prop]; + return target.values[prop]; } return (target: any)[prop]; }, set(target, prop, value) { - if (!(prop in target) && target.settings && (prop in target.settings)) { - target.settings[prop] = value; + if (!(prop in target) && target.values && (prop in target.values)) { + target.values[prop] = value; } else { (target: any)[prop] = value; } @@ -112,28 +116,28 @@ class Settings { } async loadSettings() { - let shimSettings = loadFromGM4Shim(); - let settingsValue = shimSettings + const shimSettings = loadFromGM4Shim(); + const settingsValue = shimSettings ? shimSettings : await GM.getValue(constants.settingsKey, JSON.stringify(this.defaults)); - let settings = JSON.parse(settingsValue); + const settings = JSON.parse(settingsValue); // This makes sure that when new settings are added, users will // automatically receive the default values for those new settings when // they update. - $.each(this.defaults, function (key, defaultValue) { + $.each(this.defaults, (key, defaultValue) => { if (!(key in settings)) { settings[key] = defaultValue; } }); - this.settings = settings; + this.values = settings; } async saveSettings() { - await GM.setValue(constants.settingsKey, JSON.stringify(this.settings)); + await GM.setValue(constants.settingsKey, JSON.stringify(this.values)); } } diff --git a/flow-typed/npm/angular-ui_v0.2.x.js b/flow-typed/npm/angular-ui_v0.2.x.js new file mode 100644 index 0000000..0de2a9d --- /dev/null +++ b/flow-typed/npm/angular-ui_v0.2.x.js @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2016-2018 Alexander Krivács Schrøder + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +declare module 'angular-ui' { + declare type StateData = { + url: string; + templateUrl?: string; + controller?: Function; + params: mixed; + }; + + declare interface $StateProvider { + state(name: string, stateData: StateData): void; + } + + declare type $StateParams = Object; +} diff --git a/flow-typed/npm/angular_v1.5.x.js b/flow-typed/npm/angular_v1.5.x.js index 022bc0f..8c33ccb 100644 --- a/flow-typed/npm/angular_v1.5.x.js +++ b/flow-typed/npm/angular_v1.5.x.js @@ -19,7 +19,7 @@ declare module angular { // // NOTE: if you use compile step to mangle array, replace below with // declare type $npm$angular$DependencyInjection = T - declare type $npm$angular$DependencyInjection = Array; + declare type $npm$angular$DependencyInjection = Array | Function; // Extending Array allows us to do the `jq[0]` expression and friends // to get the actual underlying Element. @@ -60,7 +60,8 @@ declare module angular { // should write something to handle it. declare type DirectiveRestrict = "A" | "E" | "AE" | "EA"; declare type Directive = {| - restrict?: DirectiveRestrict, + restrict?: DirectiveRestrict, + replace?: boolean, template?: string, templateUrl?: string, scope?: Scope, @@ -104,17 +105,17 @@ declare module angular { declare type FactoryDeclaration = ( name: string, - di: $npm$angular$DependencyInjection<(...a: Array<*>) => Object> + di: $npm$angular$DependencyInjection ) => AngularModule; declare type FilterDeclaration = ( name: string, - di: $npm$angular$DependencyInjection<(...a: Array<*>) => Function> + di: $npm$angular$DependencyInjection ) => AngularModule; declare type ServiceDeclaration = ( name: string, - di: $npm$angular$DependencyInjection<(...a: Array<*>) => Function | Object> + di: $npm$angular$DependencyInjection ) => AngularModule; declare type RunDeclaration = ( @@ -220,9 +221,14 @@ declare module angular { //---------------------------------------------------------------------------- declare type AngularHttpService = { - post: AngularHttpPost<*> + get: AngularHttpGet<*>; + post: AngularHttpPost<*>; }; + declare type AngularHttpGet = ( + url: string + ) => AngularPromise + declare type AngularHttpPost = ( url: string, data: mixed @@ -298,4 +304,51 @@ declare module angular { invokeApply?: boolean, additionalParams?: * ) => AngularPromise<*>; + + declare type $Log = { + log(...Array<*>): void; + info(...Array<*>): void; + warn(...Array<*>): void; + error(...Array<*>): void; + debug(...Array<*>): void; + } + + declare type $Location = { + absUrl(): string; + url(url?: string): string; + protocol(): string; + host(): string; + port(): number; + path(path: string|number): string|Object; + search(search:any, paramValue?: string|number|Array|boolean): Object; + hash(hash: string|number): string; + replace(): void; + state(state?: Object): Object; + } + declare type CompiledExpression = (context: Object, locals: Object) => string; + declare type $Sce = { + isEnabled(): boolean; + parseAs(type: string, expression: string): CompiledExpression; + trustAs(type: string, value: T): T; + trustAsHtml(value: T): T; + trustAsCss(value: T): T; + trustAsUrl(value: T): T; + trustAsResourceUrl(value: T): T; + trustAsJs(value: T): T; + getTrusted(type: string, maybeTrusted: T): T; + getTrustedHtml(value: T): T; + getTrustedCss(value: T): T; + getTrustedUrl(value: T): T; + getTrustedResourceUrl(value: T): T; + getTrustedJs(value: T): T; + parseAsHtml(expression: string): CompiledExpression; + parseAsCss(expression: string): CompiledExpression; + parseAsUrl(expression: string): CompiledExpression; + parseAsResourceUrl(expression: string): CompiledExpression; + parseAsJs(expression: string): CompiledExpression; + } + + declare type $Http = AngularHttpService; + + declare type $Filter = (name: string) => Function; } diff --git a/flow-typed/npm/greasemonkey_v4.x.x.js b/flow-typed/npm/greasemonkey_v4.x.x.js index a86ad02..85deac5 100644 --- a/flow-typed/npm/greasemonkey_v4.x.x.js +++ b/flow-typed/npm/greasemonkey_v4.x.x.js @@ -36,6 +36,7 @@ declare module greasemonkey { } declare class GMXHRResult extends XMLHttpRequest { + responseHeaders: string; context?: any; } @@ -53,9 +54,9 @@ declare module greasemonkey { method: "GET" | "POST" | "HEAD" | "PUT" | "PATCH" | "DELETE"; overrideMimeType?: string; password?: string; - synchronous: boolean; - timeout: number; - upload: any; + synchronous?: boolean; + timeout?: number; + upload?: any; url: string; user?: string; @@ -79,6 +80,7 @@ declare module greasemonkey { notification(text: string, title: string, image: ?string, onclick: ?() => void): void; setClipboard(text: string): void; + openInTab(url: string, open_in_background: ?boolean): void; xmlHttpRequest(details: GMXHRDetails): void; } diff --git a/flow-typed/npm/jquery_v3.x.x.js b/flow-typed/npm/jquery_v3.x.x.js index fe714b3..c2f98af 100644 --- a/flow-typed/npm/jquery_v3.x.x.js +++ b/flow-typed/npm/jquery_v3.x.x.js @@ -3279,7 +3279,7 @@ declare class JQuery { * * @param index A zero-based integer indicating which element to retrieve. */ - get(index: number): HTMLElement; + get(index: number): T; /** * Retrieve the elements matched by the jQuery object. */ diff --git a/licenseBanner.js b/licenseBanner.js index 86937c4..8d20eb1 100644 --- a/licenseBanner.js +++ b/licenseBanner.js @@ -17,7 +17,7 @@ /* global module */ -var licenseBanner = +const licenseBanner = `/* * Copyright (C) 2016-2018 Alexander Krivács Schrøder * diff --git a/package-lock.json b/package-lock.json index ea2f2b3..6a6516f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,60 @@ "@babel/highlight": "7.0.0" } }, + "@babel/core": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.1.5.tgz", + "integrity": "sha512-vOyH020C56tQvte++i+rX2yokZcRfbv/kKcw+/BCRw/cK6dvsr47aCzm8oC1XHwMSEWbqrZKzZRLzLnq6SFMsg==", + "dev": true, + "requires": { + "@babel/code-frame": "7.0.0", + "@babel/generator": "7.1.5", + "@babel/helpers": "7.1.5", + "@babel/parser": "7.1.5", + "@babel/template": "7.1.2", + "@babel/traverse": "7.1.5", + "@babel/types": "7.1.5", + "convert-source-map": "1.6.0", + "debug": "3.2.6", + "json5": "0.5.1", + "lodash": "4.17.11", + "resolve": "1.8.1", + "semver": "5.4.1", + "source-map": "0.5.7" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "dev": true, + "requires": { + "ms": "2.1.1" + } + }, + "lodash": { + "version": "4.17.11", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", + "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", + "dev": true + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "dev": true + }, + "resolve": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.8.1.tgz", + "integrity": "sha512-AicPrAC7Qu1JxPCZ9ZgCZlY35QgFnNqc+0LtbRNxnVw4TXvjQ72wnuL9JQcEBgXkI9JM8MsT9kaQoHcpCRJOYA==", + "dev": true, + "requires": { + "path-parse": "1.0.5" + } + } + } + }, "@babel/generator": { "version": "7.1.5", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.1.5.tgz", @@ -54,6 +108,21 @@ "@babel/types": "7.1.5" } }, + "@babel/helper-module-imports": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.0.0.tgz", + "integrity": "sha512-aP/hlLq01DWNEiDg4Jn23i+CXxW/owM4WpDLFUbpjxe4NS3BhLVZQ5i7E0ZrxuQ/vwekIeciyamgB1UIYxxM6A==", + "dev": true, + "requires": { + "@babel/types": "7.1.5" + } + }, + "@babel/helper-plugin-utils": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.0.0.tgz", + "integrity": "sha512-CYAOUCARwExnEixLdB6sDm2dIJ/YgEAKDM1MOeMeZu9Ld/bDgVo8aiWrXwcY7OBh+1Ea2uUcVRcxKk0GJvW7QA==", + "dev": true + }, "@babel/helper-split-export-declaration": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.0.0.tgz", @@ -63,6 +132,17 @@ "@babel/types": "7.1.5" } }, + "@babel/helpers": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.1.5.tgz", + "integrity": "sha512-2jkcdL02ywNBry1YNFAH/fViq4fXG0vdckHqeJk+75fpQ2OH+Az6076tX/M0835zA45E0Cqa6pV5Kiv9YOqjEg==", + "dev": true, + "requires": { + "@babel/template": "7.1.2", + "@babel/traverse": "7.1.5", + "@babel/types": "7.1.5" + } + }, "@babel/highlight": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.0.0.tgz", @@ -111,6 +191,35 @@ "integrity": "sha512-WXKf5K5HT6X0kKiCOezJZFljsfxKV1FpU8Tf1A7ZpGvyd/Q4hlrJm2EwoH2onaUq3O4tLDp+4gk0hHPsMyxmOg==", "dev": true }, + "@babel/plugin-syntax-flow": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.0.0.tgz", + "integrity": "sha512-zGcuZWiWWDa5qTZ6iAnpG0fnX/GOu49pGR5PFvkQ9GmKNaSphXQnlNXh/LG20sqWtNrx/eB6krzfEzcwvUyeFA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "7.0.0" + } + }, + "@babel/plugin-transform-flow-strip-types": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.0.0.tgz", + "integrity": "sha512-WhXUNb4It5a19RsgKKbQPrjmy4yWOY1KynpEbNw7bnd1QTcrT/EIl3MJvnGgpgvrKyKbqX7nUNOJfkpLOnoDKA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "7.0.0", + "@babel/plugin-syntax-flow": "7.0.0" + } + }, + "@babel/preset-flow": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/preset-flow/-/preset-flow-7.0.0.tgz", + "integrity": "sha512-bJOHrYOPqJZCkPVbG1Lot2r5OSsB+iUOaxiHdlOeB1yPWS6evswVHwvkDLZ54WTaTRIk89ds0iHmGZSnxlPejQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "7.0.0", + "@babel/plugin-transform-flow-strip-types": "7.0.0" + } + }, "@babel/template": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.1.2.tgz", @@ -818,6 +927,15 @@ "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", "dev": true }, + "convert-source-map": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.6.0.tgz", + "integrity": "sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A==", + "dev": true, + "requires": { + "safe-buffer": "5.1.1" + } + }, "core-js": { "version": "2.5.7", "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.7.tgz", @@ -1833,6 +1951,12 @@ } } }, + "grunt-babel": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/grunt-babel/-/grunt-babel-8.0.0.tgz", + "integrity": "sha512-WuiZFvGzcyzlEoPIcY1snI234ydDWeWWV5bpnB7PZsOLHcDsxWKnrR1rMWEUsbdVPPjvIirwFNsuo4CbJmsdFQ==", + "dev": true + }, "grunt-contrib-compass": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/grunt-contrib-compass/-/grunt-contrib-compass-1.1.1.tgz", @@ -1876,30 +2000,6 @@ "maxmin": "1.1.0", "uglify-es": "3.3.9", "uri-path": "1.0.0" - }, - "dependencies": { - "commander": { - "version": "2.13.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.13.0.tgz", - "integrity": "sha512-MVuS359B+YzaWqjCL/c+22gfryv+mCBPHAv3zyVI2GN8EY6IRP8VwtasXn8jyyhvvq84R4ImN1OKRtcbIasjYA==", - "dev": true - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - }, - "uglify-es": { - "version": "3.3.9", - "resolved": "https://registry.npmjs.org/uglify-es/-/uglify-es-3.3.9.tgz", - "integrity": "sha512-r+MU0rfv4L/0eeW3xZrd16t4NZfK8Ld4SWVglYBb7ez5uXFWHuVRs6xCTrf1yirs9a4j4Y27nn7SRfO6v67XsQ==", - "dev": true, - "requires": { - "commander": "2.13.0", - "source-map": "0.6.1" - } - } } }, "grunt-contrib-watch": { @@ -1956,7 +2056,7 @@ } }, "grunt-files-to-javascript-variables": { - "version": "git://github.com/alexschrod/grunt-files-to-javascript-variables.git#863aa75d3309fd90f023bc2713088889c78f6429", + "version": "git://github.com/alexschrod/grunt-files-to-javascript-variables.git#49698ce0a51eea980d38627ec8a0ac9290816274", "dev": true, "requires": { "comment-json": "0.1.11", @@ -2608,6 +2708,12 @@ "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", "dev": true }, + "json5": { + "version": "0.5.1", + "resolved": "http://registry.npmjs.org/json5/-/json5-0.5.1.tgz", + "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=", + "dev": true + }, "jsonfile": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", @@ -2831,7 +2937,7 @@ }, "mime": { "version": "1.2.11", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.2.11.tgz", + "resolved": "http://registry.npmjs.org/mime/-/mime-1.2.11.tgz", "integrity": "sha1-WCA+7Ybjpe8XrtK32evUfwpg3RA=", "dev": true }, @@ -3533,6 +3639,34 @@ "integrity": "sha512-QOdK6Z+MznqYpvrw82IsjOdUwAU2b8DGe4TIrExUdp6HlyX3QTUuUt/0YcrJBHxUuY5X9uFXNmwxC4FkW9qYuA==", "dev": true }, + "rollup-plugin-babel": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/rollup-plugin-babel/-/rollup-plugin-babel-4.0.3.tgz", + "integrity": "sha512-/PP0MgbPQyRywI4zRIJim6ySjTcOLo4kQbEbROqp9kOR3kHC3FeU++QpBDZhS2BcHtJTVZMVbBV46flbBN5zxQ==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "7.0.0", + "rollup-pluginutils": "2.3.3" + }, + "dependencies": { + "estree-walker": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.5.2.tgz", + "integrity": "sha512-XpCnW/AE10ws/kDAs37cngSkvgIR8aN3G0MS85m7dUpuK2EREo9VJ00uvw6Dg/hXEpfsE1I1TvJOJr+Z+TL+ig==", + "dev": true + }, + "rollup-pluginutils": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.3.3.tgz", + "integrity": "sha512-2XZwja7b6P5q4RZ5FhyX1+f46xi1Z3qBKigLRZ6VTZjwbN0K1IFGMlwm06Uu0Emcre2Z63l77nq/pzn+KxIEoA==", + "dev": true, + "requires": { + "estree-walker": "0.5.2", + "micromatch": "2.3.11" + } + } + } + }, "rollup-plugin-commonjs": { "version": "8.2.6", "resolved": "https://registry.npmjs.org/rollup-plugin-commonjs/-/rollup-plugin-commonjs-8.2.6.tgz", @@ -3943,6 +4077,30 @@ "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", "dev": true }, + "uglify-es": { + "version": "3.3.9", + "resolved": "https://registry.npmjs.org/uglify-es/-/uglify-es-3.3.9.tgz", + "integrity": "sha512-r+MU0rfv4L/0eeW3xZrd16t4NZfK8Ld4SWVglYBb7ez5uXFWHuVRs6xCTrf1yirs9a4j4Y27nn7SRfO6v67XsQ==", + "dev": true, + "requires": { + "commander": "2.13.0", + "source-map": "0.6.1" + }, + "dependencies": { + "commander": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.13.0.tgz", + "integrity": "sha512-MVuS359B+YzaWqjCL/c+22gfryv+mCBPHAv3zyVI2GN8EY6IRP8VwtasXn8jyyhvvq84R4ImN1OKRtcbIasjYA==", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, "uglify-js": { "version": "2.8.29", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", diff --git a/package.json b/package.json index 0a22456..babf6e0 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,8 @@ }, "homepage": "https://questionablextensions.net/", "devDependencies": { + "@babel/core": "^7.1.5", + "@babel/preset-flow": "^7.0.0", "babel-eslint": "^10.0.1", "eslint": "^5.8.0", "eslint-plugin-flowtype": "^3.2.0", @@ -25,6 +27,7 @@ "flow-remove-types": "^1.2.3", "flow-typed": "^2.5.1", "grunt": "~1.0.1", + "grunt-babel": "^8.0.0", "grunt-contrib-compass": "~1.1", "grunt-contrib-concat": "~1.0", "grunt-contrib-htmlmin": "~2.3", @@ -36,6 +39,7 @@ "grunt-rollup": "^8.1.0", "jquery": "^3.2.1", "load-grunt-tasks": "~3.5", + "rollup-plugin-babel": "^4.0.3", "rollup-plugin-commonjs": "^8.2.6", "rollup-plugin-node-resolve": "^3.0.0", "rollup-plugin-virtual": "^1.0.1" diff --git a/userScriptBanner.js b/userScriptBanner.js index f57d125..1347f17 100644 --- a/userScriptBanner.js +++ b/userScriptBanner.js @@ -17,7 +17,7 @@ /* global module */ -var userScriptBanner = +const userScriptBanner = `// ==UserScript== // @name Questionable Content Single-Page Application with Extra Features // @namespace https://questionablextensions.net/