pages: work in progress [might break]
This commit is contained in:
parent
1c2016c37e
commit
c83ef1cc54
|
@ -0,0 +1,2 @@
|
|||
*.xdc
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
This is free and unencumbered software released into the public domain.
|
||||
|
||||
Anyone is free to copy, modify, publish, use, compile, sell, or
|
||||
distribute this software, either in source code form or as a compiled
|
||||
binary, for any purpose, commercial or non-commercial, and by any
|
||||
means.
|
||||
|
||||
In jurisdictions that recognize copyright laws, the author or authors
|
||||
of this software dedicate any and all copyright interest in the
|
||||
software to the public domain. We make this dedication for the benefit
|
||||
of the public at large and to the detriment of our heirs and
|
||||
successors. We intend this dedication to be an overt act of
|
||||
relinquishment in perpetuity of all present and future rights to this
|
||||
software under copyright law.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
||||
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
For more information, please refer to <https://unlicense.org>
|
|
@ -0,0 +1,74 @@
|
|||
# Hello
|
||||
|
||||
Sample project with a simple implementation of the webxdc read and write APIs.
|
||||
|
||||
|
||||
## Demo (no server or installation required)
|
||||
|
||||
1. Open `index.html` in your web browser
|
||||
2. Click 'Add Peer' to open as many peers as you like
|
||||
3. Type a message and press 'Send' to see the update in each peer. (For Safari you might need to check the setting under Develop > Disable Local File Restrictions.)
|
||||
|
||||
|
||||
## Developing webxdc apps
|
||||
|
||||
Simply copy `webxdc.js` from this repo beside your `index.html` and you are ready to go
|
||||
to **develop and test your app in most browsers.**
|
||||
|
||||
Bundle your app using `./create-xdc.sh your-app-name`
|
||||
and **send it to your friends** 🙂
|
||||
|
||||
<img width="311" alt="Screenshot 2023-02-10 at 20 40 22" src="https://user-images.githubusercontent.com/9800740/218183018-59d0aa06-da92-445b-9cad-51e416594d31.png">
|
||||
|
||||
|
||||
## Further Hints and Troubleshooting
|
||||
|
||||
|
||||
### Limitations
|
||||
|
||||
Due to the nature of most browsers and how they scope `localStorage`,
|
||||
each emulated peer will get the same `localStorage`.
|
||||
|
||||
To really test the storage usage of your Webxdc,
|
||||
bundle the app and test it in Delta Chat directly
|
||||
where all peers get their own `localStorage`.
|
||||
Alternatively, use the more advanced [webxdc-dev](https://github.com/webxdc/webxdc-dev) tool.
|
||||
|
||||
|
||||
### Type-checking and completion
|
||||
|
||||
If you are using VSCode you can have autocompletion and type-checking even without using TypeScript by adding these two lines to your JavaScript source files:
|
||||
|
||||
```js
|
||||
//@ts-check
|
||||
/** @typedef {import('./webxdc').Webxdc} Webxdc */
|
||||
```
|
||||
|
||||
Without VSCode you need to install TypeScript and then run the check manually.
|
||||
|
||||
```sh
|
||||
npm -g typescript
|
||||
tsc --noEmit --allowJs --lib es2016,dom webxdc.js # to check if types and simulator are in sync
|
||||
tsc --noEmit --allowJs --lib es2016,dom your_js_file.js
|
||||
```
|
||||
|
||||
### Developing in Safari
|
||||
|
||||
To use the devtool in safari you need to disable the local file restrictions
|
||||
under `Develop` -> `Disable Local File Restrictions`.
|
||||
|
||||
After doing this you can use the dev tool simulator.
|
||||
|
||||
Make sure to reload (`Cmd + R`) all simulator tabs/windows to apply this setting.
|
||||
Without this option `Add Peer` seems to work (it opens a new instance), but **the instances will not be able to communicate**.
|
||||
|
||||
|
||||
### Developing on Android
|
||||
|
||||
- install Termux
|
||||
- install Python and Git in Termux
|
||||
- `git clone` the devtool repo or your fork of it
|
||||
- use `python -m http.server` to serve it for development using nano/vim
|
||||
- when you are done, use `./create-xdc.sh` for bundling
|
||||
- copy the created `.xdc` file to a location from where you can access and send it via Delta Chat
|
||||
- pro tip: you can create symbolic link to a folder in the external storage
|
|
@ -0,0 +1,27 @@
|
|||
#!/bin/sh
|
||||
|
||||
case "$1" in
|
||||
"-h" | "--help")
|
||||
echo "usage: ${0##*/} [PACKAGE_NAME]"
|
||||
exit
|
||||
;;
|
||||
"")
|
||||
PACKAGE_NAME=${PWD##*/} # '##*/' removes everything before the last slash and the last slash
|
||||
;;
|
||||
*)
|
||||
PACKAGE_NAME=${1%.xdc} # '%.xdc' removes the extension and allows PACKAGE_NAME to be given with or without extension
|
||||
;;
|
||||
esac
|
||||
|
||||
rm "$PACKAGE_NAME.xdc" 2> /dev/null
|
||||
zip -9 --recurse-paths "$PACKAGE_NAME.xdc" --exclude LICENSE README.md webxdc.js "./*.sh" "./*.xdc" -- *
|
||||
|
||||
echo "success, archive contents:"
|
||||
unzip -l "$PACKAGE_NAME.xdc"
|
||||
|
||||
# check package size
|
||||
MAXSIZE=655360
|
||||
size=$(wc -c < "$PACKAGE_NAME.xdc")
|
||||
if [ "$size" -ge $MAXSIZE ]; then
|
||||
echo "WARNING: package size exceeded the limit ($size > $MAXSIZE)"
|
||||
fi
|
Binary file not shown.
After Width: | Height: | Size: 16 KiB |
|
@ -0,0 +1,67 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Hello</title>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width"/>
|
||||
<script src="webxdc.js"></script>
|
||||
<style type="text/css">
|
||||
body {
|
||||
font-family: Helvetica, Arial, sans-serif;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Hello</h1>
|
||||
<form>
|
||||
<input id="input" type="text" placeholder="Message" autofocus required />
|
||||
<input type="submit" onclick="sendMsg(); return false;" value="Send" />
|
||||
</form>
|
||||
<p id="output"></p>
|
||||
<p><em><small id="deviceName"></small></em></p>
|
||||
<script>
|
||||
|
||||
var El = function (tag, text) {
|
||||
var el = document.createElement(tag);
|
||||
el.innerText = text || '';
|
||||
return el;
|
||||
};
|
||||
|
||||
// handle past and future state updates
|
||||
window.webxdc.setUpdateListener(function (update) {
|
||||
var output = document.getElementById('output');
|
||||
// when appending content to an element with output.innerHTML +=
|
||||
// that content is implicitly parsed, making it possible for messages
|
||||
// to be interpreted as scripts. Creating elements directly,
|
||||
// injecting content as plain text, and appending them to the DOM
|
||||
// is a much safer practice.
|
||||
[
|
||||
El('strong', update.payload.name + ':'),
|
||||
El('span', update.payload.msg),
|
||||
El('br'),
|
||||
].forEach(function (item) {
|
||||
output.appendChild(item);
|
||||
});
|
||||
});
|
||||
|
||||
function sendMsg() {
|
||||
msg = document.getElementById("input").value;
|
||||
info = 'someone typed "' + msg + '"';
|
||||
document.getElementById("input").value = '';
|
||||
|
||||
// send new updates
|
||||
window.webxdc.sendUpdate({
|
||||
payload: {
|
||||
name: window.webxdc.selfName,
|
||||
msg,
|
||||
},
|
||||
info,
|
||||
}, info);
|
||||
}
|
||||
|
||||
(function () {
|
||||
window.deviceName.innerText = 'this is ' + window.webxdc.selfName;
|
||||
})()
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,2 @@
|
|||
name = "Hello"
|
||||
source_code_url = "https://github.com/webxdc/hello"
|
|
@ -0,0 +1,324 @@
|
|||
// This file originates from
|
||||
// https://github.com/webxdc/hello/blob/master/webxdc.js
|
||||
// It's a stub `webxdc.js` that adds a webxdc API stub for easy testing in
|
||||
// browsers. In an actual webxdc environment (e.g. Delta Chat messenger) this
|
||||
// file is not used and will automatically be replaced with a real one.
|
||||
// See https://docs.webxdc.org/spec.html#webxdc-api
|
||||
let ephemeralUpdateKey = "__xdcEphemeralUpdateKey__";
|
||||
|
||||
/**
|
||||
* @typedef {import('./webxdc.d.ts').RealtimeListener} RT
|
||||
* @type {RT}
|
||||
*/
|
||||
class RealtimeListener {
|
||||
constructor() {
|
||||
this.listener = null;
|
||||
this.trashed = false;
|
||||
}
|
||||
|
||||
is_trashed() {
|
||||
return this.trashed;
|
||||
}
|
||||
|
||||
receive(data) {
|
||||
if (this.trashed) {
|
||||
throw new Error("realtime listener is trashed and can no longer be used");
|
||||
}
|
||||
if (this.listener) {
|
||||
this.listener(data);
|
||||
}
|
||||
}
|
||||
|
||||
setListener(listener) {
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
send(data) {
|
||||
if (!data instanceof Uint8Array) {
|
||||
throw new Error("realtime listener data must be a Uint8Array");
|
||||
}
|
||||
window.localStorage.setItem(
|
||||
ephemeralUpdateKey,
|
||||
JSON.stringify([window.webxdc.selfAddr, Array.from(data), Date.now()]) // Date.now() is needed to trigger the event
|
||||
);
|
||||
}
|
||||
|
||||
leave() {
|
||||
this.trashed = true;
|
||||
}
|
||||
}
|
||||
|
||||
// debug friend: document.writeln(JSON.stringify(value));
|
||||
//@ts-check
|
||||
/** @type {import('./webxdc').Webxdc<any>} */
|
||||
window.webxdc = (() => {
|
||||
var updateListener = (_) => {};
|
||||
/**
|
||||
* @type {RT | null}
|
||||
*/
|
||||
var realtimeListener = null;
|
||||
var updatesKey = "__xdcUpdatesKey__";
|
||||
window.addEventListener("storage", (event) => {
|
||||
if (event.key == null) {
|
||||
window.location.reload();
|
||||
} else if (event.key === updatesKey) {
|
||||
var updates = JSON.parse(event.newValue);
|
||||
var update = updates[updates.length - 1];
|
||||
update.max_serial = updates.length;
|
||||
console.log("[Webxdc] " + JSON.stringify(update));
|
||||
updateListener(update);
|
||||
} else if (event.key === ephemeralUpdateKey) {
|
||||
var [sender, update] = JSON.parse(event.newValue);
|
||||
if (window.webxdc.selfAddr !== sender && realtimeListener && !realtimeListener.is_trashed()) {
|
||||
realtimeListener.receive( Uint8Array.from(update));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function getUpdates() {
|
||||
var updatesJSON = window.localStorage.getItem(updatesKey);
|
||||
return updatesJSON ? JSON.parse(updatesJSON) : [];
|
||||
}
|
||||
|
||||
var params = new URLSearchParams(window.location.hash.substr(1));
|
||||
return {
|
||||
selfAddr: params.get("addr") || "device0@local.host",
|
||||
selfName: params.get("name") || "device0",
|
||||
setUpdateListener: (cb, serial = 0) => {
|
||||
var updates = getUpdates();
|
||||
var maxSerial = updates.length;
|
||||
updates.forEach((update) => {
|
||||
if (update.serial > serial) {
|
||||
update.max_serial = maxSerial;
|
||||
cb(update);
|
||||
}
|
||||
});
|
||||
updateListener = cb;
|
||||
return Promise.resolve();
|
||||
},
|
||||
joinRealtimeChannel: (cb) => {
|
||||
if (realtimeListener && realtimeListener.is_trashed()) {
|
||||
return;
|
||||
}
|
||||
rt = new RealtimeListener();
|
||||
// mimic connection establishment time
|
||||
setTimeout(() => realtimeListener = rt, 500);
|
||||
return rt;
|
||||
},
|
||||
getAllUpdates: () => {
|
||||
console.log("[Webxdc] WARNING: getAllUpdates() is deprecated.");
|
||||
return Promise.resolve([]);
|
||||
},
|
||||
sendUpdate: (update, description) => {
|
||||
var updates = getUpdates();
|
||||
var serial = updates.length + 1;
|
||||
var _update = {
|
||||
payload: update.payload,
|
||||
summary: update.summary,
|
||||
info: update.info,
|
||||
document: update.document,
|
||||
serial: serial,
|
||||
};
|
||||
updates.push(_update);
|
||||
window.localStorage.setItem(updatesKey, JSON.stringify(updates));
|
||||
_update.max_serial = serial;
|
||||
console.log(
|
||||
'[Webxdc] description="' + description + '", ' + JSON.stringify(_update)
|
||||
);
|
||||
updateListener(_update);
|
||||
},
|
||||
sendToChat: async (content) => {
|
||||
if (!content.file && !content.text) {
|
||||
alert("🚨 Error: either file or text need to be set. (or both)");
|
||||
return Promise.reject(
|
||||
"Error from sendToChat: either file or text need to be set"
|
||||
);
|
||||
}
|
||||
|
||||
/** @type {(file: Blob) => Promise<string>} */
|
||||
const blob_to_base64 = (file) => {
|
||||
const data_start = ";base64,";
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(file);
|
||||
reader.onload = () => {
|
||||
/** @type {string} */
|
||||
//@ts-ignore
|
||||
let data = reader.result;
|
||||
resolve(data.slice(data.indexOf(data_start) + data_start.length));
|
||||
};
|
||||
reader.onerror = () => reject(reader.error);
|
||||
});
|
||||
};
|
||||
|
||||
let base64Content;
|
||||
if (content.file) {
|
||||
if (!content.file.name) {
|
||||
return Promise.reject("file name is missing");
|
||||
}
|
||||
if (
|
||||
Object.keys(content.file).filter((key) =>
|
||||
["blob", "base64", "plainText"].includes(key)
|
||||
).length > 1
|
||||
) {
|
||||
return Promise.reject(
|
||||
"you can only set one of `blob`, `base64` or `plainText`, not multiple ones"
|
||||
);
|
||||
}
|
||||
|
||||
// @ts-ignore - needed because typescript imagines that blob would not exist
|
||||
if (content.file.blob instanceof Blob) {
|
||||
// @ts-ignore - needed because typescript imagines that blob would not exist
|
||||
base64Content = await blob_to_base64(content.file.blob);
|
||||
// @ts-ignore - needed because typescript imagines that base64 would not exist
|
||||
} else if (typeof content.file.base64 === "string") {
|
||||
// @ts-ignore - needed because typescript imagines that base64 would not exist
|
||||
base64Content = content.file.base64;
|
||||
// @ts-ignore - needed because typescript imagines that plainText would not exist
|
||||
} else if (typeof content.file.plainText === "string") {
|
||||
base64Content = await blob_to_base64(
|
||||
// @ts-ignore - needed because typescript imagines that plainText would not exist
|
||||
new Blob([content.file.plainText])
|
||||
);
|
||||
} else {
|
||||
return Promise.reject(
|
||||
"data is not set or wrong format, set one of `blob`, `base64` or `plainText`, see webxdc documentation for sendToChat"
|
||||
);
|
||||
}
|
||||
}
|
||||
const msg = `The app would now close and the user would select a chat to send this message:\nText: ${
|
||||
content.text ? `"${content.text}"` : "No Text"
|
||||
}\nFile: ${
|
||||
content.file
|
||||
? `${content.file.name} - ${base64Content.length} bytes`
|
||||
: "No File"
|
||||
}`;
|
||||
if (content.file) {
|
||||
const confirmed = confirm(
|
||||
msg + "\n\nDownload the file in the browser instead?"
|
||||
);
|
||||
if (confirmed) {
|
||||
var element = document.createElement("a");
|
||||
element.setAttribute(
|
||||
"href",
|
||||
"data:application/octet-stream;base64," + base64Content
|
||||
);
|
||||
element.setAttribute("download", content.file.name);
|
||||
document.body.appendChild(element);
|
||||
element.click();
|
||||
document.body.removeChild(element);
|
||||
}
|
||||
} else {
|
||||
alert(msg);
|
||||
}
|
||||
},
|
||||
importFiles: (filters) => {
|
||||
var element = document.createElement("input");
|
||||
element.type = "file";
|
||||
element.accept = [
|
||||
...(filters.extensions || []),
|
||||
...(filters.mimeTypes || []),
|
||||
].join(",");
|
||||
element.multiple = filters.multiple || false;
|
||||
const promise = new Promise((resolve, _reject) => {
|
||||
element.onchange = (_ev) => {
|
||||
console.log("element.files", element.files);
|
||||
const files = Array.from(element.files || []);
|
||||
document.body.removeChild(element);
|
||||
resolve(files);
|
||||
};
|
||||
});
|
||||
element.style.display = "none";
|
||||
document.body.appendChild(element);
|
||||
element.click();
|
||||
console.log(element);
|
||||
return promise;
|
||||
},
|
||||
};
|
||||
})();
|
||||
|
||||
window.addXdcPeer = () => {
|
||||
var loc = window.location;
|
||||
// get next peer ID
|
||||
var params = new URLSearchParams(loc.hash.substr(1));
|
||||
var peerId = Number(params.get("next_peer")) || 1;
|
||||
|
||||
// open a new window
|
||||
var peerName = "device" + peerId;
|
||||
var url =
|
||||
loc.protocol +
|
||||
"//" +
|
||||
loc.host +
|
||||
loc.pathname +
|
||||
"#name=" +
|
||||
peerName +
|
||||
"&addr=" +
|
||||
peerName +
|
||||
"@local.host";
|
||||
window.open(url);
|
||||
|
||||
// update next peer ID
|
||||
params.set("next_peer", String(peerId + 1));
|
||||
window.location.hash = "#" + params.toString();
|
||||
};
|
||||
|
||||
window.clearXdcStorage = () => {
|
||||
window.localStorage.clear();
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
window.alterXdcApp = () => {
|
||||
var styleControlPanel =
|
||||
"position: fixed; bottom:1em; left:1em; background-color: #000; opacity:0.8; padding:.5em; font-size:16px; font-family: sans-serif; color:#fff; z-index: 9999";
|
||||
var styleMenuLink =
|
||||
"color:#fff; text-decoration: none; vertical-align: middle";
|
||||
var styleAppIcon =
|
||||
"height: 1.5em; width: 1.5em; margin-right: 0.5em; border-radius:10%; vertical-align: middle";
|
||||
var title = document.getElementsByTagName("title")[0];
|
||||
if (typeof title == "undefined") {
|
||||
title = document.createElement("title");
|
||||
document.getElementsByTagName("head")[0].append(title);
|
||||
}
|
||||
title.innerText = window.webxdc.selfAddr;
|
||||
|
||||
if (window.webxdc.selfName === "device0") {
|
||||
var root = document.createElement("section");
|
||||
root.innerHTML =
|
||||
'<div id="webxdc-panel" style="' +
|
||||
styleControlPanel +
|
||||
'">' +
|
||||
'<header style="margin-bottom: 0.5em; font-size:12px;">webxdc dev tools</header>' +
|
||||
'<a href="javascript:window.addXdcPeer();" style="' +
|
||||
styleMenuLink +
|
||||
'">Add Peer</a>' +
|
||||
'<span style="' +
|
||||
styleMenuLink +
|
||||
'"> | </span>' +
|
||||
'<a id="webxdc-panel-clear" href="javascript:window.clearXdcStorage();" style="' +
|
||||
styleMenuLink +
|
||||
'">Reset</a>' +
|
||||
"<div>";
|
||||
var controlPanel = root.firstChild;
|
||||
|
||||
function loadIcon(name) {
|
||||
var tester = new Image();
|
||||
tester.onload = () => {
|
||||
root.innerHTML =
|
||||
'<img src="' + name + '" style="' + styleAppIcon + '">';
|
||||
controlPanel.insertBefore(root.firstChild, controlPanel.childNodes[1]);
|
||||
|
||||
var pageIcon = document.createElement("link");
|
||||
pageIcon.rel = "icon";
|
||||
pageIcon.href = name;
|
||||
document.head.append(pageIcon);
|
||||
};
|
||||
tester.src = name;
|
||||
}
|
||||
loadIcon("icon.png");
|
||||
loadIcon("icon.jpg");
|
||||
|
||||
document.getElementsByTagName("body")[0].append(controlPanel);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("load", window.alterXdcApp);
|
Loading…
Reference in New Issue