501 lines
14 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<script src="./SpaccDotWeb/SpaccDotWeb.js" module="SpaccDotWeb"></script>
<script src="//SpaccInc.gitlab.io/SpaccDotWeb/SpaccDotWeb.js" module="SpaccDotWeb"></script>
<script module="Meta">({
Name: "🃏️ [Matrix] Sticker Helper",
})</script>
<script module="Main" type="module">
import { html, render } from 'https://esm.sh/htm/preact/standalone';
//import axios from 'https://cdn.skypack.dev/axios';
const Spacc = SpaccDotWeb.AppInit();
const State = {};
const Defaults = {
stickerSelectorUrl: "https://maunium.net/stickers-demo/", //"https://hub.octt.eu.org/Matrix/MauniumStickerPickerFork/",
appIdentity: "org.eu.octt.MatrixStickerHelper",
appInterface: "v1",
};
let Config = localStorage.getItem('SpaccInc-Matrix-Config');
if (Config) Config = JSON.parse(Config);
else Config = {
accounts: [],
};
Config.Save = () => localStorage.setItem('SpaccInc-Matrix-Config', JSON.stringify(Config));
function ActionForm (props) {
let formElems = [];
for (const action of props.actionEntries) {
action.options ||= [];
formElems.push(html`
<button
style=${{ textAlign: 'left', width: `calc(100% - ${action.options.length * 3}em)`, display: 'inline-block' }}
dataIndex=${action.dataIndex}
onclick=${action.onclick}
disabled=${action.disabled}
>
${action.label}
</button>
`)
for (const option of action.options) {
formElems.push(html`
<button
style=${{ width: '3em', display: 'inline-block' }}
dataIndex=${option.dataIndex}
onclick=${option.onclick}
>
${option.label}
</button>
`)
}
}
return html`<div class="ActionForm">
${formElems}
</div>`
}
function TableForm (props) {
let tableRows = [];
for (const entry of props.formEntries) {
tableRows.push(html`<tr>
<td>
${entry.type !== 'button' ? entry.label : ''}
</td>
<td style=${{ width: '100%' }}>
<input
style=${{ width: '100%' }}
name=${entry.name || entry.label.toLowerCase()}
type=${entry.type}
placeholder=${entry.placeholder || entry.value}
value=${entry.type !== 'button' ? entry.value : (entry.value || entry.label)}
onclick=${entry.onclick || entry.onInteract}
onselect=${entry.onInteract}
onchange=${entry.onInteract}
oninput=${entry.onInteract}
onpaste=${entry.onInteract}
/>
</td>
</tr>`)
}
return html`<div class="TableForm" id=${props.id}>
<table>
<tbody>
${tableRows}
</tbody>
</table>
</div>`
}
function ClickImg (props) {
function deleteClick (that) {
const button = that.base.querySelector('button[name="delete"]');
button.innerHTML = '❌️ Confirm delete?';
button.onclick = function(){
}
}
return html`<div class="ClickImg">
<img
src=${props.src}
onclick=${() => {
for (const openImg of document.querySelectorAll('.ClickImg[data-opened="1"]')) {
if (openImg !== this.base ) {
openImg.dataset.opened = '0';
}
}
const isOpened = this.base.dataset.opened;
if (isOpened === '1') {
this.base.dataset.opened = '0';
} else {
this.base.dataset.opened = '1';
}
this.base.querySelector('button[name="delete"]').innerHTML = '❌️';
}}
/>
<button
name="delete"
onclick=${() => deleteClick(this)}
>
❌️
</button>
</div>`
}
const Request = (url, options) => {
const http = new XMLHttpRequest();
http.onreadystatechange = function() {
if (this.readyState === 4) {
(options.callback || function(){})(http, options.callbackData);
}
}
http.open((options.method || 'GET'), url, true);
for (const header in (options.headers || {})) {
http.setRequestHeader(header, options.headers[header]);
}
http.send(options.body);
}
const TryMatrixLoginAndSaveAndUse = (loginData) => {
/*fetch(`${loginData.homeserver}/_matrix/client/v3/login`, {
method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({
type: "m.login.password",
identifier: {
type: "m.id.user",
user: loginData.username,
},
password: loginData.password,
})
}).then(response => {
if (response.ok) {
Config.accounts.push({
homeserver: loginData.homeserver,
username: loginData.username,
token: body.token,
});
Config.Save();
Render();
} else {
alert(`Error: ${response.json()}`);
}
})*/
Request(`${loginData.homeserver}/_matrix/client/v3/login`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
type: "m.login.password",
identifier: {
type: "m.id.user",
user: loginData.username,
},
password: loginData.password,
}),
callback: function(http) {
if (http.status === 200) {
State.account = {
homeserver: loginData.homeserver,
username: loginData.username,
token: JSON.parse(http.responseText).access_token,
};
Config.accounts.push(State.account);
Config.Save();
Render(CollectionScreen);
} else {
alert(`Error: ${http.responseText}`);
document.querySelector('#AccountLoginForm input[name="loginSave"]').disabled = false;
}
},
});
}
const DataWidgetSaveRequest = (configUrl) => {
document.querySelector('input[name="reinit"]').disabled = true;
document.querySelector('button[name="commit"]').disabled = true;
State.dataWidgets.stickerpicker = {
content: {
type: "m.stickerpicker",
url: `${document.querySelector('input[name="sticker selector url"]').value}?&config=${configUrl}&theme=$theme`,
name: "Stickerpicker",
creatorUserId: `${GetMatrixUserTag(State.account)}`,
managedBy: [
`${Defaults.appIdentity}/${Defaults.appInterface}`,
],
},
sender: `${GetMatrixUserTag(State.account)}`,
state_key: "stickerpicker",
type: "m.widget",
id: "stickerpicker",
};
Request(`${State.account.homeserver}/_matrix/client/v3/user/${GetMatrixUserTag(State.account)}/account_data/m.widgets`, {
method: 'PUT',
headers: { "Content-Type": "application/json", "authorization": `Bearer ${State.account.token}` },
body: JSON.stringify(State.dataWidgets),
callback: function(http) {
const data = JSON.parse(http.responseText);
if (http.status === 200) {
console.log(data);
} else {
alert(`Error: ${http.responseText}`);
document.querySelector('button[name="commit"]').disabled = false;
}
document.querySelector('input[name="reinit"]').disabled = false;
},
});
}
const GetMatrixUserTag = (account) => `@${account.username}:${account.homeserver.split('://')[1]}`
function AccountChoice (props) {
let accountsActions = [];
for (let i=0; i<Config.accounts.length; i++) {
let account = Config.accounts[i];
accountsActions.push({
label: `🐱️ ${GetMatrixUserTag(account)}`,
onclick: () => {
State.account = account;
Render(CollectionScreen);
},
options: [ {
dataIndex: i,
label: '❌️',
onclick: () => {
Config.accounts.pop(this.dataIndex);
Config.Save();
Render();
},
} ]
})
}
return html`
<${ActionForm} actionEntries=${[...accountsActions,
{ label: '🆕️ Add new account', onclick: () => Render(AccountLogin) },
]}/>
`
}
function AccountLogin (props) {
return html`<Fragment>
<p>
Please login with your [Matrix] account.
(Your login details are processed locally and only sent to the homeserver you specified.)
</p>
<${TableForm} id="AccountLoginForm" formEntries=${[
{ label: 'Homeserver', type: 'text', value: 'https://matrix.org' },
{ label: 'Username', type: 'text', placeholder: 'AzureDiamond' },
{ label: 'Password', type: 'password', placeholder: '*******' },
{ label: 'Import via session token', type: 'checkbox' },
{ label: '🆕️ Login and Save', type: 'button', onclick: () => {
this.base.querySelector('input[name="loginSave"]').disabled = true;
const password = this.base.querySelector('input[name="password"]').value;
let loginData = {
homeserver: this.base.querySelector('input[name="homeserver"]').value,
username: this.base.querySelector('input[name="username"]').value,
};
if (this.base.querySelector('input[name="import via session token"]').checked) {
loginData.token = password;
Config.accounts.push(loginData);
Config.Save();
Render(CollectionScreen);
} else {
loginData.password = password;
TryMatrixLoginAndSaveAndUse(loginData);
}
}, name: 'loginSave' },
...(Config.accounts.length > 0
? [{ label: '✖️ Cancel', type: 'button', onclick: () => Render() }]
: [])
]}/>
</Fragment>`
}
function CollectionList (props) {
return html`<Fragment>
<div id="CollectionList"><div>
<img src="https://http.cat/100"/>
<img src="https://http.cat/101"/>
<img src="https://http.cat/100"/>
<img src="https://http.cat/101"/>
<img src="https://http.cat/100"/>
<img src="https://http.cat/101"/>
<img src="https://http.cat/100"/>
<img src="https://http.cat/101"/>
<img src="https://http.cat/100"/>
<img src="https://http.cat/101"/>
<img src="https://http.cat/100"/>
<img src="https://http.cat/101"/>
<button style=${{ verticalAlign: 'top', height: '64px' }}>
Add new
</button>
</div></div>
<${CollectionGrid}/>
</Fragment>`
}
function CollectionGrid (props) {
return html`<div id="CollectionGrid">
<${ClickImg} src="https://http.cat/100"/>
<${ClickImg} src="https://http.cat/101"/>
<${ClickImg} src="https://http.cat/102"/>
<${ClickImg} src="https://http.cat/103"/>
<${ClickImg} src="https://http.cat/200"/>
<${ClickImg} src="https://http.cat/201"/>
<${ClickImg} src="https://http.cat/202"/>
<${ClickImg} src="https://http.cat/203"/>
<${ClickImg} src="https://http.cat/204"/>
<${ClickImg} src="https://http.cat/205"/>
<button
style=${{ verticalAlign: 'top', width: '128px', height: '128px' }}
onclick=${() => this.base.querySelector('button > input[type="file"]').click()}
>
Add new
<input
style=${{ display: 'none' }}
type="file"
accept="image/jpeg,image/gif,image/png,image/webp"
multiple
onchange=${(data) => {
console.log(data.target.files);
document.querySelector('button[name="commit"]').disabled = false;
}}
/>
</button>
</div>`
}
function CollectionView (props) {
const isManaged = props.extra?.stickerpicker?.content?.managedBy?.includes(`${Defaults.appIdentity}/${Defaults.appInterface}`);
State.dataWidgets = props.extra;
if (!State.dataWidgets) State.dataWidgets = {};
new URLSearchParams(State.dataWidgets?.stickerpicker?.content?.url?.split('?')?[1]).get('config');
return html`<Fragment>
<${ActionForm} actionEntries=${[
{ label: '🔙️ Go Back', onclick: () => Render() },
{ name: 'commit', label: '📝️ Commit Changes', disabled: true, onclick: DataWidgetSaveRequest },
]}/>
<${CollectionList}/>
${!props.extras && html`<p>
Your account is currently not set-up to handle stickers.
</p>`}
${props.extras && !isManaged && html`<p>
Your account is set-up with a sticker picker, but it's not marked as being managed by this app.
This could mean that you are currently using an incompatible sticker picker.
You can try to continue anyway if you think it should work, otherwise you should reinitialize sticker data.
</p>`}
<details open=${!props.extras || !isManaged}>
<summary>
<h3 style=${{ display: 'inline' }}>
Options
</h3>
</summary>
<p>
Reinitializing sticker data for your account will simply override
the <code>stickerpicker</code> subfield in its <code>m.widgets</code> field.
</p>
<${TableForm} formEntries=${[
{ name: 'reinit', label: '💡️ Reinitialize sticker data as new', type: 'button', onclick: DataWidgetSaveRequest },
{ label: 'or', type: 'hidden' },
{ label: 'Import JSON from URL', placeholder: 'https://example.com', type: 'text', onInteract: () => {
const thisElem = this.base.querySelector('input[name="import json from url"]');
const button = this.base.querySelector('input[name="reinit"]');
if (thisElem.value === '') {
button.value = '💡️ Reinitialize sticker data as new';
} else {
button.value = '💡️ Reinitialize sticker packs from URL';
}
} },
]}/>
<details open=${false}>
<summary>
<h4 style=${{ display: 'inline' }}>
Advanced
</h4>
</summary>
<${TableForm} formEntries=${[
{ label: 'Sticker selector URL', value: Defaults.stickerSelectorUrl, type: 'text' },
]}/>
</details>
</details>
</Fragment>`
}
function CollectionScreen (props) {
Request(`${State.account.homeserver}/_matrix/client/v3/user/${GetMatrixUserTag(State.account)}/account_data/m.widgets`, {
headers: { "Content-Type": "application/json", "authorization": `Bearer ${State.account.token}` },
callback: function(http) {
const data = JSON.parse(http.responseText);
if (http.status === 200) {
Render(CollectionView, data);
} else {
if (data.errcode === 'M_NOT_FOUND') {
Render(CollectionView);
} else {
alert(`Error: ${http.responseText}`);
Render();
}
}
},
});
}
function App (props) {
return html`
<${props.screen || (Config.accounts.length > 0 ? AccountChoice : AccountLogin)} extras=${props.extras}/>
<p>
🃏️ [Matrix] Sticker Helper WIP, created with ☕️ by <a href="https://hub.octt.eu.org">OctoSpacc</a>.
Made possible by <a href="https://github.com/maunium/stickerpicker">Maunium sticker picker</a>.
</p>
<style>
:root {
--margin: 8px;
}
* {
box-sizing: border-box;
}
body, .ActionForm, .TableForm {
margin: var(--margin);
}
#CollectionList {
max-width: 100%;
overflow: auto;
}
#CollectionList > div {
width: max-content;
}
#CollectionList > div > * {
width: 64px;
margin: var(--margin);
}
.ActionForm > *,
.TableForm td > * {
display: block;
height: 2em;
width: 100%;
}
.ActionForm > * > * {
height: 2em;
}
#CollectionGrid > *,
.ClickImg {
display: inline-block;
margin: 8px;
}
.ClickImg,
.ClickImg img {
width: 128px;
}
.ClickImg[data-opened="1"], .ClickImg[data-opened="1"] > img {
width: calc(100% - var(--margin));
}
</style>
`
}
function Render (screen, extras) {
render(html`<${App} screen=${screen} extras=${extras}/>`, document.body);
}
Render()
</script>