aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAllan Wang <me@allanwang.ca>2019-02-05 23:16:29 -0500
committerGitHub <noreply@github.com>2019-02-05 23:16:29 -0500
commit8e3bfc168009c8682c4f6191d655f3ca10ae9f21 (patch)
treecf8419a06e193c08622ead5e6854b995a5eeba77
parent83fb36f666fbb934b74b5f763b8ffb2e56ca7761 (diff)
parentddfc310fde5f50ba52ef930287449c2e08faaca8 (diff)
downloadfrost-8e3bfc168009c8682c4f6191d655f3ca10ae9f21.tar.gz
frost-8e3bfc168009c8682c4f6191d655f3ca10ae9f21.tar.bz2
frost-8e3bfc168009c8682c4f6191d655f3ca10ae9f21.zip
Merge pull request #1334 from AllanWang/fix/offline-crash
Fix/offline crash
-rw-r--r--app/build.gradle1
-rw-r--r--app/src/main/assets/.babelrc9
-rw-r--r--app/src/main/assets/.gitignore4
-rw-r--r--app/src/main/assets/js/click_a.coffee48
-rw-r--r--app/src/main/assets/js/click_a.js60
-rw-r--r--app/src/main/assets/js/click_debugger.coffee14
-rw-r--r--app/src/main/assets/js/click_debugger.js20
-rw-r--r--app/src/main/assets/js/context_a.coffee59
-rw-r--r--app/src/main/assets/js/context_a.js83
-rw-r--r--app/src/main/assets/js/document_watcher.coffee24
-rw-r--r--app/src/main/assets/js/document_watcher.js38
-rw-r--r--app/src/main/assets/js/header_badges.coffee4
-rw-r--r--app/src/main/assets/js/header_badges.js14
-rw-r--r--app/src/main/assets/js/header_hider.coffee11
-rw-r--r--app/src/main/assets/js/header_hider.js19
-rw-r--r--app/src/main/assets/js/media.coffee30
-rw-r--r--app/src/main/assets/js/media.js38
-rw-r--r--app/src/main/assets/js/menu.coffee42
-rw-r--r--app/src/main/assets/js/menu.js73
-rw-r--r--app/src/main/assets/js/menu_debug.coffee42
-rw-r--r--app/src/main/assets/js/menu_debug.js73
-rw-r--r--app/src/main/assets/js/notif_msg.coffee22
-rw-r--r--app/src/main/assets/js/notif_msg.js37
-rw-r--r--app/src/main/assets/js/textarea_listener.coffee22
-rw-r--r--app/src/main/assets/js/textarea_listener.js35
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/activities/AboutActivity.kt6
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/facebook/FbItem.kt2
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/fragments/RecyclerFragmentBase.kt3
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/injectors/CssAssets.kt4
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/injectors/JsAssets.kt6
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/kotlin/Flyweight.kt92
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/utils/WebContextMenu.kt44
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/views/FrostRecyclerView.kt4
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/web/FrostJSI.kt5
-rw-r--r--app/src/main/res/xml/frost_changelog.xml4
-rw-r--r--app/src/test/kotlin/com/pitchedapps/frost/injectors/CssAssetsTest.kt16
-rw-r--r--app/src/test/kotlin/com/pitchedapps/frost/injectors/JsAssetsTest.kt16
-rw-r--r--app/src/test/kotlin/com/pitchedapps/frost/utils/CoroutineTest.kt45
-rw-r--r--app/src/web/.gitignore25
-rw-r--r--app/src/web/.idea/compiler.xml6
-rw-r--r--app/src/web/.idea/encodings.xml4
-rw-r--r--app/src/web/.idea/misc.xml6
-rw-r--r--app/src/web/.idea/modules.xml8
-rw-r--r--app/src/web/.idea/vcs.xml6
-rw-r--r--app/src/web/.idea/watcherTasks.xml25
-rw-r--r--app/src/web/README.md4
-rw-r--r--app/src/web/assets/adblock.txt (renamed from app/src/main/assets/adblock.txt)0
-rw-r--r--app/src/web/assets/css/components/round_icons.css (renamed from app/src/main/assets/css/components/round_icons.css)0
-rw-r--r--app/src/web/assets/css/components/round_icons.scss (renamed from app/src/main/assets/css/components/round_icons.scss)0
-rw-r--r--app/src/web/assets/css/core/_base.scss (renamed from app/src/main/assets/css/core/_base.scss)0
-rw-r--r--app/src/web/assets/css/core/_colors.scss (renamed from app/src/main/assets/css/core/_colors.scss)0
-rw-r--r--app/src/web/assets/css/core/_core_bg.scss (renamed from app/src/main/assets/css/core/_core_bg.scss)0
-rw-r--r--app/src/web/assets/css/core/_core_border.scss (renamed from app/src/main/assets/css/core/_core_border.scss)0
-rw-r--r--app/src/web/assets/css/core/_core_messenger.scss (renamed from app/src/main/assets/css/core/_core_messenger.scss)0
-rw-r--r--app/src/web/assets/css/core/_core_text.scss (renamed from app/src/main/assets/css/core/_core_text.scss)0
-rw-r--r--app/src/web/assets/css/core/_main.scss (renamed from app/src/main/assets/css/core/_main.scss)0
-rw-r--r--app/src/web/assets/css/core/_svg.scss (renamed from app/src/main/assets/css/core/_svg.scss)0
-rw-r--r--app/src/web/assets/css/core/core.css (renamed from app/src/main/assets/css/core/core.css)0
-rw-r--r--app/src/web/assets/css/core/core.scss (renamed from app/src/main/assets/css/core/core.scss)0
-rw-r--r--app/src/web/assets/css/themes/.gitignore (renamed from app/src/main/assets/css/themes/.gitignore)0
-rw-r--r--app/src/web/assets/css/themes/custom.css (renamed from app/src/main/assets/css/themes/custom.css)0
-rw-r--r--app/src/web/assets/css/themes/custom.scss (renamed from app/src/main/assets/css/themes/custom.scss)0
-rw-r--r--app/src/web/assets/css/themes/material_amoled.css (renamed from app/src/main/assets/css/themes/material_amoled.css)0
-rw-r--r--app/src/web/assets/css/themes/material_amoled.scss (renamed from app/src/main/assets/css/themes/material_amoled.scss)0
-rw-r--r--app/src/web/assets/css/themes/material_dark.css (renamed from app/src/main/assets/css/themes/material_dark.css)0
-rw-r--r--app/src/web/assets/css/themes/material_dark.scss (renamed from app/src/main/assets/css/themes/material_dark.scss)0
-rw-r--r--app/src/web/assets/css/themes/material_glass.css (renamed from app/src/main/assets/css/themes/material_glass.css)0
-rw-r--r--app/src/web/assets/css/themes/material_glass.scss (renamed from app/src/main/assets/css/themes/material_glass.scss)0
-rw-r--r--app/src/web/assets/css/themes/material_light.css (renamed from app/src/main/assets/css/themes/material_light.css)0
-rw-r--r--app/src/web/assets/css/themes/material_light.scss (renamed from app/src/main/assets/css/themes/material_light.scss)0
-rw-r--r--app/src/web/assets/js/click_a.js46
-rw-r--r--app/src/web/assets/js/click_a.ts57
-rw-r--r--app/src/web/assets/js/click_debugger.js12
-rw-r--r--app/src/web/assets/js/click_debugger.ts15
-rw-r--r--app/src/web/assets/js/context_a.js92
-rw-r--r--app/src/web/assets/js/context_a.ts116
-rw-r--r--app/src/web/assets/js/document_watcher.js23
-rw-r--r--app/src/web/assets/js/document_watcher.ts27
-rw-r--r--app/src/web/assets/js/header_badges.js7
-rw-r--r--app/src/web/assets/js/header_badges.ts7
-rw-r--r--app/src/web/assets/js/header_hider.js12
-rw-r--r--app/src/web/assets/js/header_hider.ts17
-rw-r--r--app/src/web/assets/js/media.js41
-rw-r--r--app/src/web/assets/js/media.ts47
-rw-r--r--app/src/web/assets/js/menu.js55
-rw-r--r--app/src/web/assets/js/menu.ts59
-rw-r--r--app/src/web/assets/js/notif_msg.js25
-rw-r--r--app/src/web/assets/js/notif_msg.ts25
-rw-r--r--app/src/web/assets/js/textarea_listener.js23
-rw-r--r--app/src/web/assets/js/textarea_listener.ts31
-rw-r--r--app/src/web/assets/pgl.yoyo.org.txt (renamed from app/src/main/assets/pgl.yoyo.org.txt)0
-rw-r--r--app/src/web/assets/typings/frost.d.ts27
-rw-r--r--app/src/web/package.json5
-rw-r--r--app/src/web/tsconfig.json25
-rw-r--r--docs/Changelog.md2
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