Commit 4795c4bf authored by Duhoux Pierre-Louis's avatar Duhoux Pierre-Louis

Finish search and timers

parent 3d41282d
......@@ -555,8 +555,52 @@
"message": "Try to reload",
"description": "Message displayed on a scan if it failed to be loaded so the user can try to force its reload"
},
"search_title": {
"message": "Search mangas",
"description": "Search dialog title"
},
"search_button": {
"message": "Search",
"description": "Button label in search panel"
},
"search_result_add": {
"message": "Add manga $1 from $2 to your reading list",
"description": "Mirror icon button in search panel to add a manga to reading list. $1 is manga name, $2 is website name"
},
"search_result_inlist": {
"message": "Manga $1 from $2 is in your reading list",
"description": "Mirror icon button in search panel when a manga is already in reading list. $1 is manga name, $2 is website name"
},
"search_mirror_disable": {
"message": "Click to disable search on $1",
"description": "Mirror icon button over search bar. Label click button to disable search on mirror. $1 is website name"
},
"search_mirror_enable": {
"message": "Click to enable search on $1",
"description": "Mirror icon button over search bar. Label click button to enable search on mirror. $1 is website name"
},
"refresh_chapters": {
"message": "Refresh chapters now",
"description": "Refresh chapters button in refresh panel"
},
"refresh_mirrors": {
"message": "Refresh websites now",
"description": "Refresh websites button in refresh panel"
},
"refresh_lists": {
"message": "Reset manga lists now",
"description": "Reset manga lists button in refresh panel"
},
"refresh_last": {
"message": "Refreshed $1 ago",
"description": "Label to tell last time something was refreshed. $1 is an i18n string telling last time it was refreshed"
},
"refresh_lists_desc": {
"message": "Delete lists of manga stored in local database. These lists are retrieved from websites on which we can get the whole manga list that can be read on the website. Deleting it does not affect your current reading list, these lists will be reloaded on next search for the enabled websites which support this functionality",
"description": "Tooltip of the reset stored manga lists button in right panel"
},
"refresh_lists_nb": {
"message": "$1 lists containing $2 entries",
"description": "Label to tell the number of stored lists. $1 is the number of lists and $2 the number of manga names and urls stored"
}
}
\ No newline at end of file
......@@ -15,7 +15,7 @@ class Updater {
checkChaptersUpdates() {
let lastUpdt = store.state.options.lastChaptersUpdate;
let frequency = store.state.options.updatechap;
if (lastUpdt + frequency < new Date().getTime()) {
if (lastUpdt + frequency < Date.now()) {
// time to refresh !
store.dispatch("updateChaptersLists");
}
......@@ -27,7 +27,7 @@ class Updater {
checkMirrorsUpdates() {
let lastUpdt = store.state.options.lastMirrorsUpdate;
let frequency = store.state.options.updatemg;
if (lastUpdt + frequency < new Date().getTime()) {
if (lastUpdt + frequency < Date.now()) {
// time to refresh !
store.dispatch("updateMirrorsLists");
}
......
......@@ -13,8 +13,8 @@ export default class {
this.mirror = obj.mirror;
this.name = obj.name;
this.url = obj.url;
this.lastChapterReadURL = obj.lastChapterReadURL || null;
this.lastChapterReadName = obj.lastChapterReadName || null;
this.lastChapterReadURL = obj.lastChapterReadURL;
this.lastChapterReadName = obj.lastChapterReadName;
this.listChaps = [];
if (obj.listChaps && typeof obj.listChaps === "string") {
this.listChaps = JSON.parse(obj.listChaps)
......@@ -30,7 +30,7 @@ export default class {
} else if (obj.cats) {
this.cats = obj.cats;
}
this.ts = obj.ts || Math.round((new Date()).getTime() / 1000);
this.ts = obj.ts || Math.round(Date.now() / 1000);
this.upts = obj.upts || 0;
}
}
\ No newline at end of file
......@@ -44,7 +44,7 @@ class MirrorsImpl {
};
script.onerror = reject;
script.async = true;
script.src = scripturl + "?ts=" + new Date().getTime(); // do not cache script implementation when loading it
script.src = scripturl + "?ts=" + Date.now(); // do not cache script implementation when loading it
});
}
......
......@@ -6,28 +6,20 @@ export default [
name: "One Piece",
mirror: "Manga Reader",
url: "https://www.mangareader.net/one-piece",
lastChapterReadName: "1",
lastChapterReadURL: "http://www.mangareader.net/one-piece/1",
},
{
name: "Naruto",
mirror: "Manga Reader",
url: "https://www.mangareader.net/naruto",
lastChapterReadName: "1",
lastChapterReadURL: "http://www.mangareader.net/naruto/1",
},
{
name: "Bleach",
mirror: "Manga Reader",
url: "https://www.mangareader.net/bleach",
lastChapterReadName: "1",
lastChapterReadURL: "http://www.mangareader.net/bleach/1",
},
{
name: "Fairy Tail",
mirror: "Manga Reader",
url: "https://www.mangareader.net/fairy-tail",
lastChapterReadName: "1",
lastChapterReadURL: "http://www.mangareader.net/fairy-tail/1",
}
];
\ No newline at end of file
......@@ -131,25 +131,103 @@ class StoreDB {
}
// Accessors for mangas list (full list of mangas from mirror)
/**
* Store a list of mangas for a mirror implementation
* @param {*} mirror
* @param {*} list
*/
storeListOfMangaForMirror(mirror, list) {
let store = this;
return this.checkInit()
.then(() => {
return new Promise((resolve, result) => {
//TODO
resolve({});
let transaction = store.db.transaction(["mangalists"], "readwrite");
transaction.onerror = function (event) {
reject("Impossible to store mangalists " + mirror + ". Error code : " + event.target.errorCode);
};
let objectStore = transaction.objectStore("mangalists");
let request = objectStore.put({
mirrorName: mirror,
list: list
});
request.onsuccess = function (event) {
resolve(event.target.result);
};
});
});
}
/**
* Get a list of manga stored from a mirror implementation
* @param {*} mirror
*/
getListOfMangaForMirror(mirror) {
let store = this;
return this.checkInit()
.then(() => {
return new Promise((resolve, result) => {
//TODO
resolve([]);
let transaction = store.db.transaction(["mangalists"]);
let objectStore = transaction.objectStore("mangalists");
let objectStoreRequest = objectStore.get(mirror);
objectStoreRequest.onsuccess = function (event) {
let obj = objectStoreRequest.result;
resolve(obj ? obj.list : obj);
};
objectStoreRequest.onerror = function (event) {
resolve([]);
}
});
});
}
/**
* Get how many lists and manga names are stored
*/
getListsOfMangaStats() {
let store = this;
return this.checkInit()
.then(() => {
return new Promise((resolve, result) => {
let transaction = store.db.transaction(["mangalists"]);
let objectStore = transaction.objectStore("mangalists");
let stat = {nb: 0, nbmangas: 0}
objectStore.openCursor().onsuccess = function (event) {
let cursor = event.target.result;
if (cursor) {
stat.nb++;
stat.nbmangas += cursor.value.list.length;
cursor.continue();
}
else {
resolve(stat);
}
};
});
});
}
/**
* Clear all stored lists of mangas for mirror implementations
*/
deleteAllListOfManga() {
let store = this;
return this.checkInit()
.then(() => {
return new Promise((resolve, result) => {
let transaction = store.db.transaction(["mangalists"], "readwrite");
transaction.onerror = function (event) {
reject("Impossible to clear mangalists. Error code : " + event.target.errorCode);
};
let objectStore = transaction.objectStore("mangalists");
let objectStoreRequest = objectStore.clear();
objectStoreRequest.onsuccess = function (event) {
resolve();
};
});
});
}
// Accessors for reading storage
storeManga(manga) {
let store = this;
......@@ -190,6 +268,9 @@ class StoreDB {
});
});
}
/**
* Return the stored list of manga (reading list)
*/
getMangaList() {
let store = this;
return this.checkInit()
......
import store from '../store';
import i18n from './i18n';
/**
* Test if current browser is Firefox
......@@ -144,4 +145,21 @@ export function mangaKey(url) {
return "_no_key_"; // should not happen !
}
return extractRootDomain(url) + "/" + afterHostURL(url);
}
\ No newline at end of file
}
/**
* Tells in human language how much time has been spent since this ts
* @param {*} ts
*/
export function lasttime(diffts) {
let diff = Math.floor( (diffts) / 1000 );
if (diff < 0) diff = 0;
if (diff < 60) return i18n("options_seconds", diff)
diff = Math.floor(diff / 60);
if (diff < 60) return i18n("options_minutes", diff)
diff = Math.floor(diff / 60);
if (diff < 24) return i18n("options_hours", diff)
diff = Math.floor(diff / 24);
if (diff < 7) return i18n("options_days", diff)
diff = Math.floor(diff / 7);
return i18n("options_weeks", diff) //TODO months , years ? --> not needed in AMR yet
}
......@@ -66,7 +66,7 @@ class HandleManga {
// check if mirror can list all mangas
if (impl.canListFullMangas) {
// check if mirror list is in local db and filter
let list = storedb.getListOfMangaForMirror(message.mirror)
let list = await storedb.getListOfMangaForMirror(message.mirror)
if (list && list.length > 0) {
// filter entries on search phrase
resolve(this.resultSearchFromArray(
......@@ -135,7 +135,7 @@ class HandleManga {
// Load mirror implementation from repo (try next repo if previous fail)
for (let repo of store.state.options["impl_repositories"]) {
let url = repo + mir.jsFile;
if (url.indexOf("localhost") > 0) url += "?ts=" + new Date().getTime();
if (url.indexOf("localhost") > 0) url += "?ts=" + Date.now();
impl = await Axios.get(url).catch(() => { }); // ignore error, jump to next repo
if (impl) break;
}
......
......@@ -53,11 +53,11 @@ class HandleKey {
//Left key or A
if ((e.which == 37) || (e.which == 65)) {
doubleTap = false;
if (self.lastpresstime !== undefined && new Date().getTime() - self.lastpresstime < 500 && self.dirpress !== undefined && self.dirpress == 1) {
if (self.lastpresstime !== undefined && Date.now() - self.lastpresstime < 500 && self.dirpress !== undefined && self.dirpress == 1) {
doubleTap = true;
}
self.dirpress = 1;
self.lastpresstime = new Date().getTime();
self.lastpresstime = Date.now();
//Get first visible image
curimg = self.whichImageIsFirst(true);
if (curimg !== null && curimg.size() > 0) {
......@@ -89,10 +89,10 @@ class HandleKey {
//Right key or D
if ((e.which == 39) || (e.which == 68)) {
doubleTap = false;
if (self.lastpresstime !== undefined && new Date().getTime() - self.lastpresstime < 500 && self.dirpress !== undefined && self.dirpress == 2) {
if (self.lastpresstime !== undefined && Date.now() - self.lastpresstime < 500 && self.dirpress !== undefined && self.dirpress == 2) {
doubleTap = true;
}
self.lastpresstime = new Date().getTime();
self.lastpresstime = Date.now();
self.dirpress = 2;
//If we are at top of the page --> move to first scan
if (window.pageYOffset === 0) {
......
......@@ -8,9 +8,9 @@
<!-- Category name and tooltip -->
<v-tooltip v-if="!staticCats" top content-class="icon-ttip" class="cat-name">
<span class="cat-name" slot="activator">{{cat.name}}</span>
<span v-if="cat.state==='include'">{{i18n("list_cat_include", cat.name)}}</span>
<span v-if="cat.state==='exclude'">{{i18n("list_cat_exclude", cat.name)}}</span>
<span v-if="!cat.state">{{i18n("list_cat_no", cat.name)}}</span>
<span v-if="cat.state==='include'" v-html='i18n("list_cat_include", cat.name)'></span>
<span v-if="cat.state==='exclude'" v-html='i18n("list_cat_exclude", cat.name)'></span>
<span v-if="!cat.state" v-html='i18n("list_cat_no", cat.name)'></span>
</v-tooltip>
<span v-if="staticCats" class="cat-name">{{cat}}</span>
<!-- Icon only me -->
......
......@@ -185,7 +185,7 @@ export default {
},
// number of days since last chapter has been published
timeUpdated() {
let nbdays = Math.floor((new Date().getTime() - this.manga.upts) / (1000 * 60 * 60 * 24));
let nbdays = Math.floor((Date.now() - this.manga.upts) / (1000 * 60 * 60 * 24));
return nbdays;
}
},
......
<template>
<v-container fluid>
<v-container fluid class="searchpanel">
<v-layout row>
<div class="mirrors-cont">
<SearchMirror v-for="ws in activatedWebsites" :key="ws.mirrorName" :mirror="ws" :search-phrase="search" @add-mangas="addMangas" />
</div>
</v-layout>
<v-layout row>
<v-flex xs4 offset-xs2>
<v-text-field v-model="searchwrite" @keyup.enter="search = searchwrite" />
<v-layout row class="searchbar">
<v-flex xs9>
<v-text-field v-model="searchwrite" @keyup.enter="launchSearch()" />
</v-flex>
<v-flex xs4>
<v-btn color="primary" @click="search = searchwrite" small>{{i18n("search_button")}}</v-btn>
<v-flex xs3>
<v-btn color="primary" @click="launchSearch()" small>{{i18n("search_button")}}</v-btn>
</v-flex>
</v-layout>
<v-layout row>
<!-- results area -->
<v-layout row class="search-results">
<v-container fluid>
<v-layout row v-for="res in results" :key="res.url">
{{res.name}} ({{res.mirror}})
<v-layout row v-for="(lst, key) in results" :key="key">
<!-- name of the manga -->
<v-flex xs4><strong>{{lst[0].name}}</strong></v-flex>
<!-- mirror icons buttons to add to list -->
<v-flex xs8>
<v-tooltip top content-class="icon-ttip" v-for="(mg, key) in lst" :key="key">
<div class="mirror-result-cont" slot="activator">
<img @click="addToList(mg)" :src="getIcon(mg.mirror)" :class="'mirror-icon ' + (isInList(mg) || mg.adding ? 'added' : '')" />
<v-icon v-if="isInList(mg)" color="green">mdi-check</v-icon>
<v-progress-circular indeterminate size="18" v-if="mg.adding" color="grey darken-4"></v-progress-circular>
</div>
<span v-if="isInList(mg)">{{i18n("search_result_inlist", mg.name, mg.mirror)}}</span>
<span v-else>{{i18n("search_result_add", mg.name, mg.mirror)}}</span>
</v-tooltip>
</v-flex>
</v-layout>
</v-container>
</v-layout>
......@@ -25,7 +39,9 @@
<script>
import i18n from "../../amr/i18n";
import browser from "webextension-polyfill";
import SearchMirror from "./SearchMirror";
import * as utils from "../../amr/utils";
export default {
data() {
......@@ -43,8 +59,59 @@ export default {
},
methods: {
i18n: (message, ...args) => i18n(message, ...args),
addMangas(mgs) {
this.results.push(...mgs);
/**
* Starts the search
*/
launchSearch() {
if (this.search === this.searchwrite) return
this.results = []
this.search = this.searchwrite
},
/**
* Add mangas to results
*/
addMangas(mgs, searchphrase) {
if (searchphrase !== this.search) return;
// rebuild list grouped by reduced mg name
let resGrouped = {}
this.results.forEach(lst => resGrouped[utils.formatMgName(lst[0].name)] = lst)
// add new mangas to grouped list
mgs.forEach(function(mg) {
mg.adding = false; // is currently be added to list (display progress)
let mgst = utils.formatMgName(mg.name)
if (resGrouped[mgst]) {
resGrouped[mgst].push(mg)
} else {
resGrouped[mgst] = [mg]
}
}.bind(this))
let lsts = []
Object.keys(resGrouped).forEach(key => lsts.push(resGrouped[key]))
// sort list of lists by length and alphabetically inside
lsts.sort((a, b) => a.length === b.length ? a[0].name.localeCompare(b[0].name) : b.length - a.length)
this.results = lsts
},
/**
* Adds a manga to reading list
*/
addToList: async function(mg) {
if (this.isInList(mg)) return;
mg.adding = true;
// call background because readManga uses jQuery (through refreshListChaps in website implementation) and its not present in popup
await browser.runtime.sendMessage(Object.assign({action: "readManga"}, mg));
mg.adding = false;
},
/**
* Returns mirror icon from its name
*/
getIcon(mirrorname) {
return this.$store.state.mirrors.all.find(mir => mir.mirrorName === mirrorname).mirrorIcon
},
/**
* True if manga is in reading list
*/
isInList(mg) {
return this.$store.state.mangas.all.findIndex(m => m.key === utils.mangaKey(mg.url)) !== -1
}
},
name: "Search",
......@@ -52,8 +119,37 @@ export default {
}
</script>
<style>
.searchpanel {
overflow: auto;
}
.mirrors-cont {
margin: auto;
}
.searchbar {
width: 450px;
margin: auto;
}
.mirror-result-cont {
display: inline-block;
position: relative;
width: 18px;
height: 18px;
margin: 4px;
cursor: pointer;
}
.mirror-result-cont > * {
position: absolute;
left:0;
top:0;
}
.mirror-result-cont > i {
font-size: 18px;
}
.mirror-icon.added {
opacity: 0.3;
}
.search-results {
font-size: 12px;
}
</style>
<template>
<div class="mirror-search">
<v-progress-circular v-if="searching" indeterminate :width="3" color="primary" class="mirror-progress"></v-progress-circular>
<img :src="mirror.mirrorIcon" :class="'mirror-search-icon ' + (disabled ? 'disabled': '')" @click.stop="disabled = !disabled" />
</div>
<v-tooltip top>
<div class="mirror-search" slot="activator">
<v-progress-circular v-if="searching" indeterminate :width="3" color="primary" class="mirror-progress"></v-progress-circular>
<img :src="mirror.mirrorIcon" :class="'mirror-search-icon ' + (disabled ? 'disabled': '')" @click.stop="disabled = !disabled" />
</div>
<span v-if="disabled">{{i18n('search_mirror_enable', mirror.mirrorName)}}</span>
<span v-else>{{i18n('search_mirror_disable', mirror.mirrorName)}}</span>
</v-tooltip>
</template>
<script>
......@@ -13,7 +17,7 @@ export default {
data() {
return {
searching: false, // currently searching
disabled: false // is disabled in search
disabled: localStorage["s." + this.mirror.mirrorName + ".disabled"] ? true : false // is disabled in search
};
},
props: [
......@@ -23,19 +27,28 @@ export default {
methods: {
i18n: (message, ...args) => i18n(message, ...args),
async search() {
if (this.disabled) return;
this.searching = true;
let searchinit = this.searchPhrase;
let mgs = await browser.runtime.sendMessage({
action: "searchList",
mirror: this.mirror.mirrorName,
search: this.searchPhrase
search: searchinit
});
this.$emit("add-mangas", mgs);
this.$emit("add-mangas", mgs, searchinit);
this.searching = false;
}
},
watch: {
"searchPhrase": function() {
this.search();
},
disabled: function(nv) {
if (nv) {
localStorage.setItem("s." + this.mirror.mirrorName + ".disabled", true);
} else {
localStorage.removeItem("s." + this.mirror.mirrorName + ".disabled");
}
}
},
name: "SearchMirror",
......
<template>
<v-container fluid text-xs-center>
<v-layout row>
<v-flex xs6 offset-xs3>
<v-btn color="primary"
class="refresh-button"
@click="updateChaps()"
:loading="loadingChapters"
:disabled="loadingChapters">
<v-icon>mdi-book-open-variant</v-icon>
{{i18n("refresh_chapters")}}
</v-btn>
{{i18n("refresh_last", lastchaps)}}
</v-flex>
</v-layout>
<v-layout row>
<v-flex xs6>
<v-btn color="primary"
class="refresh-button"
@click="updateMirrors()"
:loading="loadingMirrors"
:disabled="loadingMirrors">
<v-icon>mdi-earth</v-icon>
{{i18n("refresh_mirrors")}}
</v-btn>
{{i18n("refresh_last", lastmirs)}}
</v-flex>
<v-flex xs6>
<v-tooltip top>
<div slot="activator">
<v-btn color="primary"
class="refresh-button"
@click="resetLists()"
:loading="loadingLists"
:disabled="loadingLists">
<v-icon>mdi-format-list-bulleted</v-icon>
{{i18n("refresh_lists")}}
</v-btn>
<br/>{{i18n("refresh_lists_nb", stats.nb, stats.nbmangas)}}
</div>
<span>{{i18n("refresh_lists_desc")}}</span>
</v-tooltip>
</v-flex>
</v-layout>
</v-container>
</template>
<script>
import i18n from "../../amr/i18n";
import * as utils from "../../amr/utils";
import browser from "webextension-polyfill";
import storedb from "../../amr/storedb";
export default {
data() {
return {
/**
* Currently loading booleans for buttons display
*/
loadingChapters: false,
loadingMirrors: false,
loadingLists: false,
ticker: Date.now(),
stats: {nb:0,nbmangas:0}
};
},
mounted: async function() {
var self = this;
setInterval(function() {
self.$data.ticker = Date.now()
}, 1000);
this.stats = await storedb.getListsOfMangaStats()
},
computed: {
/**
* Convert timestamps in readable ... ago
*/
lastchaps: function() {
return utils.lasttime(
this.ticker - this.$store.state.options.lastChaptersUpdate
);
},
lastmirs: function() {
return utils.lasttime(
this.ticker - this.$store.state.options.lastMirrorsUpdate
);
}
},
methods: {
i18n: (message, ...args) => i18n(message, ...args),
/**
* 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
},
/**
* Reset manga lists stored in db
*/
async resetLists() {
this.loadingLists = true
await this.$store.dispatch("resetMirrorsMangaLists")
this.loadingLists = false
this.stats = await storedb.getListsOfMangaStats()
}
},
name: "Timers"
};
</script>
<style>
.refresh-button {
margin: 6px;
}
.refresh-button .btn__content {
text-transform: none;
}
.refresh-button i {
margin-right: 6px;
}
</style>
......@@ -10,25 +10,9 @@
<v-btn icon @click.stop="openSearch()">
<v-icon>mdi-magnify</v-icon>
</v-btn>
<v-menu bottom left>
<v-btn icon slot="activator">
<v-icon>more_vert</v-icon>
</v-btn>
<v-list>
<v-list-tile>
<v-list-tile-action>
<v-icon>mdi mdi-timer</v-icon>
</v-list-tile-action>
<v-list-tile-title>Timers</v-list-tile-title>
</v-list-tile>
<v-list-tile>