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");
}
}
......
This diff is collapsed.
......@@ -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';
/**
* initial state of the mangas module
......@@ -85,6 +87,7 @@ const actions = {
console.error(e)
}
},
/**
* Change manga display mode
* @param {*} vuex object
......@@ -106,6 +109,8 @@ const actions = {
let mg = state.all.find(manga => manga.key === key);
dispatch('updateManga', 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
......@@ -133,6 +138,8 @@ const actions = {
statsEvents.trackReadManga(mg);
statsEvents.trackReadMangaChapter(mg);
}
// refresh badge
amrUpdater.refreshBadgeAndIcon();
},
/**
* Get list of chapters for a manga
......@@ -254,6 +261,7 @@ const actions = {
} catch (e) {
// implementation was not loaded
console.error("Impossible to load mirror implementation " + mg.mirror);
console.error(e);
reject(mg);
}
});
......@@ -261,6 +269,33 @@ const actions = {
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 for everything to be updated
//update badges and icon state
amrUpdater.refreshBadgeAndIcon();
},
/**
* Change the read top on a manga
* @param {*} vuex object
......@@ -282,6 +317,8 @@ const actions = {
}
}
}
// refresh badge
amrUpdater.refreshBadgeAndIcon();
},
/**
* Import sample mangas on user request
......
import storedb from '../../amr/storedb'
import Axios from 'axios'
import * as utils from '../../amr/utils'
import iconHelper from '../../amr/icon-helper';
import mirrorsImpl from '../../amr/mirrors-impl';
/**
* initial state of the mirrors module
......@@ -38,29 +40,17 @@ const actions = {
* Get mirrors from local database, fetch it from repository if empty
* @param {*} param0
*/
async initMirrors({ commit, dispatch, rootState }) {
async initMirrors({ commit, dispatch }) {
let websites = await storedb.getWebsites(); // Get mirrors from local database
if (!websites.length) {
// No mirrors known yet, get the list
// Try all repos --> first to work wins.
for (let repo of rootState.options["impl-repositories"]) {
let ws = await Axios.get(repo + "websites.json");
if (ws && ws.data) {
let updts = []
for (let w of ws.data) {
w.activated = true;
updts.push(dispatch("updateMirror", w));
}
Promise.all(updts); // do not wait that all implementations are in db... few seconds. if we need to wait for it, just add await in front of Promise.all
websites = ws.data;
}
}
websites = await dispatch("updateMirrorsLists");
}
if (!websites.length) {
document.dispatchEvent(new CustomEvent("mirrorsError"));
} else {
// set mirrors list in store
commit('setMirrors', websites);
document.dispatchEvent(new CustomEvent("mirrorsLoaded"));
}
},
/**
......@@ -74,8 +64,50 @@ const actions = {
},
// update mirrors from repository
updateFromRepo() {
// TODO
async updateMirrorsLists({ commit, dispatch, rootState }) {
// set the blue badge
iconHelper.setBlueIcon();
// update last update ts
dispatch("setOption", {key: "lastMirrorsUpdate", value: new Date().getTime()});
let websites = [];
let config = {
headers: {
'Content-Type': 'application/json',
'Cache-Control' : 'no-cache'
}
};
// Try all repos --> first to work wins.
for (let repo of rootState.options["impl-repositories"]) {
let ws = await Axios.get(repo + "websites.json", config);
if (ws && ws.data) {
let updts = []
for (let w of ws.data) {
w.activated = true;
updts.push(
dispatch("updateMirror", w).catch(e => e) // avoid blocking the Promise.all due to an update failure
);
}
// do not wait that all implementations are in db... few seconds. as the stores have been updated instantly, we do not need to wait for it to be in db
Promise.all(updts);
websites = ws.data;
}
}
if (!websites.length) {
document.dispatchEvent(new CustomEvent("mirrorsError"));
} else {
// set mirrors list in store
commit('setMirrors', websites);
}
// reset implementations
mirrorsImpl.resetImplementations();
//update badges and icon state
iconHelper.resetIcon();
return websites;
}
}
......
import storedb from '../../amr/storedb'
import Axios from 'axios';
/**
* Default options of AMR.
......@@ -15,13 +14,14 @@ const default_options = {
"https://raw.github.com/AllMangasReader-dev/mirrors/master/"
],
shownotifications: 1, //display notifications on new chapter
nocount: 1, // 1 : display gray sharingan and normal if new chaps; 0 : badge
notificationtimer: 0, //time to clear notification auto
/**
* Options used in content scripts (included in mangas pages)
*/
displayChapters: 1, // display scans as a book
newbar: 1,
newbar: 1, // display one navigation bar on top
/**
* mode = 1 --> images are displayed on top of one another
* mode = 2 --> images are displayed two by two occidental reading mode
......@@ -35,7 +35,8 @@ const default_options = {
prefetch: 1, // load next chapter in background while reading
groupmgs: 1, // group manga with similar name (one piece and One Piece)
lrkeys: 1, // use arrows keys to read chapter
rightnext: 1,
rightnext: 1, // arrow right goes to next chapter at bottom
//TO IMPLEMENT
load: 0, //See loading progression in the title bar
imgorder: 0, //Load scans in order
......@@ -49,7 +50,13 @@ const default_options = {
colornotfollow: "blue-grey", //DONE // color of mangas which are not followed
/** Updates options */
updatechap: 1800000, // update chapters frequency
updatechap: 1800000, //DONE // update chapters frequency
updatemg: 86400000, //DONE // update mirrors frequency
checkmgstart: 0, //DONE // update chapters lists on startup
refreshspin: 1, // spin the icon while loading chapters
savebandwidth: 0, // save bandwidth while loading chapters
displayzero: 1, // display a grey zero when no new chapter
nocount: 1, // 1 : display gray sharingan and normal if new chaps; 0 : badge