Skip to content

Instantly share code, notes, and snippets.

@phuzion
Created October 1, 2021 13:16
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save phuzion/975dd4557ca495e8411f4dd5dd7ea2ab to your computer and use it in GitHub Desktop.
Save phuzion/975dd4557ca495e8411f4dd5dd7ea2ab to your computer and use it in GitHub Desktop.
Latest copy of the Questionable Extensions client script, potentially quite old
/*
* Copyright (C) 2016-2019 Alexander Krivács Schrøder <alexschrod@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
// ==UserScript==
// @name Questionable Content Single-Page Application with Extra Features
// @namespace https://questionablextensions.net/
// @version 0.6.1
// @author Alexander Krivács Schrøder
// @description Converts questionablecontent.net into a single-page application and adds extra features, such as character, location and storyline navigation.
// @homepage https://questionablextensions.net/
// @icon https://questionablextensions.net/images/icon.png
// @icon64 https://questionablextensions.net/images/icon64.png
// @updateURL https://questionablextensions.net/releases/qc-ext.latest.meta.js
// @downloadURL https://questionablextensions.net/releases/qc-ext.latest.user.js
// @supportURL https://github.com/Questionable-Content-Extensions/client/issues
// @match *://*.questionablecontent.net/
// @match *://*.questionablecontent.net/index.php
// @match *://*.questionablecontent.net/view.php*
// @require https://cdnjs.cloudflare.com/ajax/libs/jquery/2.2.1/jquery.js
// @require https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.6/js/bootstrap.min.js
// @require https://questionablextensions.net/scripts/angular.custom.js
// @require https://cdnjs.cloudflare.com/ajax/libs/angular-ui-router/0.2.18/angular-ui-router.min.js
// @connect questionablextensions.net
// @connect questionablecontent.herokuapp.com
// @connect localhost
// @grant GM.openInTab
// @grant GM.setValue
// @grant GM.getValue
// @grant GM.xmlHttpRequest
// @noframes
// ==/UserScript==
// Found at: https://gist.github.com/etienned/2934516
/*! License unknown */
jQuery.fn.changeElementType = function (newType) {
const newElements = [];
this.each(function () {
const attrs = {};
jQuery.each(this.attributes, function (idx, attr) {
attrs[attr.nodeName] = attr.nodeValue;
});
jQuery(this).replaceWith(function () {
const newElement = jQuery('<' + newType + '/>', attrs);
newElements.push(newElement.get()[0]);
return newElement.append(jQuery(this).contents());
});
});
return jQuery(newElements);
};
/*
* Copyright (C) 2016-2019 Alexander Krivács Schrøder <alexschrod@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
// Set this to true when working against your local test server.
// NEVER CHECK THIS FILE IN WITH developmentMode = true!
const developmentMode = false;
function getSiteUrl() {
return 'https://questionablextensions.net/';
}
function getWebserviceBaseUrl() {
if (developmentMode) {
return 'http://localhost:3000/api/';
} else {
return 'https://questionablecontent.herokuapp.com/api/';
}
}
const comicDataUrl = getWebserviceBaseUrl() + 'comicdata/';
const itemDataUrl = getWebserviceBaseUrl() + 'itemdata/';
const editLogUrl = getWebserviceBaseUrl() + 'log';
const constants = {
settingsKey: 'settings',
developmentMode,
siteUrl: getSiteUrl(),
comicDataUrl,
itemDataUrl,
editLogUrl,
// Comics after 3132 should have a tagline
taglineThreshold: 3132,
excludedComicsUrl: comicDataUrl + 'excluded',
addItemToComicUrl: comicDataUrl + 'additem',
removeItemFromComicUrl: comicDataUrl + 'removeitem',
setComicTitleUrl: comicDataUrl + 'settitle',
setComicTaglineUrl: comicDataUrl + 'settagline',
setPublishDateUrl: comicDataUrl + 'setpublishdate',
setGuestComicUrl: comicDataUrl + 'setguest',
setNonCanonUrl: comicDataUrl + 'setnoncanon',
setNoCastUrl: comicDataUrl + 'setnocast',
setNoLocationUrl: comicDataUrl + 'setnolocation',
setNoStorylineUrl: comicDataUrl + 'setnostoryline',
setNoTitleUrl: comicDataUrl + 'setnotitle',
setNoTaglineUrl: comicDataUrl + 'setnotagline',
itemImageUrl: itemDataUrl + 'image/',
itemFriendDataUrl: itemDataUrl + 'friends/',
itemLocationDataUrl: itemDataUrl + 'locations/',
setItemDataPropertyUrl: itemDataUrl + 'setproperty',
comicExtensions: ['png', 'gif', 'jpg'],
comicdataLoadingEvent: 'comicdata-loading',
comicdataLoadedEvent: 'comicdata-loaded',
comicdataErrorEvent: 'comicdata-error',
itemdataLoadingEvent: 'itemdata-loading',
itemdataLoadedEvent: 'itemdata-loaded',
itemdataErrorEvent: 'itemdata-error',
itemsChangedEvent: 'items-changed',
maintenanceEvent: 'maintenance',
messages: {
maintenance: 'The Questionable Extensions' + ' server is currently undergoing maintenance.' + ' Normal operation should resume within a' + ' few minutes.'
}
};
/*
* Copyright (C) 2016-2019 Alexander Krivács Schrøder <alexschrod@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
/**
* 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() {
const storagePrefix = GM.info.script.name.replace(/[^A-Z]*/g, '') + '-';
function shimGetValue(aKey, aDefault) {
const aValue = localStorage.getItem(storagePrefix + aKey);
if (null === aValue && 'undefined' !== typeof aDefault) {
return aDefault;
}
return aValue;
}
function shimDeleteValue(aKey) {
localStorage.removeItem(storagePrefix + aKey);
}
const shimSettings = shimGetValue(constants.settingsKey);
if (shimSettings) {
shimDeleteValue(constants.settingsKey);
}
return shimSettings;
}
class Settings {
constructor() {
this.defaults = {
showDebugLogs: false,
scrollToTop: true,
showAllMembers: false,
showCast: true,
showStorylines: true,
showLocations: true,
useColors: true,
skipNonCanon: false,
skipGuest: false,
editMode: false,
editModeToken: '',
showIndicatorRibbon: true,
showSmallRibbonByDefault: false,
useCorrectTimeFormat: true,
comicLoadingIndicatorDelay: 2000,
version: null
}; // This proxy makes it so you can access properties within the settings object
// directly on the settings class object also.
return new Proxy(this, {
get(target, prop) {
if (!(prop in target)) {
return target.values[prop];
}
return target[prop];
},
set(target, prop, value) {
if (!(prop in target) && target.values && prop in target.values) {
target.values[prop] = value;
} else {
target[prop] = value;
}
return true;
}
});
}
async loadSettings() {
const shimSettings = loadFromGM4Shim();
const settingsValue = shimSettings ? shimSettings : await GM.getValue(constants.settingsKey, JSON.stringify(this.defaults));
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, (key, defaultValue) => {
if (!(key in settings)) {
settings[key] = defaultValue;
}
});
this.values = settings;
}
async saveSettings() {
await GM.setValue(constants.settingsKey, JSON.stringify(this.values));
}
}
var settings = new Settings();
let variables = {
css: {},
html: {}
};
variables.css.style = '.hidden{display:none !important}@media screen and (min-width: 1015px){img[src*="/comics/"]{margin-left:-15px;margin-right:15px}}.nav,.pagination,.carousel,.panel-title a{cursor:pointer}.modal{color:#ebebeb;text-align:left}.modal input{width:auto}.modal label.disabled,.modal p.disabled{color:#999}input{margin:0 !important}a{cursor:pointer}.p-l-10px{padding-left:10px}.p-r-10px{padding-right:10px}.p-r-20px{padding-right:20px}.m-t-3px{margin-top:3px}.m-t-20px{margin-top:20px}.pos-rel{position:relative}.no-border{border:none}.absolute-dead-center{position:absolute;top:50%;left:50%;transform:translate(-50%, -50%)}.qc-error-text{color:red}.corner-ribbon{width:200px;background:#e43;position:absolute;text-align:center;line-height:50px;letter-spacing:1px;color:#f0f0f0;transform:rotate(-45deg) scale(1) translateY(0px);transition:transform 2s, opacity 1s;cursor:pointer;opacity:1}.corner-ribbon.sticky{position:fixed}.corner-ribbon.shadow{box-shadow:0 3px 3px 0 rgba(0,0,0,0.3)}.corner-ribbon.top-left{top:25px;left:-50px;transform:rotate(-45deg)}.corner-ribbon.top-right{top:25px;right:-35px;transform:rotate(45deg) scale(1) translateY(0px);clip-path:polygon(27.5% -12.5%, -12.5% 150%, 112.5% 150%, 72.5% -12.5%)}@media screen and (max-width: 1014px){.corner-ribbon.top-right{right:-50px}}.corner-ribbon.top-right.small{transform:rotate(45deg) scale(0.25) translateY(-220px)}.corner-ribbon.top-right:hover{opacity:0.4}.corner-ribbon.bottom-left{bottom:25px;left:-50px;transform:rotate(45deg)}.corner-ribbon.bottom-right{right:-50px;bottom:25px;transform:rotate(-45deg)}.corner-ribbon.white{background:#f0f0f0;color:#555}.corner-ribbon.black{background:#333}.corner-ribbon.grey{background:#999}.corner-ribbon.blue{background:#39d}.corner-ribbon.green{background:#2c7}.corner-ribbon.turquoise{background:#1b9}.corner-ribbon.purple{background:#95b}.corner-ribbon.red{background:#e43}.corner-ribbon.orange{background:#e82}.corner-ribbon.yellow{background:#ec0}.lds-dual-ring{display:inline-block;width:46px;height:64px}.lds-dual-ring:after{content:" ";display:block;width:46px;height:46px;margin:1px;border-radius:50%;border:5px solid #fff;border-color:#fff transparent #fff transparent;animation:lds-dual-ring 1.2s linear infinite}.lds-dual-ring.black:after{border:5px solid #000;border-color:#000 transparent #000 transparent}@keyframes lds-dual-ring{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}#messageSeat{position:fixed;top:0;bottom:0;left:0;right:0;pointer-events:none;z-index:10000}#messageSeat>*{pointer-events:all}#messageSeat .alert{margin-bottom:1px;font-size:15px}#qcnav_navbox{margin:40px 5px 5px 5px;text-align:center}#qcnav_navbox thead,#qcnav_navbox tbody{border:none}#qcnav_navbox>tbody>tr,#qcnav_navbox>thead>tr{background-color:#ffffff !important}#qcnav_navbox thead td,#qcnav_navbox thead th{text-align:center}#qcnav_navbox a.qc{cursor:pointer;background:transparent;padding:0px 4px;text-decoration:none}#qcnav_navbox a.qc,#qcnav_navbox a.qc:link{color:#2ba6cb}#qcnav_navbox a.qc:visited{color:#258faf}#qcnav_navbox a.qc:hover{color:#258faf}#qcnav_navbox h1{font-size:15px;margin-top:10px}#qcnav_navbox h1.first-header{margin-top:0}#qcnav_navbox h1.update-header{margin-top:0;margin-bottom:0;color:#e43;font-weight:bold}#qcnav_navbox h1 small{color:#0a0a0a;line-height:1}@media screen and (min-width: 1015px){#qcnav_navbox{margin-left:-48px;width:200px}}.qcnav_space{padding:3px}.qcnav_centered{text-align:center}.qcnav_vcentered{vertical-align:middle}.qcnav_navelement{width:100%;font-weight:bold;margin:0}.qcnav_navelement *{background-color:transparent}.qcnav_navelement a{text-decoration:none}.qcnav_navelement td{padding:3px 1px}.qcnav_navelement .no-nav{visibility:hidden}.qcnav_arrows{font-weight:bold}.qcnav_name{width:100%;text-align:center}a.qcnav_settings{font-size:x-small;text-decoration:none}.qcnav_navelement a.qcnav_name_link{cursor:pointer;text-decoration:none;color:#2ba6cb}#qcnav_item_missing_cast>table.with_color,#qcnav_item_missing_location>table.with_color,#qcnav_item_missing_storyline>table.with_color,#qcnav_item_missing_title>table.with_color,#qcnav_item_missing_tagline>table.with_color{background-color:#5f0000}#qcnav_item_missing_cast>table.with_color a.qcnav_name_link,#qcnav_item_missing_cast>table.with_color a:link,#qcnav_item_missing_cast>table.with_color a:visited,#qcnav_item_missing_location>table.with_color a.qcnav_name_link,#qcnav_item_missing_location>table.with_color a:link,#qcnav_item_missing_location>table.with_color a:visited,#qcnav_item_missing_storyline>table.with_color a.qcnav_name_link,#qcnav_item_missing_storyline>table.with_color a:link,#qcnav_item_missing_storyline>table.with_color a:visited,#qcnav_item_missing_title>table.with_color a.qcnav_name_link,#qcnav_item_missing_title>table.with_color a:link,#qcnav_item_missing_title>table.with_color a:visited,#qcnav_item_missing_tagline>table.with_color a.qcnav_name_link,#qcnav_item_missing_tagline>table.with_color a:link,#qcnav_item_missing_tagline>table.with_color a:visited{color:#ff3030}#qcnav_item_missing_cast>table.with_color a:hover,#qcnav_item_missing_cast>table.with_color a:focus,#qcnav_item_missing_location>table.with_color a:hover,#qcnav_item_missing_location>table.with_color a:focus,#qcnav_item_missing_storyline>table.with_color a:hover,#qcnav_item_missing_storyline>table.with_color a:focus,#qcnav_item_missing_title>table.with_color a:hover,#qcnav_item_missing_title>table.with_color a:focus,#qcnav_item_missing_tagline>table.with_color a:hover,#qcnav_item_missing_tagline>table.with_color a:focus{color:#ff9797}.radial-progress{border-radius:50%}.radial-progress .circle .mask,.radial-progress .circle .fill,.radial-progress .circle .inset{position:absolute;border-radius:50%}.radial-progress .circle .mask,.radial-progress .circle .fill{-webkit-backface-visibility:hidden;backface-visibility:hidden;transition:-webkit-transform 1s;transition:-ms-transform 1s;transition:transform 1s}.update-overlay{position:absolute;left:0;top:0;right:0;bottom:0;background-color:rgba(0,0,0,0.5);z-index:10000}.update-overlay-white{background-color:rgba(255,255,255,0.5)}.update-text{text-align:center;font-weight:bold;color:white}.update-overlay-white .update-text{color:black}.dropdown-add-item{max-height:250px;overflow-y:auto;word-break:break-all;word-wrap:break-word}#changeLogDialog>.modal-dialog{overflow-y:initial !important}#changeLogDialog>.modal-dialog>.modal-content>.modal-body{max-height:73.5vh;overflow-y:auto}.change-log-entry{padding-left:2em;border-left:1px dashed #8E979F}.change-log-entry p.developer-message{border-left:5px solid #8E979F;padding:20px;background-color:#697683}.change-log-entry p.developer-message a{color:#8E979F}.comic-loading-overlay{position:absolute;left:0;top:0;right:0;bottom:0;background-color:rgba(0,0,0,0.5);margin-left:-15px;margin-right:15px;text-align:center;padding-top:50px}.comic-loading-overlay p{font-weight:bold;color:white}.edit-log-pager{text-align:center;color:white;background-color:#4e5d6c}.item-details-image{width:128px;height:128px;text-align:center;position:relative;border:1px solid #485563;box-sizing:content-box;float:left;margin-right:15px;margin-bottom:24px}.item-details-image .no-image-icon{position:absolute;left:50%;top:42%;transform:translate(-50%, -50%);color:#485563;font-size:64px}.item-details-image .no-image-text{position:absolute;top:76%;transform:translateY(-50%);width:100%;font-size:20px;color:#485563;font-weight:bold}.item-details-image img{position:absolute;top:50%;transform:translate(-50%, -50%);left:50%;max-width:128px;max-height:128px}.item-details-image .left-arrow,.item-details-image .right-arrow{position:absolute;top:100%;color:inherit;font-size:24px}.item-details-image .left-arrow{left:5%}.item-details-image .right-arrow{right:5%}.item-details-image .item-details-pager{position:absolute;left:50%;top:100%;color:inherit;font-size:16px;transform:translateX(-50%)}.item-details-image .item-details-pager.point{cursor:pointer}.item-details-presence{float:right;margin-left:15px;margin-bottom:15px;position:relative}.item-details-presence .presence-text{position:absolute;top:50%;transform:translate(-50%, -50%);left:50%;font-size:20px;text-align:center}.item-details-presence .presence-text span{font-size:12px}.item-details-hr{margin-top:0;margin-bottom:10px}.settings-input{width:100%;margin-bottom:5px}qc-comic{display:inline-block;position:relative}\n';
variables.html.addItem = '<div class="input-group" id="addItem_{{a.unique}}_dropdown"><input id="addItem_{{a.unique}}_search" type="text" ng-keypress="a.keyPress($event)" ng-change="a.searchChanged()" ng-focus="a.focusSearch()" ng-blur="a.blurSearch()" ng-model="a.itemFilterText" ng-disabled="isUpdating" class="form-control" aria-label="Quick-add" placeholder="Quick-Add..."><div class="input-group-btn"><button id="addItem_{{a.unique}}_dropdownButton" type="button" ng-disabled="isUpdating" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">&nbsp;<span class="caret"></span></button><ul class="dropdown-menu dropdown-menu-right dropdown-add-item"><li ng-repeat="item in a.items | filter:a.itemFilter"><a ng-click="a.addItem(item)">{{item.shortName}}</a></li></ul></div></div>';
variables.html.changeLog = '<div class="modal fade" tabindex="-1" role="dialog" id="changeLogDialog" style="display: none"><div class="modal-dialog"><div class="modal-content"><div class="modal-header"><h3 class="modal-title">Change Log</h3></div><div class="modal-body"><div ng-if="clvm.previousVersion"><h4>Script updated!</h4><p>Your Questionable Content Extensions has been updated from v{{ clvm.previousVersion || \'none\'}} to v{{ clvm.currentVersion }}!</p></div><div ng-if="!clvm.previousVersion"><h4>Script installed!</h4><p>Thank you for installing Questionable Content Extensions!</p></div><h4>0.6.1 <small>December 8, 2019</small></h4><ul><li>Fix missing sidebar when loading a comic directly by number in URL.</li></ul><h4>0.6.0 <small>March 7, 2019</small></h4><div class="change-log-entry"><p class="developer-message">This release, like the previous one, has been mostly about code quality and features for editors and me, the developer. I\'m hoping that I can now start putting in more features for the end-user as we move towards version 0.7. If you have ideas for new features, or you have any problems with the extension as it is, don\'t hesitate to <a href="https://github.com/Questionable-Content-Extensions/client/issues">tell us about them</a>! While I have quite a lot of ideas left to implement, I\'d also love to implement ideas that aren\'t just my own.</p><h5>New features</h5><ul><li>If a comic takes too long to load, show a loading indicator. What\'s considered "too long" is configurable in the settings dialog</li><li>You can now open this change log at any time from the settings dialog, not just when a new version is out</li></ul><h5>Changes/fixes</h5><ul><li>Random comic navigation now respects exclusion settings</li></ul><h5>Editor features</h5><ul><li>Add support for the new item image system and for image uploading. This allows uploading images directly in the browser (was previously a tedious server-only job), and also allows for multiple images per item</li><li>Add edit log view. The server keeps track of all changes editors make, this view lets you see that log</li><li>Add flags for indicating whether a comic is lacking certain features, such as marking a comic as not having characters. Currently only used by the "comic missing X" feature so it doesn\'t indicate a comic having an error where missing X isn\'t erroneous</li><li>Ensure there are updating/loading indicators when changes are made, so it\'s clear that something\'s happening and that you don\'t accidentally do multiple potentially conflicting things</li></ul></div><h4>0.5.3 <small>March 1, 2019</small></h4><ul><li>Switch to using the new API server (only minor improvements client side for this version, but lays the groundwork for quicker and better improvements going forward)</li></ul><h4>0.5.2 <small>October 3, 2018</small></h4><ul><li>Use Angular\'s date formatting for a better user experience</li><li>Don\'t throw change log in user\'s face on update or fresh installation, instead show small notice</li></ul><h4>0.5.1 <small>April 8, 2017</small></h4><ul><li>Deal better with server errors and maintenance</li><li>Add support for showing the comic strip publish date</li><li>Add a ribbon indicating comic status for non-canon and guest strips</li><li>Make it possible to hit ENTER to navigate in the comic navigation widget</li><li>Show a change log on installation and update of script</li></ul><h4>0.5.0 <small>March 30, 2017</small></h4><ul><li>Show locations an item has visited/been shown together with</li><li>Show all members should always work, even when a comic has no data</li><li>Add navigation control, which lets you navigate to any specific comic #</li><li>Make "Show all members" behave much nicer than before</li></ul><h4>0.4.1 <small>December 26, 2016</small></h4><ul><li>Fix invalid editor token causing an error in the comic load routine</li></ul><h4>0.4.0 <small>December 26, 2016</small></h4><ul><li>Put comic number in site title for better browser navigation experience</li><li>Update non-color style to work with new page design</li><li>Save settings regardless of how the setting dialog is closed</li><li>Refresh comic data when editor mode is enabled</li><li>Fix "flash of unloaded content" when the script was loading</li></ul><h4>0.3.3 <small>August 19, 2016</small></h4><ul><li>Update visual style to better match new page design/layout</li></ul><h4>0.3.2 <small>August 19, 2016</small></h4><ul><li>Fix script to work with new page design/layout</li></ul><h4>0.3.1 <small>March 23, 2016</small></h4><ul><li>Fix issue with next/previous comic calculation when server reports unknown</li></ul><h4>0.3 <small>March 18, 2016</small></h4><ul><li>Use navigation data for next and previous comic from the webservice to respect the "skip guest" and "skip non-canon" settings</li><li>Add "friend list" to cast info: Who\'s seen the most together with whom</li><li>Don\'t interfere with Firefox\' Alt+Left/Alt+Right navigation</li><li>Deal properly with the two special cases of no next or no previous comic from the web service.</li><li>Show different "friend list" messages for different kinds of items</li></ul><h4>0.2.1 <small>March 13, 2016</small></h4><ul><li>Fix issues with Firefox</li></ul><h4>0.2.0 <small>March 13, 2016</small></h4><ul><li>Add keyboard shortcuts to make edit mode easier to use</li><li>Position the "no image" elements more centrally</li><li>Better choice of word doesn\'t imply it\'s the last time we\'ll ever see cast members again</li><li>Add "tagline" support to the comic</li><li>Minor user interface improvements</li><li>Show proper error message dialog instead of just logging errors to console</li></ul><h4>0.1.2 <small>March 12, 2016</small></h4><ul><li>Show "Loading..." text when data for the next comic is loading rather than keep showing the old comic\'s news</li></ul><h4>0.1.0 <small>March 10, 2016</small></h4><ul><li>Initial release</li></ul></div><div class="modal-footer"><button class="btn btn-primary" type="button" ng-click="clvm.close()">Close</button></div></div></div></div>';
variables.html.comic = '<a ng-class="{\'hidden\': c.isInitializing}" ng-href="view.php?comic={{c.comicService.nextComic}}"><img id="strip" ng-src="{{c.comicImage}}"></a><div ng-class="{\'hidden\': c.isInitializing || !c.isLoading}" class="comic-loading-overlay"><div class="lds-dual-ring"></div><p>Loading comic {{c.comicService.comic}}...</p></div><qc-ribbon></qc-ribbon>';
variables.html.comicNav = '<div class="input-group" id="comicnavDir"><span class="input-group-addon" title="Comic #">#</span> <input id="comicnavDir_comic" type="number" class="form-control" aria-label="Comic" placeholder="Comic" ng-model="cn.currentComic" min="1" max="{{cn.latestComic}}" ng-keypress="cn.keyPress($event)"><div class="input-group-btn"><button id="comicnavDir_go" type="button" class="btn btn-default" ng-click="cn.go()" title="Navigate to the specified comic #">&nbsp;<i class="fa fa-arrow-right" aria-hidden="true"></i></button></div></div>';
variables.html.date = '<div class="row"><b ng-if="d.date"><span ng-if="d.settings.useCorrectTimeFormat">{{d.date | date:\'EEEE, MMMM d, yyyy HH:mm\' }}</span> <span ng-if="!d.settings.useCorrectTimeFormat">{{d.date | date:\'EEEE, MMMM d, yyyy h:mm a\' }}</span> <span ng-if="d.approximateDate">(Approximately)</span></b></div>';
variables.html.donut = '<div style="width: {{size}}px; height: {{size}}px; background-color: {{highlightColor}}" class="radial-progress"><div class="circle"><div style="width: {{size}}px; height: {{size}}px; clip: rect({{maskClip.top}}px, {{maskClip.right}}px, {{maskClip.bottom}}px, {{maskClip.left}}px); transform: rotate({{rotation}}deg)" class="mask full"><div style="width: {{size}}px; height: {{size}}px; clip: rect({{fillClip.top}}px, {{fillClip.right}}px, {{fillClip.bottom}}px, {{fillClip.left}}px); background-color: {{color}}; transform: rotate({{rotation}}deg)" class="fill"></div></div><div style="width: {{size}}px; height: {{size}}px; clip: rect({{maskClip.top}}px, {{maskClip.right}}px, {{maskClip.bottom}}px, {{maskClip.left}}px)" class="mask half"><div style="width: {{size}}px; height: {{size}}px; clip: rect({{fillClip.top}}px, {{fillClip.right}}px, {{fillClip.bottom}}px, {{fillClip.left}}px); background-color: {{color}}; transform: rotate({{rotation}}deg)" class="fill"></div><div style="width: {{size}}px; height: {{size}}px; clip: rect({{fillClip.top}}px, {{fillClip.right}}px, {{fillClip.bottom}}px, {{fillClip.left}}px); background-color: {{color}}; transform: rotate({{fixRotation}}deg)" class="fill fix"></div></div><div style="width: {{insetSize}}px; height: {{insetSize}}px; margin-left: {{insetMargin}}px; margin-top: {{insetMargin}}px; background-color: {{innerColor}}" class="inset"></div></div></div>';
variables.html.editComicData = '<div class="modal fade" tabindex="-1" role="dialog" id="editComicDataDialog" style="display: none"><div class="modal-dialog"><div class="modal-content pos-rel"><div class="modal-header"><h3 class="modal-title">Edit Comic Data for #{{ecdvm.editData.comicData.comic}}</h3></div><div class="modal-body"><div class="row"><div class="col-sm-6"><div class="panel panel-default"><div class="panel-heading"><h3 class="panel-title">Cast</h3></div><div class="panel-body"><ul class="list-group"><li class="list-group-item clearfix" ng-repeat="castMember in ecdvm.editData.cast">{{castMember.shortName}} <span class="pull-right button-group btn-group-xs"><button ng-click="ecdvm.remove(castMember)" type="button" class="btn btn-danger"><i class="fa fa-minus"></i> Remove</button></span></li></ul></div></div></div><div class="col-sm-6"><div class="panel panel-default"><div class="panel-heading"><h3 class="panel-title">Storylines</h3></div><div class="panel-body"><ul class="list-group"><li class="list-group-item clearfix" ng-repeat="location in ecdvm.editData.storyline">{{location.shortName}} <span class="pull-right button-group btn-group-xs"><button ng-click="ecdvm.remove(location)" type="button" class="btn btn-danger"><i class="fa fa-minus"></i> Remove</button></span></li></ul></div></div></div></div><div class="row"><div class="col-sm-6"><div class="panel panel-default"><div class="panel-heading"><h3 class="panel-title">Locations</h3></div><div class="panel-body"><ul class="list-group"><li class="list-group-item clearfix" ng-repeat="location in ecdvm.editData.location">{{location.shortName}} <span class="pull-right button-group btn-group-xs"><button ng-click="ecdvm.remove(location)" type="button" class="btn btn-danger"><i class="fa fa-minus"></i> Remove</button></span></li></ul></div></div></div><div class="col-sm-6"><div class="panel panel-default"><div class="panel-heading"><h3 class="panel-title">Flags</h3></div><div class="panel-body"><div class="form-group"><label><input type="checkbox" ng-model="ecdvm.editData.comicData.isGuestComic" ng-change="ecdvm.changeGuestComic()"> Guest comic</label></div><div class="form-group"><label><input type="checkbox" ng-model="ecdvm.editData.comicData.isNonCanon" ng-change="ecdvm.changeNonCanon()"> Non-canon</label></div><div class="form-group"><label><input type="checkbox" ng-model="ecdvm.editData.comicData.hasNoCast" ng-change="ecdvm.changeNoCast()"> Has no cast</label></div><div class="form-group"><label><input type="checkbox" ng-model="ecdvm.editData.comicData.hasNoLocation" ng-change="ecdvm.changeNoLocation()"> Has no location</label></div><div class="form-group"><label><input type="checkbox" ng-model="ecdvm.editData.comicData.hasNoStoryline" ng-change="ecdvm.changeNoStoryline()"> Has no storyline</label></div><div class="form-group"><label><input type="checkbox" ng-model="ecdvm.editData.comicData.hasNoTitle" ng-change="ecdvm.changeNoTitle()"> Has no title</label></div><div class="form-group"><label><input type="checkbox" ng-model="ecdvm.editData.comicData.hasNoTagline" ng-change="ecdvm.changeNoTagline()"> Has no tagline</label></div></div></div></div></div><div class="row"><qc-add-item is-updating="ecdvm.isUpdating"></qc-add-item></div><div class="row"><div class="panel panel-default m-t-20px"><div class="panel-heading"><h3 class="panel-title">Publish date</h3></div><div class="panel-body"><qc-set-publish-date is-updating="ecdvm.isUpdating"></qc-set-publish-date></div></div></div></div><div class="modal-footer"><button class="btn btn-primary" type="button" ng-click="ecdvm.close()">Close</button></div><div ng-show="ecdvm.isUpdating" class="update-overlay"><div class="absolute-dead-center"><div class="lds-dual-ring"></div><p class="update-text">Updating...</p></div></div></div></div></div>';
variables.html.editLog = '<div class="modal fade" tabindex="-1" role="dialog" id="editLogDialog" style="display: none"><div class="modal-dialog"><div class="modal-content"><div class="modal-header"><h3 class="modal-title">Edit Log</h3></div><div class="modal-body"><div class="row"><div class="col-sm-12"><div class="panel panel-default"><div class="panel-heading"><h3 class="panel-title">Log</h3></div><div class="panel-body pos-rel"><table><thead><tr><th>Identifier</th><th>Time</th><th>Action</th></tr></thead><tbody class="no-border"><tr ng-repeat="logEntry in elvm.logEntryData.logEntries"><td>{{logEntry.identifier}}</td><td>{{logEntry.dateTime | date:\'MMM d, yyyy HH:mm\'}}</td><td>{{logEntry.action}}</td></tr><tr><td class="edit-log-pager" colspan="3"><i class="fa fa-arrow-left p-r-10px" ng-click="elvm.previousPage()"></i> {{elvm.logEntryData.page}} / {{elvm.logEntryData.pageCount}} <i class="fa fa-arrow-right p-l-10px" ng-click="elvm.nextPage()"></i></td></tr></tbody></table><div ng-show="elvm.isLoading" class="update-overlay update-overlay-white"><div class="absolute-dead-center"><div class="lds-dual-ring black"></div><p class="update-text">Loading...</p></div></div></div></div></div></div></div><div class="modal-footer"><button class="btn btn-primary" type="button" ng-click="elvm.close()">Close</button></div></div></div></div>';
variables.html.extra = '<table id="qcnav_navbox"><thead><tr ng-if="e.constants.developmentMode"><th class="qcnav_centered">DEVELOPMENT MODE</th></tr><tr><td id="qcnav_status"><i ng-show="e.isLoading" class="fa fa-refresh fa-spin"></i> <i ng-show="e.hasWarning && !e.hasError" class="fa fa-exclamation-triangle"></i> <i ng-show="e.hasError" class="fa fa-exclamation-circle"></i></td></tr></thead><tbody id="qcnav_navbody"><tr ng-repeat="message in e.messages"><td>{{ message }}</td></tr><tr ng-if="e.showWelcomeMessage"><td><h1 class="update-header">Welcome to QC&nbsp;Extensions!</h1><a class="qc" ng-click="e.showChangeLog()">See change log!</a></td></tr><tr ng-if="e.showUpdateMessage"><td><h1 class="update-header">QC Extensions updated!</h1><a class="qc" ng-click="e.showChangeLog()">See what\'s new!</a></td></tr><tr ng-if="e.settings.editMode && e.editorData.missing.any"><td><h1 class="first-header">Comics with missing items</h1><qc-extra-nav ng-if="e.editorData.missing.cast.any" id="qcnav_item_missing_cast" first-title="First strip missing cast" first-value="e.editorData.missing.cast.first" previous-title="Previous strip missing cast" previous-value="e.editorData.missing.cast.previous" next-title="Next strip missing cast" next-value="e.editorData.missing.cast.next" last-title="Last strip missing cast" last-value="e.editorData.missing.cast.last" name="Comics missing cast" name-title="Comics missing cast"></qc-extra-nav><qc-extra-nav ng-if="e.editorData.missing.location.any" id="qcnav_item_missing_location" first-title="First strip missing locations" first-value="e.editorData.missing.location.first" previous-title="Previous strip missing locations" previous-value="e.editorData.missing.location.previous" next-title="Next strip missing locations" next-value="e.editorData.missing.location.next" last-title="Last strip missing locations" last-value="e.editorData.missing.location.last" name="Comics missing locations" name-title="Comics missing locations"></qc-extra-nav><qc-extra-nav ng-if="e.editorData.missing.storyline.any" id="qcnav_item_missing_storyline" first-title="First strip missing storylines" first-value="e.editorData.missing.storyline.first" previous-title="Previous strip missing storylines" previous-value="e.editorData.missing.storyline.previous" next-title="Next strip missing storylines" next-value="e.editorData.missing.storyline.next" last-title="Last strip missing storylines" last-value="e.editorData.missing.storyline.last" name="Comics missing storylines" name-title="Comics missing storylines"></qc-extra-nav><qc-extra-nav ng-if="e.editorData.missing.title.any" id="qcnav_item_missing_title" first-title="First strip missing title" first-value="e.editorData.missing.title.first" previous-title="Previous strip missing title" previous-value="e.editorData.missing.title.previous" next-title="Next strip missing title" next-value="e.editorData.missing.title.next" last-title="Last strip missing title" last-value="e.editorData.missing.title.last" name="Comics missing title" name-title="Comics missing title"></qc-extra-nav><qc-extra-nav ng-if="e.editorData.missing.tagline.any" id="qcnav_item_missing_tagline" first-title="First strip missing tagline" first-value="e.editorData.missing.tagline.first" previous-title="Previous strip missing tagline" previous-value="e.editorData.missing.tagline.previous" next-title="Next strip missing tagline" next-value="e.editorData.missing.tagline.next" last-title="Last strip missing tagline" last-value="e.editorData.missing.tagline.last" name="Comics missing tagline" name-title="Comics missing tagline"></qc-extra-nav></td></tr><tr ng-repeat="(type, typeItems) in e.items"><td><h1 ng-class="{\'first-header\': $first && !(e.settings.editMode && e.editorData.missing.any)}">{{e.getTypeDescription(type)}}</h1><qc-extra-nav ng-repeat="item in typeItems" id="{{\'qcnav_item_\' + item.id}}" first-title="{{\'First strip with \' + item.shortName}}" first-value="item.first" previous-title="{{\'Previous strip with \' + item.shortName}}" previous-value="item.previous" next-title="{{\'Next strip with \' + item.shortName}}" next-value="item.next" last-title="{{\'Last strip with \' + item.shortName}}" last-value="item.last" name="{{item.shortName}}" name-title="{{item.name}}" click-action="e.showDetailsFor(item)"></qc-extra-nav></td></tr><tr ng-if="e.settings.editMode && e.missingDataInfo.length > 0"><td><p class="qc-error-text">This comic is missing <span ng-repeat="mdi in e.missingDataInfo">{{mdi}}<span ng-if="!$last">,&nbsp;</span></span></p></td></tr><tr ng-if="e.settings.editMode"><td class="qcnav_name"><a class="qc" ng-click="e.editComicData()" ng-class="{\'disabled\': e.isLoading}"><i class="fa fa-pencil-square"></i> Edit comic data</a></td></tr><tr ng-if="e.settings.editMode"><td class="qcnav_name"><a class="qc" ng-click="e.showEditLog()"><i class="fa fa-list-alt"></i> See edit log</a></td></tr><tr><td class="qcnav_space"><button type="button" class="btn btn-default" ng-disabled="e.isLoading" ng-click="e.comicService.refreshComicData();">Refresh</button> <button type="button" class="btn btn-default" ng-click="e.openSettings()">Settings</button></td></tr><tr><td><qc-comic-nav></qc-comic-nav></td></tr><tr ng-if="e.settings.editMode"><td><qc-add-item is-updating="e.isUpdating"></qc-add-item></td></tr><tr ng-show="e.settings.editMode"><td><qc-set-title is-updating="e.isUpdating"></qc-set-title></td></tr><tr ng-show="e.settings.editMode"><td><qc-set-tagline is-updating="e.isUpdating"></qc-set-tagline></td></tr><tr ng-repeat="(type, typeItems) in e.allItems"><td><h1 ng-bind-html="e.getTypeDescription(\'all-\' + type)"></h1><qc-extra-nav ng-repeat="item in typeItems" id="{{\'qcnav_item_\' + item.id}}" first-title="{{\'First strip with \' + item.shortName}}" first-value="item.first" previous-title="{{\'Previous strip with \' + item.shortName}}" previous-value="item.previous" next-title="{{\'Next strip with \' + item.shortName}}" next-value="item.next" last-title="{{\'Last strip with \' + item.shortName}}" last-value="item.last" name="{{item.shortName}}" name-title="{{item.name}}" click-action="e.showDetailsFor(item)"></qc-extra-nav></td></tr></tbody></table>';
variables.html.extraNav = '<table class="qcnav_navelement" ng-class="{\'with_color\': $root.settings.useColors}"><tr><td class="qcnav_arrows" title="{{firstTitle}}" ng-switch="firstValue"><i ng-switch-when="null" class="fa fa-fast-backward no-nav"></i> <a class="qc" ng-switch-default ng-href="view.php?comic={{firstValue}}"><i class="fa fa-fast-backward"></i></a></td><td class="qcnav_arrows" title="{{previousTitle}}" ng-switch="previousValue"><i ng-switch-when="null" class="fa fa-backward no-nav"></i> <a class="qc" ng-switch-default ng-href="view.php?comic={{previousValue}}"><i class="fa fa-backward"></i></a></td><td class="qcnav_name" title="{{nameTitle}}"><a class="qcnav_name_link" ng-click="clickAction()">{{name}}</a></td><td class="qcnav_arrows" title="{{nextTitle}}" ng-switch="nextValue"><i ng-switch-when="null" class="fa fa-forward no-nav"></i> <a class="qc" ng-switch-default ng-href="view.php?comic={{nextValue}}"><i class="fa fa-forward"></i></a></td><td class="qcnav_arrows" title="{{lastTitle}}" ng-switch="lastValue"><i ng-switch-when="null" class="fa fa-fast-forward no-nav"></i> <a class="qc" ng-switch-default ng-href="view.php?comic={{lastValue}}"><i class="fa fa-fast-forward"></i></a></td></tr></table>';
variables.html.itemDetails = '<div class="modal fade" tabindex="-1" role="dialog" id="itemDetailsDialog" style="display: none"><div class="modal-dialog"><div class="modal-content" class="pos-rel"><div class="modal-header"><h3 class="modal-title"><span ng-if="idvm.isLoading > 0">Loading...</span> <span ng-if="idvm.isLoading == 0">{{idvm.itemData.name}}</span></h3></div><div class="modal-body" ng-if="idvm.isLoading > 0">Loading...</div><div class="modal-body" ng-if="idvm.isLoading == 0"><div class="clearfix"><div class="item-details-image"><i ng-if="!idvm.isImagePreview && !idvm.itemData.hasImage" class="fa fa-camera no-image-icon"></i><div ng-if="!idvm.isImagePreview && !idvm.itemData.hasImage" class="no-image-text">No image</div><img ng-if="!idvm.isImagePreview && idvm.itemData.hasImage" ng-src="{{idvm.imagePaths[idvm.currentImagePath]}}" alt="{{idvm.itemData.shortName}}"> <img ng-if="idvm.isImagePreview" ng-src="{{idvm.imageFile}}" alt="{{idvm.itemData.shortName}}"> <i ng-if="!idvm.isImagePreview && idvm.imagePaths.length > 1" class="fa fa-caret-left left-arrow" ng-click="idvm.previousImage()"></i><p ng-if="!idvm.isImagePreview && idvm.imagePaths.length > 1" class="item-details-pager">{{idvm.currentImagePath + 1}} / {{idvm.imagePaths.length}}</p><p ng-if="idvm.isImagePreview" class="item-details-pager point" ng-click="idvm.isImagePreview = false" title="Click to stop the preview">Preview</p><i ng-if="!idvm.isImagePreview && idvm.imagePaths.length > 1" class="fa fa-caret-right right-arrow" ng-click="idvm.nextImage()"></i></div><div class="item-details-presence"><donut ng-if="idvm.settings.useColors" size="128" color="#{{idvm.itemData.color}}" highlight-color="{{idvm.itemData.highlightColor}}" percent="{{idvm.itemData.presence}}" border-size="30" inner-color="#4e5d6c"></donut><donut ng-if="!idvm.settings.useColors" size="128" color="black" highlight-color="white" percent="{{idvm.itemData.presence}}" border-size="30" inner-color="#4e5d6c"></donut><div class="presence-text" title="{{idvm.itemData.shortName}} has appeared in {{idvm.itemData.appearances}} out of the {{idvm.itemData.totalComics}} comics ({{idvm.itemData.presence|number:2}}%)">{{idvm.itemData.presence|number:2}}%<br><span>({{idvm.itemData.appearances}}/{{idvm.itemData.totalComics}})</span></div></div><div ng-switch="idvm.itemData.type"><p ng-switch-default><span ng-if="idvm.settings.editMode"><b><i class="fa fa-id-card" aria-hidden="true" title="Item ID"></i></b> {{idvm.itemData.id}}<br></span><b>Full name:</b> {{idvm.itemData.name}}<br><b>Short name:</b> {{idvm.itemData.shortName}}<br><b>First appearance:</b> <a ng-click="idvm.goToComic(idvm.itemData.first)">Comic {{idvm.itemData.first}}</a><br><b>Latest appearance:</b> <a ng-click="idvm.goToComic(idvm.itemData.last)">Comic {{idvm.itemData.last}}</a><br><b>Number of appearances:</b> {{idvm.itemData.appearances}} ({{idvm.itemData.presence|number:2}}%)<br><b>Identifying color:</b> <span ng-if="idvm.settings.useColors" ng-style="{\'color\': idvm.itemData.color, \'background-color\': idvm.itemData.highlightColor}">#{{idvm.itemData.color}}</span><span ng-if="!idvm.settings.useColors">#{{idvm.itemData.color}}</span></p></div></div><hr class="item-details-hr"><div class="row"><div class="col-xs-6" ng-show="idvm.itemData.locations"><p ng-switch="idvm.itemData.type">Most often <span ng-switch-when="cast">spotted at</span><span ng-switch-when="location">visited simultaneously with</span><span ng-switch-default>involves</span>:</p><ul><li ng-repeat="location in idvm.itemData.locations"><a ng-click="idvm.showInfoFor(location.id)">{{location.shortName}}</a><span ng-if="idvm.settings.editMode"> (<i class="fa fa-id-card" aria-hidden="true" title="Item ID"></i> {{location.id}})</span> in {{location.count}} comics ({{location.percentage|number:2}}%)</li></ul></div><div class="col-xs-6" ng-show="idvm.itemData.friends"><p ng-switch="idvm.itemData.type">Most often <span ng-switch-when="cast">spotted with</span><span ng-switch-when="location">visited by</span><span ng-switch-default>involves</span>:</p><ul><li ng-repeat="friend in idvm.itemData.friends"><a ng-click="idvm.showInfoFor(friend.id)">{{friend.shortName}}</a><span ng-if="idvm.settings.editMode"> (<i class="fa fa-id-card" aria-hidden="true" title="Item ID"></i> {{friend.id}})</span> in {{friend.count}} comics ({{friend.percentage|number:2}}%)</li></ul></div></div><hr ng-if="idvm.settings.editMode" class="item-details-hr"><div ng-if="idvm.settings.editMode" class="row form-horizontal"><div class="col-xs-12"><div class="form-group"><label class="col-xs-3 control-label" for="edit_{{idvm.itemData.id}}_name">Name:</label><div class="col-xs-9"><div class="input-group"><input id="edit_{{idvm.itemData.id}}_name" type="text" ng-keypress="idvm.keyPress($event, \'name\')" ng-model="idvm.itemData.name" class="form-control" aria-label="Name"> <span class="input-group-btn"><button ng-click="idvm.update(\'name\')" type="button" class="btn btn-default">Update</button></span></div></div></div></div><div class="col-xs-12"><div class="form-group"><label class="col-xs-3 control-label" for="edit_{{idvm.itemData.id}}_shortName">Short name:</label><div class="col-xs-9"><div class="input-group"><input id="edit_{{idvm.itemData.id}}_shortName" type="text" ng-keypress="idvm.keyPress($event, \'shortName\')" ng-model="idvm.itemData.shortName" class="form-control" aria-label="Short name"> <span class="input-group-btn"><button ng-click="idvm.update(\'shortName\')" type="button" class="btn btn-default">Update</button></span></div></div></div></div><div class="col-xs-12"><div class="form-group"><label class="col-xs-3 control-label" for="edit_{{idvm.itemData.id}}_color">Color:</label><div class="col-xs-9"><div class="input-group"><input id="edit_{{idvm.itemData.id}}_color" type="text" ng-keypress="idvm.keyPress($event, \'color\')" ng-model="idvm.itemData.color" class="form-control" aria-label="Color"> <span class="input-group-btn"><button ng-click="idvm.update(\'color\')" type="button" class="btn btn-default">Update</button></span></div></div></div></div><div class="col-xs-12"><div class="form-group"><label class="col-xs-3 control-label" for="edit_{{idvm.itemData.id}}_image">Image:</label><div class="col-xs-9"><div class="input-group"><input id="edit_{{idvm.itemData.id}}_image" type="file" class="form-control" aria-label="Image" file-data="idvm.imageFile" file-info="idvm.imageFileInfo"> <span class="input-group-btn"><button ng-click="idvm.previewImage()" type="button" class="btn btn-default">Preview</button></span> <span class="input-group-btn"><button ng-click="idvm.uploadImage()" type="button" class="btn btn-default">Upload</button></span></div></div></div></div></div></div><div class="modal-footer"><button class="btn btn-primary" type="button" ng-click="idvm.close()">Close</button></div><div ng-show="idvm.isUpdating" class="update-overlay"><div class="absolute-dead-center"><div class="lds-dual-ring"></div><p class="update-text">Updating...</p></div></div></div></div></div>';
variables.html.navigation = '<ul class="menu" id="comicnav"><li><a href="view.php?comic=1" ng-click="n.first($event)">First</a></li><li><a ng-href="view.php?comic={{n.comicService.previousComic}}" ng-click="n.previous($event)">Previous</a></li><li><a ng-href="view.php?comic={{n.comicService.nextComic}}" ng-click="n.next($event)">Next</a></li><li><a href="view.php?comic={{n.comicService.latestComic}}" ng-click="n.last($event)">{{n.comicService.latestComic == n.comicService.comic ? \'Last\' : \'Latest\'}}</a></li><li><a ng-href="view.php?comic={{randomComic}}" ng-click="n.random($event)">Random</a></li></ul>';
variables.html.ribbon = '<div class="corner-ribbon top-right shadow" ng-class="{\n \'blue\': r.isGuestComic,\n \'red\': !r.isGuestComic && r.isNonCanon,\n \'small\': r.isSmall\n }" ng-show="r.settings.showIndicatorRibbon && (r.isGuestComic || r.isNonCanon)" ng-click="r.isSmall = !r.isSmall" title="This comic strip is {{r.isGuestComic ? \'a guest comic\' : \'non-canon\'}} (click to {{r.isSmall ? \'enlarge\' : \'shrink\'}} the ribbon)">{{r.isGuestComic ? \'Guest Comic\' : \'Non-Canon\'}}</div>';
variables.html.setPublishDate = '<div class="m-t-3px"><div class="form-group"><div class="input-group"><input ng-keypress="s.keyPress($event)" ng-model="s.publishDate" ng-disabled="isUpdating" id="setPublishDate_{{s.unique}}_field" type="datetime-local" class="form-control" aria-label="Set publish date" placeholder="Set publish date..."><div class="input-group-btn"><button ng-click="s.setPublishDate()" ng-disabled="isUpdating" type="button" class="btn btn-default">Set</button></div></div></div><div class="form-group"><label><input type="checkbox" ng-model="s.isAccuratePublishDate" ng-change="s.setAccuratePublishDate()" ng-disabled="isUpdating"> Accurate Date</label></div></div>';
variables.html.setTagline = '<div class="input-group" class="m-t-3px"><input ng-keypress="s.keyPress($event)" ng-model="s.tagline" ng-disabled="isUpdating" id="setTagline_{{s.unique}}_field" type="text" class="form-control" aria-label="Set tagline" placeholder="Set tagline..."><div class="input-group-btn"><button ng-click="s.setTagline()" type="button" class="btn btn-default" ng-disabled="isUpdating">Set</button></div></div>';
variables.html.setTitle = '<div class="input-group" class="m-t-3px"><input ng-keypress="s.keyPress($event)" ng-model="s.title" ng-disabled="isUpdating" id="setTitle_{{s.unique}}_field" type="text" class="form-control" aria-label="Set title" placeholder="Set title..."><div class="input-group-btn"><button ng-click="s.setTitle()" type="button" class="btn btn-default" ng-disabled="isUpdating">Set</button></div></div>';
variables.html.settings = '<div class="modal fade" tabindex="-1" role="dialog" id="settingsDialog" style="display: none"><div class="modal-dialog"><div class="modal-content"><div class="modal-header"><h3 class="modal-title">Settings</h3></div><div class="modal-body"><div class="row"><div class="col-sm-6"><div class="panel panel-default"><div class="panel-heading"><h3 class="panel-title">Navigation settings</h3></div><div class="panel-body"><div class="form-group"><label><input type="checkbox" ng-model="svm.settings.scrollToTop"> Scroll to top on navigate</label><p class="small">Scrolls the page to the top on each navigation event.</p></div><div class="form-group"><label><input type="checkbox" ng-model="svm.settings.showAllMembers"> Show all members</label><p class="small">Show every cast member, storyline and location, even if they are not part of the current comic. Makes no sense to enable for normal use, but can be useful if you always want to be able to find the next/previous comic of any character/storyline/location.</p></div><div class="form-group"><label><input type="checkbox" ng-model="svm.settings.skipNonCanon"> Skip non-canon strips</label><p class="small">Skips strips that are not part of the canon. This includes guest strips, as well as Yelling Bird, OMG Turkeys and other non-story comics.</p></div><div class="form-group"><label ng-class="{\'disabled\': svm.settings.skipNonCanon}"><input type="checkbox" ng-disabled="svm.settings.skipNonCanon" ng-model="svm.settings.skipGuest"> Skip guest strips</label><p ng-class="{\'disabled\': svm.settings.skipNonCanon}" class="small">Skips strips that were not made/drawn by Jeph.</p></div></div></div></div><div class="col-sm-6"><div class="panel panel-default"><div class="panel-heading"><h3 class="panel-title">Display settings</h3></div><div class="panel-body"><div class="form-group"><label><input type="checkbox" ng-model="svm.settings.useColors"> Use colors</label><p class="small">Each cast/storyline/location can be given a color. (Most non-main characters/storylines/locations will probably be plain gray.) With this setting enabled, those colors are used in the navigation pane.</p></div><div class="form-group"><label><input type="checkbox" ng-model="svm.settings.showIndicatorRibbon"> Show comic indicator ribbon</label><p class="small">If a comic strip has been marked as being non-canon or a guest strip, display a ribbon with that information over the top-right corner of the comic strip.</p></div><div class="form-group"><label ng-class="{\'disabled\': !svm.settings.showIndicatorRibbon}"><input type="checkbox" ng-model="svm.settings.showSmallRibbonByDefault"> Show small indicator ribbon by default</label><p class="small" ng-class="{\'disabled\': !svm.settings.showIndicatorRibbon}">The size of the ribbon can be changed by clicking on it. By default, it starts out in its large size, but with this setting enabled, the ribbon will start out in its small size by default instead.</p></div><div class="form-group"><label><input type="checkbox" ng-model="svm.settings.useCorrectTimeFormat"> Use 24h clock format</label><p class="small">When showing when a comic was published (above the news section), use the 24-hour clock format.</p></div><div class="form-group"><label for="comicLoadingIndicatorDelay">Comic loading indicator delay</label><input id="comicLoadingIndicatorDelay" class="form-control settings-input" type="number" ng-model="svm.settings.comicLoadingIndicatorDelay" placeholder="Comic Loading Indicator Delay"><p class="small">How long to wait in milliseconds for the next comic to load before showing a loading indicator over the previous comic.</p></div></div></div></div><div class="col-sm-6"><div class="panel panel-default"><div class="panel-heading"><h3 class="panel-title">Advanced settings</h3></div><div class="panel-body"><div class="form-group"><label><input type="checkbox" ng-model="svm.settings.showDebugLogs"> Enable debug logs in console</label><p class="small">Print debugging information in the Javascript console. Useful for, well, debugging. Changing the setting requires a page refresh to take effect.</p></div><div class="form-group"><label><input type="checkbox" ng-model="svm.settings.editMode"> Enable editor mode</label><input class="form-control settings-input" type="text" ng-model="svm.settings.editModeToken" ng-disabled="!svm.settings.editMode" placeholder="Editor token"><p ng-class="{\'disabled\': !svm.settings.editMode}" class="small">Enables features for creating and changing the navigation data, such as adding cast members. Requires a valid editor token. If you are supposed to have one, you do. Feel free to turn on edit mode regardless if you are curious what it is like. You simply will not be able to save any changes you make.</p></div></div></div></div></div></div><div class="modal-footer"><a class="qc p-r-20px" ng-click="svm.showChangeLog()">See change log</a> <button class="btn btn-primary" type="button" ng-click="svm.close()">Close</button></div></div></div></div>';
/*
* Copyright (C) 2016-2019 Alexander Krivács Schrøder <alexschrod@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
// as well completely replace it rather than decorate it...
// http://www.bennadel.com/blog/
// 2927-overriding-core-and-custom-services-in-angularjs.htm
var decorateHttpService = function ($provide) {
// Let's take over $http and make it use Greasemonkey's cross-domain
// XMLHTTPRequests instead of the browser's.
$provide.decorator('$http', function () {
// START Code bits borrowed from angular
// (see angular's license for details)
const APPLICATION_JSON = 'application/json';
const JSON_START = /^\[|^\{(?!\{)/;
const JSON_ENDS = {
'[': /]$/,
'{': /}$/
};
const JSON_PROTECTION_PREFIX = /^\)\]\}',?\n/;
const DEFAULT_HEADERS = {
Accept: APPLICATION_JSON,
'X-QCExt-Version': GM.info.script.version
};
function isJsonLike(str) {
const jsonStart = str.match(JSON_START);
return jsonStart && JSON_ENDS[jsonStart[0]].test(str);
}
function isString(value) {
return typeof value === 'string';
}
function fromJson(json) {
return isString(json) ? JSON.parse(json) : json;
}
function defaultHttpResponseTransform(data, headers) {
if (!isString(data)) {
return data;
} // Strip json vulnerability protection prefix
// and trim whitespace
const tempData = data.replace(JSON_PROTECTION_PREFIX, '').trim();
if (!tempData) {
return data;
}
const contentType = headers('Content-Type');
if (contentType && contentType.indexOf(APPLICATION_JSON) === 0 || isJsonLike(tempData)) {
data = fromJson(tempData);
}
return data;
} // END Code bits borrowed from angular
function getHeaderFunction(headers) {
const keyedHeaders = {};
angular.forEach(headers, function (value) {
const splitValue = value.trim().split(':', 2);
if (splitValue.length < 2) {
return;
}
keyedHeaders[splitValue[0].trim()] = splitValue[1].trim();
});
return function (key) {
return keyedHeaders[key] || null;
};
}
const injector = angular.injector(['ng']);
const $q = injector.get('$q');
const ourHttp = {
get: function (url, config) {
config = config || {};
let headers = DEFAULT_HEADERS;
if (config.headers) {
headers = jQuery.extend({}, DEFAULT_HEADERS, config.headers);
}
return $q(function (resolve, reject) {
GM.xmlHttpRequest({
method: 'GET',
url: url,
headers: headers,
onload: function (gmResponse) {
const headers = getHeaderFunction(gmResponse.responseHeaders.split('\n'));
let responseData = gmResponse.response;
responseData = defaultHttpResponseTransform(responseData, headers);
const response = {
data: responseData,
status: gmResponse.status,
headers: headers,
config: config,
statusText: gmResponse.statusText
};
resolve(response);
},
onerror: function (gmResponse) {
const headers = getHeaderFunction(gmResponse.responseHeaders.split('\n'));
let responseData = gmResponse.response;
responseData = defaultHttpResponseTransform(responseData, headers);
const response = {
data: responseData,
status: gmResponse.status,
headers: headers,
config: config,
statusText: gmResponse.statusText
};
reject(response);
}
});
});
},
post: function (url, data, config) {
config = config || {};
const contentType = 'contentType' in config ? config.contentType : APPLICATION_JSON;
const dataTransform = 'dataTransform' in config ? config.dataTransform : d => JSON.stringify(d);
let headers = DEFAULT_HEADERS;
if (config.headers) {
headers = jQuery.extend({}, DEFAULT_HEADERS, config.headers);
}
if (contentType) {
headers['Content-Type'] = contentType;
}
return $q(function (resolve, reject) {
GM.xmlHttpRequest({
method: 'POST',
url: url,
data: dataTransform(data),
headers,
onload: function (gmResponse) {
const headers = getHeaderFunction(gmResponse.responseHeaders.split('\n'));
let responseData = gmResponse.response;
responseData = defaultHttpResponseTransform(responseData, headers);
const response = {
data: responseData,
status: gmResponse.status,
headers: headers,
config: config,
statusText: gmResponse.statusText
};
resolve(response);
},
onerror: function (gmResponse) {
const headers = getHeaderFunction(gmResponse.responseHeaders.split('\n'));
let responseData = gmResponse.response;
responseData = defaultHttpResponseTransform(responseData, headers);
const response = {
data: responseData,
status: gmResponse.status,
headers: headers,
config: config,
statusText: gmResponse.statusText
};
reject(response);
}
});
});
}
};
/* Methods/properties to implement for full compatibility:
* pendingRequests
* delete
* head
* jsonp
* post
* put
* patch
* defaults
*/
return ourHttp;
});
};
/*
* Copyright (C) 2016-2019 Alexander Krivács Schrøder <alexschrod@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
var decorateScope = function ($provide) {
$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;
}]);
};
/*
* Copyright (C) 2016-2019 Alexander Krivács Schrøder <alexschrod@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
var config = function (app) {
// 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: '^/$',
controller: 'comicController',
controllerAs: 'c',
template: '<div></div>'
}).state('view', {
url: '^/view.php?comic',
controller: 'comicController',
controllerAs: 'c',
template: '<div></div>'
});
$urlRouterProvider.otherwise(function ($injector, $location) {
GM.openInTab($location.$$absUrl, false);
});
$locationProvider.html5Mode(true);
$logProvider.debugEnabled(settings.values.showDebugLogs);
}]);
};
/*
* Copyright (C) 2016-2019 Alexander Krivács Schrøder <alexschrod@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
var run = function (app) {
app.run(['$rootScope', 'comicService', 'startComic', function ($rootScope, comicService, startComic) {
$rootScope.settings = settings;
comicService.gotoComic(startComic);
}]);
};
/*
* Copyright (C) 2016-2019 Alexander Krivács Schrøder <alexschrod@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
var bodyController = function (app) {
app.controller('bodyController', ['$log', '$scope', 'comicService', function ($log, $scope, comicService) {
$log.debug('START bodyController()');
const isStupidFox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
function previous() {
$scope.$apply(function () {
comicService.previous();
});
}
function next() {
$scope.$apply(function () {
comicService.next();
});
}
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) {
const shortcutRemove = window.eval('window.shortcut.remove').bind(shortcut);
shortcutRemove('Left');
shortcutRemove('Right'); // This is a sort of replacement for "shortcut". Only supports
// simple Left/Right navigation. Is missing the editor mode
// shortcuts because Firefox is behaving like shit.
window.addEventListener('keydown', function (event) {
// Only if no special keys are held down
if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) {
return;
}
if (event.keyCode === 37) {
// LEFT
previous();
} else if (event.keyCode === 39) {
// RIGHT
next();
}
}, false);
} else {
// See how nice it can be done when your browser doesn't
// actively try to sabotage you?
shortcut.remove('Left');
shortcut.remove('Right');
shortcut.add('Left', previous, {
disable_in_input: true
});
shortcut.add('Ctrl+Left', previous);
shortcut.add('Right', next, {
disable_in_input: true
});
shortcut.add('Ctrl+Right', next);
shortcut.add('Q', function () {
if (settings.values.editMode) {
$('input[id^="addItem"]').focus();
}
}, {
disable_in_input: true
});
}
$log.debug('END bodyController()');
}]);
};
/*
* Copyright (C) 2016-2019 Alexander Krivács Schrøder <alexschrod@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
var comicController = function (app) {
app.controller('comicController', ['$log', 'comicService', function ($log, comicService) {
$log.debug('START comicController()');
this.comicService = comicService;
$log.debug('END comicController()');
}]);
};
/*
* Copyright (C) 2016-2019 Alexander Krivács Schrøder <alexschrod@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
class EventHandlingControllerBase {
constructor($scope, 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();
});
});
eventService.itemDataLoadingEvent.subscribe($scope, (event, data) => {
$scope.safeApply(() => {
this._itemDataLoading();
});
});
eventService.itemDataLoadedEvent.subscribe($scope, (event, itemData) => {
$scope.safeApply(() => {
this._itemDataLoaded(itemData);
});
});
eventService.itemDataErrorEvent.subscribe($scope, (event, error) => {
$scope.safeApply(() => {
this._itemDataError(error);
});
});
eventService.maintenanceEvent.subscribe($scope, (event, error) => {
$scope.safeApply(() => {
this._maintenance();
});
});
}
_comicDataLoading(comic) {}
_comicDataLoaded(comicData) {}
_comicDataError(error) {}
_itemsChanged() {}
_itemDataLoading() {}
_itemDataLoaded(itemData) {}
_itemDataError(error) {}
_maintenance() {}
}
class SetValueControllerBase extends EventHandlingControllerBase {
constructor($scope, comicService, eventService) {
super($scope, eventService);
this.comicService = comicService;
this.unique = Math.random().toString(36).slice(-5);
}
_updateValue() {}
keyPress(event) {
if (event.keyCode === 13) {
// ENTER key
this._updateValue();
}
}
}
/*
* Copyright (C) 2016-2019 Alexander Krivács Schrøder <alexschrod@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
class TitleController extends EventHandlingControllerBase {
constructor($scope, $log, eventService) {
$log.debug('START TitleController');
super($scope, eventService);
this.$log = $log;
this.title = 'Loading Questionable Content Extension...';
$log.debug('END TitleController');
}
_comicDataLoading(comic) {
this.title = `Loading #${comic} — Questionable Content`;
}
_comicDataLoaded(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'];
var titleController = function (app) {
app.controller('titleController', TitleController);
};
/*
* Copyright (C) 2016-2019 Alexander Krivács Schrøder <alexschrod@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
// Parts based on code from:
// http://axonflux.com/handy-rgb-to-hsl-and-rgb-to-hsv-color-model-c
function hue2rgb(p, q, t) {
if (t < 0) {
t += 1;
}
if (t > 1) {
t -= 1;
}
if (t < 1 / 6) {
return p + (q - p) * 6 * t;
}
if (t < 1 / 2) {
return q;
}
if (t < 2 / 3) {
return p + (q - p) * (2 / 3 - t) * 6;
}
return p;
}
class ColorService {
constructor($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, g, b) {
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, s, l) {
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, g, b) {
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, s, v) {
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) {
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, g, b) {
return '#' + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
}
getRgbRelativeLuminance(r, g, b) {
return 0.2126 * r + 0.7152 * g + 0.0722 * b; // Per ITU-R BT.709
}
createTintOrShade(hexColor, iterations) {
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;
}
}
rgb = this.hslToRgb(hsl[0], hsl[1], hsl[2]);
return this.rgbToHexColor(rgb[0], rgb[1], rgb[2]);
}
}
var colorService = function (app) {
app.service('colorService', ['$log', function ($log) {
$log.debug('START colorService()');
const colorService = new ColorService($log);
$log.debug('END colorService()');
return colorService;
}]);
};
/*
* Copyright (C) 2016-2019 Alexander Krivács Schrøder <alexschrod@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
class ComicService {
constructor($log, $stateParams, $location, $scope, $http, latestComic, eventService, colorService, styleService, 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) {
jQuery(window).scrollTop(0);
}
} // TODO: Add proper response type
_onErrorLog(response) {
if (response.status !== 503) {
this.messageReportingService.reportError(response.data);
} else {
this.eventService.maintenanceEvent.publish();
}
return response;
} // TODO: Add proper response type
_onSuccessRefreshElseErrorLog(response) {
if (response.status === 200) {
this.refreshComicData();
} else {
this._onErrorLog(response);
}
return response;
}
_fixItem(item) {
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 = jQuery.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.maintenanceEvent.publish(response);
return;
}
if (response.status !== 200) {
this._onErrorLog(response);
this.eventService.comicDataErrorEvent.publish(response);
return;
}
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 {
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));
}
}
comicData.comic = this.comic;
this.comicData = comicData;
this.eventService.comicDataLoadedEvent.publish(this.comicData);
}).catch(errorResponse => {
this._onErrorLog(errorResponse);
this.eventService.comicDataErrorEvent.publish(errorResponse);
});
}
addItemToComic(item) {
const data = {
token: settings.values.editModeToken,
comicId: this.comic,
itemId: item.id
};
if (item.id === -1) {
data.newItemName = item.name;
data.newItemType = item.type;
}
return this.$http.post(constants.addItemToComicUrl, data).then(r => this._onSuccessRefreshElseErrorLog(r)).catch(r => this._onErrorLog(r));
}
removeItem(item) {
const data = {
token: settings.values.editModeToken,
comicId: this.comic,
itemId: item.id
};
return this.$http.post(constants.removeItemFromComicUrl, data).then(r => this._onSuccessRefreshElseErrorLog(r)).catch(r => this._onErrorLog(r));
}
setTitle(title) {
const data = {
token: settings.values.editModeToken,
comicId: this.comic,
title: title
};
return this.$http.post(constants.setComicTitleUrl, data).then(r => this._onSuccessRefreshElseErrorLog(r)).catch(r => this._onErrorLog(r));
}
setTagline(tagline) {
const data = {
token: settings.values.editModeToken,
comicId: this.comic,
tagline: tagline
};
return this.$http.post(constants.setComicTaglineUrl, data).then(r => this._onSuccessRefreshElseErrorLog(r)).catch(r => this._onErrorLog(r));
}
setPublishDate(publishDate, isAccurate) {
const data = {
token: settings.values.editModeToken,
comicId: 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) {
const data = {
token: settings.values.editModeToken,
comicId: this.comic,
flagValue: value
};
return this.$http.post(constants.setGuestComicUrl, data).then(r => this._onSuccessRefreshElseErrorLog(r)).catch(r => this._onErrorLog(r));
}
setNonCanon(value) {
const data = {
token: settings.values.editModeToken,
comicId: this.comic,
flagValue: value
};
return this.$http.post(constants.setNonCanonUrl, data).then(r => this._onSuccessRefreshElseErrorLog(r)).catch(r => this._onErrorLog(r));
}
setNoCast(value) {
const data = {
token: settings.values.editModeToken,
comicId: this.comic,
flagValue: value
};
return this.$http.post(constants.setNoCastUrl, data).then(r => this._onSuccessRefreshElseErrorLog(r)).catch(r => this._onErrorLog(r));
}
setNoLocation(value) {
const data = {
token: settings.values.editModeToken,
comicId: this.comic,
flagValue: value
};
return this.$http.post(constants.setNoLocationUrl, data).then(r => this._onSuccessRefreshElseErrorLog(r)).catch(r => this._onErrorLog(r));
}
setNoStoryline(value) {
const data = {
token: settings.values.editModeToken,
comicId: this.comic,
flagValue: value
};
return this.$http.post(constants.setNoStorylineUrl, data).then(r => this._onSuccessRefreshElseErrorLog(r)).catch(r => this._onErrorLog(r));
}
setNoTitle(value) {
const data = {
token: settings.values.editModeToken,
comicId: this.comic,
flagValue: value
};
return this.$http.post(constants.setNoTitleUrl, data).then(r => this._onSuccessRefreshElseErrorLog(r)).catch(r => this._onErrorLog(r));
}
setNoTagline(value) {
const data = {
token: settings.values.editModeToken,
comicId: this.comic,
flagValue: value
};
return this.$http.post(constants.setNoTaglineUrl, data).then(r => this._onSuccessRefreshElseErrorLog(r)).catch(r => this._onErrorLog(r));
}
gotoComic(comicNo) {
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);
}
nextRandomComic() {
let randomComic = this.comic;
while (randomComic == this.comic) {
randomComic = Math.floor(Math.random() * (parseInt(this.latestComic) + 1));
}
return randomComic;
}
async nextFilteredRandomComic() {
if (!settings.values.skipGuest && !settings.values.skipNonCanon) {
return this.nextRandomComic();
}
const urlParameters = {};
if (settings.values.skipGuest) {
urlParameters.exclusion = 'guest';
} else if (settings.values.skipNonCanon) {
urlParameters.exclusion = 'non-canon';
}
const urlQuery = jQuery.param(urlParameters);
const excludedComicsUrl = constants.excludedComicsUrl + '?' + urlQuery;
const exclusionResponse = await this.$http.get(excludedComicsUrl);
const excludedComics = exclusionResponse.data.map(c => c.comic);
if (exclusionResponse.status !== 200) {
return this.nextRandomComic();
}
this.$log.debug('comicService::nextFilteredRandomComic() excluded comics', excludedComics);
let nextRandomComic = this.comic;
while (nextRandomComic == this.comic || excludedComics.includes(nextRandomComic)) {
nextRandomComic = this.nextRandomComic();
}
return nextRandomComic;
}
}
var comicService = function (app) {
app.service('comicService', ['$log', '$stateParams', '$location', '$rootScope', '$http', 'latestComic', 'eventService', 'colorService', 'styleService', 'messageReportingService', function ($log, $stateParams, $location, $scope, $http, latestComic, eventService, colorService, styleService, 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;
}]);
};
/*
* Copyright (C) 2016-2019 Alexander Krivács Schrøder <alexschrod@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
class ItemService {
constructor($log, $scope, $http, eventService, messageReportingService, colorService) {
this.$log = $log;
this.$scope = $scope;
this.$http = $http;
this.eventService = eventService;
this.messageReportingService = messageReportingService;
this.colorService = colorService;
eventService.itemsChangedEvent.subscribe($scope, (event, data) => {
this.refreshItemData();
});
this.refreshItemData();
}
async _loadItemData() {
if (this.isLoading) {
return;
}
this.isLoading = true;
this.eventService.itemDataLoadingEvent.publish();
const response = await this.$http.get(constants.itemDataUrl);
let itemData = [];
if (response.status === 200) {
itemData = response.data;
this.eventService.itemDataLoadedEvent.publish(itemData);
} else {
if (response.status === 503) {
this.eventService.maintenanceEvent.publish();
} else {
this.eventService.itemDataErrorEvent.publish(response);
this.messageReportingService.reportError(response.data);
}
}
this.itemData = itemData;
this.isLoading = false;
}
refreshItemData() {
this._loadItemData();
}
async getItemData(itemId) {
const itemDataResponse = await this.$http.get(constants.itemDataUrl + itemId);
if (itemDataResponse.status === 200) {
const itemData = itemDataResponse.data;
itemData.highlightColor = this.colorService.createTintOrShade(itemData.color);
const friendDataRequest = this.$http.get(constants.itemFriendDataUrl + itemId);
const locationDataRequest = this.$http.get(constants.itemLocationDataUrl + itemId);
let imageDataRequest = null;
if (itemData.hasImage) {
imageDataRequest = this.$http.get(constants.itemDataUrl + itemId + '/images');
}
const handleRelationData = response => {
if (response.status === 200) {
const relationData = response.data;
jQuery.each(relationData, (_, relation) => {
relation.percentage = relation.count / itemData.appearances * 100;
});
return relationData;
}
return null;
};
const [friendDataResponse, locationDataResponse, imageDataResponse] = await Promise.all([friendDataRequest, locationDataRequest, imageDataRequest]);
itemData.friends = handleRelationData(friendDataResponse) || [];
itemData.locations = handleRelationData(locationDataResponse) || [];
if (imageDataResponse) {
if (imageDataResponse.status === 200) {
let images = imageDataResponse.data;
let imageUrls = [];
jQuery.each(images, (_, image) => {
imageUrls.push(constants.itemImageUrl + image.id);
});
itemData.imageUrls = imageUrls;
}
}
return itemData;
} else {
if (itemDataResponse.status === 503) {
this.eventService.maintenanceEvent.publish();
} else {
this.messageReportingService.reportError(itemDataResponse.data);
}
return null;
}
}
async updateProperty(id, property, value) {
const data = {
token: settings.values.editModeToken,
item: id,
property: property,
value: value
};
try {
const response = await this.$http.post(constants.setItemDataPropertyUrl, data);
if (response.status === 200) {
return true;
} else {
if (response.status === 503) {
this.eventService.maintenanceEvent.publish();
} else {
this.messageReportingService.reportError(response.data);
}
return false;
}
} catch (r) {
this.messageReportingService.reportError(r.data);
return false;
}
}
async uploadImage(itemId, imageBlob, fileName) {
const formData = new FormData();
formData.append('ItemId', String(itemId));
formData.append('Image', imageBlob, fileName);
formData.append('Token', settings.values.editModeToken);
const response = await this.$http.post(constants.itemDataUrl + 'image/upload', formData, {
contentType: undefined,
dataTransform: d => d
});
if (response.status === 200) {
return true;
} else {
if (response.status === 503) {
this.eventService.maintenanceEvent.publish();
} else {
this.messageReportingService.reportError(response.data);
}
return false;
}
}
}
var itemService = function (app) {
app.service('itemService', ['$log', '$rootScope', '$http', 'eventService', 'messageReportingService', 'colorService', function ($log, $scope, $http, eventService, messageReportingService, colorService) {
$log.debug('START itemService()');
const itemService = new ItemService($log, $scope, $http, eventService, messageReportingService, colorService);
$log.debug('END itemService()');
return itemService;
}]);
};
/*
* Copyright (C) 2016-2019 Alexander Krivács Schrøder <alexschrod@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
class Event {
constructor(eventName) {
this.eventName = eventName;
} //$FlowFixMe when Flow properly supports generics
subscribe(scope, callback) {
const handle = Event.$rootScope.$on(this.eventName, callback);
scope.$on('$destroy', handle);
}
publish(data) {
const eventData = [this.eventName, null];
if (data != null) {
eventData[1] = data;
}
Event.$log.debug('Event data: ', eventData);
Event.$rootScope.$emit.apply(Event.$rootScope, eventData);
}
}
var eventFactory = function (app) {
app.factory('eventFactory', ['$rootScope', '$log', function ($rootScope, $log) {
$log.debug('START eventFactory()');
Event.$rootScope = $rootScope;
Event.$log = $log;
$log.debug('END eventFactory()');
return eventName => new Event(eventName);
}]);
};
/*
* Copyright (C) 2016-2019 Alexander Krivács Schrøder <alexschrod@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
class EventService {
constructor($log, 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.itemDataLoadingEvent = eventFactory(constants.itemdataLoadingEvent);
this.itemDataLoadedEvent = eventFactory(constants.itemdataLoadedEvent);
this.itemDataErrorEvent = eventFactory(constants.itemdataErrorEvent); // TODO: Figure out this type
this.itemsChangedEvent = eventFactory(constants.itemsChangedEvent);
this.maintenanceEvent = eventFactory(constants.maintenanceEvent);
}
}
var eventService = function (app) {
app.service('eventService', ['$log', 'eventFactory', function ($log, eventFactory) {
$log.debug('START eventService()');
const eventService = new EventService($log, eventFactory);
$log.debug('END eventService()');
return eventService;
}]);
};
/*
* Copyright (C) 2016-2019 Alexander Krivács Schrøder <alexschrod@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
function escapeHtml(text) {
return text.replace(/["&'/<>]/g, function (a) {
return {
'"': '&quot;',
'&': '&amp;',
'\'': '&#39;',
'/': '&#47;',
'<': '&lt;',
'>': '&gt;'
}[a];
});
}
class MessageReportingService {
constructor($log, $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 = '<div class="alert alert-' + nextMessage.type + '" ' + 'id="' + unique + '" ' + 'style="display: none;" ' + 'role="alert">' + escapeHtml(nextMessage.message) + '</div>';
jQuery('#messageSeat').append(messageHtml);
const messageElement = jQuery('#' + unique);
messageElement.slideDown();
const removeMessage = () => {
messageElement.slideUp(() => {
messageElement.remove();
this._processMessages();
});
};
const timeoutHandle = this.$timeout(removeMessage, 5000, false);
messageElement.click(() => {
this.$timeout.cancel(timeoutHandle);
removeMessage();
});
}
reportMessage(type, message) {
this.messageQueue.push({
type: type,
message: message
});
if (!this.isProcessing) {
this._processMessages();
}
}
reportError(message) {
this.reportMessage('danger', message);
}
reportWarning(message) {
this.reportMessage('warning', message);
}
}
var messageReportingService = function (app) {
app.service('messageReportingService', ['$log', '$timeout', function ($log, $timeout) {
$log.debug('START messageReportingService()');
const messageReportingService = new MessageReportingService($log, $timeout);
$log.debug('END messageReportingService()');
return messageReportingService;
}]);
};
/*
* Copyright (C) 2016-2019 Alexander Krivács Schrøder <alexschrod@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
function addStyle$1(style) {
const styleElement = jQuery('<style type="text/css">' + style + '</style>');
jQuery('head').append(styleElement);
return styleElement;
}
class StyleService {
constructor($log, colorService) {
this.$log = $log;
this.colorService = colorService;
this.customStyles = {};
this.customStyleElements = {};
}
_hasCustomStyle(key) {
return key in this.customStyles;
}
_addCustomStyle(key, style) {
if (this._hasCustomStyle(key)) {
return;
}
const styleElement = addStyle$1(style);
this.customStyles[key] = style;
this.customStyleElements[key] = styleElement;
}
_removeCustomStyle(key) {
delete this.customStyles[key];
this.customStyleElements[key].remove();
delete this.customStyleElements[key];
}
addItemStyle(id, color) {
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) {
const itemId = 'item_' + id;
if (this._hasCustomStyle(itemId)) {
this._removeCustomStyle(itemId);
}
}
hasItemStyle(id) {
const itemId = 'item_' + id;
return this._hasCustomStyle(itemId);
}
}
var styleService = function (app) {
app.service('styleService', ['$log', 'colorService', function ($log, colorService) {
$log.debug('START styleService()');
const styleService = new StyleService($log, colorService);
$log.debug('END styleService()');
return styleService;
}]);
};
/*
* Copyright (C) 2016-2019 Alexander Krivács Schrøder <alexschrod@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
var donutDirective = function (app) {
app.directive('donut', function () {
return {
restrict: 'E',
scope: {
size: '@',
innerColor: '@',
color: '@',
highlightColor: '@',
percent: '@',
borderSize: '@'
},
controller: ['$scope', function ($scope) {
function calculateRotationValues() {
$scope.rotation = $scope.percent / 100 * 180;
$scope.fixRotation = $scope.rotation * 2;
}
function calculateSizeValues() {
$scope.maskClip = {
top: 0,
right: $scope.size,
bottom: $scope.size,
left: $scope.size / 2
};
$scope.fillClip = {
top: 0,
right: $scope.size / 2,
bottom: $scope.size,
left: 0
};
$scope.insetSize = $scope.size - $scope.borderSize;
$scope.insetMargin = $scope.borderSize / 2;
}
calculateRotationValues();
calculateSizeValues();
$scope.$watch('percent', calculateRotationValues);
$scope.$watch('size', calculateSizeValues);
$scope.$watch('borderSize', calculateSizeValues);
}],
template: variables.html.donut
};
});
};
/*
* Copyright (C) 2016-2019 Alexander Krivács Schrøder <alexschrod@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
var fileDataDirective = function (app) {
app.directive('fileData', function () {
return {
restrict: 'A',
scope: {
fileData: '=',
fileInfo: '='
},
link: function (scope, element, attrs) {
element.bind('change', function (changeEvent) {
const fileReader = new FileReader();
fileReader.onload = function (loadEvent) {
scope.$apply(function () {
scope.fileInfo = changeEvent.target.files[0];
scope.fileData = loadEvent.target.result;
});
};
fileReader.readAsDataURL(changeEvent.target.files[0]);
});
}
};
});
};
/*
* Copyright (C) 2016-2019 Alexander Krivács Schrøder <alexschrod@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
var onErrorDirective = function (app) {
app.directive('onError', function () {
return {
restrict: 'A',
link: function (scope, element, attrs) {
element.bind('error', function () {
scope.$apply(attrs.onError);
});
}
};
});
};
/*
* Copyright (C) 2016-2019 Alexander Krivács Schrøder <alexschrod@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
const newItemId = -1;
const maintenanceId = -2;
const errorId = -3;
const addCastTemplate = 'Add new cast member';
const addCastItem = {
id: newItemId,
type: 'cast',
shortName: `${addCastTemplate} ''`,
name: ''
};
const addStorylineTemplate = 'Add new storyline';
const addStorylineItem = {
id: newItemId,
type: 'storyline',
shortName: `${addStorylineTemplate} ''`,
name: ''
};
const addLocationTemplate = 'Add new location';
const addLocationItem = {
id: newItemId,
type: 'location',
shortName: `${addLocationTemplate} ''`,
name: ''
};
const maintenanceItem = {
id: maintenanceId,
type: 'cast',
shortName: 'Maintenance ongoing. Choose this to attempt refresh.',
name: ''
};
const errorItem = {
id: errorId,
type: 'cast',
shortName: 'Error loading item list. Choose this to attempt refresh.',
name: ''
};
function escapeRegExp(s) {
return s.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
}
let triggeredFocus = false;
let dropdownOpen = false;
let firstRun = true;
class AddItemController extends SetValueControllerBase {
constructor($scope, $log, comicService, eventService, itemService, $timeout, $filter, messageReportingService) {
$log.debug('START AddItemController');
super($scope, comicService, eventService);
this.$log = $log;
this.$timeout = $timeout;
this.$filter = $filter;
this.itemService = itemService;
this.messageReportingService = messageReportingService;
this.searchFieldId = `#addItem_${this.unique}_search`;
this.dropdownId = `#addItem_${this.unique}_dropdown`;
this.dropdownButtonId = `#addItem_${this.unique}_dropdownButton`;
this.items = [];
this.itemFilterText = '';
this.itemFilter = this.itemFilter.bind(this);
$log.debug('END AddItemController');
}
_itemDataLoaded(itemData) {
itemData = itemData.slice(0);
itemData.push(addCastItem);
itemData.push(addStorylineItem);
itemData.push(addLocationItem);
this.$scope.safeApply(() => {
this.items = itemData;
});
}
_itemDataError(error) {
this.items.length = 0;
this.items.push(errorItem);
}
_maintenance() {
this.items.length = 0;
this.items.push(maintenanceItem);
}
_updateValue() {
// Add the top item in the list
const filteredList = this.$filter('filter')(this.items, this.itemFilter);
const chosenItem = filteredList[0];
this.addItem(chosenItem);
}
_comicDataLoaded(comicData) {
this.itemFilterText = '';
this.$scope.isUpdating = false;
}
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) {
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).dropdown('toggle');
}
}, 150);
}
}
async addItem(item) {
if (item.id == maintenanceId || item.id == errorId) {
this.itemService.refreshItemData();
return;
}
this.$scope.isUpdating = true;
const response = await this.comicService.addItemToComic(item);
if (response.status === 200) {
// TODO: Maybe move this new item event logic to comicService.addItemToComic?
if (item.id == newItemId) {
this.eventService.itemsChangedEvent.publish();
}
this.$scope.safeApply(() => {
this.itemFilterText = '';
});
} else {
this.$scope.safeApply(() => {
this.$scope.isUpdating = false;
});
}
}
}
AddItemController.$inject = ['$scope', '$log', 'comicService', 'eventService', 'itemService', '$timeout', '$filter', 'messageReportingService'];
var qcAddItemDirective = function (app) {
app.directive('qcAddItem', function () {
return {
restrict: 'E',
replace: true,
scope: {
isUpdating: '='
},
controller: AddItemController,
controllerAs: 'a',
template: variables.html.addItem
};
});
};
/*
* Copyright (C) 2016-2019 Alexander Krivács Schrøder <alexschrod@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
class ChangeLogController extends EventHandlingControllerBase {
constructor($scope, $log, 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', async () => {
this.$log.debug('Saving settings...');
settings.values.version = this.currentVersion;
await settings.saveSettings();
this.$log.debug('Settings saved.');
});
$log.debug('END ChangeLogController');
}
_comicDataLoaded(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').modal('hide');
}
}
ChangeLogController.$inject = ['$scope', '$log', 'eventService'];
var qcChangeLogDirective = function (app) {
app.directive('qcChangeLog', function () {
return {
restrict: 'E',
replace: true,
scope: {},
controller: ChangeLogController,
controllerAs: 'clvm',
template: variables.html.changeLog
};
});
};
/*
* Copyright (C) 2016-2019 Alexander Krivács Schrøder <alexschrod@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
// TODO: once the directive has been set up and the image loaded, remove the original.
class ComicController extends EventHandlingControllerBase {
constructor($scope, $log, eventService, comicService, messageReportingService) {
$log.debug('START ComicController');
super($scope, eventService);
this.$log = $log;
this.comicService = comicService;
this.messageReportingService = messageReportingService;
this.isInitializing = true;
this.isLoading = true;
this.comicDataCache = {};
const comicImg = $('img[src*="/comics/"]');
this.originalComicAnchor = comicImg.parent('a');
$log.debug('END ComicController');
}
_comicImageLoaded(src) {
if (this.imageLoadingTimeout) {
window.clearTimeout(this.imageLoadingTimeout);
this.imageLoadingTimeout = null;
}
this.$scope.safeApply(() => {
if (src) {
this.comicImage = src;
}
this.isLoading = false;
if (this.isInitializing) {
if (this.originalComicAnchor) {
this.originalComicAnchor.remove();
this.originalComicAnchor = null;
}
this.isInitializing = false;
}
});
}
_comicImageError(comic) {
this.messageReportingService.reportError('Could not load image for comic ' + comic);
this._comicImageLoaded(null);
}
_extensionImageLoading(comic, imageExtension) {
const downloadingImage = new Image();
downloadingImage.onload = () => {
this.comicDataCache[comic] = imageExtension;
this._comicImageLoaded(downloadingImage.src);
};
downloadingImage.onerror = () => {
this._comicImageError(comic);
};
const imageUrl = `${window.location.origin}/comics/${comic}.${imageExtension}`;
downloadingImage.src = imageUrl;
}
_fallbackImageLoading(comic) {
let comicExtensionIndex = 0;
const downloadingImage = new Image();
downloadingImage.onload = () => {
this.$log.debug('ComicController::_fallbackImageLoading() -- Image loaded');
this.comicDataCache[comic] = constants.comicExtensions[comicExtensionIndex];
this._comicImageLoaded(downloadingImage.src);
};
downloadingImage.onerror = () => {
this.$log.debug('ComicController::_fallbackImageLoading() -- Image failed to load');
if (comicExtensionIndex < constants.comicExtensions.length - 1) {
comicExtensionIndex++;
this.$log.debug('ComicController::_fallbackImageLoading() -- Trying ' + constants.comicExtensions[comicExtensionIndex]);
const imageUrl = `${window.location.origin}/comics/${comic}.${constants.comicExtensions[comicExtensionIndex]}`;
downloadingImage.src = imageUrl;
} else {
this._comicImageError(comic);
}
};
this.$log.debug('ComicController::_fallbackImageLoading() -- Trying ' + constants.comicExtensions[comicExtensionIndex]);
const imageUrl = `${window.location.origin}/comics/${comic}.${constants.comicExtensions[comicExtensionIndex]}`;
downloadingImage.src = imageUrl;
}
_comicDataLoading(comic) {
let comicLoadingIndicatorDelay = settings.values.comicLoadingIndicatorDelay;
if (comicLoadingIndicatorDelay < 0) {
comicLoadingIndicatorDelay = 0;
}
if (this.imageLoadingTimeout) {
window.clearTimeout(this.imageLoadingTimeout);
this.imageLoadingTimeout = null;
}
this.imageLoadingTimeout = window.setTimeout(() => {
this.$log.debug('ComicController::_comicDataLoading - imageLoadingTimeout triggered');
this.imageLoadingTimeout = null;
this.$scope.safeApply(() => {
this.isLoading = true;
});
}, comicLoadingIndicatorDelay);
if (comic in this.comicDataCache) {
this._extensionImageLoading(comic, this.comicDataCache[comic]);
}
}
_comicDataLoaded(comicData) {
if (comicData.comic in this.comicDataCache) {
return;
}
if (!comicData.hasData) {
this._fallbackImageLoading(this.comicService.comic);
return;
}
if (comicData.imageType == 'unknown') {
this._fallbackImageLoading(comicData.comic);
return;
}
let imageExtension = comicData.imageType;
if (imageExtension == 'jpeg') imageExtension = 'jpg';
this._extensionImageLoading(comicData.comic, imageExtension);
}
_comicDataError(error) {
this._fallbackImageLoading(this.comicService.comic);
}
}
ComicController.$inject = ['$scope', '$log', 'eventService', 'comicService', 'messageReportingService'];
var qcComicDirective = function (app) {
app.directive('qcComic', function () {
return {
restrict: 'E',
scope: {},
controller: ComicController,
controllerAs: 'c',
template: variables.html.comic
};
});
};
/*
* Copyright (C) 2016-2019 Alexander Krivács Schrøder <alexschrod@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
class ComicNavController extends EventHandlingControllerBase {
constructor($scope, $log, comicService, eventService, latestComic) {
$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) {
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) {
if (event.keyCode === 13) {
// ENTER key
this.go();
}
}
}
ComicNavController.$inject = ['$scope', '$log', 'comicService', 'eventService', 'latestComic'];
var qcComicNavDirective = function (app) {
app.directive('qcComicNav', function () {
return {
restrict: 'E',
replace: true,
scope: {},
controller: ComicNavController,
controllerAs: 'cn',
template: variables.html.comicNav
};
});
};
/*
* Copyright (C) 2016-2019 Alexander Krivács Schrøder <alexschrod@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
class DateController extends EventHandlingControllerBase {
constructor($scope, $log, 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) {
self.date = null;
self.approximateDate = false;
}
_comicDataLoaded(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'];
var qcDateDirective = function (app) {
app.directive('qcDate', function () {
return {
restrict: 'E',
replace: true,
scope: {},
controller: DateController,
controllerAs: 'd',
template: variables.html.date
};
});
};
/*
* Copyright (C) 2016-2019 Alexander Krivács Schrøder <alexschrod@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
class EditComicDataController extends EventHandlingControllerBase {
// TODO: Make properly strongly typed
constructor($scope, $log, eventService, comicService) {
$log.debug('START EditComicDataController');
super($scope, eventService);
this.$log = $log;
this.comicService = comicService;
this.isUpdating = true;
$('#editComicDataDialog').on('show.bs.modal', () => {// If something needs to be done, do it here.
});
$log.debug('END EditComicDataController');
}
_comicDataLoading(comic) {
this.isUpdating = true;
}
_comicDataLoaded(comicData) {
const editData = {
comicData: comicData
};
if (comicData.hasData) {
angular.forEach(comicData.items, item => {
let editDataType;
if (!editData[item.type]) {
editDataType = editData[item.type] = {};
} else {
editDataType = editData[item.type];
}
editDataType[item.id] = item;
});
}
this.editData = editData;
this.isUpdating = false;
}
_handleUpdateResponse(response, resetValueKey) {
if (response.status !== 200) {
this.$scope.safeApply(() => {
if (resetValueKey) {
this.editData.comicData[resetValueKey] = !this.editData.comicData[resetValueKey];
}
this.isUpdating = false;
});
}
}
async remove(item) {
this.$scope.safeApply(() => {
this.isUpdating = true;
});
const response = await this.comicService.removeItem(item);
this._handleUpdateResponse(response);
}
changeGuestComic() {
this.isUpdating = true;
this.comicService.setGuestComic(this.editData.comicData.isGuestComic).then(r => this._handleUpdateResponse(r, 'isGuestComic'));
}
changeNonCanon() {
this.isUpdating = true;
this.comicService.setNonCanon(this.editData.comicData.isNonCanon).then(r => this._handleUpdateResponse(r, 'isNonCanon'));
}
changeNoCast() {
this.isUpdating = true;
this.comicService.setNoCast(this.editData.comicData.hasNoCast).then(r => this._handleUpdateResponse(r, 'hasNoCast'));
}
changeNoLocation() {
this.isUpdating = true;
this.comicService.setNoLocation(this.editData.comicData.hasNoLocation).then(r => this._handleUpdateResponse(r, 'hasNoLocation'));
}
changeNoStoryline() {
this.isUpdating = true;
this.comicService.setNoStoryline(this.editData.comicData.hasNoStoryline).then(r => this._handleUpdateResponse(r, 'hasNoStoryline'));
}
changeNoTitle() {
this.isUpdating = true;
this.comicService.setNoTitle(this.editData.comicData.hasNoTitle).then(r => this._handleUpdateResponse(r, 'hasNoTitle'));
}
changeNoTagline() {
this.isUpdating = true;
this.comicService.setNoTagline(this.editData.comicData.hasNoTagline).then(r => this._handleUpdateResponse(r, 'hasNoTagline'));
}
close() {
$('#editComicDataDialog').modal('hide');
}
}
EditComicDataController.$inject = ['$scope', '$log', 'eventService', 'comicService'];
var qcEditComicDataDirective = function (app) {
app.directive('qcEditComicData', function () {
return {
restrict: 'E',
replace: true,
scope: {},
controller: EditComicDataController,
controllerAs: 'ecdvm',
template: variables.html.editComicData
};
});
};
/*
* Copyright (C) 2016-2019 Alexander Krivács Schrøder <alexschrod@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
class EditLogController extends EventHandlingControllerBase {
constructor($scope, $log, $http, messageReportingService, eventService) {
$log.debug('START EditLogController');
super($scope, eventService);
this.$log = $log;
this.$http = $http;
this.messageReportingService = messageReportingService;
this.currentPage = 1;
$('#editLogDialog').on('show.bs.modal', () => {
this.currentPage = 1;
this._loadLogs();
});
$log.debug('END EditLogController');
}
_maintenance() {
this.close();
}
async _loadLogs() {
this.$scope.safeApply(() => {
this.isLoading = true;
});
const response = await this.$http.get(`${constants.editLogUrl}?page=${this.currentPage}&token=${settings.values.editModeToken}`);
this.$scope.safeApply(() => {
this.isLoading = false;
});
if (response.status === 200) {
this.$scope.safeApply(() => {
this.logEntryData = response.data;
});
} else {
if (response.status === 503) {
this.eventService.maintenanceEvent.publish();
} else {
this.messageReportingService.reportError(response.data);
}
}
}
previousPage() {
this.currentPage--;
if (this.currentPage < 1) {
this.currentPage = 1;
}
this._loadLogs();
}
nextPage() {
this.currentPage++;
if (this.currentPage > this.logEntryData.pageCount) {
this.currentPage = this.logEntryData.pageCount;
}
this._loadLogs();
}
close() {
$('#editLogDialog').modal('hide');
}
}
EditLogController.$inject = ['$scope', '$log', '$http', 'messageReportingService', 'eventService'];
var qcEditLogDirective = function (app) {
app.directive('qcEditLog', function () {
return {
restrict: 'E',
replace: true,
scope: {},
controller: EditLogController,
controllerAs: 'elvm',
template: variables.html.editLog
};
});
};
/*
* Copyright (C) 2016-2019 Alexander Krivács Schrøder <alexschrod@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
class ExtraController extends EventHandlingControllerBase {
constructor($scope, $log, $sce, comicService, eventService, messageReportingService, latestComic) {
$log.debug('START ExtraController');
super($scope, eventService);
this.$log = $log;
this.$sce = $sce;
this.comicService = comicService;
this.messageReportingService = messageReportingService;
this.settings = settings;
this.constants = constants;
this.items = {};
this.allItems = {};
this.editorData = {};
this.messages = [];
this.missingDataInfo = [];
$log.debug('END ExtraController');
}
_maintenance() {
this._reset();
this.messages.push(constants.messages.maintenance);
this.messageReportingService.reportError(constants.messages.maintenance);
this.hasWarning = true;
}
_comicDataLoading(comic) {
this._loading();
}
_comicDataLoaded(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;
}
}
const self = this;
function processItem(item) {
let items;
if (!self.items[item.type]) {
items = self.items[item.type] = [];
} else {
items = self.items[item.type];
}
items.push(item);
}
function processAllItem(item) {
let items;
if (!self.allItems[item.type]) {
items = self.allItems[item.type] = [];
} else {
items = self.allItems[item.type];
}
items.push(item);
}
if (!comicData.hasData) {
this.messages.push('This strip has no navigation data yet');
this.hasWarning = true;
if (settings.values.showAllMembers && comicData.allItems) {
angular.forEach(comicData.allItems, processAllItem);
}
return;
}
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);
}
if (!hasCast && !comicData.hasNoCast) {
this.missingDataInfo.push('cast members');
}
if (!hasLocation && !comicData.hasNoLocation) {
this.missingDataInfo.push('a location');
}
/* #if (!hasStoryline && !comicData.hasNoStoryline) {
self.missingDataInfo.push('a storyline');
}*/
if (!comicData.title && !comicData.hasNoTitle) {
this.missingDataInfo.push('a title');
}
if (!comicData.tagline && !comicData.hasNoTagline && this.comicService.comic > constants.taglineThreshold) {
this.missingDataInfo.push('a tagline');
}
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) {
this._reset();
if (error.status !== 503) {
this.messages.push('Error communicating with server');
this.hasError = true;
} else {
this.eventService.maintenanceEvent.publish();
}
}
_reset() {
this.isLoading = false;
this.isUpdating = false;
this.items = {};
this.allItems = {};
this.editorData = {};
this.messages.length = 0;
this.missingDataInfo.length = 0;
this.hasError = false;
this.hasWarning = false;
}
_loading() {
this._reset();
this.isLoading = true;
this.isUpdating = true;
this.messages.push('Loading...');
}
getTypeDescription(type) {
switch (type) {
case 'cast':
return 'Cast Members';
case 'storyline':
return 'Storylines';
case 'location':
return 'Locations';
case 'all-cast':
return this.$sce.trustAsHtml('Cast Members<br>' + '<small>(Non-Present)</small>');
case 'all-storyline':
return this.$sce.trustAsHtml('Storylines<br>' + '<small>(Non-Present)</small>');
case 'all-location':
return this.$sce.trustAsHtml('Locations<br>' + '<small>(Non-Present)</small>');
}
}
openSettings() {
$('#settingsDialog').modal('show');
}
editComicData() {
$('#editComicDataDialog').modal('show');
}
showEditLog() {
$('#editLogDialog').modal('show');
}
showDetailsFor(item) {
$('#itemDetailsDialog').data('itemId', item.id);
$('#itemDetailsDialog').modal('show');
}
showChangeLog() {
this.showWelcomeMessage = false;
this.showUpdateMessage = false;
$('#changeLogDialog').modal('show');
}
}
ExtraController.$inject = ['$scope', '$log', '$sce', 'comicService', 'eventService', 'messageReportingService', 'latestComic'];
var qcExtraDirective = function (app) {
app.directive('qcExtra', function () {
return {
restrict: 'E',
replace: true,
scope: {},
controller: ExtraController,
controllerAs: 'e',
template: variables.html.extra
};
});
};
/*
* Copyright (C) 2016-2019 Alexander Krivács Schrøder <alexschrod@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
var qcExtraNavDirective = function (app) {
app.directive('qcExtraNav', function () {
return {
restrict: 'E',
scope: {
firstValue: '=',
firstTitle: '@',
previousValue: '=',
previousTitle: '@',
nextValue: '=',
nextTitle: '@',
lastValue: '=',
lastTitle: '@',
name: '@',
nameTitle: '@',
clickAction: '&'
},
template: variables.html.extraNav
};
});
};
/*
* Copyright (C) 2016-2019 Alexander Krivács Schrøder <alexschrod@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
function nl2br(str, isXhtml) {
const breakTag = isXhtml || typeof isXhtml === 'undefined' ? '<br />' : '<br>';
return String(str).replace(/([^>\r\n]?)(\r\n|\n\r|\r|\n)/g, '$1' + breakTag + '$2');
}
const comicLinkRegexp = /<a[^>]*href=(?:"|')(?:http:\/\/(?:www\.)?questionablecontent.net\/)?view\.php\?comic=(\d+)(?:"|')[^>]*>/;
function angularizeLinks(str) {
return String(str).replace(comicLinkRegexp, '<a ng-href="view.php?comic=$1">');
}
function convertDataUritoBlob(dataUri) {
const splitDataUri = dataUri.split(','); // convert base64 to raw binary data held in a string
// doesn't handle URLEncoded DataURIs - see SO answer #6850276 for code that does this
const byteString = atob(splitDataUri[1]); // separate out the mime component
const mimeString = splitDataUri[0].split(':')[1].split(';')[0]; // write the bytes of the string to an ArrayBuffer
const buffer = new ArrayBuffer(byteString.length); // create a view into the buffer
const byteArray = new Uint8Array(buffer); // set the bytes of the buffer to the correct values
for (let i = 0; i < byteString.length; i++) {
byteArray[i] = byteString.charCodeAt(i);
} // write the ArrayBuffer to a blob, and you're done
const blob = new Blob([buffer], {
type: mimeString
});
return blob;
}
/*
* Copyright (C) 2016-2019 Alexander Krivács Schrøder <alexschrod@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
class ItemDetailsController {
constructor($log, itemService, $scope, colorService, comicService, messageReportingService, styleService) {
$log.debug('START ItemDetailsController');
this.$log = $log;
this.itemService = itemService;
this.$scope = $scope;
this.colorService = colorService;
this.comicService = comicService;
this.messageReportingService = messageReportingService;
this.styleService = styleService;
this.isLoading = true;
this.isUpdating = false;
this.settings = settings;
this.imagePaths = [];
this.currentImagePath = 0;
this.isImagePreview = false;
this.imageFile = null;
this.imageFileInfo = null;
$('#itemDetailsDialog').on('show.bs.modal', () => this._getItemDetails());
$log.debug('END ItemDetailsController');
}
async _getItemDetails() {
const itemId = $('#itemDetailsDialog').data('itemId');
this.$log.debug('ItemDetailsController::showModal() - item id:', itemId);
this.itemData = {};
this.isLoading = true;
this.imagePaths = [];
this.currentImagePath = 0;
this.isImagePreview = false;
this.imageFile = null;
this.imageFileInfo = null;
const itemData = await this.itemService.getItemData(itemId);
if (itemData) {
this.$log.debug('qcItemDetails::showModal() - ' + 'item data:', itemData);
this.$scope.safeApply(() => {
this.itemData = itemData;
this.isLoading = false;
this.isUpdating = false;
this.imagePaths = itemData.imageUrls;
this.currentImagePath = 0; // If the color changes, also update the
// highlight color
this.$scope.$watch(() => {
return this.itemData.color;
}, () => {
this.itemData.highlightColor = this.colorService.createTintOrShade(itemData.color);
});
});
} else {
this.close();
}
}
_onErrorLog(response) {
this.messageReportingService.reportError(response.data);
return response;
}
_onSuccessRefreshElseErrorLog(response) {
if (response.status === 200) {
this.comicService.refreshComicData();
} else {
this._onErrorLog(response);
}
return response;
}
showInfoFor(id) {
$('#itemDetailsDialog').data('itemId', id);
this._getItemDetails();
}
keypress(event, property) {
if (event.keyCode === 13) {
// ENTER key
this.update(property);
}
}
async update(property) {
this.$scope.safeApply(() => {
this.isUpdating = true;
});
const success = await this.itemService.updateProperty(this.itemData.id, property, this.itemData[property]);
this.$scope.safeApply(() => {
this.isUpdating = false;
});
if (success) {
if (property === 'color') {
this.$log.debug('ItemDetailsController::update() - ' + 'update item color');
this.styleService.removeItemStyle(this.itemData.id);
}
this.comicService.refreshComicData();
this._getItemDetails();
}
}
goToComic(comic) {
this.comicService.gotoComic(comic);
this.close();
}
close() {
$('#itemDetailsDialog').modal('hide');
}
previewImage() {
if (this.imageFile && this.imageFileInfo.type == 'image/png') {
this.isImagePreview = true;
}
}
async uploadImage() {
if (this.imageFile && this.imageFileInfo.type == 'image/png') {
const imageBlob = convertDataUritoBlob(this.imageFile);
this.$scope.safeApply(() => {
this.isUpdating = true;
});
const success = await this.itemService.uploadImage(this.itemData.id, imageBlob, this.imageFileInfo.name);
if (success) {
this._getItemDetails();
}
} else {
this.messageReportingService.reportError('Only PNG images are supported');
}
}
previousImage() {
this.currentImagePath--;
if (this.currentImagePath < 0) {
this.currentImagePath = 0;
}
}
nextImage() {
this.currentImagePath++;
if (this.currentImagePath >= this.imagePaths.length) {
this.currentImagePath = this.imagePaths.length - 1;
}
}
}
ItemDetailsController.$inject = ['$log', 'itemService', '$scope', 'colorService', 'comicService', 'messageReportingService', 'styleService'];
var qcItemDetailsDirective = function (app) {
app.directive('qcItemDetails', function () {
return {
restrict: 'E',
replace: true,
scope: {},
controller: ItemDetailsController,
controllerAs: 'idvm',
template: variables.html.itemDetails
};
});
};
/*
* Copyright (C) 2016-2019 Alexander Krivács Schrøder <alexschrod@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
class NavController {
constructor($scope, comicService, latestComic) {
this.$scope = $scope;
this.comicService = comicService;
this.latestComic = latestComic;
this.settings = settings;
if (this.$scope.mainDirective) {
$scope.$watchGroup([() => {
return this.settings.values.skipGuest;
}, () => {
return this.settings.values.skipNonCanon;
}], () => {
this._updateRandomComic();
});
}
}
async _updateRandomComic() {
this.$scope.randomComic = this.comicService.nextRandomComic();
const randomComic = await this.comicService.nextFilteredRandomComic();
this.$scope.safeApply(() => {
this.$scope.randomComic = randomComic;
});
}
first(event) {
event.preventDefault();
event.stopPropagation();
this.comicService.first();
}
previous(event) {
event.preventDefault();
event.stopPropagation();
this.comicService.previous();
}
next(event) {
event.preventDefault();
event.stopPropagation();
this.comicService.next();
}
last(event) {
event.preventDefault();
event.stopPropagation();
this.comicService.last();
}
random(event) {
event.preventDefault();
event.stopPropagation();
this.comicService.gotoComic(this.$scope.randomComic);
this._updateRandomComic();
}
}
NavController.$inject = ['$scope', 'comicService', 'latestComic'];
var qcNavDirective = function (app) {
app.directive('qcNav', function () {
return {
restrict: 'E',
scope: {
randomComic: '=',
mainDirective: '='
},
controller: NavController,
controllerAs: 'n',
template: variables.html.navigation
};
});
};
/*
* Copyright (C) 2016-2019 Alexander Krivács Schrøder <alexschrod@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
class NewsController extends EventHandlingControllerBase {
constructor($scope, $log, eventService, $sce) {
$log.debug('START NewsController');
super($scope, eventService);
this.$sce = $sce;
this.news = $sce.trustAsHtml('Loading...');
$log.debug('END NewsController');
}
_comicDataLoading(comic) {
this.news = this.$sce.trustAsHtml('Loading...');
}
_comicDataLoaded(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'];
var qcNewsDirective = function (app) {
app.directive('qcNews', function () {
return {
restrict: 'E',
replace: true,
scope: {},
controller: NewsController,
controllerAs: 'n',
template: '<div id="news" ng-bind-html="n.news" compile-template></div>'
};
});
};
/*
* Copyright (C) 2016-2019 Alexander Krivács Schrøder <alexschrod@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
class RibbonController extends EventHandlingControllerBase {
constructor($scope, $log, 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) {
this.isNonCanon = comicData.isNonCanon;
this.isGuestComic = comicData.isGuestComic;
}
}
RibbonController.$inject = ['$scope', '$log', 'eventService'];
var qcRibbonDirective = function (app) {
app.directive('qcRibbon', function () {
return {
restrict: 'E',
replace: true,
scope: {},
controller: RibbonController,
controllerAs: 'r',
template: variables.html.ribbon
};
});
};
/*
* Copyright (C) 2016-2019 Alexander Krivács Schrøder <alexschrod@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
class SetPublishDateController extends SetValueControllerBase {
constructor($scope, $log, comicService, eventService, 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) {
if (comicData.publishDate != null) {
this.publishDate = new Date(comicData.publishDate);
} else {
this.publishDate = null;
}
this.isAccuratePublishDate = comicData.isAccuratePublishDate;
this.$scope.isUpdating = false;
}
_updateValue() {
this.setPublishDate(false);
}
setAccuratePublishDate() {
this.setPublishDate(true);
}
async setPublishDate(setAccurate) {
if (this.publishDate == null) {
// Error
this.messageReportingService.reportWarning('The date entered is not valid!');
return;
}
this.$scope.isUpdating = true;
const response = await this.comicService.setPublishDate(this.publishDate, this.isAccuratePublishDate != null ? this.isAccuratePublishDate : false);
if (response.status !== 200) {
this.$scope.safeApply(() => {
this.$scope.isUpdating = false;
if (setAccurate) {
this.isAccuratePublishDate = !this.isAccuratePublishDate;
}
});
}
}
}
SetPublishDateController.$inject = ['$scope', '$log', 'comicService', 'eventService', 'messageReportingService'];
var qcSetPublishDateDirective = function (app) {
app.directive('qcSetPublishDate', function () {
return {
restrict: 'E',
replace: true,
scope: {
isUpdating: '='
},
controller: SetPublishDateController,
controllerAs: 's',
template: variables.html.setPublishDate
};
});
};
/*
* Copyright (C) 2016-2019 Alexander Krivács Schrøder <alexschrod@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
class SetTaglineController extends SetValueControllerBase {
constructor($scope, $log, comicService, eventService) {
$log.debug('START SetTaglineController');
super($scope, comicService, eventService);
this.$log = $log;
this.tagline = '';
$log.debug('END SetTaglineController');
}
_comicDataLoaded(comicData) {
this.tagline = comicData.tagline;
this.$scope.isUpdating = false;
}
_updateValue() {
this.setTagline();
}
async setTagline() {
this.$scope.isUpdating = true;
const response = await this.comicService.setTagline(this.tagline ? this.tagline : '');
if (response.status !== 200) {
this.$scope.safeApply(() => {
this.$scope.isUpdating = false;
});
}
}
}
SetTaglineController.$inject = ['$scope', '$log', 'comicService', 'eventService'];
var qcSetTaglineDirective = function (app) {
app.directive('qcSetTagline', function () {
return {
restrict: 'E',
replace: true,
scope: {
isUpdating: '='
},
controller: SetTaglineController,
controllerAs: 's',
template: variables.html.setTagline
};
});
};
/*
* Copyright (C) 2016-2019 Alexander Krivács Schrøder <alexschrod@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
class SettingsController {
constructor($scope, $log, 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', async () => {
$log.debug('Saving settings...');
await this.settings.saveSettings();
$log.debug('Settings saved.');
});
$log.debug('END SettingsController');
}
close() {
$('#settingsDialog').modal('hide');
}
showChangeLog() {
this.close();
$('#changeLogDialog').modal('show');
}
}
SettingsController.$inject = ['$scope', '$log', 'comicService'];
var qcSettingsDirective = function (app) {
app.directive('qcSettings', function () {
return {
restrict: 'E',
replace: true,
scope: {},
controller: SettingsController,
controllerAs: 'svm',
template: variables.html.settings
};
});
};
/*
* Copyright (C) 2016-2019 Alexander Krivács Schrøder <alexschrod@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
class SetTitleController extends SetValueControllerBase {
constructor($scope, $log, comicService, eventService) {
$log.debug('START SetTitleController');
super($scope, comicService, eventService);
this.$log = $log;
this.title = '';
$log.debug('END SetTitleController');
}
_comicDataLoaded(comicData) {
this.title = comicData.title;
this.$scope.isUpdating = false;
}
_updateValue() {
this.setTitle();
}
async setTitle() {
this.$scope.isUpdating = true;
const response = await this.comicService.setTitle(this.title ? this.title : '');
if (response.status !== 200) {
this.$scope.safeApply(() => {
this.$scope.isUpdating = false;
});
}
}
}
SetTitleController.$inject = ['$scope', '$log', 'comicService', 'eventService'];
var qcSetTitleDirective = function (app) {
app.directive('qcSetTitle', function () {
return {
restrict: 'E',
replace: true,
scope: {
isUpdating: '='
},
controller: SetTitleController,
controllerAs: 's',
template: variables.html.setTitle
};
});
};
/*
* Copyright (C) 2016-2019 Alexander Krivács Schrøder <alexschrod@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
var assemble = function (app) {
config(app);
run(app);
bodyController(app);
comicController(app);
titleController(app);
colorService(app);
comicService(app);
itemService(app);
eventFactory(app);
eventService(app);
messageReportingService(app);
styleService(app);
donutDirective(app);
fileDataDirective(app);
onErrorDirective(app);
qcAddItemDirective(app);
qcChangeLogDirective(app);
qcComicDirective(app);
qcComicNavDirective(app);
qcDateDirective(app);
qcEditComicDataDirective(app);
qcEditLogDirective(app);
qcExtraDirective(app);
qcExtraNavDirective(app);
qcItemDetailsDirective(app);
qcNavDirective(app);
qcNewsDirective(app);
qcRibbonDirective(app);
qcSetPublishDateDirective(app);
qcSetTaglineDirective(app);
qcSettingsDirective(app);
qcSetTitleDirective(app);
};
/*
* Copyright (C) 2016-2019 Alexander Krivács Schrøder <alexschrod@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
const angularApp = angular.module('qc-spa', ['ui.router']);
function setup() {
assemble(angularApp);
}
/*
* Copyright (C) 2016-2019 Alexander Krivács Schrøder <alexschrod@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
/**
* Adds a CSS <link> element to the <head> of the document.
*
* @param {string} href - URL to the CSS document
*/
function addCss(href) {
jQuery('head').prepend('<link rel="stylesheet" type="text/css" href="' + href + '">');
}
/**
* Adds an inline CSS <style> element to the <head> of the document.
*
* @param {string} style - The inline CSS document
*/
function addStyle(style) {
jQuery('head').append(jQuery('<style type="text/css">' + style + '</style>'));
}
class DomModifier {
modify() {
// Add our modal windows
jQuery('body').prepend('<qc-settings></qc-settings>');
jQuery('body').prepend('<qc-edit-comic-data></qc-edit-comic-data>');
jQuery('body').prepend('<qc-item-details></qc-item-details>');
jQuery('body').prepend('<qc-change-log></qc-change-log>');
jQuery('body').prepend('<qc-edit-log></qc-edit-log>'); // Take control over the page's title
jQuery('title').replaceWith('<title ng-controller="titleController as t">' + '{{t.title}}</title>'); // Bootstrap
addCss(constants.siteUrl + 'style/bootstrap.min.css'); // Font Awesome
addCss('https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/' + 'font-awesome.min.css'); // Style adder function
addStyle(variables.css.style); // Take over the comic link
// For some reason, Jeph didn't use id="strip" on the comic <img> on
// the front page. Whyyy????
// (In other words, we have to use this method instead of just '#strip'.)
const comicImg = jQuery('img[src*="/comics/"]');
let comicAnchor = comicImg.parent('a');
if (comicAnchor.length !== 1) {
comicImg.wrap(jQuery('<a href="" />'));
comicAnchor = comicImg.parent('a');
}
const comicImage = comicImg.get(0);
let comicLinkUrl = comicImage.src;
comicLinkUrl = comicLinkUrl.split('/');
const comic = parseInt(comicLinkUrl[comicLinkUrl.length - 1].split('.')[0]);
angularApp.constant('startComic', comic);
jQuery('body').append(jQuery('<div ui-view></div>'));
const comicDirective = jQuery('<qc-comic></qc-comic>');
comicAnchor.before(comicDirective); // Figure out what the latest comic # is based on the URL in the
// "Latest/Last" navigation button.
const latestUrl = jQuery('#comicnav a').get(3).href;
let latestComic = parseInt(latestUrl.split('=')[1]);
if (isNaN(latestComic)) {
latestComic = comic;
}
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
}
angularApp.constant('latestComic', latestComic);
jQuery(jQuery('body #comicnav').get(0)).replaceWith('<qc-nav random-comic="randomComic" main-directive="true"></qc-nav>');
jQuery('body #comicnav').replaceWith('<qc-nav random-comic="randomComic" main-directive="false"></qc-nav>');
if (jQuery('#news, #newspost').prev().prop('tagName') === 'QC-NAV') {
// There's no date section: Insert our own
jQuery('#news, #newspost').before('<qc-date></qc-date>');
} else {
// There's a date section: Replace with our own
jQuery('#news, #newspost').prev().replaceWith('<qc-date></qc-date>');
}
jQuery('#news, #newspost').replaceWith('<qc-news></qc-news>');
if (comicDirective.parent().siblings('.small-2').length === 0) {
// There's no column after the comic: Insert our own
comicDirective.parent().after('<div class="small-2 medium-expand column"></div>');
}
comicDirective.parent().siblings('.small-2').prepend('<qc-extra></qc-extra>'); // Set a base (required by Angular's html5Mode)
jQuery('head').append('<base href="' + window.location.origin + '/">'); // Set up ng-controller for <body>
jQuery('body').attr('ng-controller', 'bodyController as b'); // Fixed positioned element covering the whole page used to show messages
// See the messageReportingService for details.
jQuery('body').append('<div id="messageSeat"></div>');
}
}
/*
* Copyright (C) 2016-2019 Alexander Krivács Schrøder <alexschrod@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
(async () => {
await settings.loadSettings();
const domModifier = new DomModifier();
domModifier.modify();
setup(); // Let's go!
angular.bootstrap(jQuery('html').get(0), ['qc-spa']);
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment