1224 lines
179 KiB
HTML
1224 lines
179 KiB
HTML
|
|
<!DOCTYPE html>
|
||
|
|
<html>
|
||
|
|
<head>
|
||
|
|
<meta charset="utf-8">
|
||
|
|
<meta name="darkreader-lock">
|
||
|
|
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src https: data:; manifest-src data:;"/>
|
||
|
|
<meta name="referrer" content="no-referrer"/>
|
||
|
|
<meta name="viewport" content="width=device-width, initial-scale=1, interactive-widget=resizes-content"/>
|
||
|
|
<meta property="og:title" content="Utter"/>
|
||
|
|
<meta property="og:description" name="description" content="Have offline single-keyboard conversations with your f/os, designed for sharing"/>
|
||
|
|
<meta property="og:type" content="website"/>
|
||
|
|
<link rel="icon" href="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZmlsbD0iIzU0NTA5ZSIgZD0iTTEyIDNDNi41IDMgMiA2LjU4IDIgMTFhNy4yMTggNy4yMTggMCAwIDAgMi43NSA1LjVjMCAuNi0uNDIgMi4xNy0yLjc1IDQuNSAyLjM3LS4xMSA0LjY0LTEgNi40Ny0yLjUgMS4xNC4zMyAyLjM0LjUgMy41My41IDUuNSAwIDEwLTMuNTggMTAtOHMtNC41LTgtMTAtOCIvPjxwYXRoIGZpbGw9IiNlY2YzZjYiIGQ9Ik0xMiAxN2MtNC40MiAwLTgtMi42OS04LTZzMy41OC02IDgtNiA4IDIuNjkgOCA2LTMuNTggNi04IDYiLz48cGF0aCBmaWxsPSIjMDBkY2FmIiBkPSJNNy41IDEydi0yaC0ydjJ6Ii8+PHBhdGggZmlsbD0iI2ZjMCIgZD0iTTExLjE2NyAxMnYtMmgtMnYyaDIiLz48cGF0aCBmaWxsPSIjYjhjNmY5IiBkPSJNMTQuODMzIDEydi0yaC0ydjJoMiIvPjxwYXRoIGZpbGw9IiNmZjI0NTIiIGQ9Ik0xOC41IDEydi0yaC0ydjJoMiIvPjwvc3ZnPg==">
|
||
|
|
<link rel="manifest" href="data:application/json;base64,ewoJIm5hbWUiOiAiVXR0ZXIiLAoJImRlc2NyaXB0aW9uIjogIkhhdmUgb2ZmbGluZSBzaW5nbGUta2V5Ym9hcmQgY29udmVyc2F0aW9ucyBkZXNpZ25lZCBmb3Igc2hhcmluZyIsCgkiaWNvbnMiOiBbCgkJewoJCQkic3JjIjogImRhdGE6aW1hZ2Uvc3ZnK3htbDtiYXNlNjQsUEhOMlp5QjRiV3h1Y3owaWFIUjBjRG92TDNkM2R5NTNNeTV2Y21jdk1qQXdNQzl6ZG1jaUlIWnBaWGRDYjNnOUlqQWdNQ0F5TkNBeU5DSStQSEJoZEdnZ1ptbHNiRDBpSXpVME5UQTVaU0lnWkQwaVRURXlJRE5ETmk0MUlETWdNaUEyTGpVNElESWdNVEZoTnk0eU1UZ2dOeTR5TVRnZ01DQXdJREFnTWk0M05TQTFMalZqTUNBdU5pMHVORElnTWk0eE55MHlMamMxSURRdU5TQXlMak0zTFM0eE1TQTBMalkwTFRFZ05pNDBOeTB5TGpVZ01TNHhOQzR6TXlBeUxqTTBMalVnTXk0MU15NDFJRFV1TlNBd0lERXdMVE11TlRnZ01UQXRPSE10TkM0MUxUZ3RNVEF0T0NJdlBqeHdZWFJvSUdacGJHdzlJaU5sWTJZelpqWWlJR1E5SWsweE1pQXhOMk10TkM0ME1pQXdMVGd0TWk0Mk9TMDRMVFp6TXk0MU9DMDJJRGd0TmlBNElESXVOamtnT0NBMkxUTXVOVGdnTmkwNElEWWlMejQ4Y0dGMGFDQm1hV3hzUFNJak1EQmtZMkZtSWlCa1BTSk5OeTQxSURFeWRpMHlhQzB5ZGpKNklpOCtQSEJoZEdnZ1ptbHNiRDBpSTJaak1DSWdaRDBpVFRFeExqRTJOeUF4TW5ZdE1tZ3RNbll5YURJaUx6NDhjR0YwYUNCbWFXeHNQU0lqWWpoak5tWTVJaUJrUFNKTk1UUXVPRE16SURFeWRpMHlhQzB5ZGpKb01pSXZQanh3WVhSb0lHWnBiR3c5SWlObVpqSTBOVElpSUdROUlrMHhPQzQxSURFeWRpMHlhQzB5ZGpKb01pSXZQand2YzNablBnPT0iLAoJCQkidHlwZSI6ICJpbWFnZS9zdmcreG1sIiwKCQkJInNpemVzIjogImFueSIKCQl9LAoJCXsKCQkJInNyYyI6ICJkYXRhOmltYWdlL3BuZztiYXNlNjQsaVZCT1J3MEtHZ29BQUFBTlNVaEVVZ0FBQU1BQUFBREFDQU1BQUFCbEFwdzFBQUFBd0ZCTVZFVkhjRXhVVUo1VVVaNWFXcGRVVUo1VVVKNVRVSjVVVUo1VVVKNVVUNTVVVUo1VVVKNVVVSjVUVDU1VVVKNVRVSjVXVDU1VVVKNVVVSjVVVHFGVFVKNVRVSjVWVDUxVVVKNVVVSjVVVUo1VVVKNUtTcjlVVUo1VVVKN3M4L1lBM0svL0pGTC96QUM0eHZubTdmSlhVNkNQa01HVWxNUGo2ZkdqcGN4dGE2M0F4TjNlNU82RmhicHhjSy9aM3V0aFhxWHA4UFhhNVBmeTVxUDUyVkxLMWZqSzBPTjlmTFpkV2FQUDFPYkZ5ZCtabXNhcHJORFQyT2kzdWRkb1pxcXh0TlJQMVJoWkFBQUFIWFJTVGxNQS9CRUY3TEhlYkpVdHllWEJSRktsR29MMEM1dzVJM2ZXWDlBQmlYU2ZpVEVBQUFXNlNVUkJWSGphN05WSG1xcEFGQVhnSWloQkZOdWM3cDBvSWxFbW9xSyszdit1M2x4Q0U2cEtCL1d2NEZ6T3FROGlDSUlnQ0lJZ0NJSWdDSUlnQ0hUSTg1RzJzM3ZtYXFDcUVvQ2txb09WMmJOMzJtZ3VmM3Ywb1RZelZhaWdtak50K0pWbmJBM043a05OZlZzenR1U0xqTmUyQ2cycDlucE12c0pDczZDbDFZL3g4VytmUzkvNGhnWDVHR1ZrU2RDWlpJMlV6M3o4bndGUU12Z1o4MSsrUGdHS0pOc2dQQms5b0s1bjhJdHZBeE9jV3BCMUNSaVJiUFp2UVZsT2dLSEpVaUZNelRmQTJHcEkySm5xRWpBbjZWTm1uNzhQWFBUWmxLRHNKS2puSElYSDArdCtlOGFCaStnRzhmTjJmNTJPWVhTdVc4Sk9JZFNOclZyWnc1TWZZQ24zOG5BOHFNRmNFTXFHS3Z6Rnkzd1hhM0Q5N084alZNb3owaVNvZEExL1kyd2cvZzJ2VUVsYTBweS9EbFVPNld1UGplMzk4QUJWZElWYS9oNVVpQjRCdGhROElxalFteElxWkF2S3BUNTJjcW1xd1pTcDVOK1ViOGQ1WW1meHNmeUVqVXdodjFrZVAwQXFBdWRRZmdHNy9Pa05xWG1tckZha1dGRE04NUVxMzROaWxrSTYyTm9sNjhrU3BDdzVsZXpJM3BMMmRsREl1eUFEdHdnSzdVaHJJeWprN0pHSmZWWmN3b2kwWkV5Z3dOVkhadndyRkpnWXBKWHBDZ3I4ZXlKRHNRY0ZWbFBTeGd3S3BDNHk1YVpRUUtmMkFJNEpNcFk0bEo2QlBJQThCems0UXQ1QXBqSWdCN2s0MGhqUnZEby8vd3ZtcEpHdENlOE9JWElUUW82NUpVMnNJU2ZhSXpmN0NITFdwQUdsRCsvT01YSVVuK0ZkWCtsVXdPR09YRjBPK1FvNkZYQkN6azVkS3NnWEVDWElXUkoxK0p1WjhPNkNPZnhIWkpHYURIaVg0UWRrOEwrOWUxRnFFNGpDQUh5NEp3RWlBUUpwOEtlWGFEUjFPcjJxY1J6dCs3OVZaNXgyZEhjaUJBU1cwOW52Q1Z4aDJjdTVSRGFsNDB3Zytib3BGZGg4YmJrY1d6WWs5NlVTOTVEWUZoMWpYdkVBRkQrQ3NOMDI3ckpVNUJLU2xJNVErQkRkWHBTS1hOeEM1QmR0dmtIYlVwbHRtKzlRRHNtK1ZPWWFrcHpxT1JBOWxnbzlRdVJRUGI5aUVWTyttUGxVSzRMa3JsVG9EcEtJNm9RUW5XMUtoVFpualZjQ0Q2SXY1YlAza3ZLbG54OUZQOG9Ydm44Uy9TcGYraUFwbjMyQnlHdThFZHFxSGNCNTQrMlFBOUc5MmdIY1ErUTBQZ3Q4Vmp1QXp4RE5xSTRMMFY3dEFQWVF1VlRIaHVoSzdRQ3VJTEtwVGd6UlJ1MEFMaUNLcVk0QjBUZTFBL2dHa2ZIL0Q4RG0vZ3E1M0NmeG12dG4xT0cra0UyNGJ5VnlpTTY1YmVibUVKeXgyMDVuM0E4MFZzejhTRW5CbUE3MTIrYUhlanJsZnEweWgrU2EyY1dXYVl6bmF2RzhWWWhqeHZ4eWx4Yk1yOWNwNHg3Z29OVklRa3lYTFVOTWxETVA4bEZrakNMTXVtMGRaaVdIZWFDN0NDSGJEWjlxc0h0RHFrR3hIbWV5QjZkMG03dUtkQnUyQ1UrY1VzNTJxSGdBTFdjQldDWDlVVGl5dEV0alNRMmRNRTk4cFNnZVZlcHhRbzE1T09DY1QvSTNGUTRPdUxsaWszNVBrYzI4QUlKQ2cxc0ppbXd4ZEJFUTBNMEVrS2VCN1BHNjdNSDFEZ2N0cUwxa3hyd1FqaUlYaCswZXlrN2RkVldLS010OHhjV2dDYjFSNWc5UWp0dHJTWFRtY2l1SWxrV3JYa3ZTOGJyQXBFNGt6SnNDVUZGTVVPbm1kNXUyREE4MWJSbFNpenFUQUlNM3h2Q29ReG1Pc05zZTI1cGtPM2hya2hDQy9wdkRCQkYxS2tjVHQ3dWJwL1k4KzMvdGVmWlA3WGx1ZHJjNGpyR3dxRnNPQk93YUpDVXhCT3hhVk0weG5QV1NCTXplSU51enFIdG1QTmpiWTFJZmNnekNPTW1vRjVhUENnejZYWHE4LzN3cVhGUmcwRERWUTc4TVoxNVFqMHdiZlZybEVmVnJndDRZc3dIYU5rOE4xSEdYVTgreDBaQS9UT1BzSWtBZHgvemJ1dHh4Y2FSVittNUtBblV6Mk
|
||
|
|
<noscript><style>#prepare{display:none}</style></noscript>
|
||
|
|
<title>Flutter</title>
|
||
|
|
<style>
|
||
|
|
|
||
|
|
body {
|
||
|
|
background: #2C3433;
|
||
|
|
color: #FFF;
|
||
|
|
font-family: sans-serif;
|
||
|
|
display: flex;
|
||
|
|
flex-direction: column;
|
||
|
|
min-height: 100vh;
|
||
|
|
margin: 0;
|
||
|
|
color-scheme: dark;
|
||
|
|
}
|
||
|
|
code {
|
||
|
|
font-family: monospace;
|
||
|
|
padding: 4px 2px;
|
||
|
|
background: #F001;
|
||
|
|
font-size: 14px;
|
||
|
|
}
|
||
|
|
#prepare {
|
||
|
|
text-align: center;
|
||
|
|
line-height: 1.4;
|
||
|
|
padding: 12px;
|
||
|
|
}
|
||
|
|
#prepare, #app {
|
||
|
|
flex-grow: 1;
|
||
|
|
}
|
||
|
|
#app {
|
||
|
|
flex-direction: column;
|
||
|
|
}
|
||
|
|
#chatbox, #send {
|
||
|
|
background: #FFF2;
|
||
|
|
border: 2px #FFF4 solid;
|
||
|
|
opacity: 0.5;
|
||
|
|
color: #FFF;
|
||
|
|
border-radius: 4px;
|
||
|
|
transition: opacity 0.6s;
|
||
|
|
&:hover, &:focus {
|
||
|
|
opacity: 0.9;
|
||
|
|
}
|
||
|
|
&:active {
|
||
|
|
opacity: 1;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
#chatbox {
|
||
|
|
font-family: inherit;
|
||
|
|
font-size: inherit;
|
||
|
|
padding: 8px;
|
||
|
|
flex-grow: 1;
|
||
|
|
resize: vertical;
|
||
|
|
min-height: 42px;
|
||
|
|
box-sizing: border-box;
|
||
|
|
}
|
||
|
|
@media print {
|
||
|
|
.noprint {
|
||
|
|
display: none;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
#chatwrapper {
|
||
|
|
display: flex;
|
||
|
|
flex-direction: row;
|
||
|
|
padding: 8px;
|
||
|
|
gap: 8px;
|
||
|
|
position: sticky;
|
||
|
|
bottom: 0;
|
||
|
|
left: 0;
|
||
|
|
right: 0;
|
||
|
|
z-index: 30;
|
||
|
|
background: #2C3433CC;
|
||
|
|
}
|
||
|
|
#proxyindicator {
|
||
|
|
aspect-ratio: 1/1;
|
||
|
|
height: 42px;
|
||
|
|
body.AvatarShape-SQUARE & {
|
||
|
|
border-radius: 0px;
|
||
|
|
}
|
||
|
|
body.AvatarShape-ROUNDED & {
|
||
|
|
border-radius: 4px;
|
||
|
|
}
|
||
|
|
body.AvatarShape-CIRCLE & {
|
||
|
|
border-radius: 24px;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
#send {
|
||
|
|
width: 42px;
|
||
|
|
height: 42px;
|
||
|
|
svg {
|
||
|
|
width: 24px;
|
||
|
|
height: 24px;
|
||
|
|
}
|
||
|
|
svg path {
|
||
|
|
fill: currentColor;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
#title {
|
||
|
|
padding: 8px;
|
||
|
|
}
|
||
|
|
a:link, a:visited {
|
||
|
|
color: inherit;
|
||
|
|
}
|
||
|
|
#messages {
|
||
|
|
flex-grow: 1;
|
||
|
|
padding-left: 12px;
|
||
|
|
display: flex;
|
||
|
|
flex-direction: column;
|
||
|
|
justify-content: end;
|
||
|
|
& > div {
|
||
|
|
display: flex;
|
||
|
|
flex-direction: row;
|
||
|
|
.bubble {
|
||
|
|
padding: 8px 12px;
|
||
|
|
margin-left: 8px;
|
||
|
|
margin-bottom: 4px;
|
||
|
|
border-radius: 8px;
|
||
|
|
border-bottom-left-radius: 0;
|
||
|
|
border-top-left-radius: 0;
|
||
|
|
body.BubbleBackground-COLORED & {
|
||
|
|
background: rgba(from var(--color) r g b / 0.2);
|
||
|
|
}
|
||
|
|
body.BubbleBackground-LIGHT & {
|
||
|
|
background: #CCC;
|
||
|
|
color: #000;
|
||
|
|
}
|
||
|
|
body.BubbleBackground-DARK & {
|
||
|
|
background: #222;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
&.initial .bubble {
|
||
|
|
border-top-left-radius: 8px;
|
||
|
|
}
|
||
|
|
&.final .bubble {
|
||
|
|
border-bottom-left-radius: 8px;
|
||
|
|
}
|
||
|
|
.text {
|
||
|
|
white-space: pre-wrap;
|
||
|
|
p {
|
||
|
|
margin: 0;
|
||
|
|
margin-top: 12px;
|
||
|
|
}
|
||
|
|
p:first-child {
|
||
|
|
margin-top: 0;
|
||
|
|
}
|
||
|
|
p:first-child:last-child {
|
||
|
|
display: contents;
|
||
|
|
}
|
||
|
|
ul, ol {
|
||
|
|
margin-top: 0;
|
||
|
|
margin-bottom: 0;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
.name {
|
||
|
|
display: none;
|
||
|
|
color: oklch(from var(--color) 0.9 c h);
|
||
|
|
body.BubbleBackground-LIGHT & {
|
||
|
|
color: oklch(from var(--color) 0.4 c h);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
img {
|
||
|
|
width: 48px;
|
||
|
|
height: 4px;
|
||
|
|
object-fit: contain;
|
||
|
|
visibility: hidden;
|
||
|
|
flex-shrink: 0;
|
||
|
|
}
|
||
|
|
&.initial {
|
||
|
|
padding-left: 0;
|
||
|
|
margin-top: 12px;
|
||
|
|
img {
|
||
|
|
visibility: visible;
|
||
|
|
height: 48px;
|
||
|
|
body.AvatarShape-SQUARE & {
|
||
|
|
border-radius: 0px;
|
||
|
|
}
|
||
|
|
body.AvatarShape-ROUNDED & {
|
||
|
|
border-radius: 4px;
|
||
|
|
}
|
||
|
|
body.AvatarShape-CIRCLE & {
|
||
|
|
border-radius: 24px;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
.name {
|
||
|
|
display: block;
|
||
|
|
font-weight: bold;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
&:after {
|
||
|
|
content: "";
|
||
|
|
clear: both;
|
||
|
|
display: table;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
footer {
|
||
|
|
opacity: 0.5;
|
||
|
|
text-align: center;
|
||
|
|
padding-bottom: 12px;
|
||
|
|
}
|
||
|
|
h6 {
|
||
|
|
margin: 0;
|
||
|
|
font-weight: normal;
|
||
|
|
font-size: 14px;
|
||
|
|
opacity: 0.8;
|
||
|
|
}
|
||
|
|
|
||
|
|
.options {
|
||
|
|
display: flex;
|
||
|
|
flex-direction: row;
|
||
|
|
flex-wrap: wrap;
|
||
|
|
gap: 16px;
|
||
|
|
justify-content: center;
|
||
|
|
div {
|
||
|
|
width: 260px;
|
||
|
|
height: 200px;
|
||
|
|
background-size: cover;
|
||
|
|
background-position: center;
|
||
|
|
background-blend-mode: overlay;
|
||
|
|
border-radius: 16px;
|
||
|
|
display: flex;
|
||
|
|
justify-content: flex-end;
|
||
|
|
flex-direction: column;
|
||
|
|
align-items: center;
|
||
|
|
text-align: center;
|
||
|
|
font-weight: bold;
|
||
|
|
box-sizing: border-box;
|
||
|
|
border: 8px #000A solid;
|
||
|
|
border-bottom-width: 4px;
|
||
|
|
border-left-color: #0006;
|
||
|
|
border-top: 2px #FFFA solid;
|
||
|
|
overflow: hidden;
|
||
|
|
cursor: pointer;
|
||
|
|
transition: filter 0.4s, opacity 0.4s;
|
||
|
|
opacity: 0.8;
|
||
|
|
&:hover, &:focus {
|
||
|
|
opacity: 1;
|
||
|
|
filter: brightness(110%);
|
||
|
|
}
|
||
|
|
&:active {
|
||
|
|
opacity: 1;
|
||
|
|
filter: brightness(125%);
|
||
|
|
}
|
||
|
|
span {
|
||
|
|
background: #000A;
|
||
|
|
padding: 6px;
|
||
|
|
width: 100%;
|
||
|
|
box-sizing: border-box;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
#noscript {
|
||
|
|
text-align: center;
|
||
|
|
padding: 16px;
|
||
|
|
font-size: 24px;
|
||
|
|
.fakeout {
|
||
|
|
font-family: serif;
|
||
|
|
font-size: 16px;
|
||
|
|
animation-name: goaway;
|
||
|
|
animation-duration: .5s;
|
||
|
|
animation-delay: .5s;
|
||
|
|
animation-fill-mode: forwards;
|
||
|
|
}
|
||
|
|
.realmessage {
|
||
|
|
opacity: 0;
|
||
|
|
animation-name: goaway;
|
||
|
|
animation-duration: .5s;
|
||
|
|
animation-delay: .5s;
|
||
|
|
animation-direction: reverse;
|
||
|
|
animation-fill-mode: forwards;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
.dialog-outer {
|
||
|
|
position: fixed;
|
||
|
|
top: 0; left: 0;
|
||
|
|
width: 100%;
|
||
|
|
height: 100%;
|
||
|
|
background: #0028;
|
||
|
|
z-index: 1000;
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
justify-content: center;
|
||
|
|
.dialog {
|
||
|
|
position: fixed;
|
||
|
|
background: #2C3433;
|
||
|
|
z-index: 1000;
|
||
|
|
border-radius: 16px;
|
||
|
|
box-shadow: 0px 2px 8px 4px #000, inset 0px -4px 0px 2px #54509e88;
|
||
|
|
overflow: hidden;
|
||
|
|
max-height: 90vh;
|
||
|
|
display: flex;
|
||
|
|
flex-direction: column;
|
||
|
|
.them {
|
||
|
|
display: flex;
|
||
|
|
flex-direction: column;
|
||
|
|
max-width: 480px;
|
||
|
|
width: 100vw;
|
||
|
|
padding: 32px;
|
||
|
|
gap: 16px;
|
||
|
|
box-sizing: border-box;
|
||
|
|
overflow-y: auto;
|
||
|
|
flex-shrink: 1;
|
||
|
|
&.scroll {
|
||
|
|
overflow-y: scroll;
|
||
|
|
}
|
||
|
|
&:has(hr:first-child) {
|
||
|
|
padding-top: 0;
|
||
|
|
overflow-y: hidden;
|
||
|
|
flex-shrink: 0;
|
||
|
|
}
|
||
|
|
.input-field {
|
||
|
|
display: flex;
|
||
|
|
flex-direction: column;
|
||
|
|
gap: 8px;
|
||
|
|
input, button, select {
|
||
|
|
font-size: inherit;
|
||
|
|
font-family: inherit;
|
||
|
|
padding: 4px;
|
||
|
|
}
|
||
|
|
.beside {
|
||
|
|
display: flex;
|
||
|
|
flex-direction: row;
|
||
|
|
gap: 8px;
|
||
|
|
align-items: center;
|
||
|
|
input {
|
||
|
|
flex-grow: 1;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
img {
|
||
|
|
width: 96px;
|
||
|
|
height: 96px;
|
||
|
|
object-fit: contain;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
.proxy {
|
||
|
|
.beside {
|
||
|
|
gap: 0px;
|
||
|
|
input {
|
||
|
|
width: 4px;
|
||
|
|
}
|
||
|
|
.prefix {
|
||
|
|
text-align: right;
|
||
|
|
padding-right: 0;
|
||
|
|
}
|
||
|
|
.suffix {
|
||
|
|
text-align: left;
|
||
|
|
padding-left: 0;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
.member {
|
||
|
|
border-left: 4px var(--color) solid;
|
||
|
|
display: flex;
|
||
|
|
flex-direction: row;
|
||
|
|
gap: 8px;
|
||
|
|
padding: 8px;
|
||
|
|
align-items: center;
|
||
|
|
background-color: transparent;
|
||
|
|
transition: background-color 0.4s;
|
||
|
|
cursor: pointer;
|
||
|
|
img {
|
||
|
|
width: 48px;
|
||
|
|
height: 48px;
|
||
|
|
}
|
||
|
|
span {
|
||
|
|
font-size: 18px;
|
||
|
|
}
|
||
|
|
&:hover, &:focus {
|
||
|
|
background-color: rgba(from var(--color) r g b / 0.1);
|
||
|
|
}
|
||
|
|
&:active {
|
||
|
|
background-color: rgba(from var(--color) r g b / 0.2);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
hr {
|
||
|
|
width: 100%;
|
||
|
|
height: 1px;
|
||
|
|
min-height: 1px;
|
||
|
|
max-height: 1px;
|
||
|
|
flex-shrink: 0;
|
||
|
|
border: 0;
|
||
|
|
background: currentColor;
|
||
|
|
}
|
||
|
|
b {
|
||
|
|
font-size: 24px;
|
||
|
|
display: block;
|
||
|
|
padding: 12px 32px;
|
||
|
|
background: #54509e;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
.warning {
|
||
|
|
b {
|
||
|
|
color: #F06;
|
||
|
|
}
|
||
|
|
border: 2px #F03 solid;
|
||
|
|
background-image: repeating-linear-gradient(60deg,
|
||
|
|
#F03F 5px,
|
||
|
|
#F032 5px,
|
||
|
|
#F032 9px,
|
||
|
|
#F03F 10px,
|
||
|
|
#F03F 14px,
|
||
|
|
#F032 15px,
|
||
|
|
#F032 20px
|
||
|
|
), linear-gradient(#F03, #F03);
|
||
|
|
background-size: 12px 100%, 2px 100%;
|
||
|
|
background-repeat: no-repeat;
|
||
|
|
background-position: 0px 0px, 12px 0px;
|
||
|
|
background-color: #F002;
|
||
|
|
padding: 12px;
|
||
|
|
padding-left: 14px;
|
||
|
|
max-width: 720px;
|
||
|
|
border-radius: 8px;
|
||
|
|
margin: auto;
|
||
|
|
text-wrap: balance;
|
||
|
|
}
|
||
|
|
|
||
|
|
input[type=file] {
|
||
|
|
display: none;
|
||
|
|
}
|
||
|
|
|
||
|
|
@keyframes goaway {
|
||
|
|
from { opacity: 1 }
|
||
|
|
to { opacity: 0 }
|
||
|
|
}
|
||
|
|
|
||
|
|
.inappfooter {
|
||
|
|
display: flex;
|
||
|
|
flex-direction: row;
|
||
|
|
flex-wrap: wrap;
|
||
|
|
align-items: center;
|
||
|
|
padding: 0 8px;
|
||
|
|
span {
|
||
|
|
padding: 8px 0;
|
||
|
|
white-space: pre-wrap;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
.gap {
|
||
|
|
flex-grow: 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
.plural-journey {
|
||
|
|
image-rendering: pixelated;
|
||
|
|
aspect-ratio: 3/2;
|
||
|
|
height: 20px;
|
||
|
|
vertical-align: middle;
|
||
|
|
border: 2px black solid;
|
||
|
|
}
|
||
|
|
</style>
|
||
|
|
</head>
|
||
|
|
<body>
|
||
|
|
<noscript>
|
||
|
|
<div id="noscript">
|
||
|
|
<div class="fakeout" aria-hidden>You need to enable JavaScript to run this app.</div>
|
||
|
|
<div class="realmessage">
|
||
|
|
<b>Flutter requires JavaScript to operate</b>, as it runs fully inside your browser.<br/>
|
||
|
|
Please enable JavaScript and reload the page. <a href="https://git.sleeping.town/krynesthesia/Utter">Read the source?</a>
|
||
|
|
<p>
|
||
|
|
<small>And no, it's not a React app.</small>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</noscript>
|
||
|
|
<div id="prepare">
|
||
|
|
<h1>Flutter, a playground fork of Utter v1.7.1</h1>
|
||
|
|
A tool for ephemeral single-keyboard conversations, this instance geared toward self-shippers, ready to share with alt text.<br/>
|
||
|
|
<p>
|
||
|
|
<div class="warning">
|
||
|
|
<b>Warning</b>: <i>Flutter does not save or upload message data anywhere</i>. Reloading the page, closing your browser, apps getting background killed, rebooting your phone, etc will all <b>delete every message</b>. It is <i>ephemeral</i>.
|
||
|
|
</div>
|
||
|
|
<p>
|
||
|
|
<h2>Get Started</h2>
|
||
|
|
<div class="options">
|
||
|
|
|
||
|
|
<div id="scratch" style="background-color: #2C5; background-image: url('data:image/webp;base64,UklGRixFAABXRUJQVlA4ICBFAADwQwKdASqXApcCPrVSok0nJLIrqPM7AkAWiWltocDWL07Er+2//Ozn/Ysvffi5+hXPmtxpMhc2Rl3wM8/6sXjfTfsr5MPoP97/6fUsf0/fudj/zfsC/sL6gGZk4b6BX+15NvtL1I6ebMQ/8/nk+R/6//28k/i3/226//bW/9V/9eO3/b+6fYz/5+Qf5dwclFvmN/twfm5t546fO7h6FD/89L/3r6eRhREwwEbfn3bCXggAHpXUxuLWZB5GiGAFQERfyupc7CjWVLs3kFJcVsFTiNTlDDQFCo4w9NSPZ/vMPDRKxvu/ff5i3w7q7ZtHy0Fswb953j78dtDJDrG4C4FscC2zrQ2OMNIDh2bds2iYGyKnLRzsuiFE9YDkPvRQIHyPWFwA4WT+Tyk0PpZWEKa1hqxNyfiT7ZS8lkmd9btSmyCOtZctE0RcmtONrQtPmuxB67cs8Oz80yiVBlmxqI48edNm+uQNdDCq73QcnmoZnvrBN0vMsRgbrKFw9tqI7jeqH/DDUUPXzWddhygxIcv1Nv9IoBtdWNGTM/cz+NdcKiH3yFZqJfbBLcipQ2Ni2I5Go7l9hlq3QsKezVfH1AMerII5Y9kYkrpGuvGiUfEQAX9dY8cozru8oKtXTIKgMQbplWJcv0xH5aWbqR3ojLQho5tRgmK54Sa0S5eZX4Ay5HdhLdDvatl1ZxGIIaFmBvxQPnClZbc13wFfrcSRtzEFeW59bS874Xi1ZU8w/R8mUb9FNz78J743K1/3sP58k1X+U/lAB8MWWw7XfvUUpM44wlkonTJ5Yo/M1RPVaMe8pF4H47Yh1dFSF8sp2iWGDlZ6Q6Qdxr3lhapJEzCxVgBlrbDSDCbDU15n0odErWLxBqMinYReHv4nt4awqnKOIKS4PmRY1W83j6RJMNCDLRQ2D68cmCcfSlcCBYFP4EAAkrCK2QdOsO1aJL6axvuOfEOvWEFtOIRCn7irU/OFtSKA/J+uEl7aTIM8I5dxCjqTa9YuQ9wH6codIvVVQXpX/5793Kzefl1iOUfm6yi60p9b7D2F4i0iSHf51gw32SA5oc0O7Z/zlUcWCpQ05O7TvTLnqdq+tLW9MfytJjuzZYoOlQh31yJ0fVZ6WHxE2mU5kl4tMBQaNdCvf45BtwMAWSu36t8QCTjU//6rfYp+d+fgaltzGXU3DDWgC30iH/oOiX/PPQhAw3MDQQLHesqOxsRo71nx+f5HXroOaiB8qkp0Xod4JtI0ts175K4O0AUSmmcvZ5jqc9iu6YpYmrsFCzGfQY7Z4vtbfExmceml15g5xoW6v6G/yayq7uCuXsX6MZUs7oEMG+YXD7RZWljJqW7Zs6T/xd3YVsYok+T0p1hOyvvAEnq5gbcRbW2Sm/aUz/Qa7qkVnV0p0RQtp0dsVS4drvl6axYc9Lva5gkM72nzCgeC4vagdQGvFGkjIWPzTi6V2C+0UgIZIZ5tkGnVQzR7WKyUp7+VmIppda1ii7FBpuZSUW+bnoDzHx+XQqVa4D5HHBYhGM0GZ8btGs+Ny3jJQYd5cXvVFqA0jDgUfDvPb9eZwDjuMOJjsNqkwJp5io6CrTp2o/kMBcJNgQZHjI8/xiRak5bIp3kGkA0M/5YmshFgciFhFS7m7i+uexvVJ6M2zXg/40oKdUyRnH7+CXh8eBwkf3mOTTwsyhBj0yNaXo7jJO4ToAuyqpkKrCUokI6ArvpdixKW8r2lnSDUUpVf7SNxUlmnrmQFmJGc/goAKovcPPr8fULvuuq10MutGS9CpaCnzc4WsAoAL4TUkgkN56A+JjnGVpJoBkx3KDcdGzB/JoaNBOPmGZRx90Qycp2nNFT2q6RLL6aPQZqBYj6CBw00SfqgKkxAk6vlM3Se6S3PVi9PidpNFFCl/c3K6nnDjFzD6N0/Ujoai4zmqfeHHO4thn9fp8kuZ2PJcRRTyE9JHO5FFOlLYmFKU5GvjXC/d+xK2NEdKVEbMiVfb2HpFpV4MWN7veXDBMqpmGGw3tdJgV40tYsc+x3z0zSbpAbTK9Zs3O3sgoEnYCJZ0hghEu2OlQLWiHf4OLid7lMrT88aCRxyUnhq7VAdOqKEpPOlx6Uc0DGy7/jIWqF7FKn2sM2CCLNN+5+12qhZ0XxlD9hLu4AO48ds6kN/HVY2T2VRCUBn2wPr1ygb1wavY1qMaC7kzbtWVgsJmF+XLA8hwX0d70L9qZHfVaDrX34/bcuqz/jpu+A/Qwar+qKgIelpIW5uyhl3KVvvuCkmqnBY0sLGvrLJTphKbgMAdhv4wabWj6jtVt6NEC6R9SElDYw+AVNrMGYepGppjzqP+xnJ9M43sTls2vdFOhs5ot9lGpnlfOvQXZ0vpdhZQHOq+sbKnKRhYp0EI6eliyBponXAD+TwpkWlmUVJEAVM+omz0pV0KwEPOO2j8Qh8xdCdfCWXgaOjPQNRgOJgFBwV6wFdJhQDDP3b3r7PdMbBjMu2XbO9lAbWp1CzrXMZ9cjMoovgXyhCb4UF9yCWIJdoPaucGU6XLCCWDfStMC4XeX1EJ2eSiJtgJeuV801/TcEPGkOYeY2dInk9TUwFG5xA3GUnXx/rKEaYBWrkRJvWDfxKnA5GW1wlCgPeuqqmS/XoSm8FI4eGbrZvA5WId94k6/EXwyIgmUH0oShpXMr7TlVUWmwwLhNm3oB2ZtzaGBv66W3Sth0uOXHyq3jkgGg8mS+tmQv3OsrOgA+TrCfSY5lsUYO8Yw2KJfiOMOHJE/2x/btizJ/QyqIPMRHkwCPYHnwKhnu6DEbaVVm/3BzMzON1rFndJgCgC4P4LOrSiLjVBc4lJPxfkUWRMvoeWrYNbHYiM2YBW76p3btMSc0+UPEfRRaz9R5Dwv+NifTCBd2ZLRGhYDboHg7iNPb/HjNWExfYqFN02CuyTBeq7I0z+iA6PZWOBJRLnxHOfNX0IqV9uVU0+sidR/H0bJBMh4Zr1+o6VTVKVXJ51O5+0rlv3jzbEXgyWjjCSBz+ffJ8IhOA314moTtb444HZUr9J2C1ZHj61nEK3nXOUP92vjxiSyH7pr5irkr+7BsWJZ2FAEUh3j/v+AcOUQMadsirzNVSDD6LcjaKWS8ou+aDwTjOLnDSdMF64Dv6gYd/BMQ2+UAVTxn1T7hK/XrURAiBU+rFuQEIMzBZvH4qb7QAvmiV43eqkDXleCTQXLj8fRBWk7zfi5eYOoD1z4hDOFFQQN+PVLqJgZpT00mn/yJQZbgAOoVecLsu1WzTSTQOfhAp2kCDxvD2iSqhJvKBuwNMH1s5F4Cs6uwEtBQeJScNcqdHyJL6M1KfEHBD1On/Cp0fwZspy5wu5E1QBzwnf4km2IXPCfbuMmRkPH5kHMB6y9qSQOdjB0UD8kI1Q11H5zIK7AnOYxbWyVNlvrlfUEskVagOXJ687QOzcMwJHfWJ26l14JpjmHpKQrIRRVSO+saLU7MJCrF3uLdR9kZbYvAXP26nQ/MV/eLziFDfXUr2AikB4rJJud8KDUrY8lwXB+cr38L/4m91w4QaPu5XWKSqu7kV0+5xoS5quJy7fDJVPHxtSk0fgd80SEaoZNrsbExoP6Is0wgvpqhHx2P17AhrUuevO+Hmt1rWiLrY7jwGWAIIskjoj+BlwCCtrNisSBrOHVLBRkIQ0nQLgrlaTCZUz+NgTWybS48hjDUJCo+R9npqXc1CaB8FbplADdY2bztKvR/G2qvzTJ/ug6Tqbix/LCcPTORkVjBF7hTwz5XpNUEekZv0dRF29uhM+vXKES8Da0jQIRuorUtMYGrdOXnAHuRTvo6LTsWptwAtrjG48ZAEXVBWuxUB/9nVpDiBSFmWx3McDKIACJjuyblqO2giXkCPpn2+o9aVt3vc2CsQcEDohCkovpXnpqEwnO6NWDdsP19pPg9zCYwwwBLOSQP7K/7/KavKA7MejOS1gzk49dIaweXCR/3VWitzMXf3QI0LTnvnvJgvw
|
||
|
|
<span>Start from Scratch</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<input type="file" id="pluralkit-upload" accept=".json,application/json">
|
||
|
|
<input type="file" id="tupperbox-upload" accept=".json,application/json">
|
||
|
|
<p>
|
||
|
|
Data is stored solely in your browser for later use.<br>
|
||
|
|
<b>It will not be uploaded anywhere</b>, this tool runs entirely in your browser.<br/>
|
||
|
|
Flutter can be installed as a PWA if your browser supports that, or you can just save this page to an HTML file and open it.
|
||
|
|
<p>
|
||
|
|
<small>* PluralKit's format has become something of a de-facto standard, so other JSONs also work here — such as /plu/ral, and many tools such as Simply Plural can export in PluralKit's format.</small>
|
||
|
|
<footer>
|
||
|
|
Copyright © 2025 <a href="https://exa.y2k.diy">Exa</a> and contributors
|
||
|
|
— no frameworks, no AI
|
||
|
|
— <a href="https://donate.y2k.diy">help us buy food</a>
|
||
|
|
— <a href="https://git.sleeping.town/krynesthesia/Utter">read the source / report bugs</a><br/>
|
||
|
|
<br/>
|
||
|
|
PluralKit logo by @tedkalashnikov on Discord, original Myriad design by @layl on Discord.
|
||
|
|
</footer>
|
||
|
|
</div>
|
||
|
|
<div id="app" style="display:none">
|
||
|
|
<div id="messages">
|
||
|
|
<i id="credit">generated by utter.y2k.diy</i>
|
||
|
|
</div>
|
||
|
|
<div id="chatwrapper" class="noprint">
|
||
|
|
<img src="" id="proxyindicator">
|
||
|
|
<textarea id="chatbox" rows="1"></textarea>
|
||
|
|
<button id="send">
|
||
|
|
<svg viewBox="0 0 26 26"><path d="M24.906 0a.99.99 0 0 0-.375.125l-24 13a.985.985 0 0 0-.504.969.99.99 0 0 0 .692.844l6.375 1.906c.148 1.18.812 6.285.937 7.281.125.992.797 1.164 1.469.25.453-.617 3.125-4.375 3.125-4.375l5.688 5.688a.99.99 0 0 0 1.656-.438l6-24A.99.99 0 0 0 24.906 0ZM23.47 2.938l-5.032 20.125-5.656-5.657L21 6 8.219 15.125 3.563 13.75Z"/></svg>
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
<div class="noprint inappfooter">
|
||
|
|
<span><a href="#" id="copyalt">copy alt text</a></span>
|
||
|
|
<span> • <a href="https://donate.y2k.diy">help us buy food</a></span>
|
||
|
|
<span> • <a href="#" data-open="options">options</a></span>
|
||
|
|
<span> • utter v1.7.1 by <a href="https://exa.y2k.diy">exa</a> and contributors ♥</span>
|
||
|
|
<span class="gap"></span>
|
||
|
|
<img class="plural-journey" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAAECAIAAADAusJtAAAAGElEQVR42mPQPLGAQbf+AUO29X4GQ0NDAC18BRYQeR0nAAAAAElFTkSuQmCC">
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div id="options" class="dialog-outer" style="display:none">
|
||
|
|
<div class="dialog">
|
||
|
|
<b>options</b>
|
||
|
|
<div class="them">
|
||
|
|
<a data-open="settings" data-close="options" href="#">general settings</a>
|
||
|
|
<a data-open="system" data-close="options" href="#">edit system information</a>
|
||
|
|
<a data-open="members" data-close="options" href="#">edit members</a>
|
||
|
|
<a id="export-system" href="#">export system information in PluralKit format</a>
|
||
|
|
<a id="reset" href="#" style="color:#F00">reset all data</a>
|
||
|
|
<hr>
|
||
|
|
<a href="https://git.sleeping.town/krynesthesia/Utter">view the source code</a>
|
||
|
|
<a href="https://git.sleeping.town/krynesthesia/Utter/issues">report a bug or request a feature</a>
|
||
|
|
<hr>
|
||
|
|
<a data-close="options" href="#">close this dialog</a>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div id="settings" class="dialog-outer" style="display:none">
|
||
|
|
<div class="dialog">
|
||
|
|
<b>general settings</b>
|
||
|
|
<div class="them">
|
||
|
|
<div class="input-field">
|
||
|
|
<label for="autoproxy-mode">autoproxy mode</label>
|
||
|
|
<select id="autoproxy-mode">
|
||
|
|
<option value="DEFAULT" selected>none</option>
|
||
|
|
<option value="LATCH">latch</option>
|
||
|
|
</select>
|
||
|
|
</div>
|
||
|
|
<div class="input-field">
|
||
|
|
<label for="avatar-shape">avatar shape</label>
|
||
|
|
<select id="avatar-shape">
|
||
|
|
<option value="SQUARE">square</option>
|
||
|
|
<option value="ROUNDED" selected>rounded</option>
|
||
|
|
<option value="CIRCLE">circle</option>
|
||
|
|
</select>
|
||
|
|
</div>
|
||
|
|
<div class="input-field">
|
||
|
|
<label for="bubble-background">bubble background</label>
|
||
|
|
<select id="bubble-background">
|
||
|
|
<option value="COLORED" selected>colored</option>
|
||
|
|
<option value="LIGHT">light</option>
|
||
|
|
<option value="DARK">dark</option>
|
||
|
|
</select>
|
||
|
|
</div>
|
||
|
|
<hr>
|
||
|
|
<a data-open="options" data-close="settings" href="#">return</a>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div id="system" class="dialog-outer" style="display:none">
|
||
|
|
<div class="dialog">
|
||
|
|
<b>system</b>
|
||
|
|
<div class="them">
|
||
|
|
<div class="input-field">
|
||
|
|
<label for="system-name">name</label>
|
||
|
|
<input type="text" id="system-name" placeholder="<none>">
|
||
|
|
</div>
|
||
|
|
<div class="input-field">
|
||
|
|
<label for="system-color">color</label>
|
||
|
|
<div class="beside">
|
||
|
|
<input type="color" id="system-color">
|
||
|
|
<input type="text" id="system-color-text" pattern="^#[0-9A-Fa-f]{6}$">
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="input-field">
|
||
|
|
<label for="system-avatar">avatar</label>
|
||
|
|
<img id="system-avatar-preview">
|
||
|
|
<div class="beside">
|
||
|
|
<input type="text" id="system-avatar" placeholder="<none>">
|
||
|
|
<button id="system-avatar-upload">load from file…</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<hr>
|
||
|
|
<a data-open="members" data-close="system" href="#">edit members</a>
|
||
|
|
<a data-open="options" data-close="system" href="#">return</a>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div id="members" class="dialog-outer" style="display:none">
|
||
|
|
<div class="dialog">
|
||
|
|
<b>members</b>
|
||
|
|
<div class="them scroll" id="members-target">
|
||
|
|
<div class="member" style="--color: #FFF" id="newmember">
|
||
|
|
<img src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZD0iTTE5LDEzSDEzVjE5SDExVjEzSDVWMTFIMTFWNUgxM1YxMUgxOVYxM1oiIGZpbGw9IiNGRkYiLz48L3N2Zz4K">
|
||
|
|
<span>New Member</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="them">
|
||
|
|
<hr>
|
||
|
|
<a data-open="options" data-close="members" href="#">return</a>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div id="editmember" class="dialog-outer" style="display:none">
|
||
|
|
<div class="dialog">
|
||
|
|
<b>edit member</b>
|
||
|
|
<div class="them scroll">
|
||
|
|
<div class="input-field">
|
||
|
|
<label for="member-name">name</label>
|
||
|
|
<input type="text" id="member-name" placeholder="<none>">
|
||
|
|
</div>
|
||
|
|
<div class="input-field">
|
||
|
|
<label for="member-color">color</label>
|
||
|
|
<div class="beside">
|
||
|
|
<input type="color" id="member-color">
|
||
|
|
<input type="text" id="member-color-text" pattern="^#[0-9A-Fa-f]{6}$">
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="input-field">
|
||
|
|
<label for="member-avatar">avatar</label>
|
||
|
|
<img id="member-avatar-preview">
|
||
|
|
<div class="beside">
|
||
|
|
<input type="text" id="member-avatar" placeholder="<none>">
|
||
|
|
<button id="member-avatar-upload">load from file…</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<label id="proxies-target">proxies</label>
|
||
|
|
<div class="input-field proxy">
|
||
|
|
<div class="beside">
|
||
|
|
<input type="text" class="prefix">
|
||
|
|
<span>text</span>
|
||
|
|
<input type="text" class="suffix">
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="input-field" id="newproxy">
|
||
|
|
<button>add new</button>
|
||
|
|
</div>
|
||
|
|
<small>(blank out the prefix and suffix for a proxy to remove it)</small>
|
||
|
|
</div>
|
||
|
|
<div class="them">
|
||
|
|
<hr>
|
||
|
|
<div style="display:flex;flex-direction:row">
|
||
|
|
<a id="finisheditingmember" data-open="members" data-close="editmember" href="#">return</a>
|
||
|
|
<div style="flex-grow:1"></div>
|
||
|
|
<a id="deletemember" data-open="members" data-close="editmember" href="#" style="color:#F00">delete</a>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<input type="file" id="system-avatar-upload-field" accept="image/*">
|
||
|
|
<input type="file" id="member-avatar-upload-field" accept="image/*">
|
||
|
|
<script>
|
||
|
|
|
||
|
|
/*! markdown-it 14.1.0 https://github.com/markdown-it/markdown-it @license MIT */
|
||
|
|
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).markdownit=e()}(this,(function(){"use strict";const t={};function e(r,n){"string"!=typeof n&&(n=e.defaultChars);const s=function(e){let r=t[e];if(r)return r;r=t[e]=[];for(let t=0;t<128;t++){const e=String.fromCharCode(t);r.push(e)}for(let t=0;t<e.length;t++){const n=e.charCodeAt(t);r[n]="%"+("0"+n.toString(16).toUpperCase()).slice(-2)}return r}(n);return r.replace(/(%[a-f0-9]{2})+/gi,(function(t){let e="";for(let r=0,n=t.length;r<n;r+=3){const i=parseInt(t.slice(r+1,r+3),16);if(i<128)e+=s[i];else{if(192==(224&i)&&r+3<n){const n=parseInt(t.slice(r+4,r+6),16);if(128==(192&n)){const t=i<<6&1984|63&n;e+=t<128?"\ufffd\ufffd":String.fromCharCode(t),r+=3;continue}}if(224==(240&i)&&r+6<n){const n=parseInt(t.slice(r+4,r+6),16),s=parseInt(t.slice(r+7,r+9),16);if(128==(192&n)&&128==(192&s)){const t=i<<12&61440|n<<6&4032|63&s;e+=t<2048||t>=55296&&t<=57343?"\ufffd\ufffd\ufffd":String.fromCharCode(t),r+=6;continue}}if(240==(248&i)&&r+9<n){const n=parseInt(t.slice(r+4,r+6),16),s=parseInt(t.slice(r+7,r+9),16),o=parseInt(t.slice(r+10,r+12),16);if(128==(192&n)&&128==(192&s)&&128==(192&o)){let t=i<<18&1835008|n<<12&258048|s<<6&4032|63&o;t<65536||t>1114111?e+="\ufffd\ufffd\ufffd\ufffd":(t-=65536,e+=String.fromCharCode(55296+(t>>10),56320+(1023&t))),r+=9;continue}}e+="\ufffd"}}return e}))}e.defaultChars=";/?:@&=+$,#",e.componentChars="";const r={};function n(t,e,s){"string"!=typeof e&&(s=e,e=n.defaultChars),void 0===s&&(s=!0);const i=function(t){let e=r[t];if(e)return e;e=r[t]=[];for(let t=0;t<128;t++){const r=String.fromCharCode(t);/^[0-9a-z]$/i.test(r)?e.push(r):e.push("%"+("0"+t.toString(16).toUpperCase()).slice(-2))}for(let r=0;r<t.length;r++)e[t.charCodeAt(r)]=t[r];return e}(e);let o="";for(let e=0,r=t.length;e<r;e++){const n=t.charCodeAt(e);if(s&&37===n&&e+2<r&&/^[0-9a-f]{2}$/i.test(t.slice(e+1,e+3)))o+=t.slice(e,e+3),e+=2;else if(n<128)o+=i[n];else if(n>=55296&&n<=57343){if(n>=55296&&n<=56319&&e+1<r){const r=t.charCodeAt(e+1);if(r>=56320&&r<=57343){o+=encodeURIComponent(t[e]+t[e+1]),e++;continue}}o+="%EF%BF%BD"}else o+=encodeURIComponent(t[e])}return o}function s(t){let e="";return e+=t.protocol||"",e+=t.slashes?"//":"",e+=t.auth?t.auth+"@":"",t.hostname&&-1!==t.hostname.indexOf(":")?e+="["+t.hostname+"]":e+=t.hostname||"",e+=t.port?":"+t.port:"",e+=t.pathname||"",e+=t.search||"",e+=t.hash||"",e}function i(){this.protocol=null,this.slashes=null,this.auth=null,this.port=null,this.hostname=null,this.hash=null,this.search=null,this.pathname=null}n.defaultChars=";/?:@&=+$,-_.!~*'()#",n.componentChars="-_.!~*'()";const o=/^([a-z0-9.+-]+:)/i,u=/:[0-9]*$/,c=/^(\/\/?(?!\/)[^\?\s]*)(\?[^\s]*)?$/,a=["{","}","|","\\","^","`"].concat(["<",">",'"',"`"," ","\r","\n","\t"]),l=["'"].concat(a),h=["%","/","?",";","#"].concat(l),p=["/","?","#"],f=/^[+a-z0-9A-Z_-]{0,63}$/,d=/^([+a-z0-9A-Z_-]{0,63})(.*)$/,_={javascript:!0,"javascript:":!0},m={http:!0,https:!0,ftp:!0,gopher:!0,file:!0,"http:":!0,"https:":!0,"ftp:":!0,"gopher:":!0,"file:":!0};function g(t,e){if(t&&t instanceof i)return t;const r=new i;return r.parse(t,e),r}i.prototype.parse=function(t,e){let r,n,s,i=t;if(i=i.trim(),!e&&1===t.split("#").length){const t=c.exec(i);if(t)return this.pathname=t[1],t[2]&&(this.search=t[2]),this}let u=o.exec(i);if(u&&(u=u[0],r=u.toLowerCase(),this.protocol=u,i=i.substr(u.length)),(e||u||i.match(/^\/\/[^@\/]+@[^@\/]+/))&&(s="//"===i.substr(0,2),!s||u&&_[u]||(i=i.substr(2),this.slashes=!0)),!_[u]&&(s||u&&!m[u])){let t,e,r=-1;for(let t=0;t<p.length;t++)n=i.indexOf(p[t]),-1!==n&&(-1===r||n<r)&&(r=n);e=-1===r?i.lastIndexOf("@"):i.lastIndexOf("@",r),-1!==e&&(t=i.slice(0,e),i=i.slice(e+1),this.auth=t),r=-1;for(let t=0;t<h.length;t++)n=i.indexOf(h[t]),-1!==n&&(-1===r||n<r)&&(r=n);-1===r&&(r=i.length),":"===i[r-1]&&r--;const s=i.slice(0,r);i=i.slice(r),this.parseHost(s),this.hostname=this.hostname||"";const o="["===this.hostname[0]&&"]"===this.hostname[this.hostname.length-1];if(!o){cons
|
||
|
|
</script>
|
||
|
|
<script>
|
||
|
|
|
||
|
|
// bootleg jquery
|
||
|
|
const $ = document.querySelector.bind(document);
|
||
|
|
const $$ = (s) => Array.prototype.slice.apply(document.querySelectorAll(s));
|
||
|
|
EventTarget.prototype.on = EventTarget.prototype.addEventListener;
|
||
|
|
|
||
|
|
const ProxyMode = {
|
||
|
|
DEFAULT: "DEFAULT",
|
||
|
|
LATCH: "LATCH",
|
||
|
|
};
|
||
|
|
|
||
|
|
const AvatarShape = {
|
||
|
|
SQUARE: "SQUARE",
|
||
|
|
ROUNDED: "ROUNDED",
|
||
|
|
CIRCLE: "CIRCLE",
|
||
|
|
};
|
||
|
|
|
||
|
|
const BubbleBackground = {
|
||
|
|
COLORED: "COLORED",
|
||
|
|
LIGHT: "LIGHT",
|
||
|
|
DARK: "DARK",
|
||
|
|
};
|
||
|
|
|
||
|
|
const md = markdownit();
|
||
|
|
|
||
|
|
const defaultProxyImg = $("link[rel=icon]").href;
|
||
|
|
|
||
|
|
const chatbox = $("#chatbox");
|
||
|
|
const proxyindicator = $("#proxyindicator");
|
||
|
|
const messages = $("#messages");
|
||
|
|
|
||
|
|
let system = null;
|
||
|
|
proxyindicator.src = defaultProxyImg;
|
||
|
|
|
||
|
|
let selectedMember = {name:"This should not appear",color:"FF00FF",avatar_url:"data:image/webp;base64,UklGRioAAABXRUJQVlA4TB0AAAAvf8AfABAIJPurB/D8z3//J6D54PffgPOb338DDgA=",proxy_tags:[]};
|
||
|
|
|
||
|
|
const initializers = [];
|
||
|
|
|
||
|
|
let lastSender = null;
|
||
|
|
let lastMsg = null;
|
||
|
|
let altText = [];
|
||
|
|
let latchedMember = null;
|
||
|
|
|
||
|
|
function getProxyMode() {
|
||
|
|
return system.config?.utter?.autoproxy_mode ?? ProxyMode.DEFAULT;
|
||
|
|
}
|
||
|
|
|
||
|
|
function getAvatarShape() {
|
||
|
|
return system.config?.utter?.avatar_shape ?? AvatarShape.ROUNDED;
|
||
|
|
}
|
||
|
|
|
||
|
|
function getBubbleBackground() {
|
||
|
|
return system.config?.utter?.bubble_background ?? BubbleBackground.COLORED;
|
||
|
|
}
|
||
|
|
|
||
|
|
function utterCfg() {
|
||
|
|
if (!system.config) system.config = {};
|
||
|
|
if (!system.config.utter) system.config.utter = {};
|
||
|
|
return system.config.utter;
|
||
|
|
}
|
||
|
|
|
||
|
|
Object.entries({
|
||
|
|
"system-name": {
|
||
|
|
get: () => system.name,
|
||
|
|
set: it => { system.name = it },
|
||
|
|
},
|
||
|
|
"system-color": {
|
||
|
|
get: () => system.color ? '#'+system.color : '#AAAAAA',
|
||
|
|
set: it => { system.color = it.substring(1) },
|
||
|
|
},
|
||
|
|
"system-avatar": {
|
||
|
|
get: () => system.avatar_url,
|
||
|
|
set: it => { system.avatar_url = it },
|
||
|
|
},
|
||
|
|
|
||
|
|
"member-name": {
|
||
|
|
get: () => selectedMember.name,
|
||
|
|
set: it => { selectedMember.name = it },
|
||
|
|
},
|
||
|
|
"member-color": {
|
||
|
|
get: () => selectedMember.color ? '#'+selectedMember.color : '#AAAAAA',
|
||
|
|
set: it => { selectedMember.color = it.substring(1) },
|
||
|
|
},
|
||
|
|
"member-avatar": {
|
||
|
|
get: () => selectedMember.avatar_url,
|
||
|
|
set: it => { selectedMember.avatar_url = it },
|
||
|
|
},
|
||
|
|
|
||
|
|
"autoproxy-mode": {
|
||
|
|
get: () => getProxyMode(),
|
||
|
|
set: it => { utterCfg().autoproxy_mode = it },
|
||
|
|
},
|
||
|
|
"avatar-shape": {
|
||
|
|
get: () => getAvatarShape(),
|
||
|
|
set: it => { utterCfg().avatar_shape = it },
|
||
|
|
},
|
||
|
|
"bubble-background": {
|
||
|
|
get: () => getBubbleBackground(),
|
||
|
|
set: it => { utterCfg().bubble_background = it },
|
||
|
|
},
|
||
|
|
}).forEach(([id, {get, set}]) => {
|
||
|
|
const e = $("#"+id);
|
||
|
|
e.on('input', () => {
|
||
|
|
set(e.value);
|
||
|
|
save();
|
||
|
|
});
|
||
|
|
initializers.push(() => {
|
||
|
|
e.value = get();
|
||
|
|
e.dispatchEvent(new CustomEvent("input"));
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
function populateProxies() {
|
||
|
|
const t = $("#proxies-target");
|
||
|
|
const rm = []
|
||
|
|
for (const e of t.parentElement.children) {
|
||
|
|
if (e.classList.contains("proxy")) {
|
||
|
|
rm.push(e);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
rm.forEach(e => e.remove());
|
||
|
|
|
||
|
|
selectedMember.proxy_tags.forEach(tag => {
|
||
|
|
const field = document.createElement("div");
|
||
|
|
field.className = "input-field proxy";
|
||
|
|
|
||
|
|
const beside = document.createElement("div");
|
||
|
|
beside.className = "beside";
|
||
|
|
field.appendChild(beside);
|
||
|
|
|
||
|
|
const prefix = document.createElement("input");
|
||
|
|
prefix.type = "text";
|
||
|
|
prefix.className = "prefix";
|
||
|
|
prefix.value = tag.prefix ?? "";
|
||
|
|
beside.appendChild(prefix);
|
||
|
|
|
||
|
|
prefix.on('input', () => {
|
||
|
|
tag.prefix = prefix.value;
|
||
|
|
save();
|
||
|
|
});
|
||
|
|
|
||
|
|
const text = document.createElement("span");
|
||
|
|
text.textContent = "text";
|
||
|
|
beside.appendChild(text);
|
||
|
|
|
||
|
|
const suffix = document.createElement("input");
|
||
|
|
suffix.type = "text";
|
||
|
|
suffix.className = "suffix";
|
||
|
|
suffix.value = tag.suffix ?? "";
|
||
|
|
beside.appendChild(suffix);
|
||
|
|
|
||
|
|
suffix.on('input', () => {
|
||
|
|
tag.suffix = suffix.value;
|
||
|
|
save();
|
||
|
|
});
|
||
|
|
|
||
|
|
t.after(field);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
initializers.push(populateProxies);
|
||
|
|
|
||
|
|
function save() {
|
||
|
|
for (let v of Object.values(AvatarShape)) {
|
||
|
|
document.body.classList.remove("AvatarShape-"+v);
|
||
|
|
}
|
||
|
|
for (let v of Object.values(BubbleBackground)) {
|
||
|
|
document.body.classList.remove("BubbleBackground-"+v);
|
||
|
|
}
|
||
|
|
document.body.classList.add("AvatarShape-"+getAvatarShape());
|
||
|
|
document.body.classList.add("BubbleBackground-"+getBubbleBackground());
|
||
|
|
system.members.forEach(m => delete m.matched_proxy_tag);
|
||
|
|
localStorage.setItem('system', JSON.stringify(system));
|
||
|
|
}
|
||
|
|
|
||
|
|
function assert(b, msg) {
|
||
|
|
if (!b) throw new Error("Assertion failed: "+msg);
|
||
|
|
}
|
||
|
|
|
||
|
|
function populateMembers() {
|
||
|
|
const t = $("#members-target");
|
||
|
|
const rm = []
|
||
|
|
for (const e of t.children) {
|
||
|
|
if (e.id === "newmember") continue;
|
||
|
|
rm.push(e);
|
||
|
|
}
|
||
|
|
rm.forEach(e => e.remove());
|
||
|
|
system.members.forEach(m => {
|
||
|
|
const e = document.createElement('div');
|
||
|
|
e.className = "member";
|
||
|
|
e.style.setProperty("--color", '#'+(m.color || 'AAA'));
|
||
|
|
|
||
|
|
const img = document.createElement('img');
|
||
|
|
setAvatar(m, img);
|
||
|
|
e.appendChild(img);
|
||
|
|
|
||
|
|
const name = document.createElement('span');
|
||
|
|
name.textContent = m.name;
|
||
|
|
e.appendChild(name);
|
||
|
|
|
||
|
|
t.prepend(e);
|
||
|
|
|
||
|
|
e.on('click', () => {
|
||
|
|
selectedMember = m;
|
||
|
|
initializers.forEach(f => f());
|
||
|
|
$("#members").style.display = 'none';
|
||
|
|
$("#editmember").style.display = 'flex';
|
||
|
|
});
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
$("#newmember").on('click', () => {
|
||
|
|
selectedMember = {
|
||
|
|
id: randid(),
|
||
|
|
name: "New Member",
|
||
|
|
color: 'AAA',
|
||
|
|
avatar_url: null,
|
||
|
|
proxy_tags: []
|
||
|
|
};
|
||
|
|
system.members.push(selectedMember);
|
||
|
|
initializers.forEach(f => f());
|
||
|
|
$("#members").style.display = 'none';
|
||
|
|
$("#editmember").style.display = 'flex';
|
||
|
|
});
|
||
|
|
|
||
|
|
$("#finisheditingmember").on('click', () => {
|
||
|
|
selectedMember.proxy_tags = selectedMember.proxy_tags.filter(t => t.prefix?.length || t.suffix?.length);
|
||
|
|
populateMembers();
|
||
|
|
});
|
||
|
|
$("#deletemember").on('click', (e) => {
|
||
|
|
e.preventDefault();
|
||
|
|
system.members = system.members.filter(m => m.id !== selectedMember.id);
|
||
|
|
populateMembers();
|
||
|
|
save();
|
||
|
|
});
|
||
|
|
$("#newproxy").on('click', () => {
|
||
|
|
selectedMember.proxy_tags.push({
|
||
|
|
prefix: "example: ",
|
||
|
|
suffix: ""
|
||
|
|
});
|
||
|
|
populateProxies();
|
||
|
|
save();
|
||
|
|
});
|
||
|
|
|
||
|
|
function start() {
|
||
|
|
$("#prepare").style.display = 'none';
|
||
|
|
$("#app").style.display = 'flex';
|
||
|
|
|
||
|
|
system = JSON.parse(localStorage.getItem('system'));
|
||
|
|
proxyindicator.src = system.avatar_url || defaultProxyImg;
|
||
|
|
|
||
|
|
populateMembers();
|
||
|
|
|
||
|
|
initializers.forEach(f => f());
|
||
|
|
}
|
||
|
|
|
||
|
|
if (localStorage.getItem('system')) {
|
||
|
|
start();
|
||
|
|
}
|
||
|
|
|
||
|
|
function randid() {
|
||
|
|
return "utr"+Math.floor(Math.random()*65535).toString(16);
|
||
|
|
}
|
||
|
|
|
||
|
|
function entwine(a, b) {
|
||
|
|
a.on('click', b.click.bind(b));
|
||
|
|
}
|
||
|
|
function entangle(a, b) {
|
||
|
|
let reentering = false;
|
||
|
|
function _(a, b) {
|
||
|
|
a.on('input', () => {
|
||
|
|
if (reentering) return;
|
||
|
|
reentering = true;
|
||
|
|
b.value = a.value
|
||
|
|
b.dispatchEvent(new CustomEvent("input"));
|
||
|
|
reentering = false;
|
||
|
|
});
|
||
|
|
}
|
||
|
|
_(a, b);
|
||
|
|
_(b, a);
|
||
|
|
}
|
||
|
|
|
||
|
|
entwine($("#pluralkit"), $("#pluralkit-upload"));
|
||
|
|
entwine($("#tupperbox"), $("#tupperbox-upload"));
|
||
|
|
|
||
|
|
entangle($("#system-color"), $("#system-color-text"));
|
||
|
|
entangle($("#member-color"), $("#member-color-text"));
|
||
|
|
|
||
|
|
$("#scratch").on('click', () => {
|
||
|
|
localStorage.setItem('system', JSON.stringify({
|
||
|
|
id: randid(),
|
||
|
|
name: "New System",
|
||
|
|
color: null,
|
||
|
|
avatar_url: null,
|
||
|
|
members: []
|
||
|
|
}));
|
||
|
|
start();
|
||
|
|
$("#system").style.display = "flex";
|
||
|
|
});
|
||
|
|
|
||
|
|
$$("[data-open]").forEach((e) => {
|
||
|
|
e.on('click', (v) => {
|
||
|
|
v.preventDefault();
|
||
|
|
$('#'+e.dataset.open).style.display = 'flex';
|
||
|
|
});
|
||
|
|
});
|
||
|
|
$$("[data-close]").forEach((e) => {
|
||
|
|
e.on('click', (v) => {
|
||
|
|
v.preventDefault();
|
||
|
|
$('#'+e.dataset.close).style.display = 'none';
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
const pkupload = $("#pluralkit-upload");
|
||
|
|
pkupload.value = null;
|
||
|
|
pkupload.on('input', () => {
|
||
|
|
const fr = new FileReader();
|
||
|
|
fr.onload = () => {
|
||
|
|
try {
|
||
|
|
const j = JSON.parse(fr.result);
|
||
|
|
assert(j, "JSON is null");
|
||
|
|
assert(j.members, "Members array is missing");
|
||
|
|
assert(j.members.length > 0, "No members are defined");
|
||
|
|
j.members.forEach((m) => {
|
||
|
|
if (typeof m.color === "number") {
|
||
|
|
// /plu/ral puts the naked int into the JSON
|
||
|
|
m.color = (m.color+0xF000000).toString(16).substring(1);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
localStorage.setItem('system', fr.result);
|
||
|
|
start();
|
||
|
|
} catch (e) {
|
||
|
|
alert("Error parsing JSON: "+e);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
fr.onerror = () => {
|
||
|
|
alert("Error reading file");
|
||
|
|
};
|
||
|
|
fr.readAsText(pkupload.files[0]);
|
||
|
|
});
|
||
|
|
|
||
|
|
const tbupload = $("#tupperbox-upload");
|
||
|
|
tbupload.value = null;
|
||
|
|
tbupload.on('input', () => {
|
||
|
|
const fr = new FileReader();
|
||
|
|
fr.onload = () => {
|
||
|
|
try {
|
||
|
|
const j = JSON.parse(fr.result);
|
||
|
|
assert(j, "JSON is null");
|
||
|
|
assert(j.tuppers, "Tuppers array is missing");
|
||
|
|
assert(j.tuppers.length > 0, "No tuppers are defined");
|
||
|
|
localStorage.setItem('system', JSON.stringify({
|
||
|
|
id: "imported-tupperbox-system-"+randid(),
|
||
|
|
members: j.tuppers.map(t => ({
|
||
|
|
id: t.id,
|
||
|
|
name: t.name,
|
||
|
|
avatar_url: t.avatar_url,
|
||
|
|
keep_proxy: t.show_brackets,
|
||
|
|
proxy_tags: t.brackets ? [
|
||
|
|
{
|
||
|
|
prefix: t.brackets[0],
|
||
|
|
suffix: t.brackets[1]
|
||
|
|
}
|
||
|
|
] : []
|
||
|
|
}))
|
||
|
|
}));
|
||
|
|
start();
|
||
|
|
} catch (e) {
|
||
|
|
alert("Error parsing JSON: "+e);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
fr.onerror = () => {
|
||
|
|
alert("Error reading file");
|
||
|
|
};
|
||
|
|
fr.readAsText(tbupload.files[0]);
|
||
|
|
});
|
||
|
|
|
||
|
|
function uploadfield(idprefix) {
|
||
|
|
const target = $("#"+idprefix);
|
||
|
|
const field = $("#"+idprefix+"-upload-field");
|
||
|
|
const preview = $("#"+idprefix+"-preview");
|
||
|
|
entwine($("#"+idprefix+"-upload"), field);
|
||
|
|
field.on('input', () => {
|
||
|
|
const fr = new FileReader();
|
||
|
|
fr.onload = () => {
|
||
|
|
target.value = fr.result;
|
||
|
|
target.dispatchEvent(new CustomEvent('input'));
|
||
|
|
};
|
||
|
|
fr.onerror = () => {
|
||
|
|
alert("Error reading file");
|
||
|
|
};
|
||
|
|
fr.readAsDataURL(field.files[0]);
|
||
|
|
});
|
||
|
|
target.on('input', () => {
|
||
|
|
preview.src = target.value;
|
||
|
|
});
|
||
|
|
preview.src = target.value;
|
||
|
|
}
|
||
|
|
|
||
|
|
uploadfield("system-avatar");
|
||
|
|
uploadfield("member-avatar");
|
||
|
|
|
||
|
|
function hasProxyTag(text, tag) {
|
||
|
|
return ((!tag.prefix || text.startsWith(tag.prefix)) && (!tag.suffix || text.endsWith(tag.suffix)));
|
||
|
|
}
|
||
|
|
|
||
|
|
function getMember(text) {
|
||
|
|
const searchedMember = system.members.find(m => {
|
||
|
|
return m.proxy_tags.find(t => {
|
||
|
|
if (hasProxyTag(text, t)) {
|
||
|
|
m.matched_proxy_tag = t;
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
return false;
|
||
|
|
});
|
||
|
|
});
|
||
|
|
if (searchedMember) {
|
||
|
|
return searchedMember;
|
||
|
|
} else if (getProxyMode() === ProxyMode.LATCH) {
|
||
|
|
if (text.startsWith("\\")) return;
|
||
|
|
if (latchedMember) {
|
||
|
|
delete latchedMember.matched_proxy_tag;
|
||
|
|
return latchedMember;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
function colorblock(color) {
|
||
|
|
return 'data:image/svg+xml;base64,'+btoa('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1 1"><rect width="1" height="1" fill="#'+color+'"/></svg>');
|
||
|
|
}
|
||
|
|
|
||
|
|
function setAvatar(member, ele) {
|
||
|
|
if (!ele) ele = proxyindicator;
|
||
|
|
if (ele.src && ele.src === member.avatar_url) return;
|
||
|
|
const cb = colorblock(member.color || 'AAA');
|
||
|
|
ele.src = cb;
|
||
|
|
if (!member.avatar_url) return;
|
||
|
|
const img = new Image();
|
||
|
|
img.onload = () => {
|
||
|
|
ele.src = img.src;
|
||
|
|
};
|
||
|
|
img.onerror = () => {
|
||
|
|
console.warn("Failed to load "+member.avatar_url+", falling back on color block");
|
||
|
|
};
|
||
|
|
img.src = member.avatar_url;
|
||
|
|
}
|
||
|
|
|
||
|
|
function send() {
|
||
|
|
let text = chatbox.value;
|
||
|
|
chatbox.value = "";
|
||
|
|
chatbox.rows = 1;
|
||
|
|
|
||
|
|
const member = getMember(text) ?? system;
|
||
|
|
if (!member.keep_proxy && member.matched_proxy_tag) {
|
||
|
|
const t = member.matched_proxy_tag;
|
||
|
|
text = text.substring(t.prefix?.length ?? 0, text.length - (t.suffix?.length ?? 0));
|
||
|
|
}
|
||
|
|
if (text.startsWith("\\") && getProxyMode() === ProxyMode.LATCH && latchedMember) {
|
||
|
|
latchedMember = null;
|
||
|
|
text = text.substring(1); // not what pk does due to inability to modify unproxied messages, but it's what it *should* do
|
||
|
|
}
|
||
|
|
text = text.trim();
|
||
|
|
if (text.length === 0) return;
|
||
|
|
const msg = document.createElement('div');
|
||
|
|
if (lastSender !== member.id) {
|
||
|
|
lastSender = member.id;
|
||
|
|
msg.classList.add('initial');
|
||
|
|
} else if (lastMsg) {
|
||
|
|
lastMsg.classList.remove('final');
|
||
|
|
}
|
||
|
|
msg.classList.add('final');
|
||
|
|
msg.style.setProperty('--color', '#'+(member.color ?? 'AAA'));
|
||
|
|
|
||
|
|
const avatar = document.createElement('img');
|
||
|
|
setAvatar(member, avatar);
|
||
|
|
msg.appendChild(avatar);
|
||
|
|
|
||
|
|
const bubble = document.createElement('div');
|
||
|
|
bubble.className = 'bubble';
|
||
|
|
|
||
|
|
const name = document.createElement('div');
|
||
|
|
name.className = 'name';
|
||
|
|
name.textContent = member.name;
|
||
|
|
bubble.appendChild(name);
|
||
|
|
|
||
|
|
const textEle = document.createElement('div');
|
||
|
|
textEle.className = 'text';
|
||
|
|
// very bad but fuck if i can be bothered to implement a proper markdown-it extension
|
||
|
|
textEle.innerHTML = md.render(text.replace(/^-# /g, "###### ")).trim();
|
||
|
|
bubble.appendChild(textEle);
|
||
|
|
|
||
|
|
msg.appendChild(bubble);
|
||
|
|
|
||
|
|
lastMsg = msg;
|
||
|
|
|
||
|
|
messages.appendChild(msg);
|
||
|
|
window.scroll(0, 99999999);
|
||
|
|
|
||
|
|
altText.push(member.name+": "+text.trim());
|
||
|
|
|
||
|
|
if (getProxyMode() !== ProxyMode.LATCH) {
|
||
|
|
setAvatar(system);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
chatbox.on('input', () => {
|
||
|
|
const currentMember = getMember(chatbox.value);
|
||
|
|
if (currentMember) {
|
||
|
|
latchedMember = currentMember;
|
||
|
|
}
|
||
|
|
setAvatar(currentMember ?? system);
|
||
|
|
});
|
||
|
|
|
||
|
|
$("#send").on('click', send);
|
||
|
|
|
||
|
|
window.on('keydown', (e) => {
|
||
|
|
if (e.keyCode === 13 && document.hasFocus() && document.activeElement === chatbox) {
|
||
|
|
if (!e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey) {
|
||
|
|
e.preventDefault();
|
||
|
|
send();
|
||
|
|
} else {
|
||
|
|
chatbox.rows++;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
});
|
||
|
|
$("#copyalt").on('click', (e) => {
|
||
|
|
e.preventDefault();
|
||
|
|
window.navigator.clipboard.writeText('A chat log '+$("#credit").textContent.trim()+'.\r\n\r\n'+altText.join('\r\n'));
|
||
|
|
});
|
||
|
|
$("#reset").on('click', (e) => {
|
||
|
|
e.preventDefault();
|
||
|
|
localStorage.clear();
|
||
|
|
location.reload();
|
||
|
|
});
|
||
|
|
|
||
|
|
$("#export-system").on('click', (e) => {
|
||
|
|
e.preventDefault();
|
||
|
|
const a = document.createElement('a');
|
||
|
|
save();
|
||
|
|
a.href = 'data:application/json;utf8,'+encodeURIComponent(JSON.stringify(system));
|
||
|
|
a.download = 'utter-system.json';
|
||
|
|
a.click();
|
||
|
|
});
|
||
|
|
</script>
|
||
|
|
</body>
|
||
|
|
</html>
|