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

Options page all two first tab

parent e58d4539
...@@ -193,6 +193,10 @@ ...@@ -193,6 +193,10 @@
"message": "Update chapters list every", "message": "Update chapters list every",
"description": "Option update chapters list" "description": "Option update chapters list"
}, },
"options_seconds": {
"message": "$1 seconds",
"description": "Display a number of seconds"
},
"options_minutes": { "options_minutes": {
"message": "$1 minutes", "message": "$1 minutes",
"description": "Display a number of minutes" "description": "Display a number of minutes"
...@@ -212,5 +216,74 @@ ...@@ -212,5 +216,74 @@
"options_update_chap_btn": { "options_update_chap_btn": {
"message": "Refresh now", "message": "Refresh now",
"description": "Button refresh chapters list 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 browser from "webextension-polyfill";
import * as utils from "../amr/utils"; import * as utils from "./utils";
import store from "../store"; import store from "../store";
class IconHelper { class IconHelper {
...@@ -18,28 +18,43 @@ class IconHelper { ...@@ -18,28 +18,43 @@ class IconHelper {
this.animationSpeed = 50; this.animationSpeed = 50;
this.rotation = 0; this.rotation = 0;
this.icon = document.createElement('img'); 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 = 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 * Set AMR icon to blue sharingan
*/ */
setBlueIcon() { 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 * Set AMR icon to grayscale sharingan
*/ */
setBWIcon() { 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 * Set AMR icon to default sharingan
*/ */
resetIcon() { 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) * Set AMR icon to spinning sharingan (normal or grayscale depending on options)
...@@ -47,12 +62,13 @@ class IconHelper { ...@@ -47,12 +62,13 @@ class IconHelper {
spinIcon() { spinIcon() {
if (!utils.isFirefox()) { if (!utils.isFirefox()) {
// chrome does not support animated svg as icon // chrome does not support animated svg as icon
this.requireStop = false;
this.waitSpinning(); this.waitSpinning();
} else { } else {
if (store.state.options.nocount == 1 && !store.getters.hasNewMangas()) { if (store.state.options.nocount == 1 && !store.getters.hasNewMangas) {
browser.browserAction.setIcon({ path: "icons/icon_32_bw.svg" }); browser.browserAction.setIcon({ path: "/icons/icon_32_bw.svg" });
} else { } else {
browser.browserAction.setIcon({ path: "icons/icon_32.svg" }); browser.browserAction.setIcon({ path: "/icons/icon_32.svg" });
} }
} }
} }
...@@ -68,7 +84,7 @@ class IconHelper { ...@@ -68,7 +84,7 @@ class IconHelper {
this.canvasContext.clearRect(0, 0, this.canvas.width, this.canvas.height); this.canvasContext.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.canvasContext.translate(this.canvas.width / 2, this.canvas.height / 2); this.canvasContext.translate(this.canvas.width / 2, this.canvas.height / 2);
this.canvasContext.rotate(2 * Math.PI * (doEase ? this.ease(this.rotation) : this.rotation)); 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); this.canvasContext.drawImage(this.icon_bw, -this.canvas.width / 2, -this.canvas.height / 2);
} else { } else {
this.canvasContext.drawImage(this.icon, -this.canvas.width / 2, -this.canvas.height / 2); this.canvasContext.drawImage(this.icon, -this.canvas.width / 2, -this.canvas.height / 2);
...@@ -82,16 +98,17 @@ class IconHelper { ...@@ -82,16 +98,17 @@ class IconHelper {
* Animation loop * Animation loop
*/ */
waitSpinning() { waitSpinning() {
if (this.requireStop) {
this.requireStop = false;
return;
}
this.rotation += 1 / this.animationFrames; this.rotation += 1 / this.animationFrames;
if (this.rotation > 1) { if (this.rotation > 1) {
this.rotation = this.rotation - 1; this.rotation = this.rotation - 1;
this.doEase = false; this.doEase = false;
} }
this.drawIconAtRotation(false); this.drawIconAtRotation(false);
var _self = this; setTimeout(this.waitSpinning.bind(this), this.animationSpeed);
setTimeout(function () {
_self.waitSpinning();
}, this.animationSpeed);
} }
/** /**
* Ease for rotation * Ease for rotation
......
import 'regenerator-runtime/runtime'; import 'regenerator-runtime/runtime';
import Axios from 'axios';
import store from '../store'; import store from '../store';
/** /**
...@@ -19,6 +18,13 @@ class MirrorsImpl { ...@@ -19,6 +18,13 @@ class MirrorsImpl {
})(this); })(this);
} }
/**
* Removes the implementation cache
*/
resetImplementations() {
this.implementations = [];
}
/** /**
* Load a mirror implementation file and return a Promise containing implementation object when loaded * Load a mirror implementation file and return a Promise containing implementation object when loaded
* @param {*} scripturl * @param {*} scripturl
...@@ -37,7 +43,7 @@ class MirrorsImpl { ...@@ -37,7 +43,7 @@ class MirrorsImpl {
}; };
script.onerror = reject; script.onerror = reject;
script.async = true; 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 browser from "webextension-polyfill";
import store from "../store";
/**
* Managa browser notifications
*/
class Notification { 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 * Create a notification when a new chapter is released on a manga
* @param {} mg manga to notify for * @param {} mg manga to notify for
...@@ -16,57 +46,30 @@ class Notification { ...@@ -16,57 +46,30 @@ class Notification {
// Notification data added to letiables to be used by the old or by the new notification API. // 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 description = "... has new chapter(s) on " + mangaData.mirror + "! Click anywhere to open the next unread chapter.";
let title = mangaData.name; 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; let url = mangaData.url;
if (browser.notifications) { if (browser.notifications) {
// The new API have no notification object, so can't save data on it. // 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. // 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) // (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 let curId = this.currentId++;
// to move it to another place for the sake of better code organization. this.notifications["amr_" + curId] = url;
// 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 notificationOptions = { let notificationOptions = {
type: "basic", type: "basic",
title: title, title: title,
message: description, message: description,
iconUrl: icon iconUrl: icon
}; };
// Add the callback to ALL notifications opened by AMR.
// This can sure be a issue with another notifications AMR opens. // opens the notification.
browser.notifications.onClicked.addListener(notificationClickCallback); browser.notifications.create("amr_" + curId, notificationOptions);
// To prevent the notification array from growing //Auto close notification if required
browser.notifications.onClosed.addListener(notificationCloseCallback); if (store.state.options.notificationtimer > 0) {
// And finally opens de notification. The third parameter is a creation callback, setTimeout(function() {
// which I think is not needed here. browser.notifications.clear("amr_" + curId);
browser.notifications.create("amr" + mg.lastNotificationID, notificationOptions, function () { }); }, store.state.options.notificationtimer);
}
} }
} }
} }
......
...@@ -67,7 +67,10 @@ export function debug(message) { ...@@ -67,7 +67,10 @@ export function debug(message) {
export function serializeVuexObject(obj) { export function serializeVuexObject(obj) {
return JSON.parse(JSON.stringify(obj)) // For an unknown reason, better than Object.assign({}, obj) in Firefox 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) { const extractHostname = function(url) {
var hostname; var hostname;
//find & remove protocol (http, ftp, etc.) and get hostname //find & remove protocol (http, ftp, etc.) and get hostname
...@@ -86,7 +89,10 @@ const extractHostname = function(url) { ...@@ -86,7 +89,10 @@ const extractHostname = function(url) {
return hostname; return hostname;
} }
/**
* Extract the root domain of a url without subdomain
* @param {*} url
*/
const extractRootDomain = function(url) { const extractRootDomain = function(url) {
var domain = extractHostname(url), var domain = extractHostname(url),
splitArr = domain.split('.'), splitArr = domain.split('.'),
...@@ -104,7 +110,10 @@ const extractRootDomain = function(url) { ...@@ -104,7 +110,10 @@ const extractRootDomain = function(url) {
} }
return domain; return domain;
} }
/**
* Extract the part of a url following the domain
* @param {*} url
*/
const afterHostURL = function(url) { const afterHostURL = function(url) {
var after; var after;
//find & remove protocol (http, ftp, etc.) and get hostname //find & remove protocol (http, ftp, etc.) and get hostname
...@@ -117,6 +126,10 @@ const afterHostURL = function(url) { ...@@ -117,6 +126,10 @@ const afterHostURL = function(url) {
} }
return after; return after;
} }
/**
* Calculate manga key for a url (just host name, without subdomain followed by url of manga)
* @param {*} url
*/
export function mangaKey(url) { export function mangaKey(url) {
if (!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.") 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 Handler from './handler';
import IconHelper from './icon-helper'; import IconHelper from '../amr/icon-helper';
import store from '../store'; import store from '../store';
import * as utils from '../amr/utils'; import * as utils from '../amr/utils';
import amrUpdater from '../amr/amr-updater';
// Initialize store // Initialize store
(async () => { (async () => {
// Set blue icon until AMR is loaded // Blue icon while loading
IconHelper.setBlueIcon(); IconHelper.setBlueIcon();
// Turn icon back to normal when mirrors loaded
document.addEventListener("mirrorsLoaded", () => IconHelper.resetIcon());
/** /**
* Initialize AMR options from locaStorage * Initialize AMR options from locaStorage
*/ */
...@@ -17,7 +15,7 @@ import * as utils from '../amr/utils'; ...@@ -17,7 +15,7 @@ import * as utils from '../amr/utils';
await store.dispatch('initOptions'); await store.dispatch('initOptions');
/** /**
* Initialize mirrors list in store from DB * Initialize mirrors list in store from DB or repo
*/ */
utils.debug("Initialize mirrors"); utils.debug("Initialize mirrors");
await store.dispatch('initMirrors'); await store.dispatch('initMirrors');
...@@ -28,10 +26,22 @@ import * as utils from '../amr/utils'; ...@@ -28,10 +26,22 @@ import * as utils from '../amr/utils';
utils.debug("Initialize mangas"); utils.debug("Initialize mangas");
await store.dispatch('initMangasFromDB'); 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 // Starts message handling
utils.debug("Initialize message handler"); utils.debug("Initialize message handler");
Handler.handle(); 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 * 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 * 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']; ...@@ -17,7 +17,8 @@ const contentCss = ['content/content.css'];
class HandleManga { class HandleManga {
handle(message, sender) { handle(message, sender) {
let key = utils.mangaKey(message.url); let key;
if (message.url) key = utils.mangaKey(message.url);
switch (message.action) { switch (message.action) {
case "pagematchurls": case "pagematchurls":
// content script included, test if a mirror match the page and load AMR in tab // content script included, test if a mirror match the page and load AMR in tab
...@@ -46,6 +47,9 @@ class HandleManga { ...@@ -46,6 +47,9 @@ class HandleManga {
.then(() => store.dispatch('readManga', message)); // set reading to current chapter .then(() => store.dispatch('readManga', message)); // set reading to current chapter
case "importSamples": case "importSamples":
return store.dispatch("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");
} }
} }
......
This diff is collapsed.
...@@ -5,6 +5,8 @@ import notifications from '../../amr/notifications'; ...@@ -5,6 +5,8 @@ import notifications from '../../amr/notifications';
import statsEvents from '../../amr/stats-events'; import statsEvents from '../../amr/stats-events';
import * as utils from "../../amr/utils"; import * as utils from "../../amr/utils";
import samples from "../../amr/samples"; import samples from "../../amr/samples";
import amrUpdater from '../../amr/amr-updater';
import iconHelper from '../../amr/icon-helper';
/** /**
* initial state of the mangas module * initial state of the mangas module
...@@ -85,6 +87,7 @@ const actions = { ...@@ -85,6 +87,7 @@ const actions = {
console.error(e) console.error(e)
} }
}, },
/** /**
* Change manga display mode * Change manga display mode
* @param {*} vuex object * @param {*} vuex object
...@@ -106,6 +109,8 @@ const actions = { ...@@ -106,6 +109,8 @@ const actions = {
let mg = state.all.find(manga => manga.key === key); let mg = state.all.find(manga => manga.key === key);
dispatch('updateManga', mg); dispatch('updateManga', mg);
statsEvents.trackResetManga(mg); statsEvents.trackResetManga(mg);
// refresh badge
amrUpdater.refreshBadgeAndIcon();
}, },
/** /**
* Read a manga : update latest read chapter if the current chapter is more recent than the previous one * Read a manga : update latest read chapter if the current chapter is more recent than the previous one
...@@ -133,6 +138,8 @@ const actions = { ...@@ -133,6 +138,8 @@ const actions = {
statsEvents.trackReadManga(mg); statsEvents.trackReadManga(mg);
statsEvents.trackReadMangaChapter(mg); statsEvents.trackReadMangaChapter(mg);
} }
// refresh badge
amrUpdater.refreshBadgeAndIcon();
}, },
/** /**
* Get list of chapters for a manga * Get list of chapters for a manga
...@@ -254,6 +261,7 @@ const actions = { ...@@ -254,6 +261,7 @@ const actions = {
} catch (e) { } catch (e) {
// implementation was not loaded // implementation was not loaded
console.error("Impossible to load mirror implementation " + mg.mirror); console.error("Impossible to load mirror implementation " + mg.mirror);
console.error(e);
reject(mg); reject(mg);
} }
}); });
...@@ -261,6 +269,33 @@ const actions = { ...@@ -261,6 +269,33 @@ const actions = {
return Promise.resolve(mg); return Promise.resolve(mg);
} }
}, },
/**
* Update all mangas chapters lists
* @param {*} param0
* @param {*} message
*/
async updateChaptersLists({ dispatch, commit, getters, state }) {
// spin the badge
iconHelper.spinIcon();
// update last update ts
dispatch("setOption", {key: "lastChaptersUpdate", value: new Date().getTime()});
// refresh all mangas chapters lists
let refchaps = [];
for (let mg of state.all) {
refchaps.push(
// we catch the reject from the promise to prevent the Promise.all to fail due to a rejected promise. Thanks to that, Promise.all will wait that each manga is refreshed, even if it does not work
dispatch("refreshLastChapters", {url: mg.url}).catch(e => e)
);
}
await Promise.all(refchaps); // wait