diff options
95 files changed, 1052 insertions, 897 deletions
diff --git a/app/build.gradle b/app/build.gradle index 5fc249bf..4025568a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -129,6 +129,7 @@ android { main.java.srcDirs += 'src/main/kotlin' test.java.srcDirs += 'src/test/kotlin' androidTest.java.srcDirs += 'src/androidTest/kotlin' + main.assets.srcDirs += ['src/web/assets'] } packagingOptions { diff --git a/app/src/main/assets/.babelrc b/app/src/main/assets/.babelrc deleted file mode 100644 index 7302f727..00000000 --- a/app/src/main/assets/.babelrc +++ /dev/null @@ -1,9 +0,0 @@ -{ - "presets": [ - ["env",{ - "targets": { - "browsers": ["android >= 36", "chrome >= 51"] - } - }] - ] -}
\ No newline at end of file diff --git a/app/src/main/assets/.gitignore b/app/src/main/assets/.gitignore deleted file mode 100644 index f195f4ab..00000000 --- a/app/src/main/assets/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -.idea/ -node_modules/ -.sass-cache/ -package-lock.json
\ No newline at end of file diff --git a/app/src/main/assets/js/click_a.coffee b/app/src/main/assets/js/click_a.coffee deleted file mode 100644 index e032b4ad..00000000 --- a/app/src/main/assets/js/click_a.coffee +++ /dev/null @@ -1,48 +0,0 @@ -prevented = false - -_frostAClick = (e) -> - - ### - # Commonality; check for valid target - ### - element = e.target or e.srcElement - if element.tagName != "A" - element = element.parentNode - # Notifications is two layers under - if element.tagName != "A" - element = element.parentNode - if element.tagName == "A" - if !prevented - url = element.getAttribute("href") - console.log "Click Intercept #{url}" - # if frost is injected, check if loading the url through an overlay works - if Frost?.loadUrl(url) == true - e.stopPropagation() - e.preventDefault() - else - console.log "Click Intercept Prevented" - return - -### -# On top of the click event, we must stop it for long presses -# Since that will conflict with the context menu -# Note that we only override it on conditions where the context menu -# Will occur -### - -_frostPreventClick = -> - console.log "Click prevented" - prevented = true - return - -document.addEventListener "click", _frostAClick, true -clickTimeout = undefined -document.addEventListener "touchstart", ((e) -> - clickTimeout = setTimeout(_frostPreventClick, 400) - return -), true -document.addEventListener "touchend", ((e) -> - prevented = false - clearTimeout clickTimeout - return -), true diff --git a/app/src/main/assets/js/click_a.js b/app/src/main/assets/js/click_a.js deleted file mode 100644 index e3ea7f31..00000000 --- a/app/src/main/assets/js/click_a.js +++ /dev/null @@ -1,60 +0,0 @@ -"use strict"; - -(function () { - - /* - * On top of the click event, we must stop it for long presses - * Since that will conflict with the context menu - * Note that we only override it on conditions where the context menu - * Will occur - */ - var _frostAClick, _frostPreventClick, clickTimeout, prevented; - - prevented = false; - - _frostAClick = function _frostAClick(e) { - /* - * Commonality; check for valid target - */ - var element, url; - element = e.target || e.srcElement; - if (element.tagName !== "A") { - element = element.parentNode; - } - // Notifications is two layers under - if (element.tagName !== "A") { - element = element.parentNode; - } - if (element.tagName === "A") { - if (!prevented) { - url = element.getAttribute("href"); - console.log("Click Intercept " + url); - // if frost is injected, check if loading the url through an overlay works - if ((typeof Frost !== "undefined" && Frost !== null ? Frost.loadUrl(url) : void 0) === true) { - e.stopPropagation(); - e.preventDefault(); - } - } else { - console.log("Click Intercept Prevented"); - } - } - }; - - _frostPreventClick = function _frostPreventClick() { - console.log("Click prevented"); - prevented = true; - }; - - document.addEventListener("click", _frostAClick, true); - - clickTimeout = void 0; - - document.addEventListener("touchstart", function (e) { - clickTimeout = setTimeout(_frostPreventClick, 400); - }, true); - - document.addEventListener("touchend", function (e) { - prevented = false; - clearTimeout(clickTimeout); - }, true); -}).call(undefined);
\ No newline at end of file diff --git a/app/src/main/assets/js/click_debugger.coffee b/app/src/main/assets/js/click_debugger.coffee deleted file mode 100644 index 057bb207..00000000 --- a/app/src/main/assets/js/click_debugger.coffee +++ /dev/null @@ -1,14 +0,0 @@ -# for desktop only - -_frostAContext = (e) -> - - ### - # Commonality; check for valid target - ### - element = e.target or e.currentTarget or e.srcElement - if !element - return - console.log "Clicked element: #{element.tagName} #{element.className}" - return - -document.addEventListener 'contextmenu', _frostAContext, true diff --git a/app/src/main/assets/js/click_debugger.js b/app/src/main/assets/js/click_debugger.js deleted file mode 100644 index 71db586a..00000000 --- a/app/src/main/assets/js/click_debugger.js +++ /dev/null @@ -1,20 +0,0 @@ -'use strict'; - -(function () { - // for desktop only - var _frostAContext; - - _frostAContext = function _frostAContext(e) { - /* - * Commonality; check for valid target - */ - var element; - element = e.target || e.currentTarget || e.srcElement; - if (!element) { - return; - } - console.log('Clicked element: ' + element.tagName + ' ' + element.className); - }; - - document.addEventListener('contextmenu', _frostAContext, true); -}).call(undefined);
\ No newline at end of file diff --git a/app/src/main/assets/js/context_a.coffee b/app/src/main/assets/js/context_a.coffee deleted file mode 100644 index 0dca1b7f..00000000 --- a/app/src/main/assets/js/context_a.coffee +++ /dev/null @@ -1,59 +0,0 @@ -# context menu for links -# largely mimics click_a.js -# we will also bind a listener here to notify the activity not to deal with viewpager scrolls -longClick = false - -_frostAContext = (e) -> - Frost?.longClick true - longClick = true - - ### - # Commonality; check for valid target - ### - - element = e.target or e.currentTarget or e.srcElement - if !element - return - if element.tagName != "A" - element = element.parentNode - #Notifications is two layers under - if element.tagName != "A" - element = element.parentNode - if element.tagName == "A" and element.getAttribute("href") != "#" - url = element.getAttribute("href") - if !url - return - text = element.parentNode.innerText - # check if image item exists, first in children and then in parent - image = element.querySelector("[style*=\"background-image: url(\"]") - if !image - image = element.parentNode.querySelector("[style*=\"background-image: url(\"]") - if image - imageUrl = window.getComputedStyle(image, null).backgroundImage.trim().slice(4, -1) - console.log "Context image: #{imageUrl}" - Frost?.loadImage imageUrl, text - e.stopPropagation() - e.preventDefault() - return - # check if true img exists - img = element.querySelector("img[src*=scontent]") - if img - imgUrl = img.src - console.log "Context img #{imgUrl}" - Frost?.loadImage imgUrl, text - e.stopPropagation() - e.preventDefault() - return - console.log "Context Content #{url} #{text}" - Frost?.contextMenu url, text - e.stopPropagation() - e.preventDefault() - return - -document.addEventListener "contextmenu", _frostAContext, true -document.addEventListener "touchend", ((e) -> - if longClick - Frost?.longClick false - longClick = false - return -), true diff --git a/app/src/main/assets/js/context_a.js b/app/src/main/assets/js/context_a.js deleted file mode 100644 index b39a6542..00000000 --- a/app/src/main/assets/js/context_a.js +++ /dev/null @@ -1,83 +0,0 @@ -"use strict"; - -(function () { - // context menu for links - // largely mimics click_a.js - // we will also bind a listener here to notify the activity not to deal with viewpager scrolls - var _frostAContext, longClick; - - longClick = false; - - _frostAContext = function _frostAContext(e) { - /* - * Commonality; check for valid target - */ - var element, image, imageUrl, img, imgUrl, text, url; - if (typeof Frost !== "undefined" && Frost !== null) { - Frost.longClick(true); - } - longClick = true; - element = e.target || e.currentTarget || e.srcElement; - if (!element) { - return; - } - if (element.tagName !== "A") { - element = element.parentNode; - } - //Notifications is two layers under - if (element.tagName !== "A") { - element = element.parentNode; - } - if (element.tagName === "A" && element.getAttribute("href") !== "#") { - url = element.getAttribute("href"); - if (!url) { - return; - } - text = element.parentNode.innerText; - // check if image item exists, first in children and then in parent - image = element.querySelector("[style*=\"background-image: url(\"]"); - if (!image) { - image = element.parentNode.querySelector("[style*=\"background-image: url(\"]"); - } - if (image) { - imageUrl = window.getComputedStyle(image, null).backgroundImage.trim().slice(4, -1); - console.log("Context image: " + imageUrl); - if (typeof Frost !== "undefined" && Frost !== null) { - Frost.loadImage(imageUrl, text); - } - e.stopPropagation(); - e.preventDefault(); - return; - } - // check if true img exists - img = element.querySelector("img[src*=scontent]"); - if (img) { - imgUrl = img.src; - console.log("Context img " + imgUrl); - if (typeof Frost !== "undefined" && Frost !== null) { - Frost.loadImage(imgUrl, text); - } - e.stopPropagation(); - e.preventDefault(); - return; - } - console.log("Context Content " + url + " " + text); - if (typeof Frost !== "undefined" && Frost !== null) { - Frost.contextMenu(url, text); - } - e.stopPropagation(); - e.preventDefault(); - } - }; - - document.addEventListener("contextmenu", _frostAContext, true); - - document.addEventListener("touchend", function (e) { - if (longClick) { - if (typeof Frost !== "undefined" && Frost !== null) { - Frost.longClick(false); - } - longClick = false; - } - }, true); -}).call(undefined);
\ No newline at end of file diff --git a/app/src/main/assets/js/document_watcher.coffee b/app/src/main/assets/js/document_watcher.coffee deleted file mode 100644 index 11cf7d53..00000000 --- a/app/src/main/assets/js/document_watcher.coffee +++ /dev/null @@ -1,24 +0,0 @@ -# emit key once half the viewport is covered - -isReady = -> - if not (document?.body?) - return false - return document.body.scrollHeight > innerHeight + 100 - -if isReady() - console.log("Already ready") - Frost?.isReady() - return - -console.log("Injected document watcher") - -observer = new MutationObserver(() -> - if isReady() - observer.disconnect() - Frost?.isReady() - console.log("Documented surpassed height in #{performance.now()}") -) - -observer.observe document, - childList: true - subtree: true
\ No newline at end of file diff --git a/app/src/main/assets/js/document_watcher.js b/app/src/main/assets/js/document_watcher.js deleted file mode 100644 index 4613dc87..00000000 --- a/app/src/main/assets/js/document_watcher.js +++ /dev/null @@ -1,38 +0,0 @@ -"use strict"; - -(function () { - // emit key once half the viewport is covered - var isReady, observer; - - isReady = function isReady() { - if (!((typeof document !== "undefined" && document !== null ? document.body : void 0) != null)) { - return false; - } - return document.body.scrollHeight > innerHeight + 100; - }; - - if (isReady()) { - console.log("Already ready"); - if (typeof Frost !== "undefined" && Frost !== null) { - Frost.isReady(); - } - return; - } - - console.log("Injected document watcher"); - - observer = new MutationObserver(function () { - if (isReady()) { - observer.disconnect(); - if (typeof Frost !== "undefined" && Frost !== null) { - Frost.isReady(); - } - return console.log("Documented surpassed height in " + performance.now()); - } - }); - - observer.observe(document, { - childList: true, - subtree: true - }); -}).call(undefined);
\ No newline at end of file diff --git a/app/src/main/assets/js/header_badges.coffee b/app/src/main/assets/js/header_badges.coffee deleted file mode 100644 index e9702751..00000000 --- a/app/src/main/assets/js/header_badges.coffee +++ /dev/null @@ -1,4 +0,0 @@ -# bases the header contents if it exists -header = document.getElementById("mJewelNav") -if header != null - Frost?.handleHeader header.outerHTML diff --git a/app/src/main/assets/js/header_badges.js b/app/src/main/assets/js/header_badges.js deleted file mode 100644 index 13447229..00000000 --- a/app/src/main/assets/js/header_badges.js +++ /dev/null @@ -1,14 +0,0 @@ -"use strict"; - -(function () { - // bases the header contents if it exists - var header; - - header = document.getElementById("mJewelNav"); - - if (header !== null) { - if (typeof Frost !== "undefined" && Frost !== null) { - Frost.handleHeader(header.outerHTML); - } - } -}).call(undefined);
\ No newline at end of file diff --git a/app/src/main/assets/js/header_hider.coffee b/app/src/main/assets/js/header_hider.coffee deleted file mode 100644 index 40510c79..00000000 --- a/app/src/main/assets/js/header_hider.coffee +++ /dev/null @@ -1,11 +0,0 @@ -header = document.querySelector('#header') - -if !header - return - -jewel = header.querySelector('#mJewelNav') - -if !jewel - return - -header.style.display = 'none'
\ No newline at end of file diff --git a/app/src/main/assets/js/header_hider.js b/app/src/main/assets/js/header_hider.js deleted file mode 100644 index f29887ee..00000000 --- a/app/src/main/assets/js/header_hider.js +++ /dev/null @@ -1,19 +0,0 @@ -'use strict'; - -(function () { - var header, jewel; - - header = document.querySelector('#header'); - - if (!header) { - return; - } - - jewel = header.querySelector('#mJewelNav'); - - if (!jewel) { - return; - } - - header.style.display = 'none'; -}).call(undefined);
\ No newline at end of file diff --git a/app/src/main/assets/js/media.coffee b/app/src/main/assets/js/media.coffee deleted file mode 100644 index a15a5279..00000000 --- a/app/src/main/assets/js/media.coffee +++ /dev/null @@ -1,30 +0,0 @@ -# we will handle media events -_frostMediaClick = (e) -> - - element = e.target or e.srcElement - if !element?.dataset.sigil?.toLowerCase().includes("inlinevideo") - return - - i = 0 - while !element.hasAttribute("data-store") - if ++i > 2 - return - element = element.parentNode - - try - dataStore = JSON.parse(element.dataset.store) - catch e - return - - url = dataStore.src - - if !url || !url.startsWith("http") - return - - console.log "Inline video #{url}" - if Frost?.loadVideo url, dataStore.animatedGifVideo - e.stopPropagation() - e.preventDefault() - return - -document.addEventListener "click", _frostMediaClick, true
\ No newline at end of file diff --git a/app/src/main/assets/js/media.js b/app/src/main/assets/js/media.js deleted file mode 100644 index 5b1a3776..00000000 --- a/app/src/main/assets/js/media.js +++ /dev/null @@ -1,38 +0,0 @@ -"use strict"; - -(function () { - // we will handle media events - var _frostMediaClick; - - _frostMediaClick = function _frostMediaClick(e) { - var dataStore, element, i, ref, url; - element = e.target || e.srcElement; - if (!(element != null ? (ref = element.dataset.sigil) != null ? ref.toLowerCase().includes("inlinevideo") : void 0 : void 0)) { - return; - } - i = 0; - while (!element.hasAttribute("data-store")) { - if (++i > 2) { - return; - } - element = element.parentNode; - } - try { - dataStore = JSON.parse(element.dataset.store); - } catch (error) { - e = error; - return; - } - url = dataStore.src; - if (!url || !url.startsWith("http")) { - return; - } - console.log("Inline video " + url); - if (typeof Frost !== "undefined" && Frost !== null ? Frost.loadVideo(url, dataStore.animatedGifVideo) : void 0) { - e.stopPropagation(); - e.preventDefault(); - } - }; - - document.addEventListener("click", _frostMediaClick, true); -}).call(undefined);
\ No newline at end of file diff --git a/app/src/main/assets/js/menu.coffee b/app/src/main/assets/js/menu.coffee deleted file mode 100644 index 384496f7..00000000 --- a/app/src/main/assets/js/menu.coffee +++ /dev/null @@ -1,42 +0,0 @@ -# click menu and move contents to main view -viewport = document.querySelector("#viewport") -root = document.querySelector("#root") -if !viewport - console.log "Menu.js: viewport is null" -if !root - console.log "Menu.js: root is null" -y = new MutationObserver((mutations) -> - viewport.removeAttribute "style" - root.removeAttribute "style" - return -) -y.observe viewport, attributes: true -y.observe root, attributes: true -x = new MutationObserver(() -> - menu = document.querySelector(".mSideMenu") - if menu != null - x.disconnect() - console.log "Found side menu" - while root.firstChild - root.removeChild root.firstChild - while menu.childNodes.length - console.log "append" - viewport.appendChild menu.childNodes[0] - Frost?.emit 0 - setTimeout (-> - y.disconnect() - console.log "Unhook styler" - return - ), 500 - return -) -jewel = document.querySelector("#mJewelNav") -if !jewel - console.log "Menu.js: jewel is null" -x.observe jewel, - childList: true - subtree: true -menuA = document.querySelector("#bookmarks_jewel").querySelector("a") -if !menuA - console.log "Menu.js: jewel is null" -menuA.click()
\ No newline at end of file diff --git a/app/src/main/assets/js/menu.js b/app/src/main/assets/js/menu.js deleted file mode 100644 index bfdca4a3..00000000 --- a/app/src/main/assets/js/menu.js +++ /dev/null @@ -1,73 +0,0 @@ -"use strict"; - -(function () { - // click menu and move contents to main view - var jewel, menuA, root, viewport, x, y; - - viewport = document.querySelector("#viewport"); - - root = document.querySelector("#root"); - - if (!viewport) { - console.log("Menu.js: viewport is null"); - } - - if (!root) { - console.log("Menu.js: root is null"); - } - - y = new MutationObserver(function (mutations) { - viewport.removeAttribute("style"); - root.removeAttribute("style"); - }); - - y.observe(viewport, { - attributes: true - }); - - y.observe(root, { - attributes: true - }); - - x = new MutationObserver(function () { - var menu; - menu = document.querySelector(".mSideMenu"); - if (menu !== null) { - x.disconnect(); - console.log("Found side menu"); - while (root.firstChild) { - root.removeChild(root.firstChild); - } - while (menu.childNodes.length) { - console.log("append"); - viewport.appendChild(menu.childNodes[0]); - } - if (typeof Frost !== "undefined" && Frost !== null) { - Frost.emit(0); - } - setTimeout(function () { - y.disconnect(); - console.log("Unhook styler"); - }, 500); - } - }); - - jewel = document.querySelector("#mJewelNav"); - - if (!jewel) { - console.log("Menu.js: jewel is null"); - } - - x.observe(jewel, { - childList: true, - subtree: true - }); - - menuA = document.querySelector("#bookmarks_jewel").querySelector("a"); - - if (!menuA) { - console.log("Menu.js: jewel is null"); - } - - menuA.click(); -}).call(undefined);
\ No newline at end of file diff --git a/app/src/main/assets/js/menu_debug.coffee b/app/src/main/assets/js/menu_debug.coffee deleted file mode 100644 index 54b265f4..00000000 --- a/app/src/main/assets/js/menu_debug.coffee +++ /dev/null @@ -1,42 +0,0 @@ -# click menu and move contents to main view -viewport = document.querySelector("#viewport") -root = document.querySelector("#root") -if !viewport - console.log "Menu.js: viewport is null" -if !root - console.log "Menu.js: root is null" -y = new MutationObserver((mutations) -> - viewport.removeAttribute "style" - root.removeAttribute "style" - return -) -y.observe viewport, attributes: true -y.observe root, attributes: true -x = new MutationObserver((mutations) -> - menu = document.querySelector(".mSideMenu") - if menu != null - x.disconnect() - console.log "Found side menu" - while root.firstChild - root.removeChild root.firstChild - while menu.childNodes.length - console.log "append" - viewport.appendChild menu.childNodes[0] - Frost?.handleHtml viewport.outerHTML - setTimeout (-> - y.disconnect() - console.log "Unhook styler" - return - ), 500 - return -) -jewel = document.querySelector("#mJewelNav") -if !jewel - console.log "Menu.js: jewel is null" -x.observe jewel, - childList: true - subtree: true -menuA = document.querySelector("#bookmarks_jewel").querySelector("a") -if !menuA - console.log "Menu.js: jewel is null" -menuA.click() diff --git a/app/src/main/assets/js/menu_debug.js b/app/src/main/assets/js/menu_debug.js deleted file mode 100644 index 7ecbf276..00000000 --- a/app/src/main/assets/js/menu_debug.js +++ /dev/null @@ -1,73 +0,0 @@ -"use strict"; - -(function () { - // click menu and move contents to main view - var jewel, menuA, root, viewport, x, y; - - viewport = document.querySelector("#viewport"); - - root = document.querySelector("#root"); - - if (!viewport) { - console.log("Menu.js: viewport is null"); - } - - if (!root) { - console.log("Menu.js: root is null"); - } - - y = new MutationObserver(function (mutations) { - viewport.removeAttribute("style"); - root.removeAttribute("style"); - }); - - y.observe(viewport, { - attributes: true - }); - - y.observe(root, { - attributes: true - }); - - x = new MutationObserver(function (mutations) { - var menu; - menu = document.querySelector(".mSideMenu"); - if (menu !== null) { - x.disconnect(); - console.log("Found side menu"); - while (root.firstChild) { - root.removeChild(root.firstChild); - } - while (menu.childNodes.length) { - console.log("append"); - viewport.appendChild(menu.childNodes[0]); - } - if (typeof Frost !== "undefined" && Frost !== null) { - Frost.handleHtml(viewport.outerHTML); - } - setTimeout(function () { - y.disconnect(); - console.log("Unhook styler"); - }, 500); - } - }); - - jewel = document.querySelector("#mJewelNav"); - - if (!jewel) { - console.log("Menu.js: jewel is null"); - } - - x.observe(jewel, { - childList: true, - subtree: true - }); - - menuA = document.querySelector("#bookmarks_jewel").querySelector("a"); - - if (!menuA) { - console.log("Menu.js: jewel is null"); - } - - menuA.click(); -}).call(undefined);
\ No newline at end of file diff --git a/app/src/main/assets/js/notif_msg.coffee b/app/src/main/assets/js/notif_msg.coffee deleted file mode 100644 index 1c3f8e38..00000000 --- a/app/src/main/assets/js/notif_msg.coffee +++ /dev/null @@ -1,22 +0,0 @@ -# binds callbacks to an invisible webview to take in the search events -finished = false -x = new MutationObserver((mutations) -> - _f_thread = document.querySelector("#threadlist_rows") - if !_f_thread - return - console.log "Found message threads #{_f_thread.outerHTML}" - Frost?.handleHtml _f_thread.outerHTML - finished = true - x.disconnect() - return -) -x.observe document, - childList: true - subtree: true -setTimeout (-> - if !finished - finished = true - console.log "Message thread timeout cancellation" - Frost?.handleHtml "" - return -), 20000 diff --git a/app/src/main/assets/js/notif_msg.js b/app/src/main/assets/js/notif_msg.js deleted file mode 100644 index 134ad4f0..00000000 --- a/app/src/main/assets/js/notif_msg.js +++ /dev/null @@ -1,37 +0,0 @@ -"use strict"; - -(function () { - // binds callbacks to an invisible webview to take in the search events - var finished, x; - - finished = false; - - x = new MutationObserver(function (mutations) { - var _f_thread; - _f_thread = document.querySelector("#threadlist_rows"); - if (!_f_thread) { - return; - } - console.log("Found message threads " + _f_thread.outerHTML); - if (typeof Frost !== "undefined" && Frost !== null) { - Frost.handleHtml(_f_thread.outerHTML); - } - finished = true; - x.disconnect(); - }); - - x.observe(document, { - childList: true, - subtree: true - }); - - setTimeout(function () { - if (!finished) { - finished = true; - console.log("Message thread timeout cancellation"); - if (typeof Frost !== "undefined" && Frost !== null) { - Frost.handleHtml(""); - } - } - }, 20000); -}).call(undefined);
\ No newline at end of file diff --git a/app/src/main/assets/js/textarea_listener.coffee b/app/src/main/assets/js/textarea_listener.coffee deleted file mode 100644 index 950f663e..00000000 --- a/app/src/main/assets/js/textarea_listener.coffee +++ /dev/null @@ -1,22 +0,0 @@ -# focus listener for textareas -# since swipe to refresh is quite sensitive, we will disable it -# when we detect a user typing -# note that this extends passed having a keyboard opened, -# as a user may still be reviewing his/her post -# swiping should automatically be reset on refresh - -_frostFocus = (e) -> - element = e.target or e.srcElement - console.log "Frost focus", element.tagName - if element.tagName == "TEXTAREA" - Frost?.disableSwipeRefresh true - return - -_frostBlur = (e) -> - element = e.target or e.srcElement - console.log "Frost blur", element.tagName - Frost?.disableSwipeRefresh false - return - -document.addEventListener "focus", _frostFocus, true -document.addEventListener "blur", _frostBlur, true diff --git a/app/src/main/assets/js/textarea_listener.js b/app/src/main/assets/js/textarea_listener.js deleted file mode 100644 index 41d77159..00000000 --- a/app/src/main/assets/js/textarea_listener.js +++ /dev/null @@ -1,35 +0,0 @@ -"use strict"; - -(function () { - // focus listener for textareas - // since swipe to refresh is quite sensitive, we will disable it - // when we detect a user typing - // note that this extends passed having a keyboard opened, - // as a user may still be reviewing his/her post - // swiping should automatically be reset on refresh - var _frostBlur, _frostFocus; - - _frostFocus = function _frostFocus(e) { - var element; - element = e.target || e.srcElement; - console.log("Frost focus", element.tagName); - if (element.tagName === "TEXTAREA") { - if (typeof Frost !== "undefined" && Frost !== null) { - Frost.disableSwipeRefresh(true); - } - } - }; - - _frostBlur = function _frostBlur(e) { - var element; - element = e.target || e.srcElement; - console.log("Frost blur", element.tagName); - if (typeof Frost !== "undefined" && Frost !== null) { - Frost.disableSwipeRefresh(false); - } - }; - - document.addEventListener("focus", _frostFocus, true); - - document.addEventListener("blur", _frostBlur, true); -}).call(undefined);
\ No newline at end of file diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/AboutActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/AboutActivity.kt index 8d849bff..f4c1244f 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/activities/AboutActivity.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/AboutActivity.kt @@ -27,6 +27,7 @@ import ca.allanwang.kau.about.LibraryIItem import ca.allanwang.kau.adapters.FastItemThemedAdapter import ca.allanwang.kau.adapters.ThemableIItem import ca.allanwang.kau.adapters.ThemableIItemDelegate +import ca.allanwang.kau.logging.KL import ca.allanwang.kau.utils.bindView import ca.allanwang.kau.utils.dimenPixelSize import ca.allanwang.kau.utils.resolveDrawable @@ -79,7 +80,8 @@ class AboutActivity : AboutActivityBase(null, { ) val l = libs.prepareLibraries(this, include, null, false, true, true) -// l.forEach { KL.d{"Lib ${it.definedName}"} } + if (BuildConfig.DEBUG) + l.forEach { KL.d { "Lib ${it.definedName}" } } return l } @@ -155,7 +157,7 @@ class AboutActivity : AboutActivityBase(null, { val c = itemView.context val size = c.dimenPixelSize(R.dimen.kau_avatar_bounds) images = arrayOf<Pair<IIcon, () -> Unit>>( - GoogleMaterial.Icon.gmd_arrow_downward to { c.startLink(R.string.github_downloads_url) }, + GoogleMaterial.Icon.gmd_file_download to { c.startLink(R.string.github_downloads_url) }, CommunityMaterial.Icon2.cmd_reddit to { c.startLink(R.string.reddit_url) }, CommunityMaterial.Icon.cmd_github_circle to { c.startLink(R.string.github_url) }, CommunityMaterial.Icon2.cmd_slack to { c.startLink(R.string.slack_url) }, diff --git a/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbItem.kt b/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbItem.kt index 6cf6f41b..9ee34ab7 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbItem.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbItem.kt @@ -46,7 +46,7 @@ enum class FbItem( FEED_TOP_STORIES(R.string.top_stories, GoogleMaterial.Icon.gmd_star, "home.php?sk=h_nor"), FRIENDS(R.string.friends, GoogleMaterial.Icon.gmd_person_add, "friends/center/requests"), GROUPS(R.string.groups, GoogleMaterial.Icon.gmd_group, "groups"), - MARKETPLACE(R.string.marketplace, CommunityMaterial.Icon2.cmd_home_currency_usd, "marketplace"), + MARKETPLACE(R.string.marketplace, GoogleMaterial.Icon.gmd_store, "marketplace"), MENU(R.string.menu, GoogleMaterial.Icon.gmd_menu, "settings", ::MenuFragment), MESSAGES(R.string.messages, MaterialDesignIconic.Icon.gmi_comments, "messages"), NOTES(R.string.notes, CommunityMaterial.Icon2.cmd_note, "notes"), diff --git a/app/src/main/kotlin/com/pitchedapps/frost/fragments/RecyclerFragmentBase.kt b/app/src/main/kotlin/com/pitchedapps/frost/fragments/RecyclerFragmentBase.kt index 9f26f3f7..37af690b 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/fragments/RecyclerFragmentBase.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/fragments/RecyclerFragmentBase.kt @@ -30,6 +30,7 @@ import com.pitchedapps.frost.utils.L import com.pitchedapps.frost.utils.frostJsoup import com.pitchedapps.frost.views.FrostRecyclerView import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.isActive import kotlinx.coroutines.withContext /** @@ -53,7 +54,7 @@ abstract class RecyclerFragment<T, Item : IItem<*, *>> : BaseFragment(), Recycle val data = try { reloadImpl(progress) } catch (e: Exception) { - L.e(e) { "Recycler reload fail" } + L.e(e) { "Recycler reload fail $baseUrl" } null } withMainContext { diff --git a/app/src/main/kotlin/com/pitchedapps/frost/injectors/CssAssets.kt b/app/src/main/kotlin/com/pitchedapps/frost/injectors/CssAssets.kt index 0caeda1a..a466feec 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/injectors/CssAssets.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/injectors/CssAssets.kt @@ -19,6 +19,7 @@ package com.pitchedapps.frost.injectors import android.content.Context import android.graphics.Color import android.webkit.WebView +import androidx.annotation.VisibleForTesting import ca.allanwang.kau.kotlin.lazyContext import ca.allanwang.kau.utils.adjustAlpha import ca.allanwang.kau.utils.colorToBackground @@ -43,7 +44,8 @@ enum class CssAssets(val folder: String = THEME_FOLDER) : InjectorContract { MATERIAL_LIGHT, MATERIAL_DARK, MATERIAL_AMOLED, MATERIAL_GLASS, CUSTOM, ROUND_ICONS("components") ; - private val file = "${name.toLowerCase(Locale.CANADA)}.css" + @VisibleForTesting + internal val file = "${name.toLowerCase(Locale.CANADA)}.css" /** * Note that while this can be loaded from any thread, it is typically done through [load] diff --git a/app/src/main/kotlin/com/pitchedapps/frost/injectors/JsAssets.kt b/app/src/main/kotlin/com/pitchedapps/frost/injectors/JsAssets.kt index 4b1bde43..e0be7977 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/injectors/JsAssets.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/injectors/JsAssets.kt @@ -18,6 +18,7 @@ package com.pitchedapps.frost.injectors import android.content.Context import android.webkit.WebView +import androidx.annotation.VisibleForTesting import ca.allanwang.kau.kotlin.lazyContext import com.pitchedapps.frost.utils.L import kotlinx.coroutines.Dispatchers @@ -32,11 +33,12 @@ import java.util.Locale * The enum name must match the css file name */ enum class JsAssets : InjectorContract { - MENU, MENU_DEBUG, CLICK_A, CONTEXT_A, MEDIA, HEADER_BADGES, HEADER_HIDER, TEXTAREA_LISTENER, NOTIF_MSG, + MENU, CLICK_A, CONTEXT_A, MEDIA, HEADER_BADGES, HEADER_HIDER, TEXTAREA_LISTENER, NOTIF_MSG, DOCUMENT_WATCHER ; - private val file = "${name.toLowerCase(Locale.CANADA)}.js" + @VisibleForTesting + internal val file = "${name.toLowerCase(Locale.CANADA)}.js" private val injector = lazyContext { try { val content = it.assets.open("js/$file").bufferedReader().use(BufferedReader::readText) diff --git a/app/src/main/kotlin/com/pitchedapps/frost/kotlin/Flyweight.kt b/app/src/main/kotlin/com/pitchedapps/frost/kotlin/Flyweight.kt index 914ce151..56acfc11 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/kotlin/Flyweight.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/kotlin/Flyweight.kt @@ -16,11 +16,14 @@ */ package com.pitchedapps.frost.kotlin +import com.pitchedapps.frost.utils.L import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.isActive import kotlinx.coroutines.launch @@ -64,57 +67,60 @@ class Flyweight<K, V>( completeExceptionally(result.exceptionOrNull()!!) } + private val errHandler = CoroutineExceptionHandler { _, throwable -> L.d { "FbAuth failed ${throwable.message}" } } + init { - job = scope.launch(Dispatchers.IO) { - launch { - while (isActive) { - select<Unit> { - /* - * New request received. Continuation should be fulfilled eventually - */ - actionChannel.onReceive { (key, completable) -> - val lastUpdate = conditionMap[key] - val lastResult = resultMap[key] - // Valid value, retrieved within acceptable time - if (lastResult != null && lastUpdate != null && System.currentTimeMillis() - lastUpdate < maxAge) { - completable.completeWith(lastResult) - } else { - val valueRequestPending = key in pendingMap - pendingMap.getOrPut(key) { mutableListOf() }.add(completable) - if (!valueRequestPending) - fulfill(key) + job = + scope.launch(Dispatchers.IO + SupervisorJob() + errHandler) { + launch { + while (isActive) { + select<Unit> { + /* + * New request received. Continuation should be fulfilled eventually + */ + actionChannel.onReceive { (key, completable) -> + val lastUpdate = conditionMap[key] + val lastResult = resultMap[key] + // Valid value, retrieved within acceptable time + if (lastResult != null && lastUpdate != null && System.currentTimeMillis() - lastUpdate < maxAge) { + completable.completeWith(lastResult) + } else { + val valueRequestPending = key in pendingMap + pendingMap.getOrPut(key) { mutableListOf() }.add(completable) + if (!valueRequestPending) + fulfill(key) + } } - } - /* - * Invalidator received. Existing result associated with key should not be used. - * Note that any unfulfilled request and future requests should still operate, but with a new value. - */ - invalidatorChannel.onReceive { key -> - if (key !in resultMap) { - // Nothing to invalidate. - // If pending requests exist, they are already in the process of being updated. - return@onReceive + /* + * Invalidator received. Existing result associated with key should not be used. + * Note that any unfulfilled request and future requests should still operate, but with a new value. + */ + invalidatorChannel.onReceive { key -> + if (key !in resultMap) { + // Nothing to invalidate. + // If pending requests exist, they are already in the process of being updated. + return@onReceive + } + conditionMap.remove(key) + resultMap.remove(key) + if (pendingMap[key]?.isNotEmpty() == true) + // Refetch value for pending requests + fulfill(key) } - conditionMap.remove(key) - resultMap.remove(key) - if (pendingMap[key]?.isNotEmpty() == true) - // Refetch value for pending requests - fulfill(key) - } - /* - * Value request fulfilled. Should now fulfill pending requests - */ - receiverChannel.onReceive { (key, result) -> - conditionMap[key] = System.currentTimeMillis() - resultMap[key] = result - pendingMap.remove(key)?.forEach { - it.completeWith(result) + /* + * Value request fulfilled. Should now fulfill pending requests + */ + receiverChannel.onReceive { (key, result) -> + conditionMap[key] = System.currentTimeMillis() + resultMap[key] = result + pendingMap.remove(key)?.forEach { + it.completeWith(result) + } } } } } } - } } /* diff --git a/app/src/main/kotlin/com/pitchedapps/frost/utils/WebContextMenu.kt b/app/src/main/kotlin/com/pitchedapps/frost/utils/WebContextMenu.kt index 62330e4d..fbaa4574 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/utils/WebContextMenu.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/utils/WebContextMenu.kt @@ -20,7 +20,6 @@ import android.content.Context import ca.allanwang.kau.utils.copyToClipboard import ca.allanwang.kau.utils.shareText import ca.allanwang.kau.utils.string -import ca.allanwang.kau.utils.toast import com.pitchedapps.frost.R import com.pitchedapps.frost.activities.MainActivity import com.pitchedapps.frost.facebook.formattedFbUrl @@ -29,19 +28,19 @@ import com.pitchedapps.frost.facebook.formattedFbUrl * Created by Allan Wang on 2017-07-07. */ fun Context.showWebContextMenu(wc: WebContext) { - - var title = wc.url + if (wc.isEmpty) return + var title = wc.url ?: string(R.string.menu) title = title.substring(title.indexOf("m/") + 1) //just so if defaults to 0 in case it's not .com/ if (title.length > 100) title = title.substring(0, 100) + '\u2026' + val menuItems = WebContextType.values + .filter { it.constraint(wc) } + materialDialogThemed { title(title) - items(WebContextType.values.map { - if (it == WebContextType.COPY_TEXT && wc.text == null) return@map null - this@showWebContextMenu.string(it.textId) - }.filterNotNull()) + items(menuItems.map { string(it.textId) }) itemsCallback { _, _, position, _ -> - WebContextType[position].onClick(this@showWebContextMenu, wc) + menuItems[position].onClick(this@showWebContextMenu, wc) } dismissListener { //showing the dialog interrupts the touch down event, so we must ensure that the viewpager's swipe is enabled @@ -50,18 +49,23 @@ fun Context.showWebContextMenu(wc: WebContext) { } } -class WebContext(val unformattedUrl: String, val text: String?) { - val url = unformattedUrl.formattedFbUrl +class WebContext(val unformattedUrl: String?, val text: String?) { + val url: String? = unformattedUrl?.formattedFbUrl + inline val hasUrl get() = unformattedUrl != null + inline val hasText get() = text != null + inline val isEmpty get() = !hasUrl && !hasText } -enum class WebContextType(val textId: Int, val onClick: (c: Context, wc: WebContext) -> Unit) { - OPEN_LINK(R.string.open_link, { c, wc -> c.launchWebOverlay(wc.unformattedUrl) }), - COPY_LINK(R.string.copy_link, { c, wc -> c.copyToClipboard(wc.url) }), - COPY_TEXT( - R.string.copy_text, - { c, wc -> if (wc.text != null) c.copyToClipboard(wc.text) else c.toast(R.string.no_text) }), - SHARE_LINK(R.string.share_link, { c, wc -> c.shareText(wc.url) }), - DEBUG_LINK(R.string.debug_link, { c, wc -> +enum class WebContextType( + val textId: Int, + val constraint: (wc: WebContext) -> Boolean, + val onClick: (c: Context, wc: WebContext) -> Unit +) { + OPEN_LINK(R.string.open_link, { it.hasUrl }, { c, wc -> c.launchWebOverlay(wc.unformattedUrl!!) }), + COPY_LINK(R.string.copy_link, { it.hasUrl }, { c, wc -> c.copyToClipboard(wc.url) }), + COPY_TEXT(R.string.copy_text, { it.hasText }, { c, wc -> c.copyToClipboard(wc.text) }), + SHARE_LINK(R.string.share_link, { it.hasUrl }, { c, wc -> c.shareText(wc.url) }), + DEBUG_LINK(R.string.debug_link, { it.hasUrl }, { c, wc -> c.materialDialogThemed { title(R.string.debug_link) content(R.string.debug_link_desc) @@ -69,8 +73,8 @@ enum class WebContextType(val textId: Int, val onClick: (c: Context, wc: WebCont onPositive { _, _ -> c.sendFrostEmail(R.string.debug_link_subject) { message = c.string(R.string.debug_link_content) - addItem("Unformatted url", wc.unformattedUrl) - addItem("Formatted url", wc.url) + addItem("Unformatted url", wc.unformattedUrl!!) + addItem("Formatted url", wc.url!!) } } } diff --git a/app/src/main/kotlin/com/pitchedapps/frost/views/FrostRecyclerView.kt b/app/src/main/kotlin/com/pitchedapps/frost/views/FrostRecyclerView.kt index 860bf36c..ce7437a7 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/views/FrostRecyclerView.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/views/FrostRecyclerView.kt @@ -27,8 +27,10 @@ import com.pitchedapps.frost.contracts.FrostContentContainer import com.pitchedapps.frost.contracts.FrostContentCore import com.pitchedapps.frost.contracts.FrostContentParent import com.pitchedapps.frost.fragments.RecyclerContentContract +import com.pitchedapps.frost.utils.L import com.pitchedapps.frost.utils.Prefs import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.isActive import kotlinx.coroutines.launch /** @@ -74,7 +76,7 @@ class FrostRecyclerView @JvmOverloads constructor( if (Prefs.animate) fadeOut(onFinish = onReloadClear) scope.launch { parent.refreshChannel.offer(true) - val loaded = recyclerContract.reload { parent.progressChannel.offer(it) } + recyclerContract.reload { parent.progressChannel.offer(it) } parent.progressChannel.offer(100) parent.refreshChannel.offer(false) if (Prefs.animate) circularReveal() diff --git a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostJSI.kt b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostJSI.kt index 19d16e87..50a5e2e1 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostJSI.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostJSI.kt @@ -76,10 +76,9 @@ class FrostJSI(val web: FrostWebView) { } @JavascriptInterface - fun contextMenu(url: String, text: String?) { - if (!text.isIndependent) return + fun contextMenu(url: String?, text: String?) { //url will be formatted through webcontext - web.post { context.showWebContextMenu(WebContext(url, text)) } + web.post { context.showWebContextMenu(WebContext(url.takeIf { it.isIndependent }, text)) } } /** diff --git a/app/src/main/res/xml/frost_changelog.xml b/app/src/main/res/xml/frost_changelog.xml index f90ecf37..ed014604 100644 --- a/app/src/main/res/xml/frost_changelog.xml +++ b/app/src/main/res/xml/frost_changelog.xml @@ -8,8 +8,8 @@ <version title="v2.2.2" /> <item text="New marketplace shortcut" /> - <item text="" /> - <item text="" /> + <item text="Fix crash when internet disconnects (may still need app restart)" /> + <item text="Improve JS code" /> <item text="" /> <item text="" /> diff --git a/app/src/test/kotlin/com/pitchedapps/frost/injectors/CssAssetsTest.kt b/app/src/test/kotlin/com/pitchedapps/frost/injectors/CssAssetsTest.kt new file mode 100644 index 00000000..0613d28e --- /dev/null +++ b/app/src/test/kotlin/com/pitchedapps/frost/injectors/CssAssetsTest.kt @@ -0,0 +1,16 @@ +package com.pitchedapps.frost.injectors + +import java.io.File +import kotlin.test.Test +import kotlin.test.assertTrue + +class CssAssetsTest { + + @Test + fun verifyAssetsExist() { + CssAssets.values().forEach { asset -> + val file = File("src/web/assets/css/${asset.folder}/${asset.file}").absoluteFile + assertTrue(file.exists(), "${asset.name} not found at ${file.path}") + } + } +}
\ No newline at end of file diff --git a/app/src/test/kotlin/com/pitchedapps/frost/injectors/JsAssetsTest.kt b/app/src/test/kotlin/com/pitchedapps/frost/injectors/JsAssetsTest.kt new file mode 100644 index 00000000..eab62b27 --- /dev/null +++ b/app/src/test/kotlin/com/pitchedapps/frost/injectors/JsAssetsTest.kt @@ -0,0 +1,16 @@ +package com.pitchedapps.frost.injectors + +import java.io.File +import kotlin.test.Test +import kotlin.test.assertTrue + +class JsAssetsTest { + + @Test + fun verifyAssetsExist() { + JsAssets.values().forEach { asset -> + val file = File("src/web/assets/js/${asset.file}").absoluteFile + assertTrue(file.exists(), "${asset.name} not found at ${file.path}") + } + } +}
\ No newline at end of file diff --git a/app/src/test/kotlin/com/pitchedapps/frost/utils/CoroutineTest.kt b/app/src/test/kotlin/com/pitchedapps/frost/utils/CoroutineTest.kt index fb302648..e93f507c 100644 --- a/app/src/test/kotlin/com/pitchedapps/frost/utils/CoroutineTest.kt +++ b/app/src/test/kotlin/com/pitchedapps/frost/utils/CoroutineTest.kt @@ -16,9 +16,11 @@ */ package com.pitchedapps.frost.utils +import com.pitchedapps.frost.kotlin.Flyweight import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.async import kotlinx.coroutines.channels.BroadcastChannel @@ -243,4 +245,47 @@ class CoroutineTest { ) } } + + @Test + fun exceptionChecks() { + val mainTag = "main-test" + val mainDispatcher = Executors.newSingleThreadExecutor { r -> + Thread(r, mainTag) + }.asCoroutineDispatcher() + val channel = Channel<Int>() + + val job = SupervisorJob() + + val flyweight = Flyweight<Int, Int>(GlobalScope, 200L) { + throw java.lang.RuntimeException("Flyweight exception") + } + + suspend fun crash(): Boolean = withContext(Dispatchers.IO) { + try { + withContext(Dispatchers.Default) { + flyweight.fetch(0).await() + } + true + } catch (e: java.lang.Exception) { + false + } + } + + runBlocking(mainDispatcher + job) { + launch { + val i = channel.receive() + println("Received $i") + } + launch { + println("A") + println(crash()) + println("B") + channel.offer(1) + } +// launch { +// delay(2000) +// job.cancel() +// } + } + } } diff --git a/app/src/web/.gitignore b/app/src/web/.gitignore new file mode 100644 index 00000000..76a547ef --- /dev/null +++ b/app/src/web/.gitignore @@ -0,0 +1,25 @@ +node_modules/ +.sass-cache/ +package-lock.json + +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml diff --git a/app/src/web/.idea/compiler.xml b/app/src/web/.idea/compiler.xml new file mode 100644 index 00000000..1a2fb332 --- /dev/null +++ b/app/src/web/.idea/compiler.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="TypeScriptCompiler"> + <option name="recompileOnChanges" value="true" /> + </component> +</project>
\ No newline at end of file diff --git a/app/src/web/.idea/encodings.xml b/app/src/web/.idea/encodings.xml new file mode 100644 index 00000000..15a15b21 --- /dev/null +++ b/app/src/web/.idea/encodings.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="Encoding" addBOMForNewFiles="with NO BOM" /> +</project>
\ No newline at end of file diff --git a/app/src/web/.idea/misc.xml b/app/src/web/.idea/misc.xml new file mode 100644 index 00000000..28a804d8 --- /dev/null +++ b/app/src/web/.idea/misc.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="JavaScriptSettings"> + <option name="languageLevel" value="ES6" /> + </component> +</project>
\ No newline at end of file diff --git a/app/src/web/.idea/modules.xml b/app/src/web/.idea/modules.xml new file mode 100644 index 00000000..e2d63b96 --- /dev/null +++ b/app/src/web/.idea/modules.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="ProjectModuleManager"> + <modules> + <module fileurl="file://$PROJECT_DIR$/.idea/assets.iml" filepath="$PROJECT_DIR$/.idea/assets.iml" /> + </modules> + </component> +</project>
\ No newline at end of file diff --git a/app/src/web/.idea/vcs.xml b/app/src/web/.idea/vcs.xml new file mode 100644 index 00000000..c2365ab1 --- /dev/null +++ b/app/src/web/.idea/vcs.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="VcsDirectoryMappings"> + <mapping directory="$PROJECT_DIR$/../../.." vcs="Git" /> + </component> +</project>
\ No newline at end of file diff --git a/app/src/web/.idea/watcherTasks.xml b/app/src/web/.idea/watcherTasks.xml new file mode 100644 index 00000000..32d1e6f4 --- /dev/null +++ b/app/src/web/.idea/watcherTasks.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="ProjectTasksOptions"> + <TaskOptions isEnabled="true"> + <option name="arguments" value="--no-source-map --update $FileName$:$FileNameWithoutExtension$.css" /> + <option name="checkSyntaxErrors" value="true" /> + <option name="description" /> + <option name="exitCodeBehavior" value="ERROR" /> + <option name="fileExtension" value="scss" /> + <option name="immediateSync" value="true" /> + <option name="name" value="SCSS" /> + <option name="output" value="$FileNameWithoutExtension$.css" /> + <option name="outputFilters"> + <array /> + </option> + <option name="outputFromStdout" value="false" /> + <option name="program" value="sass" /> + <option name="runOnExternalChanges" value="true" /> + <option name="scopeName" value="Project Files" /> + <option name="trackOnlyRoot" value="true" /> + <option name="workingDir" value="$FileDir$" /> + <envs /> + </TaskOptions> + </component> +</project>
\ No newline at end of file diff --git a/app/src/web/README.md b/app/src/web/README.md new file mode 100644 index 00000000..29981033 --- /dev/null +++ b/app/src/web/README.md @@ -0,0 +1,4 @@ +# Frost for Facebook Assets + +This is the root project for the assets, which is primarily css and js. +The assets that will be added to Android are within the `assets` folder. diff --git a/app/src/main/assets/adblock.txt b/app/src/web/assets/adblock.txt index a35d95c8..a35d95c8 100644 --- a/app/src/main/assets/adblock.txt +++ b/app/src/web/assets/adblock.txt diff --git a/app/src/main/assets/css/components/round_icons.css b/app/src/web/assets/css/components/round_icons.css index c765d2ab..c765d2ab 100644 --- a/app/src/main/assets/css/components/round_icons.css +++ b/app/src/web/assets/css/components/round_icons.css diff --git a/app/src/main/assets/css/components/round_icons.scss b/app/src/web/assets/css/components/round_icons.scss index c00fe1bf..c00fe1bf 100644 --- a/app/src/main/assets/css/components/round_icons.scss +++ b/app/src/web/assets/css/components/round_icons.scss diff --git a/app/src/main/assets/css/core/_base.scss b/app/src/web/assets/css/core/_base.scss index 472319fe..472319fe 100644 --- a/app/src/main/assets/css/core/_base.scss +++ b/app/src/web/assets/css/core/_base.scss diff --git a/app/src/main/assets/css/core/_colors.scss b/app/src/web/assets/css/core/_colors.scss index 1411a857..1411a857 100644 --- a/app/src/main/assets/css/core/_colors.scss +++ b/app/src/web/assets/css/core/_colors.scss diff --git a/app/src/main/assets/css/core/_core_bg.scss b/app/src/web/assets/css/core/_core_bg.scss index 21c20bcc..21c20bcc 100644 --- a/app/src/main/assets/css/core/_core_bg.scss +++ b/app/src/web/assets/css/core/_core_bg.scss diff --git a/app/src/main/assets/css/core/_core_border.scss b/app/src/web/assets/css/core/_core_border.scss index c366bc14..c366bc14 100644 --- a/app/src/main/assets/css/core/_core_border.scss +++ b/app/src/web/assets/css/core/_core_border.scss diff --git a/app/src/main/assets/css/core/_core_messenger.scss b/app/src/web/assets/css/core/_core_messenger.scss index 608fc23d..608fc23d 100644 --- a/app/src/main/assets/css/core/_core_messenger.scss +++ b/app/src/web/assets/css/core/_core_messenger.scss diff --git a/app/src/main/assets/css/core/_core_text.scss b/app/src/web/assets/css/core/_core_text.scss index 154cee84..154cee84 100644 --- a/app/src/main/assets/css/core/_core_text.scss +++ b/app/src/web/assets/css/core/_core_text.scss diff --git a/app/src/main/assets/css/core/_main.scss b/app/src/web/assets/css/core/_main.scss index 3e972f93..3e972f93 100644 --- a/app/src/main/assets/css/core/_main.scss +++ b/app/src/web/assets/css/core/_main.scss diff --git a/app/src/main/assets/css/core/_svg.scss b/app/src/web/assets/css/core/_svg.scss index 8c714438..8c714438 100644 --- a/app/src/main/assets/css/core/_svg.scss +++ b/app/src/web/assets/css/core/_svg.scss diff --git a/app/src/main/assets/css/core/core.css b/app/src/web/assets/css/core/core.css index 1d48fa35..1d48fa35 100644 --- a/app/src/main/assets/css/core/core.css +++ b/app/src/web/assets/css/core/core.css diff --git a/app/src/main/assets/css/core/core.scss b/app/src/web/assets/css/core/core.scss index 38086529..38086529 100644 --- a/app/src/main/assets/css/core/core.scss +++ b/app/src/web/assets/css/core/core.scss diff --git a/app/src/main/assets/css/themes/.gitignore b/app/src/web/assets/css/themes/.gitignore index 01d06441..01d06441 100644 --- a/app/src/main/assets/css/themes/.gitignore +++ b/app/src/web/assets/css/themes/.gitignore diff --git a/app/src/main/assets/css/themes/custom.css b/app/src/web/assets/css/themes/custom.css index e38c6de0..e38c6de0 100644 --- a/app/src/main/assets/css/themes/custom.css +++ b/app/src/web/assets/css/themes/custom.css diff --git a/app/src/main/assets/css/themes/custom.scss b/app/src/web/assets/css/themes/custom.scss index 50c029fb..50c029fb 100644 --- a/app/src/main/assets/css/themes/custom.scss +++ b/app/src/web/assets/css/themes/custom.scss diff --git a/app/src/main/assets/css/themes/material_amoled.css b/app/src/web/assets/css/themes/material_amoled.css index c821003e..c821003e 100644 --- a/app/src/main/assets/css/themes/material_amoled.css +++ b/app/src/web/assets/css/themes/material_amoled.css diff --git a/app/src/main/assets/css/themes/material_amoled.scss b/app/src/web/assets/css/themes/material_amoled.scss index 19190126..19190126 100644 --- a/app/src/main/assets/css/themes/material_amoled.scss +++ b/app/src/web/assets/css/themes/material_amoled.scss diff --git a/app/src/main/assets/css/themes/material_dark.css b/app/src/web/assets/css/themes/material_dark.css index 0dc739eb..0dc739eb 100644 --- a/app/src/main/assets/css/themes/material_dark.css +++ b/app/src/web/assets/css/themes/material_dark.css diff --git a/app/src/main/assets/css/themes/material_dark.scss b/app/src/web/assets/css/themes/material_dark.scss index 18b8b461..18b8b461 100644 --- a/app/src/main/assets/css/themes/material_dark.scss +++ b/app/src/web/assets/css/themes/material_dark.scss diff --git a/app/src/main/assets/css/themes/material_glass.css b/app/src/web/assets/css/themes/material_glass.css index 3bf9530f..3bf9530f 100644 --- a/app/src/main/assets/css/themes/material_glass.css +++ b/app/src/web/assets/css/themes/material_glass.css diff --git a/app/src/main/assets/css/themes/material_glass.scss b/app/src/web/assets/css/themes/material_glass.scss index 0c61a38c..0c61a38c 100644 --- a/app/src/main/assets/css/themes/material_glass.scss +++ b/app/src/web/assets/css/themes/material_glass.scss diff --git a/app/src/main/assets/css/themes/material_light.css b/app/src/web/assets/css/themes/material_light.css index c00dd12f..c00dd12f 100644 --- a/app/src/main/assets/css/themes/material_light.css +++ b/app/src/web/assets/css/themes/material_light.css diff --git a/app/src/main/assets/css/themes/material_light.scss b/app/src/web/assets/css/themes/material_light.scss index 7ec58463..7ec58463 100644 --- a/app/src/main/assets/css/themes/material_light.scss +++ b/app/src/web/assets/css/themes/material_light.scss diff --git a/app/src/web/assets/js/click_a.js b/app/src/web/assets/js/click_a.js new file mode 100644 index 00000000..be69bb8c --- /dev/null +++ b/app/src/web/assets/js/click_a.js @@ -0,0 +1,46 @@ +"use strict"; +(function () { + var prevented = false; + var _frostAClick = function (e) { + var target = e.target || e.currentTarget || e.srcElement; + if (!(target instanceof Element)) { + console.log("No element found"); + return; + } + var element = target; + for (var i = 0; i < 2; i++) { + if (element.tagName !== 'A') { + element = element.parentElement; + } + } + if (element.tagName === 'A') { + if (!prevented) { + var url = element.getAttribute('href'); + if (!url || url === '#') { + return; + } + console.log("Click intercept " + url); + if (Frost.loadUrl(url)) { + e.stopPropagation(); + e.preventDefault(); + } + } + else { + console.log("Click intercept prevented"); + } + } + }; + var _frostPreventClick = function () { + console.log("Click _frostPrevented"); + prevented = true; + }; + document.addEventListener('click', _frostAClick, true); + var clickTimeout = undefined; + document.addEventListener('touchstart', function () { + clickTimeout = setTimeout(_frostPreventClick, 400); + }, true); + document.addEventListener('touchend', function () { + prevented = false; + clearTimeout(clickTimeout); + }, true); +}).call(undefined); diff --git a/app/src/web/assets/js/click_a.ts b/app/src/web/assets/js/click_a.ts new file mode 100644 index 00000000..5023610e --- /dev/null +++ b/app/src/web/assets/js/click_a.ts @@ -0,0 +1,57 @@ +(function () { + let prevented = false; + + const _frostAClick = (e: Event) => { + // check for valid target + const target = e.target || e.currentTarget || e.srcElement; + if (!(target instanceof Element)) { + console.log("No element found"); + return + } + let element: Element = target; + // Notifications are two layers under + for (let i = 0; i < 2; i++) { + if (element.tagName !== 'A') { + element = <Element>element.parentElement; + } + } + if (element.tagName === 'A') { + if (!prevented) { + const url = element.getAttribute('href'); + if (!url || url === '#') { + return + } + console.log(`Click intercept ${url}`); + // If Frost is injected, check if loading the url through an overlay works + if (Frost.loadUrl(url)) { + e.stopPropagation(); + e.preventDefault(); + } + } else { + console.log("Click intercept prevented") + } + } + }; + + /* + * On top of the click event, we must stop it for long presses + * Since that will conflict with the context menu + * Note that we only override it on conditions where the context menu + * Will occur + */ + const _frostPreventClick = () => { + console.log("Click _frostPrevented"); + prevented = true; + }; + + document.addEventListener('click', _frostAClick, true); + let clickTimeout: number | undefined = undefined; + document.addEventListener('touchstart', () => { + clickTimeout = setTimeout(_frostPreventClick, 400); + }, true); + document.addEventListener('touchend', () => { + prevented = false; + clearTimeout(clickTimeout) + }, true); +}).call(undefined); + diff --git a/app/src/web/assets/js/click_debugger.js b/app/src/web/assets/js/click_debugger.js new file mode 100644 index 00000000..16729899 --- /dev/null +++ b/app/src/web/assets/js/click_debugger.js @@ -0,0 +1,12 @@ +"use strict"; +(function () { + var _frostAContext = function (e) { + var element = e.target || e.currentTarget || e.srcElement; + if (!(element instanceof Element)) { + console.log("No element found"); + return; + } + console.log("Clicked element " + element.tagName + " " + element.className); + }; + document.addEventListener('contextmenu', _frostAContext, true); +}).call(undefined); diff --git a/app/src/web/assets/js/click_debugger.ts b/app/src/web/assets/js/click_debugger.ts new file mode 100644 index 00000000..088271fa --- /dev/null +++ b/app/src/web/assets/js/click_debugger.ts @@ -0,0 +1,15 @@ +// For desktop only + +(function () { + const _frostAContext = (e: Event) => { + // Commonality; check for valid target + const element = e.target || e.currentTarget || e.srcElement; + if (!(element instanceof Element)) { + console.log("No element found"); + return + } + console.log(`Clicked element ${element.tagName} ${element.className}`); + }; + + document.addEventListener('contextmenu', _frostAContext, true); +}).call(undefined); diff --git a/app/src/web/assets/js/context_a.js b/app/src/web/assets/js/context_a.js new file mode 100644 index 00000000..410553bd --- /dev/null +++ b/app/src/web/assets/js/context_a.js @@ -0,0 +1,92 @@ +"use strict"; +(function () { + var longClick = false; + var _frostCopyComment = function (e, target) { + if (!target.hasAttribute('data-commentid')) { + return false; + } + var text = target.innerText; + console.log("Copy comment " + text); + Frost.contextMenu(null, text); + return true; + }; + var _frostCopyPost = function (e, target) { + if (target.tagName !== 'A') { + return false; + } + var parent1 = target.parentElement; + if (!parent1 || parent1.tagName !== 'DIV') { + return false; + } + var parent2 = parent1.parentElement; + if (!parent2 || !parent2.classList.contains('story_body_container')) { + return false; + } + var url = target.getAttribute('href'); + var text = parent1.innerText; + console.log("Copy post " + url + " " + text); + Frost.contextMenu(url, text); + return true; + }; + var _frostImage = function (e, target) { + var element = target; + for (var i = 0; i < 2; i++) { + if (element.tagName !== 'A') { + element = element.parentElement; + } + } + if (element.tagName !== 'A') { + return false; + } + var url = element.getAttribute('href'); + if (!url || url === '#') { + return false; + } + var text = element.parentElement.innerText; + var image = element.querySelector("[style*=\"background-image: url(\"]"); + if (!image) { + image = element.parentElement.querySelector("[style*=\"background-image: url(\"]"); + } + if (image) { + var imageUrl = window.getComputedStyle(image, null).backgroundImage.trim().slice(4, -1); + console.log("Context image: " + imageUrl); + Frost.loadImage(imageUrl, text); + return true; + } + var img = element.querySelector("img[src*=scontent]"); + if (img instanceof HTMLMediaElement) { + var imgUrl = img.src; + console.log("Context img: " + imgUrl); + Frost.loadImage(imgUrl, text); + return true; + } + console.log("Context content " + url + " " + text); + Frost.contextMenu(url, text); + return true; + }; + var handlers = [_frostCopyComment, _frostCopyPost, _frostImage]; + var _frostAContext = function (e) { + Frost.longClick(true); + longClick = true; + var target = e.target || e.currentTarget || e.srcElement; + if (!(target instanceof HTMLElement)) { + console.log("No element found"); + return; + } + for (var _i = 0, handlers_1 = handlers; _i < handlers_1.length; _i++) { + var h = handlers_1[_i]; + if (h(e, target)) { + e.stopPropagation(); + e.preventDefault(); + return; + } + } + }; + document.addEventListener('contextmenu', _frostAContext, true); + document.addEventListener('touchend', function () { + if (longClick) { + Frost.longClick(false); + longClick = false; + } + }, true); +}).call(undefined); diff --git a/app/src/web/assets/js/context_a.ts b/app/src/web/assets/js/context_a.ts new file mode 100644 index 00000000..4751bbdc --- /dev/null +++ b/app/src/web/assets/js/context_a.ts @@ -0,0 +1,116 @@ +/** + * Context menu for links + * Largely mimics click_a.js + */ + +(function () { + let longClick = false; + + /** + * Given event and target, return true if handled and false otherwise. + */ + type EventHandler = (e: Event, target: HTMLElement) => Boolean + + const _frostCopyComment: EventHandler = (e, target) => { + if (!target.hasAttribute('data-commentid')) { + return false; + } + const text = target.innerText; + console.log(`Copy comment ${text}`); + Frost.contextMenu(null, text); + return true; + }; + + /** + * Posts should click a tag, with two parents up being div.story_body_container + */ + const _frostCopyPost: EventHandler = (e, target) => { + if (target.tagName !== 'A') { + return false; + } + const parent1 = target.parentElement; + if (!parent1 || parent1.tagName !== 'DIV') { + return false; + } + const parent2 = parent1.parentElement; + if (!parent2 || !parent2.classList.contains('story_body_container')) { + return false; + } + const url = target.getAttribute('href'); + const text = parent1.innerText; + console.log(`Copy post ${url} ${text}`); + Frost.contextMenu(url, text); + return true; + }; + + const _frostImage: EventHandler = (e, target) => { + let element: Element = target; + // Notifications are two layers under + for (let i = 0; i < 2; i++) { + if (element.tagName !== 'A') { + element = <Element>element.parentElement; + } + } + if (element.tagName !== 'A') { + return false; + } + const url = element.getAttribute('href'); + if (!url || url === '#') { + return false; + } + const text = (<HTMLElement>element.parentElement).innerText; + // Check if image item exists, first in children and then in parent + let image = element.querySelector("[style*=\"background-image: url(\"]"); + if (!image) { + image = (<Element>element.parentElement).querySelector("[style*=\"background-image: url(\"]") + } + if (image) { + const imageUrl = (<String>window.getComputedStyle(image, null).backgroundImage).trim().slice(4, -1); + console.log(`Context image: ${imageUrl}`); + Frost.loadImage(imageUrl, text); + return true; + } + // Check if true img exists + const img = element.querySelector("img[src*=scontent]"); + if (img instanceof HTMLMediaElement) { + const imgUrl = img.src; + console.log(`Context img: ${imgUrl}`); + Frost.loadImage(imgUrl, text); + return true; + } + console.log(`Context content ${url} ${text}`); + Frost.contextMenu(url, text); + return true; + }; + + const handlers = [_frostCopyComment, _frostCopyPost, _frostImage]; + + const _frostAContext = (e: Event) => { + Frost.longClick(true); + longClick = true; + + /* + * Commonality; check for valid target + */ + const target = e.target || e.currentTarget || e.srcElement; + if (!(target instanceof HTMLElement)) { + console.log("No element found"); + return + } + for (const h of handlers) { + if (h(e, target)) { + e.stopPropagation(); + e.preventDefault(); + return + } + } + }; + + document.addEventListener('contextmenu', _frostAContext, true); + document.addEventListener('touchend', () => { + if (longClick) { + Frost.longClick(false); + longClick = false + } + }, true); +}).call(undefined); diff --git a/app/src/web/assets/js/document_watcher.js b/app/src/web/assets/js/document_watcher.js new file mode 100644 index 00000000..12252201 --- /dev/null +++ b/app/src/web/assets/js/document_watcher.js @@ -0,0 +1,23 @@ +"use strict"; +(function () { + var isReady = function () { + return document.body.scrollHeight > innerHeight + 100; + }; + if (isReady()) { + console.log('Already ready'); + Frost.isReady(); + return; + } + console.log('Injected document watcher'); + var observer = new MutationObserver(function () { + if (isReady()) { + observer.disconnect(); + Frost.isReady(); + console.log("Documented surpassed height in " + performance.now()); + } + }); + observer.observe(document, { + childList: true, + subtree: true + }); +}).call(undefined); diff --git a/app/src/web/assets/js/document_watcher.ts b/app/src/web/assets/js/document_watcher.ts new file mode 100644 index 00000000..e671149c --- /dev/null +++ b/app/src/web/assets/js/document_watcher.ts @@ -0,0 +1,27 @@ +// Emit key once half the viewport is covered +(function () { + const isReady = () => { + return document.body.scrollHeight > innerHeight + 100 + }; + + if (isReady()) { + console.log('Already ready'); + Frost.isReady(); + return + } + + console.log('Injected document watcher'); + + const observer = new MutationObserver(() => { + if (isReady()) { + observer.disconnect(); + Frost.isReady(); + console.log(`Documented surpassed height in ${performance.now()}`); + } + }); + + observer.observe(document, { + childList: true, + subtree: true + }) +}).call(undefined); diff --git a/app/src/web/assets/js/header_badges.js b/app/src/web/assets/js/header_badges.js new file mode 100644 index 00000000..b1ceee05 --- /dev/null +++ b/app/src/web/assets/js/header_badges.js @@ -0,0 +1,7 @@ +"use strict"; +(function () { + var header = document.getElementById('mJewelNav'); + if (header) { + Frost.handleHeader(header.outerHTML); + } +}).call(undefined); diff --git a/app/src/web/assets/js/header_badges.ts b/app/src/web/assets/js/header_badges.ts new file mode 100644 index 00000000..473749f2 --- /dev/null +++ b/app/src/web/assets/js/header_badges.ts @@ -0,0 +1,7 @@ +// Fetches the header contents if it exists +(function() { + const header = document.getElementById('mJewelNav'); + if (header) { + Frost.handleHeader(header.outerHTML); + } +}).call(undefined); diff --git a/app/src/web/assets/js/header_hider.js b/app/src/web/assets/js/header_hider.js new file mode 100644 index 00000000..faa9f66d --- /dev/null +++ b/app/src/web/assets/js/header_hider.js @@ -0,0 +1,12 @@ +"use strict"; +(function () { + var header = document.querySelector('#header'); + if (!header) { + return; + } + var jewel = header.querySelector('#mJewelNav'); + if (!jewel) { + return; + } + header.style.display = 'none'; +}).call(undefined); diff --git a/app/src/web/assets/js/header_hider.ts b/app/src/web/assets/js/header_hider.ts new file mode 100644 index 00000000..1a8f27f2 --- /dev/null +++ b/app/src/web/assets/js/header_hider.ts @@ -0,0 +1,17 @@ +(function () { + const header = document.querySelector('#header'); + + if (!header) { + return + } + + const jewel = header.querySelector('#mJewelNav'); + + if (!jewel) { + return + } + + (<HTMLElement>header).style.display = 'none' +}).call(undefined); + + diff --git a/app/src/web/assets/js/media.js b/app/src/web/assets/js/media.js new file mode 100644 index 00000000..baeba0a1 --- /dev/null +++ b/app/src/web/assets/js/media.js @@ -0,0 +1,41 @@ +"use strict"; +(function () { + var _frostMediaClick = function (e) { + var target = e.target || e.srcElement; + if (!(target instanceof HTMLElement)) { + return; + } + var element = target; + var dataset = element.dataset; + if (!dataset || !dataset.sigil || dataset.sigil.toLowerCase().indexOf('inlinevideo') == -1) { + return; + } + var i = 0; + while (!element.hasAttribute('data-store')) { + if (++i > 2) { + return; + } + element = element.parentNode; + } + var store = element.dataset.store; + if (!store) { + return; + } + var dataStore; + try { + dataStore = JSON.parse(store); + } + catch (e) { + return; + } + var url = dataStore.src; + if (!url || url.lastIndexOf('http', 0) !== 0) { + return; + } + console.log("Inline video " + url); + if (Frost.loadVideo(url, dataStore.animatedGifVideo || false)) { + e.stopPropagation(); + } + }; + document.addEventListener('click', _frostMediaClick, true); +}).call(undefined); diff --git a/app/src/web/assets/js/media.ts b/app/src/web/assets/js/media.ts new file mode 100644 index 00000000..5b9b1a54 --- /dev/null +++ b/app/src/web/assets/js/media.ts @@ -0,0 +1,47 @@ +// Handles media events +(function () { + const _frostMediaClick = (e: Event) => { + const target = e.target || e.srcElement; + if (!(target instanceof HTMLElement)) { + return + } + let element: HTMLElement = target; + const dataset = element.dataset; + if (!dataset || !dataset.sigil || dataset.sigil.toLowerCase().indexOf('inlinevideo') == -1) { + return + } + let i = 0; + while (!element.hasAttribute('data-store')) { + if (++i > 2) { + return + } + element = <HTMLElement>element.parentNode; + } + const store = element.dataset.store; + if (!store) { + return + } + + let dataStore; + + try { + dataStore = JSON.parse(store) + } catch (e) { + return + } + + const url = dataStore.src; + + // !startsWith; see https://stackoverflow.com/a/36876507/4407321 + if (!url || url.lastIndexOf('http', 0) !== 0) { + return + } + + console.log(`Inline video ${url}`); + if (Frost.loadVideo(url, dataStore.animatedGifVideo || false)) { + e.stopPropagation() + } + }; + + document.addEventListener('click', _frostMediaClick, true); +}).call(undefined); diff --git a/app/src/web/assets/js/menu.js b/app/src/web/assets/js/menu.js new file mode 100644 index 00000000..b6a30209 --- /dev/null +++ b/app/src/web/assets/js/menu.js @@ -0,0 +1,55 @@ +"use strict"; +(function () { + var viewport = document.querySelector("#viewport"); + var root = document.querySelector("#root"); + var bookmarkJewel = document.querySelector("#bookmarks_jewel"); + if (!viewport || !root || !bookmarkJewel) { + console.log('Menu.js: main elements not found'); + Frost.emit(0); + return; + } + var menuA = bookmarkJewel.querySelector("a"); + if (!menuA) { + console.log('Menu.js: menu links not found'); + Frost.emit(0); + return; + } + var jewel = document.querySelector('#mJewelNav'); + if (!jewel) { + console.log('Menu.js: jewel is null'); + return; + } + var y = new MutationObserver(function () { + viewport.removeAttribute('style'); + root.removeAttribute('style'); + }); + y.observe(viewport, { + attributes: true + }); + y.observe(root, { + attributes: true + }); + var x = new MutationObserver(function () { + var menu = document.querySelector('.mSideMenu'); + if (menu) { + x.disconnect(); + console.log("Found side menu"); + while (root.firstChild) { + root.removeChild(root.firstChild); + } + while (menu.childNodes.length) { + viewport.appendChild(menu.childNodes[0]); + } + Frost.emit(0); + setTimeout(function () { + y.disconnect(); + console.log('Unhook styler'); + }, 500); + } + }); + x.observe(jewel, { + childList: true, + subtree: true + }); + menuA.click(); +}).call(undefined); diff --git a/app/src/web/assets/js/menu.ts b/app/src/web/assets/js/menu.ts new file mode 100644 index 00000000..6f9dbf16 --- /dev/null +++ b/app/src/web/assets/js/menu.ts @@ -0,0 +1,59 @@ +// Click menu and move contents to main view +(function () { + const viewport = document.querySelector("#viewport"); + const root = document.querySelector("#root"); + const bookmarkJewel = document.querySelector("#bookmarks_jewel"); + if (!viewport || !root || !bookmarkJewel) { + console.log('Menu.js: main elements not found'); + Frost.emit(0); + return + } + const menuA = bookmarkJewel.querySelector("a"); + if (!menuA) { + console.log('Menu.js: menu links not found'); + Frost.emit(0); + return + } + const jewel = document.querySelector('#mJewelNav'); + if (!jewel) { + console.log('Menu.js: jewel is null'); + return + } + + const y = new MutationObserver(() => { + viewport.removeAttribute('style'); + root.removeAttribute('style'); + }); + + y.observe(viewport, { + attributes: true + }); + y.observe(root, { + attributes: true + }); + + const x = new MutationObserver(() => { + const menu = document.querySelector('.mSideMenu'); + if (menu) { + x.disconnect(); + console.log("Found side menu"); + // Transfer elements + while (root.firstChild) { + root.removeChild(root.firstChild); + } + while (menu.childNodes.length) { + viewport.appendChild(menu.childNodes[0]); + } + Frost.emit(0); + setTimeout(() => { + y.disconnect(); + console.log('Unhook styler'); + }, 500); + } + }); + x.observe(jewel, { + childList: true, + subtree: true + }); + menuA.click(); +}).call(undefined); diff --git a/app/src/web/assets/js/notif_msg.js b/app/src/web/assets/js/notif_msg.js new file mode 100644 index 00000000..bcff697b --- /dev/null +++ b/app/src/web/assets/js/notif_msg.js @@ -0,0 +1,25 @@ +"use strict"; +(function () { + var finished = false; + var x = new MutationObserver(function () { + var _f_thread = document.querySelector('#threadlist_rows'); + if (!_f_thread) { + return; + } + console.log("Found message threads " + _f_thread.outerHTML); + Frost.handleHtml(_f_thread.outerHTML); + finished = true; + x.disconnect(); + }); + x.observe(document, { + childList: true, + subtree: true + }); + setTimeout(function () { + if (!finished) { + finished = true; + console.log('Message thread timeout cancellation'); + Frost.handleHtml(""); + } + }, 20000); +}).call(undefined); diff --git a/app/src/web/assets/js/notif_msg.ts b/app/src/web/assets/js/notif_msg.ts new file mode 100644 index 00000000..b7ce7a19 --- /dev/null +++ b/app/src/web/assets/js/notif_msg.ts @@ -0,0 +1,25 @@ +// Binds callback to an invisible webview to take in the search events +(function () { + let finished = false; + const x = new MutationObserver(() => { + const _f_thread = document.querySelector('#threadlist_rows'); + if (!_f_thread) { + return + } + console.log(`Found message threads ${_f_thread.outerHTML}`); + Frost.handleHtml(_f_thread.outerHTML); + finished = true; + x.disconnect(); + }); + x.observe(document, { + childList: true, + subtree: true + }); + setTimeout(() => { + if (!finished) { + finished = true; + console.log('Message thread timeout cancellation'); + Frost.handleHtml("") + } + }, 20000); +}).call(undefined); diff --git a/app/src/web/assets/js/textarea_listener.js b/app/src/web/assets/js/textarea_listener.js new file mode 100644 index 00000000..1ec9b663 --- /dev/null +++ b/app/src/web/assets/js/textarea_listener.js @@ -0,0 +1,23 @@ +"use strict"; +(function () { + var _frostFocus = function (e) { + var element = e.target || e.srcElement; + if (!(element instanceof Element)) { + return; + } + console.log("FrostJSI focus, " + element.tagName); + if (element.tagName === 'TEXTAREA') { + Frost.disableSwipeRefresh(true); + } + }; + var _frostBlur = function (e) { + var element = e.target || e.srcElement; + if (!(element instanceof Element)) { + return; + } + console.log("FrostJSI blur, " + element.tagName); + Frost.disableSwipeRefresh(false); + }; + document.addEventListener("focus", _frostFocus, true); + document.addEventListener("blur", _frostBlur, true); +}).call(undefined); diff --git a/app/src/web/assets/js/textarea_listener.ts b/app/src/web/assets/js/textarea_listener.ts new file mode 100644 index 00000000..062f5bf6 --- /dev/null +++ b/app/src/web/assets/js/textarea_listener.ts @@ -0,0 +1,31 @@ +/* + * focus listener for textareas + * since swipe to refresh is quite sensitive, we will disable it + * when we detect a user typing + * note that this extends passed having a keyboard opened, + * as a user may still be reviewing his/her post + * swiping should automatically be reset on refresh + */ +(function () { + const _frostFocus = (e: Event) => { + const element = e.target || e.srcElement; + if (!(element instanceof Element)) { + return + } + console.log(`FrostJSI focus, ${element.tagName}`); + if (element.tagName === 'TEXTAREA') { + Frost.disableSwipeRefresh(true); + } + }; + + const _frostBlur = (e: Event) => { + const element = e.target || e.srcElement; + if (!(element instanceof Element)) { + return + } + console.log(`FrostJSI blur, ${element.tagName}`); + Frost.disableSwipeRefresh(false); + }; + document.addEventListener("focus", _frostFocus, true); + document.addEventListener("blur", _frostBlur, true); +}).call(undefined); diff --git a/app/src/main/assets/pgl.yoyo.org.txt b/app/src/web/assets/pgl.yoyo.org.txt index 63d6fa41..63d6fa41 100644 --- a/app/src/main/assets/pgl.yoyo.org.txt +++ b/app/src/web/assets/pgl.yoyo.org.txt diff --git a/app/src/web/assets/typings/frost.d.ts b/app/src/web/assets/typings/frost.d.ts new file mode 100644 index 00000000..8f60c9dd --- /dev/null +++ b/app/src/web/assets/typings/frost.d.ts @@ -0,0 +1,27 @@ +declare interface FrostJSI { + loadUrl(url: string | null): boolean + + loadVideo(url: string | null, isGif: boolean): boolean + + reloadBaseUrl(animate: boolean) + + contextMenu(url: string | null, text: string | null) + + longClick(start: boolean) + + disableSwipeRefresh(disable: boolean) + + loadLogin() + + loadImage(imageUrl: string, text: string | null) + + emit(flag: number) + + isReady() + + handleHtml(html: string | null) + + handleHeader(html: string | null) +} + +declare var Frost: FrostJSI; diff --git a/app/src/web/package.json b/app/src/web/package.json new file mode 100644 index 00000000..c80696b3 --- /dev/null +++ b/app/src/web/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "typescript": "^3.3.1" + } +} diff --git a/app/src/web/tsconfig.json b/app/src/web/tsconfig.json new file mode 100644 index 00000000..ea88e28e --- /dev/null +++ b/app/src/web/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "es3", + "allowJs": true, + "skipLibCheck": true, +// "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "esnext", + "moduleResolution": "node", +// "resolveJsonModule": true, + "isolatedModules": false, +// "noEmit": true, + // Extras + "strictNullChecks": true, + "noImplicitAny": true, + "allowUnreachableCode": true, + "allowUnusedLabels": true, + "removeComments": true + }, + "include": [ + "assets/js", "assets/typings" + ] +} diff --git a/docs/Changelog.md b/docs/Changelog.md index abf15cd9..09345c5e 100644 --- a/docs/Changelog.md +++ b/docs/Changelog.md @@ -2,6 +2,8 @@ ## v2.2.2 * New marketplace shortcut +* Fix crash when internet disconnects (may still need app restart) +* Improve JS code ## v2.2.1 * Update theme |