OctoSpaccHub/public/MatrixStickerHelper/index.html

539 lines
16 KiB
HTML
Raw Normal View History

<!DOCTYPE html>
<script src="./SpaccDotWeb/SpaccDotWeb.js" module="SpaccDotWeb"></script>
<script src="//SpaccInc.gitlab.io/SpaccDotWeb/SpaccDotWeb.js" module="SpaccDotWeb"></script>
<script src="https://googlechrome.github.io/dialog-polyfill/dist/dialog-polyfill.js"></script>
<script module="Meta">({
Name: "🃏️ [Matrix] Sticker Helper",
})</script>
<script module="Main" type="module">
const Spacc = SpaccDotWeb.AppInit();
</script>
<script module="App" type="module">
import { html, render } from 'https://esm.sh/htm/preact/standalone';
import axios from 'https://cdn.skypack.dev/axios';
const State = {};
const Defaults = {
stickerSelectorUrl: "https://maunium.net/stickers-demo/",
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}
name=${action.name}
>
${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) {
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';
}
}}
/>
<${SecureButton}
name="delete"
modalText='❌️ Confirm delete?'
onclick=${() => {
alert(1)
}}
>
❌️
</${SecureButton}>
</div>`
}
function ShowModal (text, action, question) {
document.querySelector('#Modal > p').innerHTML = text;
document.querySelector('#Modal > button[name="confirm"]').onclick = action;
document.querySelector('#Modal').showModal();
}
function SecureButton (props) {
return html`
<button
class="SecureButton ${props.class}"
name=${props.name}
onclick=${() => ShowModal('❌️ Confirm delete?', props.onclick)}
>
${props.children}
</button>
`
}
function 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);
}
async function TryMatrixLoginAndSaveAndUse (loginData) {
try {
const response = await axios.post(`${loginData.homeserver}/_matrix/client/v3/login`, {
type: "m.login.password",
identifier: {
type: "m.id.user",
user: loginData.username,
},
password: loginData.password,
});
State.account = {
homeserver: loginData.homeserver,
username: loginData.username,
token: response.data.access_token,
};
Config.accounts.push(State.account);
Config.Save();
Render(CollectionScreen);
} catch(err) {
alert(`Error trying to get a session token: ${err.response.request.response}`);
document.querySelector('#AccountLoginForm input[name="loginSave"]').disabled = false;
}
}
async function 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",
};
try {
const response = await axios.put(`${State.account.homeserver}/_matrix/client/v3/user/${GetMatrixUserTag(State.account)}/account_data/m.widgets`, State.dataWidgets, {
headers: { "Authorization": `Bearer ${State.account.token}` },
});
console.log(response.data);
Render(CollectionScreen);
} catch(err) {
alert(`Error updating account widget data: ${err.response.request.response}`);
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) {
let packElems = [];
for (const pack of (props.extras?.dataPacks?.packs || [])) {
packElems.push(html`
<img
src="${props.extras?.dataPacks?.homeserver_url}/_matrix/media/r0/thumbnail/${pack.stickers[0].info.thumbnail_url.split('mxc://')[1]}?&height=128&width=128&method=scale"
onclick=${null}
/>
`);
}
return html`<${CollectionViewMenu} optionsOpen=${false}>
<div id="CollectionList"><div>
<button style=${{ verticalAlign: 'top', height: '64px' }}>
Create new pack
</button>
${packElems}
</div></div>
<${CollectionGrid}/>
</${CollectionViewMenu}>`
}
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"/>
<button
style=${{ verticalAlign: 'top', width: '128px', height: '128px' }}
onclick=${() => this.base.querySelector('button > input[type="file"]').click()}
>
Upload new
<br/><br/>
<small>(Only image files supported for now)</small>
<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>`
}
async function CollectionScreen (props) {
try {
const response = await axios.get(`${State.account.homeserver}/_matrix/client/v3/user/${GetMatrixUserTag(State.account)}/account_data/m.widgets`, {
headers: { "Authorization": `Bearer ${State.account.token}` },
});
Render(CollectionFetchData, response.data);
} catch(err) {
if (err.response.data.errcode === 'M_NOT_FOUND') {
Render(CollectionFetchData);
} else {
alert(`Error fetching account widget data: ${err.response.request.response}`);
Render();
}
}
}
async function CollectionFetchData (props) {
const packsUrl = (new URLSearchParams(props.dataWidgets?.stickerpicker?.content?.url?.split('?')[1])).get('config');
if (packsUrl) {
try {
const response = await axios.get(packsUrl).data;
Render(CollectionView, {
dataWidgets: props.extras,
dataPacks: response.data,
});
} catch(err) {
alert(`Error: ${err.response.request.response}`);
Render();
}
} else {
Render(CollectionView, {
dataWidgets: props.extras,
});
}
}
function CollectionView (props) {
const isManaged = props.extras?.dataWidgets?.stickerpicker?.content?.managedBy?.includes(`${Defaults.appIdentity}/${Defaults.appInterface}`);
State.dataWidgets = props.extras?.dataWidgets;
if (!State.dataWidgets) State.dataWidgets = {};
return html`<${CollectionViewMenu} optionsOpen=${!props.dataWidgets || !props.isManaged}>
${!props.extras?.dataWidgets && html`<p>
Your account must first be set-up to handle stickers.
</p>`}
${props.extras?.dataWidgets && !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>
<p>
<button
class="ActionButton"
onclick=${() => {
Render(CollectionList, props.extras?.dataPacks)
}}
>
⏭️ Continue
</button>
</p>
`}
${isManaged && Render(CollectionList, props.extras?.dataPacks)}
</${CollectionViewMenu}>`
}
function CollectionViewMenu (props) {
return html`<Fragment class="CollectionViewMenu">
<${ActionForm} actionEntries=${[
{ label: '🔙️ Go Back', onclick: () => Render() },
{ name: 'commit', label: '📝️ Commit Changes', disabled: true, onclick: () => DataWidgetSaveRequest(packsUrl) },
]}/>
${props.children}
<details open=${props.optionsOpen}>
<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(this.base.querySelector('input[name="import json from url"]').value) },
{ 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 App (props) {
return html`<Fragment>
<div class="Dynamic">
<${props.screen || (Config.accounts.length > 0 ? AccountChoice : AccountLogin)} extras=${props.extras}/>
</div>
<div class="Static">
<dialog id="Modal">
<p></p>
<button onclick=${() => document.querySelector('#Modal').close()}>🔙️ Back</button>
<button name="confirm">⏩️ Confirm</button>
</dialog>
<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);
}
.ActionButton,
.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>
</div>
</Fragment>`
}
function Render (screen, extras) {
render(html`<${App} screen=${screen} extras=${extras}/>`, document.body);
// idk why that section gets duplicated, it worked fine until a bit before, we do this to fix it
for (const type of ['.Static', '.CollectionViewMenu']) {
const elems = document.querySelectorAll(type);
for (let i=0; i<(elems.length-1); i++) {
elems[i].remove();
}
}
dialogPolyfill.registerDialog(document.querySelector('#Modal'));
}
Render()
</script>