824 lines
29 KiB
HTML
824 lines
29 KiB
HTML
<!DOCTYPE html>
|
||
<!-- TODO:
|
||
* deprecate old pseudo-framework and use SpaccDotWeb
|
||
* options menu/zone?
|
||
* tools for js/css injection
|
||
* open file via drag&drop
|
||
* multiple frames loaded simoultaneously
|
||
* windowing system
|
||
* reordering tabs
|
||
* automatically add useful meta tags to injected HTML data URIs?
|
||
* URL hash parameters documentation and more features, also implement extended syntax mode
|
||
* full-screen edit of HTML data URIs? (probably better to not include this and make a dedicated app)
|
||
* loading notices
|
||
* decide how to handle resized iframe border offset dimensions
|
||
-->
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="utf-8"/>
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||
<meta property="og:url" content="https://hub.octt.eu.org/FramesBrowser/"/>
|
||
<link rel="canonical" href="https://hub.octt.eu.org/FramesBrowser/"/>
|
||
<link rel="shortcut icon" type="image/x-icon" href="./icon.png"/>
|
||
<link rel="apple-touch-icon" href="./icon.png"/>
|
||
<link rel="manifest" href="./WebManifest.json"/>
|
||
<title>🪟️ Frames Browser</title>
|
||
<meta name="description" content="iFrame-based HTML5 Browser for fun and development"/>
|
||
<meta property="og:title" content="🪟️ Frames Browser"/>
|
||
<meta property="og:description" content="iFrame-based HTML5 Browser for fun and development"/>
|
||
<style>
|
||
:root {
|
||
--BaseMargin: 8px;
|
||
--BtnHeight: calc(1rem + var(--BaseMargin));
|
||
--BtnActionHeight: calc(2rem + var(--BaseMargin));
|
||
--BtnActionWidth: calc(3rem + var(--BaseMargin));
|
||
--ColorBg: #f0f0f0;
|
||
--ColorFg: #0f0f0f;
|
||
}
|
||
* {
|
||
box-sizing: border-box;
|
||
}
|
||
body {
|
||
margin: 0px;
|
||
max-width: 100vw;
|
||
max-height: 100%;
|
||
position: absolute;
|
||
}
|
||
button {
|
||
height: var(--BtnHeight);
|
||
}
|
||
#MainAppContent {
|
||
height: 100%;
|
||
}
|
||
iframe, #IframeMain, #IframeMainBox {
|
||
border: none;
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
#IframeMainBox {
|
||
overflow: hidden;
|
||
resize: both;
|
||
}
|
||
#IframeMainBox[style] {
|
||
border: 4px dashed gray;
|
||
}
|
||
#IframeMainContainer {
|
||
width: 100vw;
|
||
position: relative;
|
||
overflow: auto;
|
||
}
|
||
#BoxControls {
|
||
overflow: auto;
|
||
}
|
||
#BoxControls table, #BoxControls table td, #BoxControls table td > * {
|
||
height: var(--BtnActionHeight);
|
||
min-width: var(--BtnActionWidth);
|
||
padding-top: 0;
|
||
padding-bottom: 0;
|
||
border-spacing: 0;
|
||
}
|
||
#BoxControls table td > button {
|
||
word-spacing: 100vw;
|
||
}
|
||
#BtnFullscreen {
|
||
position: absolute;
|
||
top: 0;
|
||
right: 0;
|
||
z-index: 1;
|
||
margin: 8px;
|
||
}
|
||
.BoxPopup {
|
||
left: 0;
|
||
right: 0;
|
||
}
|
||
.BoxPopup.Container {
|
||
position: absolute;
|
||
top: 10vh;
|
||
z-index: 1;
|
||
text-align: center;
|
||
}
|
||
.BoxPopup.Content {
|
||
position: relative;
|
||
margin-left: auto;
|
||
margin-right: auto;
|
||
padding: 8px;
|
||
width: fit-content;
|
||
max-height: 80vh;
|
||
overflow: auto;
|
||
box-shadow: gray 4px 4px 4px 0px;
|
||
color: var(--ColorFg);
|
||
background-color: var(--ColorBg);
|
||
}
|
||
.BtnAction {
|
||
height: var(--BtnActionHeight);
|
||
}
|
||
div.padded {
|
||
padding: 8px;
|
||
}
|
||
</style>
|
||
<script src="../../shared/OctoHub-Global.js"></script>
|
||
<script src="../../../SpaccDotWeb/SpaccDotWeb.Alt.js"></script><!--dev-->
|
||
<script src="../../node_modules/SpaccDotWeb/Build/SpaccDotWeb.Alt.min.js"></script><!--prod-->
|
||
<script> window.FramesBrowser = { Lib: {} }; </script>
|
||
<script src="./utils.js"></script>
|
||
</head>
|
||
<body>
|
||
<button id="BtnFullscreen" onclick="ToggleFullscreen()">🎞️ Menu</button>
|
||
<div id="BoxControls"><table><tr>
|
||
<!--<td><button onclick="ShowAppInfo()">ℹ️ Info</button></td>-->
|
||
<td><button onclick="ToggleDevTools()">📐️ Tools</button></td>
|
||
<td>
|
||
<button id="BtnFile" onclick="this.nextElementSibling.click()">📄 File</button>
|
||
<input type="file" hidden="hidden" style="display: none;" onchange="LoadFile(this.files[0])"/>
|
||
</td>
|
||
<td><button onclick="ZoomFrame()">🔍️ Zoom</button></td>
|
||
<td><button onclick="ResizeFrame()">↘️ Size</button></td>
|
||
<td><button onclick="ToggleFullscreen()">🎞️ Hide</button></td>
|
||
<td><button onclick="ListFrames()">🪟 Tabs</button></td>
|
||
<td style="width: 100%;"><input id="InputUri" type="text" style="min-width: 100%;" placeholder="🔗️ Enter URI..." onkeydown="InputHandleKey(event)"/></td>
|
||
<td><button id="BtnLoad" onclick="LoadFrame()">↩️ Load</button></td>
|
||
<td><button id="BtnExcise" onclick="ExciseFrame()">↗️ Excise</button></td>
|
||
<td><button onclick="FrameDispatch('Screenshot')">📷️ Shot</button></td>
|
||
<td><button onclick="FrameDispatch('Print')">🖨️ Print</button></td>
|
||
<tr></table></div>
|
||
<div id="BoxHandy"></div>
|
||
<noscript><div class="NoScript padded"><p>
|
||
This is an actual app, not a badly-made website.
|
||
<br>
|
||
It needs JavaScript to work, so you need to enable it.
|
||
<br>
|
||
The code is fully open, and you can review it with "View Page Source".
|
||
</p><hr/></div></noscript>
|
||
<div id="MainAppContent"><div class="padded">
|
||
<h1>🪟️ Frames Browser</h1>
|
||
<p>Frames Browser is an iFrame-based HTML5 browser made for fun and development. Use the above menu to operate the app.</p>
|
||
<!--<p>Note: the app is still in development and data handling may break between versions! Backup your data externally to avoid losing it.</p>-->
|
||
<p>You can also visit, or go back to, my home page (OctoSpacc Hub): <a href="https://hub.octt.eu.org">hub.octt.eu.org</a>!</p>
|
||
<!-- TODO <h2>Open-Source, Licensing, Disclaimers</h2> -->
|
||
<h2>Usage and Help</h2>
|
||
<p>
|
||
This app exists because I needed an efficient interface for visualizing specific websites, HTML data in general,
|
||
especially via copypaste of data URIs, and allow for even novice users to handle data shared this way.
|
||
<br/>
|
||
Being itself a webapp, unfortunately it isn't a replacement for any web browser, but that isn't its goal.
|
||
This is instead made to be a tool as cross-platform, lightweight, flexible and immediate to use as possible for my scope.
|
||
A webpage working with iFrames, that can even function offline, has revealed itself to be the best possible option.
|
||
<br/>
|
||
In the past I planned to introduce some fancy features like split tabs,
|
||
but for now I don't really know yet... stay tuned!!
|
||
<br/>
|
||
You can read the changelog below, or
|
||
<a href="https://octospacc.altervista.org/?s=%23FramesBrowser">follow occasional project updates and stories on my development blog</a>.
|
||
</p>
|
||
<p>Some tips:</p><ul>
|
||
<li>The above menu bar takes minimal vertical space and as much horizontal space as possible. On smaller displays, items get moved outside the screen and can be reached simply via scrolling.</li>
|
||
<li>You can open as many new tabs as you want, pointing to whatever URL or uploaded file content, but <a href="https://developer.mozilla.org/en-US/docs/Web/API/Storage_API/Storage_quotas_and_eviction_criteria#web_storage">keep in mind that your browser may limit the amount of storable data</a>.</li>
|
||
<li>You can go back to this info page at any moment by clicking "<code>[ Root Window ]</code>" in the "<code>🪟 Tabs</code>" menu.</li>
|
||
<li>Clicking "<code>📐️ Tools</code>" will open or close <a href="https://eruda.liriliri.io">the Eruda console</a> as desired. (Note that Eruda is currently only injected in the root window and not inside any iFrame.)</li>
|
||
</ul>
|
||
<h2>Changelog</h2>
|
||
<h3>2024-09-14</h3><ul>
|
||
<li>Fix mobile browsers' UI shifting in place, causing part of the bottom of the iFrame to be hidden.</li>
|
||
<li>Handle code injection in frames efficiently, by externally loading library dependencies.</li>
|
||
<li>Always show modal with screenshot preview after taking one, show download button instead of automatically downloading.</li>
|
||
</ul>
|
||
<h3>2024-09-13</h3><ul>
|
||
<li>Better normalized the action buttons' width.</li>
|
||
<li>Allow resizing frames (and then resetting the size), by hand or by inputting numbers.</li>
|
||
<li>Add "<code>🖨️ Print</code>" and (experimental, thanks to <a href="https://html2canvas.hertzen.com">html2canvas</a>) "<code>🖼️ Screenshot</code>" features for any current frame. (Note: this requires code injection and will not work 100%.)</li>
|
||
</ul>
|
||
<h3>2024-03-19</h3><ul>
|
||
<li>Remove info button from toolbar for now.</li>
|
||
<li>Update info page also adding Usage and Help section with tips.</li>
|
||
<li>Fixed interference between frame zoom and menu hiding features.</li>
|
||
<li>Slight improvements to the UI (buttons and margins).</li>
|
||
<li>Optimize storage by automatically removing a saved URL once no open tab points to it.</li>
|
||
<!--<li>Make current tab not persist after reload if changed when opened from hash flags.</li>-->
|
||
</ul>
|
||
<h3>2024-03-18</h3><ul>
|
||
<li>First implementation of handling of URL hash parameters (reduced or "quick" format).</li>
|
||
<li>Open a <a href="https://hub.octt.eu.org/FramesBrowser/#_1|u=https://example.com">specific URI</a> or <a href="https://hub.octt.eu.org/FramesBrowser/#_1|h=<h2>Hello World!</h2>">raw HTML data</a> via URL hash (click the links to try!).</li>
|
||
<li>Add "fullscreen" (hide the main app bar) option to GUI (persistent) and quick URL hash flags.</li>
|
||
<li>Fixed some issues with handling UTF8 data URIs.</li>
|
||
<li>Make frame zoom option persistent.</li>
|
||
</ul>
|
||
<h3>2024-03-17</h3><ul>
|
||
<li>Improve internal app state data handling (breaking previous version).</li>
|
||
<li>Add data dump and reset options appearing on boot to users with pre-update database.</li>
|
||
<li>Add link back to my hub in this README.</li>
|
||
</ul>
|
||
<h3>2024-03-15</h3><ul>
|
||
<li>Improve some crusty code.</li>
|
||
<li>Add this default/sample HTML text into the app, visible via the otherwise useless Root Window.</li>
|
||
<li>Add symbol for indicating current zoom level on zoom button.</li>
|
||
<li>Add counter for open frames (goes up to 99 before becoming generic).</li>
|
||
<li>Better handling of devtools, removed floating button and always use dedicated button.</li>
|
||
</ul>
|
||
<h3>2023-09-23 and before</h3><ul>
|
||
<li>MVP version of the app with finished UI and basic features.</li>
|
||
<li>Eruda DevTools.</li>
|
||
<li>In-frame page zoom with 3 levels.</li>
|
||
<li>Creating, deleting, and switching to different frames, inputting URL.</li>
|
||
<li>Persistent app state saving frames and their URLs.</li>
|
||
<li>Excise frame content into another native browser tab (via opening the same URL)</li>
|
||
<li>Import file into data URI from local file.</li>
|
||
</ul>
|
||
<script>
|
||
Array.from(document.querySelectorAll('a[href]')).forEach(function(el){
|
||
el.onclick = function(){ top.location = this.href }
|
||
})
|
||
</script>
|
||
</div></div>
|
||
<!--<script src="./html2canvas.min.wrappedLib.js"></script>-->
|
||
<script>
|
||
var AppData, SesAppData, SesAppDataBak;
|
||
function SaveAppData(){
|
||
localStorage.setItem('org.eu.octt.FramesBrowser.v1', JSON.stringify({ ...AppData, ...(SesAppData.optionsFromUrl ? SesAppDataBak : SesAppData) }));
|
||
};
|
||
|
||
var FrameZoomLevels = [50, 200];
|
||
var SampleHtmlContent = `<!DOCTYPE html><html><head><meta charset="utf-8"/></head><body>${MainAppContent.innerHTML}</body></html>`;
|
||
MainAppContent.innerHTML = `<div id="IframeMainContainer">
|
||
<div id="IframeMainBox">
|
||
<iframe id="IframeMain"></iframe>
|
||
</div>
|
||
</div>`;
|
||
document.body.style = 'overflow: hidden; height: 100%;';
|
||
|
||
var htmlInjectPrefix = ((location.protocol === 'file:') ? '.' : location.href.split('#')[0].split('/').slice(0, -1).join('/'));
|
||
var FrameHtmlInjectable = (`
|
||
<${'script'} src="../../../SpaccDotWeb/SpaccDotWeb.Alt.js"></${'script'}><!--dev-->
|
||
<${'script'} src="${htmlInjectPrefix}/../../node_modules/SpaccDotWeb/Build/SpaccDotWeb.Alt.min.js"></${'script'}><!--prod-->
|
||
<${'script'} src="${htmlInjectPrefix}/node_modules/html2canvas/dist/html2canvas.min.js"></${'script'}>
|
||
<${'script'}> window.FramesBrowser = ${JSON.stringify(window.FramesBrowser)};
|
||
(` + (function(){
|
||
for (var name in window.FramesBrowser.Lib) {
|
||
var content = window.FramesBrowser.Lib[name];
|
||
if ((typeof content) === 'string') {
|
||
content = eval(content);
|
||
if ((typeof content) === 'boolean') {
|
||
window.FramesBrowser.Lib[name] = window[name];
|
||
}
|
||
}
|
||
}
|
||
window.FramesBrowser.Print = (function(){
|
||
window.print();
|
||
});
|
||
window.FramesBrowser.Screenshot = (function(){
|
||
// TODO must somehow load images in canvas with cors proxy or it becomes tainted and undownloadable
|
||
// for now just show on screen and let user download via browser actions
|
||
function postFinished () {
|
||
window.parent.postMessage({ FramesBrowser: "Screenshot:0" }, '*');
|
||
}
|
||
window.parent.postMessage({ FramesBrowser: "Screenshot:1" }, '*');
|
||
html2canvas(document.body, { allowTaint: true }).then(function(canvas){
|
||
/*try {
|
||
Object.assign(document.createElement('a'), {
|
||
download: `FramesBrowser-Screenshot-${(new Date).toJSON()}`,
|
||
href: canvas.toDataURL('image/png'),
|
||
}).click();
|
||
} catch(err) {
|
||
console.error(err);
|
||
SpaccDotWeb.ShowModal({
|
||
text: "Use the browser's context menu to download the image.",
|
||
extraHTML: `<div style="overflow: auto;"></div>`,
|
||
}).then(function(modalEl){
|
||
modalEl.querySelector('div').appendChild(canvas);
|
||
});
|
||
}*/
|
||
var imageData;
|
||
try {
|
||
imageData = canvas.toDataURL('image/png');
|
||
} catch(err) {
|
||
console.error(err);
|
||
}
|
||
SpaccDotWeb.ShowModal({
|
||
text: ((!imageData) && "Use the browser's context menu to download the image."),
|
||
action: (imageData && (function(){
|
||
Object.assign(document.createElement('a'), {
|
||
download: `FramesBrowser-Screenshot-${(new Date).toJSON()}`,
|
||
href: imageData,
|
||
}).click();
|
||
})),
|
||
confirmText: '📥️ Download',
|
||
extraHTML: '<div style="overflow: auto;"></div>',
|
||
buttonsPosition: 'top',
|
||
}).then(function(modalEl){
|
||
modalEl.querySelector('div').appendChild(canvas);
|
||
});
|
||
postFinished();
|
||
}).catch(function(err){
|
||
console.error(err);
|
||
postFinished();
|
||
});
|
||
});
|
||
window.addEventListener('message', (function(messageEv){
|
||
var action = (messageEv.data && messageEv.data.FramesBrowser);
|
||
if (action) {
|
||
FramesBrowser[action]();
|
||
}
|
||
}));
|
||
}) + `)(); </${'script'}>`);
|
||
|
||
function $new (tag, props) {
|
||
var el = document.createElement(tag);
|
||
if (props) {
|
||
Object.keys(props).forEach(function(key){
|
||
el[key] = props[key];
|
||
});
|
||
};
|
||
return el;
|
||
}
|
||
|
||
function $request (url, opts) {
|
||
if (!opts.method) {
|
||
opts.method = 'GET';
|
||
};
|
||
var req = new XMLHttpRequest();
|
||
req.onreadystatechange = function(){
|
||
if (this.readyState == 4) {
|
||
opts.callback(req.responseText);
|
||
};
|
||
};
|
||
req.open(opts.method, url, true);
|
||
req.send();
|
||
}
|
||
|
||
function InputHandleKey (ev) {
|
||
// Enter
|
||
if (ev.keyCode == 13) {
|
||
LoadFrame();
|
||
};
|
||
}
|
||
|
||
function GetTabUrlFromTabIndex (index) {
|
||
return (index === -1 ? null : AppData.urls[AppData.tabs[index].urlIndex]);
|
||
}
|
||
|
||
// TODO fallback to no-inject loading, or data: inject loading, in case of failure (mainly regarding the external domain)
|
||
var htmlInjectIndex = 0;
|
||
function makeRealFrameUrl (url, isFrameRoot) {
|
||
if (isFrameRoot) {
|
||
url = createDataUrl('text/html', SampleHtmlContent, 'utf8');
|
||
}
|
||
if (isDataUrl(url)) {
|
||
var [mime, body, encoding] = extractDataUrl(url);
|
||
if (mime.toLowerCase() !== 'text/html') {
|
||
return url;
|
||
}
|
||
//url = (((location.protocol === 'file:') ? '.' /* `./${Date.now()}/..` */ : 'http://data-sandbox.octt.eu.org')
|
||
// + /* `/html-data-url-loader.html#${Date.now()}#` */ `/html-data-url-loader-${htmlInjectIndex = Math.abs(1- htmlInjectIndex)}.html#`
|
||
// + createDataUrl(mime, patchFrameHtml(body), ((location.protocol === 'file:') ? (encoding || 'utf8') : 'base64'), (location.protocol !== 'file:')));
|
||
url = createDataUrl(mime, patchFrameHtml(body), (encoding || 'utf8'));
|
||
if (location.protocol === 'file:') {
|
||
url = `./html-data-url-loader-${htmlInjectIndex = Math.abs(1- htmlInjectIndex)}.html#${url}`;
|
||
}
|
||
}
|
||
return url;
|
||
}
|
||
|
||
// TODO check why on Chromium mobile this here is not enough, force-reloading any page breaks the iFrame and requires page reload
|
||
function loadUrlInFrame (url) {
|
||
IframeMain.src = ''; // needed for URLs with hashes
|
||
IframeMain.src = url;
|
||
/* // (Firefox issue?) sometimes wrapper page isn't loaded properly and iframe remains blank, we fix it here
|
||
if (url === 'about:blank') {
|
||
return;
|
||
}
|
||
var loadInterval = setInterval((function(){
|
||
try {
|
||
if (IframeMain.contentWindow.location.toString() === 'about:blank') {
|
||
IframeMain.src = url;
|
||
} else {
|
||
throw '';
|
||
}
|
||
} catch(err) {
|
||
clearInterval(loadInterval);
|
||
}
|
||
}), 100);*/
|
||
}
|
||
|
||
function ShowFrame (index) {
|
||
var isFrameRoot = (index === -1);
|
||
var url = (GetTabUrlFromTabIndex(index) || '');
|
||
ListFramesClose();
|
||
AppData.currentTabIndex = index;
|
||
SaveAppData();
|
||
InputUri.disabled = BtnFile.disabled = BtnLoad.disabled = BtnExcise.disabled = isFrameRoot;
|
||
InputUri.value = url;
|
||
loadUrlInFrame(makeRealFrameUrl(url, isFrameRoot));
|
||
}
|
||
|
||
function SaveUrl (url) {
|
||
if (url) {
|
||
InputUri.value = url;
|
||
}
|
||
url = InputUri.value;
|
||
var urlIndex = AppData.urls.indexOf(url);
|
||
if (urlIndex === -1) {
|
||
// it's a new url, store it
|
||
AppData.urls.push(url);
|
||
urlIndex = (AppData.urls.length - 1);
|
||
}
|
||
AppData.tabs[AppData.currentTabIndex].urlIndex = urlIndex;
|
||
PruneUnusedUrls();
|
||
SaveAppData();
|
||
return url;
|
||
}
|
||
|
||
function PruneUnusedUrls () {
|
||
var urlsNew = [];
|
||
for (var urlIndex in AppData.urls) {
|
||
if (AppData.tabs.filter(function(tab){
|
||
var usesUrl = (tab.urlIndex === Number(urlIndex));
|
||
if (usesUrl) {
|
||
tab.urlIndex = urlsNew.length;
|
||
}
|
||
return usesUrl;
|
||
}).length > 0) {
|
||
urlsNew.push(AppData.urls[urlIndex]);
|
||
}
|
||
}
|
||
AppData.urls = urlsNew;
|
||
SaveAppData();
|
||
}
|
||
|
||
function AddFrame () {
|
||
AppData.tabs = AppData.tabs.concat([{}]);
|
||
ListFrames();
|
||
ShowFrame(AppData.tabs.length - 1);
|
||
SaveAppData();
|
||
RefreshFramesCounter();
|
||
}
|
||
|
||
function CloseFrame (index) {
|
||
AppData.tabs.splice(index, 1);
|
||
PruneUnusedUrls();
|
||
if (AppData.currentTabIndex === index) {
|
||
ShowFrame(-1);
|
||
} else if (AppData.currentTabIndex > index) {
|
||
AppData.currentTabIndex--;
|
||
};
|
||
ListFrames(); ListFrames();
|
||
SaveAppData();
|
||
RefreshFramesCounter();
|
||
}
|
||
|
||
function RefreshFramesCounter () {
|
||
var count = AppData.tabs.length;
|
||
var countHtml = '';
|
||
if (count > 99) {
|
||
countHtml = '(...)';
|
||
} else if (count > 0) {
|
||
countHtml = `(${count})`;
|
||
};
|
||
document.querySelector('button[onclick="ListFrames()"]').textContent = `🪟${countHtml} Tabs`;
|
||
}
|
||
|
||
function GetNeededIframeHeight (hScale=100) {
|
||
return (SesAppData.fullscreen ? `${hScale}%` : `calc(${hScale}% - (var(--BtnActionHeight) * ${hScale / 100}))`);
|
||
}
|
||
|
||
function ApplyFullscreen () {
|
||
if (SesAppData.fullscreen) {
|
||
BoxControls.style.display = 'none';
|
||
BtnFullscreen.style.display = '';
|
||
} else {
|
||
BoxControls.style.display = '';
|
||
BtnFullscreen.style.display = 'none';
|
||
}
|
||
ApplyFrameZoom();
|
||
}
|
||
|
||
function ToggleFullscreen () {
|
||
SesAppData.fullscreen = !SesAppData.fullscreen;
|
||
ApplyFullscreen();
|
||
SaveAppData();
|
||
}
|
||
|
||
function LoadFrame () {
|
||
var url = InputUri.value;
|
||
if (!url.toLowerCase().startsWith('javascript:')) {
|
||
loadUrlInFrame(makeRealFrameUrl(SaveUrl(), false));
|
||
}
|
||
}
|
||
|
||
function isDataUrl (url) {
|
||
return url.toLowerCase().startsWith('data:');
|
||
}
|
||
|
||
function createDataUrl (mime, body, encoding, reverse) {
|
||
switch (encoding) { default:
|
||
break; case 'utf8':
|
||
body = encodeURIComponent(body);
|
||
break; case 'base64':
|
||
body = btoa(unescape(encodeURIComponent(body)));
|
||
//break; case '46esab':
|
||
// body = reverseString(btoa(unescape(encodeURIComponent(body))));
|
||
}
|
||
if (reverse) {
|
||
mime = reverseString(mime);
|
||
body = reverseString(body);
|
||
encoding = reverseString(encoding);
|
||
}
|
||
return `${!reverse ? 'data' : 'atad'}:${mime};${encoding},${body}`;
|
||
}
|
||
|
||
function patchFrameHtml (html) {
|
||
var replaced = false;
|
||
for (var ref of ["</head>", "</body>", "</html>"]) {
|
||
if (html.includes(ref)) {
|
||
html = html.replace(ref, (FrameHtmlInjectable + ref));
|
||
replaced = true;
|
||
break;
|
||
}
|
||
}
|
||
if (!replaced) {
|
||
html = (html + FrameHtmlInjectable);
|
||
}
|
||
return html;
|
||
}
|
||
|
||
function ExciseFrame () {
|
||
var url = SaveUrl();
|
||
if (isDataUrl(url)) {
|
||
opendatauri(url);
|
||
} else {
|
||
open(url, '_blank');
|
||
};
|
||
}
|
||
|
||
function LoadFile (file) {
|
||
var reader = new FileReader();
|
||
reader.onload = function(){
|
||
InputUri.value = reader.result;
|
||
LoadFrame();
|
||
};
|
||
reader.readAsDataURL(file);
|
||
}
|
||
|
||
function ApplyFrameZoom () {
|
||
var level = FrameZoomLevels[SesAppData.frameZoomIndex];
|
||
var levelopp = FrameZoomLevels[FrameZoomLevels.length - 1 - SesAppData.frameZoomIndex];
|
||
['bottom', 'top', 'left', 'right', 'scale', 'width'].forEach(function(prop){
|
||
IframeMainContainer.style[prop] = '';
|
||
});
|
||
if (level < 100) {
|
||
IframeMainContainer.style.bottom = `calc(${level}% - 16px)`;
|
||
IframeMainContainer.style.right = `${level}vw`;
|
||
} else {
|
||
IframeMainContainer.style.top = `calc(${levelopp/2}% - 8px)`;
|
||
IframeMainContainer.style.left = `${levelopp/2}vw`;
|
||
}
|
||
if (SesAppData.frameZoomIndex !== -1) {
|
||
IframeMainContainer.style.scale = (level / 100);
|
||
IframeMainContainer.style.width = `${levelopp}vw`;
|
||
}
|
||
IframeMainContainer.style.height = GetNeededIframeHeight(levelopp);
|
||
var zoomButton = document.querySelector('button[onclick="ZoomFrame()"]');
|
||
if (level < 100) {
|
||
zoomButton.textContent = '🔍️(-) Zoom';
|
||
} else if (level > 100) {
|
||
zoomButton.textContent = '🔍️(+) Zoom';
|
||
} else {
|
||
zoomButton.textContent = '🔍️ Zoom';
|
||
};
|
||
}
|
||
|
||
function ZoomFrame () {
|
||
if (SesAppData.frameZoomIndex === FrameZoomLevels.length - 1) {
|
||
SesAppData.frameZoomIndex = -1;
|
||
} else {
|
||
SesAppData.frameZoomIndex ++;
|
||
};
|
||
ApplyFrameZoom();
|
||
SaveAppData();
|
||
}
|
||
|
||
function ResizeFrame () {
|
||
var prevStyle = IframeMainBox.style.length;
|
||
IframeMainBox.removeAttribute('style');
|
||
if (!prevStyle) {
|
||
var props = (prompt("Format: width,height") || '').trim();
|
||
if (!props) {
|
||
return;
|
||
}
|
||
props = (props
|
||
.replaceAll(' ', ',').replaceAll('\t', ',')
|
||
.replaceAll(':', ',').replaceAll(';', ',')
|
||
.replaceAll(',,', ',').replaceAll(',,', ','));
|
||
if ((!props.includes(',')) && props.includes('x')) {
|
||
props = props.replace('x', ',');
|
||
}
|
||
props = props.split(',');
|
||
if (!(props[0] || props[1])) {
|
||
return;
|
||
}
|
||
for (var i=0; i<props.length; i++) {
|
||
if (!props[i]) {
|
||
props[i] = '100%';
|
||
} else if (!isNaN(props[i])) {
|
||
props[i] += 'px';
|
||
}
|
||
}
|
||
IframeMainBox.style = `width: ${props[0]}; height: ${props[1]};`;
|
||
}
|
||
}
|
||
|
||
function FrameDispatch (action) {
|
||
var url = ((AppData.currentTabIndex !== -1) && SaveUrl());
|
||
if (url && (!isDataUrl(url))) {
|
||
IframeMain.src = createDataUrl('text/plain', 'Reloading, please wait...');
|
||
// TODO use dynamic proxy
|
||
fetch(`https://corsproxy.io/?${encodeURIComponent(url)}`).then(function(response){
|
||
response.text().then(function(html){
|
||
AddFrame();
|
||
SaveUrl(createDataUrl('text/html', html, 'utf8'));
|
||
ShowFrame(AppData.currentTabIndex);
|
||
});
|
||
}).catch(function(error){
|
||
console.error(error);
|
||
IframeMain.src = createDataUrl('text/plain', error);
|
||
});
|
||
return;
|
||
}
|
||
IframeMain.contentWindow.postMessage({ FramesBrowser: action }, '*');
|
||
return true;
|
||
}
|
||
|
||
function ListFrames () {
|
||
if (!ListFramesClose()){
|
||
var Box = NewBoxPopup('BoxFramesList');
|
||
var BtnAdd = $new('button', { className: 'BtnAction', innerHTML: '➕ Add', onclick: AddFrame });
|
||
Box.Content.appendChild(BtnAdd);
|
||
var BoxList = $new('ul', { style: 'text-align: initial;' });
|
||
Box.Content.appendChild(BoxList);
|
||
var LiMain = $new('li');
|
||
BoxList.appendChild(LiMain);
|
||
var BtnMain = $new('button', { innerHTML: '[ Root Window ]', onclick: /*ShowRootFrame*/function(){ ShowFrame(-1) }, disabled: AppData.currentTabIndex === -1 });
|
||
LiMain.appendChild(BtnMain);
|
||
for (var i=0; i<AppData.tabs.length; i++) {
|
||
var li = $new('li');
|
||
li.ItemIndex = i;
|
||
BoxList.appendChild(li);
|
||
var open = $new('button', { innerHTML: ` ${(GetTabUrlFromTabIndex(i) || '').slice(0, 16)} `, onclick: function(){ShowFrame(this.parentElement.ItemIndex)}, disabled: AppData.currentTabIndex === i });
|
||
li.append(open);
|
||
var close = $new('button', { innerHTML: '✖️', style: 'float: right;', onclick: function(){CloseFrame(this.parentElement.ItemIndex)} });
|
||
li.append(close);
|
||
};
|
||
};
|
||
}
|
||
|
||
function ListFramesClose () {
|
||
var exist = document.querySelector('#BoxFramesList');
|
||
if (exist) {
|
||
BoxFramesList.remove();
|
||
};
|
||
return exist;
|
||
}
|
||
|
||
var isDevToolsOpen = false;
|
||
function ToggleDevTools () {
|
||
if (typeof(eruda) === 'undefined') {
|
||
$request('https://cdn.jsdelivr.net/npm/eruda', { callback: function(text){
|
||
eval(text);
|
||
eruda.init();
|
||
eruda._shadowRoot.querySelector('div.eruda-entry-btn').style.display = 'none';
|
||
eruda.show();
|
||
isDevToolsOpen = true;
|
||
} });
|
||
} else {
|
||
eruda[isDevToolsOpen ? 'hide' : 'show']();
|
||
isDevToolsOpen = !isDevToolsOpen;
|
||
}
|
||
}
|
||
|
||
function NewBoxPopup (id) {
|
||
var Container = $new('div', { id: id, className: 'BoxPopup Container' });
|
||
var Content = $new('div', { className: 'BoxPopup Content' });
|
||
Container.appendChild(Content);
|
||
var BtnClose = $new('button', { className: 'BtnAction', innerHTML: '❌ Close', onclick: function(){this.parentElement.parentElement.remove()} });
|
||
Content.appendChild(BtnClose);
|
||
BoxHandy.appendChild(Container);
|
||
return { Container, Content };
|
||
}
|
||
|
||
function opendatauri (url) {
|
||
var [mime, body, encoding] = extractDataUrl(url);
|
||
var bytes = new Array(body.length);
|
||
for (var i = 0; i < body.length; i++) {
|
||
bytes[i] = body.charCodeAt(i);
|
||
};
|
||
window.open(URL.createObjectURL(
|
||
new Blob([new Uint8Array(bytes)], { type: `${mime};${encoding || 'utf8'}` })
|
||
), '_blank');
|
||
}
|
||
|
||
// https://stackoverflow.com/questions/45053624/convert-hex-to-binary-in-javascript/45054052#45054052
|
||
function hex2bin (hex) {
|
||
return (parseInt(hex, 16).toString(2)).padStart((hex.length * 4), '0');
|
||
}
|
||
|
||
function AlertMigrateAppData () {
|
||
var stored = Object.keys(localStorage);
|
||
if (stored.includes('FramesBrowser.CurrentFrames') || stored.includes('FramesBrowser.FrameIndexes') || stored.includes('FramesBrowser.url')) {
|
||
var overlay = document.createElement('div');
|
||
overlay.style = 'position: absolute; width: 100vw; height: 100vh; top: 0; background: white; color: black;';
|
||
overlay.innerHTML = `<p>The app handling of data has changed in the last update. Please copy and backup externally all the data you need and reset the app to continue using it.</p>
|
||
<button>Reset</button>
|
||
<p>Your open URLs:</p><ul>${JSON.parse(localStorage.getItem('FramesBrowser.CurrentFrames'))?.map(function(item){
|
||
return `<li>${item.replaceAll('<','<').replaceAll('>','>')}</li>`;
|
||
}).join('') || '<p>[nothing previously saved]</p>'}</ul>`;
|
||
overlay.querySelector('button').onclick = function(){
|
||
['CurrentFrames','FrameIndexes','url'].forEach(function(key){
|
||
localStorage.removeItem(`FramesBrowser.${key}`);
|
||
});
|
||
location.reload();
|
||
};
|
||
document.body.appendChild(overlay);
|
||
}
|
||
}
|
||
|
||
(new ResizeObserver(function(){
|
||
document.querySelector('button[onclick="ResizeFrame()"]').textContent = (IframeMainBox.style.length ? '↘️(~) Size' : '↘️ Size');
|
||
})).observe(IframeMainBox);
|
||
|
||
window.onhashchange = (function(){ location.reload() });
|
||
|
||
window.onload = (function(){
|
||
Array.from(document.querySelectorAll('noscript, .NoScript')).forEach(function(el){ el.remove() });
|
||
AppData = (JSON.parse(localStorage.getItem('org.eu.octt.FramesBrowser.v1')) || {});
|
||
SesAppData = {
|
||
optionsFromUrl: (AppData.optionsFromUrl || false),
|
||
fullscreen: (AppData.fullscreen || false),
|
||
frameZoomIndex: (isNaN(parseInt(AppData.frameZoomIndex)) ? -1 : AppData.frameZoomIndex),
|
||
};
|
||
SesAppDataBak = structuredClone(SesAppData);
|
||
AppData = {
|
||
tabs: (AppData.tabs || []),
|
||
urls: (AppData.urls || []),
|
||
currentTabIndex: (isNaN(parseInt(AppData.currentTabIndex)) ? -1 : AppData.currentTabIndex),
|
||
};
|
||
AlertMigrateAppData();
|
||
SaveAppData();
|
||
InputUri.value = GetTabUrlFromTabIndex(AppData.currentTabIndex);
|
||
ApplyFullscreen();
|
||
ApplyFrameZoom();
|
||
ShowFrame(AppData.currentTabIndex);
|
||
RefreshFramesCounter();
|
||
if (location.hash) {
|
||
var tokens = location.hash.slice(1).split('|');
|
||
if (tokens[0].startsWith('_')) {
|
||
// leading underscore indicates a restricted quick query, succeding charaters are identifiers for versioning
|
||
var paramsVersion = tokens[0].slice(1);
|
||
var flags = {};
|
||
SesAppData.optionsFromUrl = true;
|
||
tokens = tokens.slice(1);
|
||
switch (paramsVersion) {
|
||
case '1':
|
||
while (tokens.length > 0) {
|
||
var opt = tokens[0];
|
||
var optLow = opt.toLowerCase();
|
||
if (optLow.startsWith('f=')) {
|
||
var flagStr = hex2bin(opt.slice(2));
|
||
var flags = {
|
||
//disallowPrefsOverride: flagStr[0],
|
||
//fullscreen: flagStr[3],
|
||
immersiveView: flagStr[3],
|
||
};
|
||
for (var flag in flags) {
|
||
flags[flag] = !!Number(flags[flag]);
|
||
}
|
||
} else if (optLow.startsWith('u=') || optLow.startsWith('h=')) {
|
||
// load data URI, or HTML content
|
||
tokens[0] = tokens[0].slice(2);
|
||
var fieldData = tokens.join('|');
|
||
var url = ((optLow.startsWith('h=') ? 'data:text/html;utf8,' : '') + fieldData);
|
||
if (GetTabUrlFromTabIndex(AppData.currentTabIndex) !== url) {
|
||
AddFrame();
|
||
InputUri.value = url;
|
||
LoadFrame();
|
||
}
|
||
flags.immersiveView && !SesAppData.fullscreen && ToggleFullscreen();
|
||
!flags.immersiveView && SesAppData.fullscreen && ToggleFullscreen();
|
||
break;
|
||
}
|
||
tokens = tokens.slice(1);
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
});
|
||
|
||
window.addEventListener('message', (function(messageEv){
|
||
var data = (messageEv.data && messageEv.data.FramesBrowser);
|
||
if (data) {
|
||
data = data.split(':');
|
||
document.querySelector(`button[onclick="FrameDispatch('${data[0]}')"]`).disabled = parseInt(data[1]);
|
||
}
|
||
}));
|
||
</script>
|
||
</body>
|
||
</html>
|