Commit 01b5b199 authored by Duhoux Pierre-Louis's avatar Duhoux Pierre-Louis

Options page all two first tab

parent e58d4539
......@@ -193,6 +193,10 @@
"message": "Update chapters list every",
"description": "Option update chapters list"
},
"options_seconds": {
"message": "$1 seconds",
"description": "Display a number of seconds"
},
"options_minutes": {
"message": "$1 minutes",
"description": "Display a number of minutes"
......@@ -212,5 +216,74 @@
"options_update_chap_btn": {
"message": "Refresh now",
"description": "Button refresh chapters list now"
},
"options_gen_update_mir_label": {
"message": "Update mirrors list every",
"description": "Option update mirrors list"
},
"options_update_mir_btn": {
"message": "Refresh now",
"description": "Button refresh mirrors list now"
},
"options_gen_checkmgstart_opt": {
"message": "Update on startup",
"description": "Option Update on startup"
},
"options_gen_checkmgstart_desc": {
"message": "Update chapters list on browser startup",
"description": "Option Update on startup description"
},
"options_gen_refreshspin_opt": {
"message": "Spin icon",
"description": "Option Spin icon"
},
"options_gen_refreshspin_desc": {
"message": "Spin the sharingan icon when All Mangas Reader is updating chapters list.",
"description": "Option Spin icon description"
},
"options_gen_savebandwidth_opt": {
"message": "Save bandwidth",
"description": "Option Save bandwidth"
},
"options_gen_savebandwidth_desc": {
"message": "Save bandwidth while loading chapter list (this options waits for one manga to be updated before updating the next one, update is slower)",
"description": "Option Save bandwidth description"
},
"options_gen_displayzero_opt": {
"message": "Grey \"0\" on icon",
"description": "Option grey 0 on icon"
},
"options_gen_displayzero_desc": {
"message": "Display a grey \"0\" on AMR's icon when there is no new manga.",
"description": "Option grey 0 on icon description"
},
"options_gen_nocount_opt": {
"message": "Grey sharingan as icon",
"description": "Option grey sharingan as icon"
},
"options_gen_nocount_desc": {
"message": "Do not display a badge with the number of unread manga but display AMR's icon in color if there are new mangas and in grey if not.",
"description": "Option grey sharingan as icon description"
},
"options_gen_notifs": {
"message": "Notifications",
"description": "This is the title of the options page section notifications"
},
"options_gen_shownotifications_opt": {
"message": "Notify on new chapter",
"description": "Option Notify on new chapter"
},
"options_gen_shownotifications_desc": {
"message": "Show browser notification when new chapters are found.",
"description": "Option Notify on new chapter description"
},
"options_gen_notificationtimer_label": {
"message": "Time before the notification close itself",
"description": "Option select time before notif closes"
},
"options_gen_notificationtimer_def": {
"message": "Closed by user action",
"description": "Option closed by user action for option select time before notif closes"
}
}
\ No newline at end of file
import store from '../store';
import iconHelper from './icon-helper';
class Updater {
/**
* Initialize refresh checkers
*/
load() {
this.checkChaptersUpdates();
this.checkMirrorsUpdates();
}
/**
* Check if we need to refresh chapters lists according to frequency every minutes
*/
checkChaptersUpdates() {
let lastUpdt = store.state.options.lastChaptersUpdate;
let frequency = store.state.options.updatechap;
if (lastUpdt + frequency < new Date().getTime()) {
// time to refresh !
store.dispatch("updateChaptersLists");
}
setTimeout(this.checkChaptersUpdates.bind(this), 60 * 1000); // check every minutes
}
/**
* Check if we need to refresh mirrors lists according to frequency every minutes
*/
checkMirrorsUpdates() {
let lastUpdt = store.state.options.lastMirrorsUpdate;
let frequency = store.state.options.updatemg;
if (lastUpdt + frequency < new Date().getTime()) {
// time to refresh !
store.dispatch("updateMirrorsLists");
}
setTimeout(this.checkMirrorsUpdates.bind(this), 60 * 1000); // check every minutes
}
/**
* Refresh badge and icon
*/
refreshBadgeAndIcon() {
let nbnew = store.getters.nbNewMangas;
if (store.state.options.nocount == 1) {
iconHelper.resetBadge(); // remove badge
// display a grey badge if no new mangas
if (nbnew > 0) iconHelper.resetIcon();
else iconHelper.setBWIcon();
} else {
iconHelper.resetIcon();
if (store.state.options.displayzero === 1) {
iconHelper.updateBadge(nbnew);
} else {
if (nbnew == 0) iconHelper.resetBadge();
else iconHelper.updateBadge(nbnew);
}
}
}
}
export default (new Updater)
\ No newline at end of file
import browser from "webextension-polyfill";
import * as utils from "../amr/utils";
import * as utils from "./utils";
import store from "../store";
class IconHelper {
......@@ -18,28 +18,43 @@ class IconHelper {
this.animationSpeed = 50;
this.rotation = 0;
this.icon = document.createElement('img');
this.icon.src = 'icons/icon_32.png';
this.icon.src = '/icons/icon_32.png';
this.icon_bw = document.createElement('img');
this.icon_bw.src = 'icons/icon_32_bw.png';
this.icon_bw.src = '/icons/icon_32_bw.png';
}
this.requireStop = false;
}
updateBadge(nb) {
browser.browserAction.setBadgeText({text: ""+nb});
if (nb === 0) {
//set grey background
browser.browserAction.setBadgeBackgroundColor({color:"#aaaaaa"});
} else {
//set red background
browser.browserAction.setBadgeBackgroundColor({color:"red"});
}
}
resetBadge() {
browser.browserAction.setBadgeText({text: ""});
}
/**
* Set AMR icon to blue sharingan
*/
setBlueIcon() {
browser.browserAction.setIcon({ path: "icons/icon_32_blue.png" });
browser.browserAction.setIcon({ path: "/icons/icon_32_blue.png" });
}
/**
* Set AMR icon to grayscale sharingan
*/
setBWIcon() {
browser.browserAction.setIcon({ path: "icons/icon_32_bw.png" });
browser.browserAction.setIcon({ path: "/icons/icon_32_bw.png" });
}
/**
* Set AMR icon to default sharingan
*/
resetIcon() {
browser.browserAction.setIcon({ path: "icons/icon_32.png" });
this.requireStop = true;
browser.browserAction.setIcon({ path: "/icons/icon_32.png" });
}
/**
* Set AMR icon to spinning sharingan (normal or grayscale depending on options)
......@@ -47,12 +62,13 @@ class IconHelper {
spinIcon() {
if (!utils.isFirefox()) {
// chrome does not support animated svg as icon
this.requireStop = false;
this.waitSpinning();
} else {
if (store.state.options.nocount == 1 && !store.getters.hasNewMangas()) {
browser.browserAction.setIcon({ path: "icons/icon_32_bw.svg" });
if (store.state.options.nocount == 1 && !store.getters.hasNewMangas) {
browser.browserAction.setIcon({ path: "/icons/icon_32_bw.svg" });
} else {
browser.browserAction.setIcon({ path: "icons/icon_32.svg" });
browser.browserAction.setIcon({ path: "/icons/icon_32.svg" });
}
}
}
......@@ -68,7 +84,7 @@ class IconHelper {
this.canvasContext.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.canvasContext.translate(this.canvas.width / 2, this.canvas.height / 2);
this.canvasContext.rotate(2 * Math.PI * (doEase ? this.ease(this.rotation) : this.rotation));
if (store.state.options.nocount == 1 && !store.getters.hasNewMangas()) {
if (store.state.options.nocount == 1 && !store.getters.hasNewMangas) {
this.canvasContext.drawImage(this.icon_bw, -this.canvas.width / 2, -this.canvas.height / 2);
} else {
this.canvasContext.drawImage(this.icon, -this.canvas.width / 2, -this.canvas.height / 2);
......@@ -82,16 +98,17 @@ class IconHelper {
* Animation loop
*/
waitSpinning() {
if (this.requireStop) {
this.requireStop = false;
return;
}
this.rotation += 1 / this.animationFrames;
if (this.rotation > 1) {
this.rotation = this.rotation - 1;
this.doEase = false;
}
this.drawIconAtRotation(false);
var _self = this;
setTimeout(function () {
_self.waitSpinning();
}, this.animationSpeed);
setTimeout(this.waitSpinning.bind(this), this.animationSpeed);
}
/**
* Ease for rotation
......
import 'regenerator-runtime/runtime';
import Axios from 'axios';
import store from '../store';
/**
......@@ -19,6 +18,13 @@ class MirrorsImpl {
})(this);
}
/**
* Removes the implementation cache
*/
resetImplementations() {
this.implementations = [];
}
/**
* Load a mirror implementation file and return a Promise containing implementation object when loaded
* @param {*} scripturl
......@@ -37,7 +43,7 @@ class MirrorsImpl {
};
script.onerror = reject;
script.async = true;
script.src = scripturl;
script.src = scripturl + "?ts=" + new Date().getTime(); // do not cache script implementation when loading it
});
}
......
import browser from "webextension-polyfill";
import store from "../store";
/**
* Managa browser notifications
*/
class Notification {
constructor() {
// store current opened notifications
this.notifications = {};
// last id of notification
this.currentId = 0;
// Callback function to notification click.
let _self = this;
let notificationClickCallback = function (id) {
if (_self.notifications[id] !== undefined) {
browser.tabs.create({
"url": _self.notifications[id]
});
// It deletes the used URL to avoid unbounded object growing.
// Well, if the notification isn't clicked the said growing is not avoided.
// If this proves to be a issue a close callback should be added too.
delete _self.notifications[id];
}
}, notificationCloseCallback = function (id) {
if (_self.notifications[id] !== undefined) delete _self.notifications[id];
}
// Add the callback to ALL notifications opened by AMR.
browser.notifications.onClicked.addListener(notificationClickCallback);
// To prevent the notification array from growing
browser.notifications.onClosed.addListener(notificationCloseCallback);
}
/**
* Create a notification when a new chapter is released on a manga
* @param {} mg manga to notify for
......@@ -16,57 +46,30 @@ class Notification {
// Notification data added to letiables to be used by the old or by the new notification API.
let description = "... has new chapter(s) on " + mangaData.mirror + "! Click anywhere to open the next unread chapter.";
let title = mangaData.name;
let icon = browser.extension.getURL('icons/icon_32.png');
let icon = browser.extension.getURL('/icons/icon_32.png');
let url = mangaData.url;
if (browser.notifications) {
// The new API have no notification object, so can't save data on it.
// Hence, the URL must be saved under a global object, mapped by ID.
// (no one would like to click a manga notification and ending up opening another manga)
// For now, those global data is being saved here. But I think it would be better
// to move it to another place for the sake of better code organization.
// And because there are other notifications being opened elsewhere in the code too.
// TODO probably a problem here, do not store additional fields on a manga from the store as vuex won't reflect it.
// --> BETTER to store it on local singleton
if (mg.notifications === undefined) {
mg.notifications = {};
}
if (mg.lastNotificationID === undefined) {
mg.lastNotificationID = 1;
} else {
// lastNotificationID can, if the browser is open a sufficient amount of time
// and a lot of new manga chapters are found, grow beyond the number upper limit.
// But this is so unlikely to happen...
mg.lastNotificationID++;
}
mg.notifications["amr" + mg.lastNotificationID] = url;
// Callback function to notification click.
let notificationClickCallback = function (id) {
if (mg.notifications[id] !== undefined) {
browser.tabs.create({
"url": mg.notifications[id]
});
// It deletes the used URL to avoid unbounded object growing.
// Well, if the notification isn't clicked the said growing is not avoided.
// If this proves to be a issue a close callback should be added too.
delete mg.notifications[id];
}
}, notificationCloseCallback = function (id) {
if (mg.notifications[id] !== undefined) delete mg.notifications[id];
}
let curId = this.currentId++;
this.notifications["amr_" + curId] = url;
let notificationOptions = {
type: "basic",
title: title,
message: description,
iconUrl: icon
};
// Add the callback to ALL notifications opened by AMR.
// This can sure be a issue with another notifications AMR opens.
browser.notifications.onClicked.addListener(notificationClickCallback);
// To prevent the notification array from growing
browser.notifications.onClosed.addListener(notificationCloseCallback);
// And finally opens de notification. The third parameter is a creation callback,
// which I think is not needed here.
browser.notifications.create("amr" + mg.lastNotificationID, notificationOptions, function () { });
// opens the notification.
browser.notifications.create("amr_" + curId, notificationOptions);
//Auto close notification if required
if (store.state.options.notificationtimer > 0) {
setTimeout(function() {
browser.notifications.clear("amr_" + curId);
}, store.state.options.notificationtimer);
}
}
}
}
......
......@@ -67,7 +67,10 @@ export function debug(message) {
export function serializeVuexObject(obj) {
return JSON.parse(JSON.stringify(obj)) // For an unknown reason, better than Object.assign({}, obj) in Firefox
}
/**
* Extract the full host name
* @param {*} url
*/
const extractHostname = function(url) {
var hostname;
//find & remove protocol (http, ftp, etc.) and get hostname
......@@ -86,7 +89,10 @@ const extractHostname = function(url) {
return hostname;
}
/**
* Extract the root domain of a url without subdomain
* @param {*} url
*/
const extractRootDomain = function(url) {
var domain = extractHostname(url),
splitArr = domain.split('.'),
......@@ -104,7 +110,10 @@ const extractRootDomain = function(url) {
}
return domain;
}
/**
* Extract the part of a url following the domain
* @param {*} url
*/
const afterHostURL = function(url) {
var after;
//find & remove protocol (http, ftp, etc.) and get hostname
......@@ -117,6 +126,10 @@ const afterHostURL = function(url) {
}
return after;
}
/**
* Calculate manga key for a url (just host name, without subdomain followed by url of manga)
* @param {*} url
*/
export function mangaKey(url) {
if (!url) {
console.error("A manga key has been requested for undefined url, it will be melted in your database with other mangas with same issue, check the implementation of the mirror where your read this manga.")
......
import Handler from './handler';
import IconHelper from './icon-helper';
import IconHelper from '../amr/icon-helper';
import store from '../store';
import * as utils from '../amr/utils';
import amrUpdater from '../amr/amr-updater';
// Initialize store
(async () => {
// Set blue icon until AMR is loaded
// Blue icon while loading
IconHelper.setBlueIcon();
// Turn icon back to normal when mirrors loaded
document.addEventListener("mirrorsLoaded", () => IconHelper.resetIcon());
/**
* Initialize AMR options from locaStorage
*/
......@@ -17,7 +15,7 @@ import * as utils from '../amr/utils';
await store.dispatch('initOptions');
/**
* Initialize mirrors list in store from DB
* Initialize mirrors list in store from DB or repo
*/
utils.debug("Initialize mirrors");
await store.dispatch('initMirrors');
......@@ -28,10 +26,22 @@ import * as utils from '../amr/utils';
utils.debug("Initialize mangas");
await store.dispatch('initMangasFromDB');
// set icon and badge
amrUpdater.refreshBadgeAndIcon();
/**
* If option update chapters lists on startup --> do it
*/
if (store.state.options.checkmgstart === 1) {
store.dispatch("updateChaptersLists");
}
// Starts message handling
utils.debug("Initialize message handler");
Handler.handle();
// Check if we need to refresh chapters lists, mirrors lists and launch automatic checker
amrUpdater.load();
/**
* The function below increments the reading of each manga in the list from a chapter each 2 seconds
* You can observe that when you open the popup, these modifications are propagated in real time to the popup
......
......@@ -17,7 +17,8 @@ const contentCss = ['content/content.css'];
class HandleManga {
handle(message, sender) {
let key = utils.mangaKey(message.url);
let key;
if (message.url) key = utils.mangaKey(message.url);
switch (message.action) {
case "pagematchurls":
// content script included, test if a mirror match the page and load AMR in tab
......@@ -46,6 +47,9 @@ class HandleManga {
.then(() => store.dispatch('readManga', message)); // set reading to current chapter
case "importSamples":
return store.dispatch("importSamples");
case "updateChaptersLists":
// updates all mangas lists (do it in background if called from popup because it requires jQuery)
return store.dispatch("updateChaptersLists");
}
}
......
......@@ -130,11 +130,79 @@
{{ i18n("options_gen_update_chap_label") }} :
</v-flex>
<v-flex xs4>
<v-select v-model="updatechap" v-on:change="setOption('updatechap')" :items="update_chap_values">
<v-select v-model="updatechap" :items="update_chap_values">
</v-select>
</v-flex>
<v-flex>
<v-btn color="primary" class="btn-sel" small>{{i18n("options_update_chap_btn")}}</v-btn>
<v-btn color="primary" class="btn-sel" small
@click="updateChaps()"
:loading="loadingChapters"
:disabled="loadingChapters">
{{i18n("options_update_chap_btn")}}
</v-btn>
</v-flex>
</v-layout>
</v-container>
</div>
<!-- Update mirrors list -->
<div class="subtitle">
<v-container fluid class="opt-container">
<v-layout row wrap>
<v-flex xs4 class="sel-title">
{{ i18n("options_gen_update_mir_label") }} :
</v-flex>
<v-flex xs4>
<v-select v-model="updatemg" :items="update_mir_values">
</v-select>
</v-flex>
<v-flex>
<v-btn color="primary" class="btn-sel" small
@click="updateMirrors()"
:loading="loadingMirrors"
:disabled="loadingMirrors">
{{i18n("options_update_mir_btn")}}
</v-btn>
</v-flex>
</v-layout>
</v-container>
</div>
<!-- Update on startup -->
<div class="subtitle">{{i18n('options_gen_checkmgstart_desc')}}</div>
<v-checkbox v-model="checkmgstart" @change="setOption('checkmgstart')"
:label="i18n('options_gen_checkmgstart_opt')"></v-checkbox>
<!-- Spin icon while loading chapters -->
<div class="subtitle">{{i18n('options_gen_refreshspin_desc')}}</div>
<v-checkbox v-model="refreshspin" @change="setOption('refreshspin')"
:label="i18n('options_gen_refreshspin_opt')"></v-checkbox>
<!-- Save bandwidth while loading chapters -->
<div class="subtitle">{{i18n('options_gen_savebandwidth_desc')}}</div>
<v-checkbox v-model="savebandwidth" @change="setOption('savebandwidth')"
:label="i18n('options_gen_savebandwidth_opt')"></v-checkbox>
<!-- Display grey 0 when no new -->
<div class="subtitle">{{i18n('options_gen_displayzero_desc')}}</div>
<v-checkbox v-model="displayzero" @change="setOption('displayzero')"
:label="i18n('options_gen_displayzero_opt')"></v-checkbox>
<!-- Grey sharingan if no new -->
<div class="subtitle">{{i18n('options_gen_nocount_desc')}}</div>
<v-checkbox v-model="nocount" @change="setOption('nocount')"
:label="i18n('options_gen_nocount_opt')"></v-checkbox>
<!-- Notifications -->
<div class="headline">{{ i18n("options_gen_notifs") }}</div>
<!-- Notify on new chapter -->
<div class="subtitle">{{i18n('options_gen_shownotifications_desc')}}</div>
<v-checkbox v-model="shownotifications" @change="setOption('shownotifications')"
:label="i18n('options_gen_shownotifications_opt')"></v-checkbox>
<!-- Time to close notification -->
<div class="subtitle">
<v-container fluid class="opt-container">
<v-layout row wrap>
<v-flex xs6 class="sel-title">
{{ i18n("options_gen_notificationtimer_label") }} :
</v-flex>
<v-flex xs6>
<v-select v-model="notificationtimer" :items="notificationtimer_values">
</v-select>
</v-flex>
</v-layout>
</v-container>
......@@ -149,6 +217,8 @@
</template>
<script>
import i18n from "../../amr/i18n";
import browser from "webextension-polyfill";
import amrUpdater from "../../amr/amr-updater";
/**
* Converters to format options in db and in page (ex : booleans are store as 0:1 in db)
......@@ -173,7 +243,13 @@ const converters = {
"newTab",
"groupmgs",
"displastup",
"dark"
"dark",
"checkmgstart",
"refreshspin",
"savebandwidth",
"displayzero",
"nocount",
"shownotifications"
]
}
};
......@@ -205,16 +281,33 @@ export default {
"grey"
],
update_chap_values: [
{value: 5 * 60 * 1000, text: i18n("options_minutes", 5)},
{value: 10 * 60 * 1000, text: i18n("options_minutes", 10)},
{value: 15 * 60 * 1000, text: i18n("options_minutes", 15)},
{value: 30 * 60 * 1000, text: i18n("options_minutes", 30)},
{value: 1 * 60 * 60 * 1000, text: i18n("options_hours", 1)},
{value: 2 * 60 * 60 * 1000, text: i18n("options_hours", 2)},
{value: 6 * 60 * 60 * 1000, text: i18n("options_hours", 6)},
{value: 12 * 60 * 60 * 1000, text: i18n("options_hours", 12)},
{value: 24 * 60 * 60 * 1000, text: i18n("options_days", 1)},
{value: 7 * 24 * 60 * 60 * 1000, text: i18n("options_week", 1)},
{ value: 5 * 60 * 1000, text: i18n("options_minutes", 5) },
{ value: 10 * 60 * 1000, text: i18n("options_minutes", 10) },
{ value: 15 * 60 * 1000, text: i18n("options_minutes", 15) },
{ value: 30 * 60 * 1000, text: i18n("options_minutes", 30) },
{ value: 1 * 60 * 60 * 1000, text: i18n("options_hours", 1) },
{ value: 2 * 60 * 60 * 1000, text: i18n("options_hours", 2) },
{ value: 6 * 60 * 60 * 1000, text: i18n("options_hours", 6) },
{ value: 12 * 60 * 60 * 1000, text: i18n("options_hours", 12) },
{ value: 24 * 60 * 60 * 1000, text: i18n("options_days", 1) },
{ value: 7 * 24 * 60 * 60 * 1000, text: i18n("options_week", 1) }
],
loadingChapters: false,
update_mir_values: [
{ value: 24 * 60 * 60 * 1000, text: i18n("options_days", 1) },
{ value: 2 * 24 * 60 * 60 * 1000, text: i18n("options_days", 2) },
{ value: 7 * 24 * 60 * 60 * 1000, text: i18n("options_week", 1) },
{ value: 2 * 7 * 24 * 60 * 60 * 1000, text: i18n("options_week", 2) }
],
loadingMirrors: false,
notificationtimer_values: [
{ value: 0, text: i18n("options_gen_notificationtimer_def") },
{ value: 5 * 1000, text: i18n("options_seconds", 5) },
{ value: 10 * 1000, text: i18n("options_seconds", 10) },
{ value: 15 * 1000, text: i18n("options_seconds", 15) },
{ value: 30 * 1000, text: i18n("options_seconds", 30) },
{ value: 60 * 1000, text: i18n("options_minutes", 1) },
{ value: 2 * 60 * 1000, text: i18n("options_minutes", 2) }
]
};
// add all options properties in data model; this properties are the right one in store because synchronization with background has been called by encapsuler (popup.js / other) before initializing vue
......@@ -227,6 +320,24 @@ export default {
});
return res;
},
watch: {
/**
* For v-select, v-model is updated after event change is called so we
* need to watch the value to update the model properly
*/
updatechap: function(n, o) {
this.setOption("updatechap");
},
updatemg: function(n, o) {
this.setOption("updatemg");
},
displayzero: function() {
amrUpdater.refreshBadgeAndIcon();
},
nocount: function() {
amrUpdater.refreshBadgeAndIcon();
}
},
methods: {
i18n: (message, ...args) => i18n(message, ...args),
/**
......@@ -241,8 +352,24 @@ export default {
if (prop === optstr) val = converters[key].toDb(val);
}
});
console.log(optstr + " --> " + val + " - " + typeof val);
this.$store.dispatch("setOption", { key: optstr, value: val });
},
/**
* Updates chapters lists
*/
async updateChaps() {
this.loadingChapters = true;
//We don't call the store updateChaptersLists because when refreshing chapters, it will use jQuery (inside implementations), which is not loaded in the popup, let's do it in background
await browser.runtime.sendMessage({ action: "updateChaptersLists" });
this.loadingChapters = false;
},
/**
* Update mirrors lists
*/
async updateMirrors() {
this.loadingMirrors = true;
await this.$store.dispatch("updateMirrorsLists");
this.loadingMirrors = false;
}
}
};
......
......@@ -5,6 +5,8 @@ import notifications from '../../amr/notifications';
import statsEvents from '../../amr/stats-events';
import * as utils from "../../amr/utils";
import samples from "../../amr/samples";
import amrUpdater from '../../amr/amr-updater';
import iconHelper from '../../amr/icon-helper';