diff --git a/src/server/internalServer.cpp b/src/server/internalServer.cpp index 8b9ae36ff..4d344edc0 100644 --- a/src/server/internalServer.cpp +++ b/src/server/internalServer.cpp @@ -304,7 +304,7 @@ InternalServer::get_matching_if_none_match_etag(const RequestContext& r) const std::unique_ptr InternalServer::build_homepage(const RequestContext& request) { - return ContentResponse::build(*this, RESOURCE::templates::index_html, get_default_data(), "text/html; charset=utf-8"); + return ContentResponse::build(*this, RESOURCE::templates::index_html, get_default_data(), "text/html; charset=utf-8", true); } std::unique_ptr InternalServer::handle_meta(const RequestContext& request) diff --git a/src/server/internalServer.h b/src/server/internalServer.h index 0106014c6..c37c736f2 100644 --- a/src/server/internalServer.h +++ b/src/server/internalServer.h @@ -106,7 +106,7 @@ class InternalServer { std::string m_server_id; friend std::unique_ptr Response::build(const InternalServer& server); - friend std::unique_ptr ContentResponse::build(const InternalServer& server, const std::string& content, const std::string& mimetype); + friend std::unique_ptr ContentResponse::build(const InternalServer& server, const std::string& content, const std::string& mimetype, bool isHomePage); friend std::unique_ptr ItemResponse::build(const InternalServer& server, const RequestContext& request, const zim::Item& item); friend std::unique_ptr Response::build_500(const InternalServer& server, const std::string& msg); diff --git a/src/server/response.cpp b/src/server/response.cpp index b1e0ecf35..3ebec43d2 100644 --- a/src/server/response.cpp +++ b/src/server/response.cpp @@ -349,21 +349,21 @@ ContentResponse::ContentResponse(const std::string& root, bool verbose, bool wit add_header(MHD_HTTP_HEADER_CONTENT_TYPE, m_mimeType); } -std::unique_ptr ContentResponse::build(const InternalServer& server, const std::string& content, const std::string& mimetype) +std::unique_ptr ContentResponse::build(const InternalServer& server, const std::string& content, const std::string& mimetype, bool isHomePage) { return std::unique_ptr(new ContentResponse( server.m_root, server.m_verbose.load(), - server.m_withTaskbar, + server.m_withTaskbar && !isHomePage, server.m_withLibraryButton, server.m_blockExternalLinks, content, mimetype)); } -std::unique_ptr ContentResponse::build(const InternalServer& server, const std::string& template_str, kainjow::mustache::data data, const std::string& mimetype) { +std::unique_ptr ContentResponse::build(const InternalServer& server, const std::string& template_str, kainjow::mustache::data data, const std::string& mimetype, bool isHomePage) { auto content = render_template(template_str, data); - return ContentResponse::build(server, content, mimetype); + return ContentResponse::build(server, content, mimetype, isHomePage); } ItemResponse::ItemResponse(bool verbose, const zim::Item& item, const std::string& mimetype, const ByteRange& byterange) : diff --git a/src/server/response.h b/src/server/response.h index 76bb82f7a..8d113ad7f 100644 --- a/src/server/response.h +++ b/src/server/response.h @@ -79,8 +79,8 @@ class Response { class ContentResponse : public Response { public: ContentResponse(const std::string& root, bool verbose, bool withTaskbar, bool withLibraryButton, bool blockExternalLinks, const std::string& content, const std::string& mimetype); - static std::unique_ptr build(const InternalServer& server, const std::string& content, const std::string& mimetype); - static std::unique_ptr build(const InternalServer& server, const std::string& template_str, kainjow::mustache::data data, const std::string& mimetype); + static std::unique_ptr build(const InternalServer& server, const std::string& content, const std::string& mimetype, bool isHomePage = false); + static std::unique_ptr build(const InternalServer& server, const std::string& template_str, kainjow::mustache::data data, const std::string& mimetype, bool isHomePage = false); void set_taskbar(const std::string& bookName, const std::string& bookTitle); diff --git a/static/resources_list.txt b/static/resources_list.txt index 6c0570442..383c8228e 100644 --- a/static/resources_list.txt +++ b/static/resources_list.txt @@ -19,6 +19,10 @@ skin/jquery-ui/jquery-ui.theme.min.css skin/jquery-ui/jquery-ui.min.css skin/caret.png skin/taskbar.js +skin/langList.js +skin/categoryList.js +skin/iso6391To3.js +skin/isotope.pkgd.min.js skin/index.js skin/taskbar.css skin/block_external.js diff --git a/static/skin/categoryList.js b/static/skin/categoryList.js new file mode 100644 index 000000000..80d36e9da --- /dev/null +++ b/static/skin/categoryList.js @@ -0,0 +1,19 @@ +// eslint-disable-next-line no-unused-vars +const categoryList = { + "other": "Other", + "gutenberg": "Gutenberg", + "mooc": "Mooc", + "phet": "Phet", + "psiram": "Psiram", + "stack_exchange": "Stack Exchange", + "ted": "Ted", + "vikidia": "Vikidia", + "wikibooks": "Wikibooks", + "wikinews": "Wikinews", + "wikipedia": "Wikipedia", + "wikiquote": "Wikiquote", + "wikisource": "Wikisource", + "wikiversity": "Wikiversity", + "wikivoyage": "Wikivoyage", + "wiktionary": "Wiktionary" +} \ No newline at end of file diff --git a/static/skin/index.js b/static/skin/index.js index e0a063cac..88225de19 100644 --- a/static/skin/index.js +++ b/static/skin/index.js @@ -4,13 +4,25 @@ start: 0, count: viewPortToCount() }; + const filterTypes = ['lang', 'category', 'q']; + const bookMap = new Map(); + let footer; + let fadeOutDiv; + let iso; let isFetching = false; + let noResultInjected = false; + let params = new URLSearchParams(window.location.search); let timer; function queryUrlBuilder() { let url = `${root}/catalog/search?`; url += Object.keys(incrementalLoadingParams).map(key => `${key}=${incrementalLoadingParams[key]}`).join("&"); - return url; + params.forEach((value, key) => {url+= value ? `&${key}=${value}` : ''}); + return (url); + } + + function htmlEncode(str) { + return str.replace(/[\u00A0-\u9999<>\&]/gim, (i) => `&#${i.charCodeAt(0)};`); } function viewPortToCount(){ @@ -21,7 +33,7 @@ return node.querySelector(query).innerHTML; } - function generateBookHtml(book) { + function generateBookHtml(book, sort = false) { const link = book.querySelector('link').getAttribute('href'); const title = getInnerHtml(book, 'title'); const description = getInnerHtml(book, 'summary'); @@ -30,35 +42,143 @@ const articleCount = getInnerHtml(book, 'articleCount'); const mediaCount = getInnerHtml(book, 'mediaCount'); - return ``; + return linkTag; } - async function loadAndDisplayBooks() { - if (isFetching) return; - isFetching = true; - fetch(queryUrlBuilder()).then(async (resp) => { + function toggleFooter(show=false) { + if (show) { + footer.style.display = 'block'; + } else { + footer.style.display = 'none'; + fadeOutDiv.style.display = 'block'; + } + } + + async function loadBooks() { + const loader = document.querySelector('.loader'); + loader.style.display = 'block'; + return await fetch(queryUrlBuilder()).then(async (resp) => { const data = new window.DOMParser().parseFromString(await resp.text(), 'application/xml'); const books = data.querySelectorAll('entry'); - let bookHtml = ''; - books.forEach((book) => {bookHtml += generateBookHtml(book)}); - document.querySelector('.book__list').innerHTML += bookHtml; + books.forEach((book, idx) => { + bookMap.set(getInnerHtml(book, 'id'), idx); + }); incrementalLoadingParams.start += books.length; - if (books.length < incrementalLoadingParams.count) { + if (parseInt(data.querySelector('totalResults').innerHTML) === bookMap.size) { incrementalLoadingParams.count = 0; + toggleFooter(true); + } else { + toggleFooter(); } - isFetching = false; + loader.style.display = 'none'; + return books; }); } + async function loadAndDisplayOptions(nodeQuery, query) { + // currently taking an object in place of query, will replace it with query while fetching data from backend later on. + document.querySelector(nodeQuery).innerHTML += Object.keys(query) + .map((option) => {return ``}) + .join(''); + } + + function checkAndInjectEmptyMessage() { + if (!bookMap.size) { + if (!noResultInjected) { + noResultInjected = true; + iso.remove(document.getElementsByClassName('book__list')[0].getElementsByTagName('a')); + iso.layout(); + const spanTag = document.createElement('span'); + spanTag.setAttribute('class', 'noResults'); + spanTag.innerHTML = `No result. Would you like to reset filter?`; + document.querySelector('body').append(spanTag); + spanTag.getElementsByTagName('a')[0].onclick = (event) => { + event.preventDefault(); + window.history.pushState({}, null, `${window.location.href.split('?')[0]}?lang=`); + resetAndFilter(); + filterTypes.forEach(key => {document.getElementsByName(key)[0].value = params.get(key) || ''}); + }; + } + return true; + } else if (noResultInjected) { + noResultInjected = false; + document.getElementsByClassName('noResults')[0].remove(); + } + return false; + } + + async function loadAndDisplayBooks(sort = false) { + if (isFetching) return; + isFetching = true; + await loadAndDisplayBooksUnguarded(sort); + isFetching = false; + } + + async function loadAndDisplayBooksUnguarded(sort) { + let books = await loadBooks(); + if (checkAndInjectEmptyMessage()) {return} + const booksToFilter = new Set(); + const booksToDelete = new Set(); + iso.arrange({ + filter: function (idx, elem) { + const id = elem.getAttribute('data-id'); + const retVal = bookMap.has(id); + if (retVal) { + booksToFilter.add(id); + if (sort) { + elem.setAttribute('data-idx', bookMap[id]); + iso.updateSortData(elem); + } + } else { + booksToDelete.add(elem); + } + return retVal; + } + }); + books = [...books].filter((book) => {return !booksToFilter.has(getInnerHtml(book, 'id'))}); + booksToDelete.forEach(book => {iso.remove(book);}); + books.forEach((book) => {iso.insert(generateBookHtml(book, sort))}); + } + + async function resetAndFilter(filterType = '', filterValue = '') { + isFetching = false; + incrementalLoadingParams.start = 0; + incrementalLoadingParams.count = viewPortToCount(); + fadeOutDiv.style.display = 'none'; + bookMap.clear(); + params = new URLSearchParams(window.location.search); + if (filterType) { + params.set(filterType, filterValue); + window.history.pushState({}, null, `${window.location.href.split('?')[0]}?${params.toString()}`); + } + await loadAndDisplayBooks(true); + } + + window.addEventListener('popstate', async () => { + await resetAndFilter(); + filterTypes.forEach(key => {document.getElementsByName(key)[0].value = params.get(key) || ''}); + }); + async function loadSubset() { - if (incrementalLoadingParams.count && window.innerHeight + window.scrollY >= document.body.offsetHeight) { - loadAndDisplayBooks(); + if (window.innerHeight + window.scrollY >= document.body.offsetHeight) { + if (incrementalLoadingParams.count) { + loadAndDisplayBooks(); + } + else { + fadeOutDiv.style.display = 'none'; + } } } @@ -73,6 +193,37 @@ window.addEventListener('scroll', loadSubset); window.onload = async () => { - loadAndDisplayBooks(); + iso = new Isotope( '.book__list', { + itemSelector: '.book', + getSortData:{ + weight: function( itemElem ) { + const index = itemElem.getAttribute('data-idx'); + return index ? parseInt(index) : Infinity; + } + }, + sortBy: 'weight' + }); + footer = document.getElementById('kiwixfooter'); + fadeOutDiv = document.getElementById('fadeOut'); + await loadAndDisplayBooks(); + await loadAndDisplayOptions('#languageFilter', langList); + await loadAndDisplayOptions('#categoryFilter', categoryList); + filterTypes.forEach((filter) => { + const filterTag = document.getElementsByName(filter)[0]; + filterTag.addEventListener('change', () => {resetAndFilter(filterTag.name, filterTag.value)}); + }); + params.forEach((value, key) => {document.getElementsByName(key)[0].value = value}); + document.getElementById('kiwixSearchForm').onsubmit = (event) => {event.preventDefault()}; + if (!window.location.search) { + const browserLang = navigator.language.split('-')[0]; + if (browserLang.length === 3) { + document.getElementById('languageFilter').value = browserLang; + langFilter.dispatchEvent(new Event('change')); + } else { + const langFilter = document.getElementById('languageFilter'); + langFilter.value = iso6391To3[browserLang]; + langFilter.dispatchEvent(new Event('change')); + } + } } })(); diff --git a/static/skin/iso6391To3.js b/static/skin/iso6391To3.js new file mode 100644 index 000000000..540f3eb8d --- /dev/null +++ b/static/skin/iso6391To3.js @@ -0,0 +1,124 @@ +// eslint-disable-next-line no-unused-vars +const iso6391To3 = { + "aa": "aar", + "af": "afr", + "ak": "aka", + "am": "amh", + "ar": "ara", + "as": "asm", + "az": "aze", + "ba": "bak", + "be": "bel", + "bg": "bul", + "bm": "bam", + "bn": "ben", + "bo": "bod", + "br": "bre", + "bs": "bos", + "ca": "cat", + "ce": "che", + "co": "cos", + "cs": "ces", + "cv": "chv", + "cy": "cym", + "da": "dan", + "de": "deu", + "dz": "dzo", + "ee": "ewe", + "en": "eng", + "es": "spa", + "et": "est", + "eu": "eus", + "fa": "fas", + "ff": "ful", + "fi": "fin", + "fo": "fao", + "fr": "fra", + "ga": "gle", + "gl": "glg", + "gn": "grn", + "gu": "guj", + "gv": "glv", + "ha": "hau", + "he": "heb", + "hi": "hin", + "hr": "hrv", + "hu": "hun", + "hy": "hye", + "id": "ind", + "ig": "ibo", + "is": "isl", + "it": "ita", + "iu": "iku", + "ja": "jpn", + "jv": "jav", + "ka": "kat", + "ki": "kik", + "kk": "kaz", + "km": "khm", + "kn": "kan", + "ko": "kor", + "ks": "kas", + "ku": "kur", + "kw": "cor", + "ky": "kir", + "lb": "ltz", + "lg": "lug", + "ln": "lin", + "lo": "lao", + "lt": "lit", + "lv": "lav", + "mg": "mlg", + "mi": "mri", + "mk": "mkd", + "ml": "mal", + "mn": "mon", + "mr": "mar", + "mt": "mlt", + "my": "mya", + "nl": "nld", + "ny": "nya", + "om": "orm", + "pl": "pol", + "pt": "por", + "qu": "que", + "rm": "roh", + "rn": "run", + "ro": "ron", + "ru": "rus", + "rw": "kin", + "sa": "san", + "sd": "snd", + "sg": "sag", + "si": "sin", + "sk": "slk", + "sl": "slv", + "sn": "sna", + "so": "som", + "sq": "sqi", + "sr": "srp", + "ss": "ssw", + "sv": "swe", + "ta": "tam", + "te": "tel", + "tg": "tgk", + "th": "tha", + "ti": "tir", + "tk": "tuk", + "tn": "tsn", + "tr": "tur", + "ts": "tso", + "tt": "tat", + "ug": "uig", + "uk": "ukr", + "ur": "urd", + "uz": "uzb", + "ve": "ven", + "vi": "vie", + "wa": "wln", + "wo": "wol", + "xh": "xho", + "yo": "yor", + "zh": "zho", + "zu": "zul" +} \ No newline at end of file diff --git a/static/skin/isotope.pkgd.min.js b/static/skin/isotope.pkgd.min.js new file mode 100644 index 000000000..7ca671cbe --- /dev/null +++ b/static/skin/isotope.pkgd.min.js @@ -0,0 +1,12 @@ +/*! + * Isotope PACKAGED v3.0.6 + * + * Licensed GPLv3 for open source use + * or Isotope Commercial License for commercial use + * + * https://isotope.metafizzy.co + * Copyright 2010-2018 Metafizzy + */ + +!function(t,e){"function"==typeof define&&define.amd?define("jquery-bridget/jquery-bridget",["jquery"],function(i){return e(t,i)}):"object"==typeof module&&module.exports?module.exports=e(t,require("jquery")):t.jQueryBridget=e(t,t.jQuery)}(window,function(t,e){"use strict";function i(i,s,a){function u(t,e,o){var n,s="$()."+i+'("'+e+'")';return t.each(function(t,u){var h=a.data(u,i);if(!h)return void r(i+" not initialized. Cannot call methods, i.e. "+s);var d=h[e];if(!d||"_"==e.charAt(0))return void r(s+" is not a valid method");var l=d.apply(h,o);n=void 0===n?l:n}),void 0!==n?n:t}function h(t,e){t.each(function(t,o){var n=a.data(o,i);n?(n.option(e),n._init()):(n=new s(o,e),a.data(o,i,n))})}a=a||e||t.jQuery,a&&(s.prototype.option||(s.prototype.option=function(t){a.isPlainObject(t)&&(this.options=a.extend(!0,this.options,t))}),a.fn[i]=function(t){if("string"==typeof t){var e=n.call(arguments,1);return u(this,t,e)}return h(this,t),this},o(a))}function o(t){!t||t&&t.bridget||(t.bridget=i)}var n=Array.prototype.slice,s=t.console,r="undefined"==typeof s?function(){}:function(t){s.error(t)};return o(e||t.jQuery),i}),function(t,e){"function"==typeof define&&define.amd?define("ev-emitter/ev-emitter",e):"object"==typeof module&&module.exports?module.exports=e():t.EvEmitter=e()}("undefined"!=typeof window?window:this,function(){function t(){}var e=t.prototype;return e.on=function(t,e){if(t&&e){var i=this._events=this._events||{},o=i[t]=i[t]||[];return o.indexOf(e)==-1&&o.push(e),this}},e.once=function(t,e){if(t&&e){this.on(t,e);var i=this._onceEvents=this._onceEvents||{},o=i[t]=i[t]||{};return o[e]=!0,this}},e.off=function(t,e){var i=this._events&&this._events[t];if(i&&i.length){var o=i.indexOf(e);return o!=-1&&i.splice(o,1),this}},e.emitEvent=function(t,e){var i=this._events&&this._events[t];if(i&&i.length){i=i.slice(0),e=e||[];for(var o=this._onceEvents&&this._onceEvents[t],n=0;n