redo login/register pages and install button

This commit is contained in:
MathMan05 2025-04-29 14:30:46 -05:00
parent e776bf6d3f
commit ba5ab05408
11 changed files with 698 additions and 600 deletions

View file

@ -1,4 +1,5 @@
import {I18n} from "./i18n.js"; import {I18n} from "./i18n.js";
import {makeRegister} from "./register.js";
import {mobile} from "./utils/utils.js"; import {mobile} from "./utils/utils.js";
console.log(mobile); console.log(mobile);
const serverbox = document.getElementById("instancebox") as HTMLDivElement; const serverbox = document.getElementById("instancebox") as HTMLDivElement;
@ -127,7 +128,7 @@ fetch("/instances.json")
div.append(statbox); div.append(statbox);
div.onclick = (_) => { div.onclick = (_) => {
if (instance.online) { if (instance.online) {
window.location.href = "/register.html?instance=" + encodeURI(instance.name); makeRegister(true, instance.name);
} else { } else {
alert(I18n.getTranslation("home.warnOffiline")); alert(I18n.getTranslation("home.warnOffiline"));
} }

View file

@ -10,7 +10,7 @@ import {I18n} from "./i18n.js";
let templateID = new URLSearchParams(window.location.search).get("templateID"); let templateID = new URLSearchParams(window.location.search).get("templateID");
await I18n.done; await I18n.done;
if (!Localuser.users.currentuser) { if (!(sessionStorage.getItem("currentuser") || Localuser.users.currentuser)) {
window.location.href = "/login.html"; window.location.href = "/login.html";
return; return;
} }

View file

@ -3,7 +3,7 @@ import {Channel} from "./channel.js";
import {Direct} from "./direct.js"; import {Direct} from "./direct.js";
import {AVoice} from "./audio/voice.js"; import {AVoice} from "./audio/voice.js";
import {User} from "./user.js"; import {User} from "./user.js";
import {createImg, getapiurls, getBulkUsers, SW} from "./utils/utils.js"; import {createImg, getapiurls, getBulkUsers, installPGet, SW} from "./utils/utils.js";
import {getBulkInfo, setTheme, Specialuser} from "./utils/utils.js"; import {getBulkInfo, setTheme, Specialuser} from "./utils/utils.js";
import { import {
channeljson, channeljson,
@ -1719,7 +1719,8 @@ class Localuser {
required: true, required: true,
password: true, password: true,
}); });
form.addTextInput(I18n.getTranslation("localuser.2faCode"), "code", {required: true}); form.addTextInput(I18n.localuser["2faCode:"](), "code", {required: true});
debugger;
form.setValue("secret", secret); form.setValue("secret", secret);
}); });
} }
@ -2197,6 +2198,15 @@ class Localuser {
this.instanceStats(); this.instanceStats();
}); });
})(); })();
const installP = installPGet();
if (installP) {
const c = settings.addButton(I18n.localuser.install());
c.addText(I18n.localuser.installDesc());
c.addButtonInput("", I18n.localuser.installJank(), async () => {
//@ts-expect-error have to do this :3
await installP.prompt();
});
}
settings.show(); settings.show();
} }
readonly botTokens: Map<string, string> = new Map(); readonly botTokens: Map<string, string> = new Map();

View file

@ -22,35 +22,6 @@
</style> </style>
</head> </head>
<body class="no-theme"> <body class="no-theme">
<div id="logindiv">
<h1>Login</h1>
<form id="form" submit="check(e)">
<label for="instance" id="instanceField"><b>Instance:</b></label>
<p id="verify"></p>
<input
type="search"
list="instances"
placeholder="Instance URL"
name="instance"
id="instancein"
value=""
required
/>
<label for="uname" id="emailField"><b>Email:</b></label>
<input type="text" placeholder="Enter email address" name="uname" id="uname" required />
<label for="psw" id="pwField"><b>Password:</b></label>
<input type="password" placeholder="Enter Password" name="psw" id="psw" required />
<p class="wrongred" id="wrong"></p>
<div id="h-captcha"></div>
<button type="submit" id="loginButton">Login</button>
</form>
<a href="/register.html" id="switch">Don't have an account?</a>
<div id="recover"></div>
</div>
<datalist id="instances"></datalist>
<script src="/login.js" type="module"></script> <script src="/login.js" type="module"></script>
</body> </body>
</html> </html>

View file

@ -1,10 +1,10 @@
import {getBulkInfo, Specialuser} from "./utils/utils.js"; import {instanceinfo, adduser} from "./utils/utils.js";
import {I18n} from "./i18n.js"; import {I18n} from "./i18n.js";
import {Dialog, FormError} from "./settings.js"; import {Dialog} from "./settings.js";
import {checkInstance} from "./utils/utils.js"; import {makeRegister} from "./register.js";
function generateRecArea() { function generateRecArea(recover = document.getElementById("recover")) {
const recover = document.getElementById("recover");
if (!recover) return; if (!recover) return;
recover.innerHTML = "";
const can = localStorage.getItem("canRecover"); const can = localStorage.getItem("canRecover");
if (can) { if (can) {
const a = document.createElement("a"); const a = document.createElement("a");
@ -13,181 +13,63 @@ function generateRecArea() {
recover.append(a); recover.append(a);
} }
} }
checkInstance.alt = async (e) => { const recMap = new Map<string, Promise<boolean>>();
const recover = document.getElementById("recover"); async function recover(e: instanceinfo, recover = document.getElementById("recover")) {
if (!recover) return; const prom = new Promise<boolean>(async (res) => {
if (!recover) {
res(false);
return;
}
recover.innerHTML = ""; recover.innerHTML = "";
try { try {
if (!(await recMap.get(e.api))) {
if (recMap.has(e.api)) {
throw Error("can't recover");
}
recMap.set(e.api, prom);
const json = (await (await fetch(e.api + "/policies/instance/config")).json()) as { const json = (await (await fetch(e.api + "/policies/instance/config")).json()) as {
can_recover_account: boolean; can_recover_account: boolean;
}; };
if (!json || !json.can_recover_account) throw Error("can't recover account"); if (!json || !json.can_recover_account) throw Error("can't recover account");
}
res(true);
localStorage.setItem("canRecover", "true"); localStorage.setItem("canRecover", "true");
generateRecArea(); generateRecArea(recover);
} catch { } catch {
res(false);
localStorage.removeItem("canRecover"); localStorage.removeItem("canRecover");
generateRecArea(); generateRecArea(recover);
} finally {
res(false);
} }
};
await I18n.done;
generateRecArea();
(async () => {
await I18n.done;
const instanceField = document.getElementById("instanceField");
const emailField = document.getElementById("emailField");
const pwField = document.getElementById("pwField");
const loginButton = document.getElementById("loginButton");
const noAccount = document.getElementById("switch");
if (instanceField && emailField && pwField && loginButton && noAccount) {
instanceField.textContent = I18n.getTranslation("htmlPages.instanceField");
emailField.textContent = I18n.getTranslation("htmlPages.emailField");
pwField.textContent = I18n.getTranslation("htmlPages.pwField");
loginButton.textContent = I18n.getTranslation("htmlPages.loginButton");
noAccount.textContent = I18n.getTranslation("htmlPages.noAccount");
}
})();
function trimswitcher() {
const json = getBulkInfo();
const map = new Map();
for (const thing in json.users) {
const user = json.users[thing];
let wellknown = user.serverurls.wellknown;
if (wellknown.at(-1) !== "/") {
wellknown += "/";
}
wellknown = (user.id || user.email) + "@" + wellknown;
if (map.has(wellknown)) {
const otheruser = map.get(wellknown);
if (otheruser[1].serverurls.wellknown.at(-1) === "/") {
delete json.users[otheruser[0]];
map.set(wellknown, [thing, user]);
} else {
delete json.users[thing];
}
} else {
map.set(wellknown, [thing, user]);
}
}
for (const thing in json.users) {
if (thing.at(-1) === "/") {
const user = json.users[thing];
delete json.users[thing];
json.users[thing.slice(0, -1)] = user;
}
}
localStorage.setItem("userinfos", JSON.stringify(json));
console.log(json);
}
function adduser(user: typeof Specialuser.prototype.json) {
user = new Specialuser(user);
const info = getBulkInfo();
info.users[user.uid] = user;
info.currentuser = user.uid;
sessionStorage.setItem("currentuser", user.uid);
localStorage.setItem("userinfos", JSON.stringify(info));
return user;
}
const instancein = document.getElementById("instancein") as HTMLInputElement;
let timeout: ReturnType<typeof setTimeout> | string | number | undefined | null = null;
// let instanceinfo;
const switchurl = document.getElementById("switch") as HTMLAreaElement;
if (switchurl) {
switchurl.href += window.location.search;
const instance = new URLSearchParams(window.location.search).get("instance");
console.log(instance);
if (instance) {
instancein.value = instance;
checkInstance(instance);
}
}
if (instancein) {
console.log(instancein);
instancein.addEventListener("keydown", () => {
const verify = document.getElementById("verify");
verify!.textContent = I18n.getTranslation("login.waiting");
if (timeout !== null && typeof timeout !== "string") {
clearTimeout(timeout);
}
timeout = setTimeout(() => checkInstance((instancein as HTMLInputElement).value), 1000);
}); });
if (
localStorage.getItem("instanceinfo") &&
!new URLSearchParams(window.location.search).get("instance")
) {
const json = JSON.parse(localStorage.getItem("instanceinfo")!);
if (json.value) {
(instancein as HTMLInputElement).value = json.value;
} else {
(instancein as HTMLInputElement).value = json.wellknown;
}
}
} }
async function login(username: string, password: string, captcha: string) { export async function makeLogin(trasparentBg = false, instance = "") {
if (captcha === "") { const dialog = new Dialog("");
captcha = ""; const opt = dialog.options;
} opt.addTitle(I18n.login.login());
const options = { const picker = opt.addInstancePicker(
method: "POST", (info) => {
body: JSON.stringify({
login: username,
password,
undelete: false,
captcha_key: captcha,
}),
headers: {
"Content-type": "application/json; charset=UTF-8",
},
};
try {
const info = JSON.parse(localStorage.getItem("instanceinfo")!);
const api = info.login + (info.login.startsWith("/") ? "/" : ""); const api = info.login + (info.login.startsWith("/") ? "/" : "");
return await fetch(api + "/auth/login", options) form.fetchURL = api + "/auth/login";
.then((response) => response.json()) recover(info, rec);
.then((response) => { },
console.log(response, response.message); {
if (response.message === "Invalid Form Body") { instance,
return response.errors.login._errors[0].message; },
console.log("test"); );
} dialog.show(trasparentBg);
//this.serverurls||!this.email||!this.token
console.log(response);
if (response.captcha_sitekey) { const form = opt.addForm(
const capt = document.getElementById("h-captcha");
if (capt!.children.length) {
eval("hcaptcha.reset()");
} else {
const capty = document.createElement("div");
capty.classList.add("h-captcha");
capty.setAttribute("data-sitekey", response.captcha_sitekey);
const script = document.createElement("script");
script.src = "https://js.hcaptcha.com/1/api.js";
capt!.append(script);
capt!.append(capty);
}
} else {
console.log(response);
if (response.ticket) {
const better = new Dialog("");
const form = better.options.addForm(
"", "",
(res: any) => { (res) => {
if (res.message) { if ("token" in res && typeof res.token == "string") {
throw new FormError(ti, res.message);
} else {
console.warn(res);
if (!res.token) return;
adduser({ adduser({
serverurls: JSON.parse(localStorage.getItem("instanceinfo") as string), serverurls: JSON.parse(localStorage.getItem("instanceinfo") as string),
email: username, email: email.value,
token: res.token, token: res.token,
}).username = username; }).username = email.value;
const redir = new URLSearchParams(window.location.search).get("goback"); const redir = new URLSearchParams(window.location.search).get("goback");
if (redir) { if (redir) {
window.location.href = redir; window.location.href = redir;
@ -197,64 +79,32 @@ async function login(username: string, password: string, captcha: string) {
} }
}, },
{ {
fetchURL: api + "/auth/mfa/totp", submitText: I18n.login.login(),
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-type": "application/json; charset=UTF-8",
}, },
vsmaller: true,
}, },
); );
form.addTitle(I18n.getTranslation("2faCode")); const button = form.button.deref();
const ti = form.addTextInput("", "code"); picker.giveButton(button);
better.show(); button?.classList.add("createAccount");
} else {
console.warn(response);
if (!response.token) return;
adduser({
serverurls: JSON.parse(localStorage.getItem("instanceinfo")!),
email: username,
token: response.token,
}).username = username;
const redir = new URLSearchParams(window.location.search).get("goback");
if (redir) {
window.location.href = redir;
} else {
window.location.href = "/channels/@me";
}
return "";
}
}
});
} catch (error) {
console.error("Error:", error);
}
}
async function check(e: SubmitEvent) { const email = form.addTextInput(I18n.htmlPages.userField(), "login");
e.preventDefault(); form.addTextInput(I18n.htmlPages.pwField(), "password", {password: true});
const target = e.target as HTMLFormElement; form.addCaptcha();
const h = await login( const a = document.createElement("a");
(target[1] as HTMLInputElement).value, a.onclick = () => {
(target[2] as HTMLInputElement).value, dialog.hide();
(target[3] as HTMLInputElement).value, makeRegister(trasparentBg);
); };
const wrongElement = document.getElementById("wrong"); a.textContent = I18n.htmlPages.noAccount();
if (wrongElement) { const rec = document.createElement("div");
wrongElement.textContent = h; form.addHTMLArea(rec);
} form.addHTMLArea(a);
console.log(h);
} }
if (document.getElementById("form")) { await I18n.done;
const form = document.getElementById("form"); if (window.location.pathname.startsWith("/login")) {
if (form) { makeLogin();
form.addEventListener("submit", (e: SubmitEvent) => check(e));
}
} }
//this currently does not work, and need to be implemented better at some time.
if (!localStorage.getItem("SWMode")) {
localStorage.setItem("SWMode", "SWOn");
}
trimswitcher();
export {adduser};

View file

@ -22,64 +22,6 @@
</style> </style>
</head> </head>
<body class="no-theme"> <body class="no-theme">
<div id="logindiv">
<h1>Create an account</h1>
<form id="register" submit="registertry(e)">
<div>
<label for="instance" id="instanceField"><b>Instance:</b></label>
<p id="verify"></p>
<input
type="search"
list="instances"
placeholder="Instance URL"
id="instancein"
name="instance"
value=""
required
/>
</div>
<div>
<label for="uname" id="emailField"><b>Email:</b></label>
<input type="text" placeholder="Enter Email" name="uname" id="uname" required />
</div>
<div>
<label for="uname" id="userField"><b>Username:</b></label>
<input type="text" placeholder="Enter Username" name="username" id="username" required />
</div>
<div>
<label for="psw" id="pwField"><b>Password:</b></label>
<input type="password" placeholder="Enter Password" name="psw" id="psw" required />
</div>
<div>
<label for="psw2" id="pw2Field"><b>Enter password again:</b></label>
<input
type="password"
placeholder="Enter Password Again"
name="psw2"
id="psw2"
required
/>
</div>
<div>
<label for="date" id="dobField"><b>Date of birth:</b></label>
<input type="date" id="date" name="date" />
</div>
<div>
<b id="TOSbox">I agree to the <a href="" id="TOSa">Terms of Service</a>:</b>
<input type="checkbox" id="TOS" name="TOS" />
</div>
<p class="wrongred" id="wrong"></p>
<div id="h-captcha"></div>
<button type="submit" class="dontgrow" id="createAccount">Create account</button>
</form>
<a href="/login.html" id="switch" id="alreadyHave">Already have an account?</a>
</div>
<datalist id="instances"></datalist>
<script src="/register.js" type="module"></script> <script src="/register.js" type="module"></script>
</body> </body>
</html> </html>

View file

@ -1,169 +1,100 @@
import {I18n} from "./i18n.js"; import {I18n} from "./i18n.js";
import {checkInstance} from "./utils/utils.js"; import {adduser} from "./utils/utils.js";
import {adduser} from "./login.js"; import {makeLogin} from "./login.js";
import {MarkDown} from "./markdown.js"; import {MarkDown} from "./markdown.js";
await I18n.done; import {Dialog, FormError} from "./settings.js";
const registerElement = document.getElementById("register"); export async function makeRegister(trasparentBg = false, instance = "") {
if (registerElement) { const dialog = new Dialog("");
registerElement.addEventListener("submit", registertry); const opt = dialog.options;
} opt.addTitle(I18n.htmlPages.createAccount());
(async () => { const picker = opt.addInstancePicker(
await I18n.done; (info) => {
const userField = document.getElementById("userField"); const api = info.login + (info.login.startsWith("/") ? "/" : "");
const pw2Field = document.getElementById("pw2Field"); form.fetchURL = api + "/auth/register";
const dobField = document.getElementById("dobField"); tosLogic(md);
const createAccount = document.getElementById("createAccount");
const alreadyHave = document.getElementById("alreadyHave");
if (userField && pw2Field && alreadyHave && createAccount && dobField) {
userField.textContent = I18n.getTranslation("htmlPages.userField");
pw2Field.textContent = I18n.getTranslation("htmlPages.pw2Field");
dobField.textContent = I18n.getTranslation("htmlPages.dobField");
createAccount.textContent = I18n.getTranslation("htmlPages.createAccount");
alreadyHave.textContent = I18n.getTranslation("htmlPages.alreadyHave");
}
})();
async function registertry(e: Event) {
e.preventDefault();
const elements = (e.target as HTMLFormElement).elements as HTMLFormControlsCollection;
const email = (elements[1] as HTMLInputElement).value;
const username = (elements[2] as HTMLInputElement).value;
const password = (elements[3] as HTMLInputElement).value;
const confirmPassword = (elements[4] as HTMLInputElement).value;
const dateofbirth = (elements[5] as HTMLInputElement).value;
const consent = (elements[6] as HTMLInputElement).checked;
const captchaKey = (elements[7] as HTMLInputElement)?.value;
if (password !== confirmPassword) {
(document.getElementById("wrong") as HTMLElement).textContent = I18n.getTranslation(
"localuser.PasswordsNoMatch",
);
return;
}
const instanceInfo = JSON.parse(localStorage.getItem("instanceinfo") ?? "{}");
const apiurl = new URL(instanceInfo.api);
let add = "";
const token = new URLSearchParams(window.location.search).get("token");
if (token) {
add = "?" + new URLSearchParams([["token", token]]).toString();
}
try {
const response = await fetch(apiurl + "/auth/register" + add, {
body: JSON.stringify({
date_of_birth: dateofbirth,
email,
username,
password,
consent,
captcha_key: captchaKey,
}),
headers: {
"content-type": "application/json",
Referrer: window.location.href,
}, },
method: "POST", {instance},
}); );
dialog.show(trasparentBg);
const data = await response.json(); const form = opt.addForm(
"",
if (data.captcha_sitekey) { (res) => {
const capt = document.getElementById("h-captcha"); if ("token" in res && typeof res.token == "string") {
if (capt && !capt.children.length) {
const capty = document.createElement("div");
capty.classList.add("h-captcha");
capty.setAttribute("data-sitekey", data.captcha_sitekey);
const script = document.createElement("script");
script.src = "https://js.hcaptcha.com/1/api.js";
capt.append(script);
capt.append(capty);
} else {
eval("hcaptcha.reset()");
}
return;
}
if (!data.token) {
handleErrors(data.errors, elements);
} else {
adduser({ adduser({
serverurls: instanceInfo, serverurls: JSON.parse(localStorage.getItem("instanceinfo") as string),
email, email: email.value,
token: data.token, token: res.token,
}).username = username; }).username = user.value;
localStorage.setItem("token", data.token);
const redir = new URLSearchParams(window.location.search).get("goback"); const redir = new URLSearchParams(window.location.search).get("goback");
window.location.href = redir ? redir : "/channels/@me"; if (redir) {
} window.location.href = redir;
} catch (error) {
console.error("Registration failed:", error);
}
}
function handleErrors(errors: any, elements: HTMLFormControlsCollection) {
if (errors.consent) {
showError(elements[6] as HTMLElement, errors.consent._errors[0].message);
} else if (errors.password) {
showError(
elements[3] as HTMLElement,
I18n.getTranslation("register.passwordError:", errors.password._errors[0].message),
);
} else if (errors.username) {
showError(
elements[2] as HTMLElement,
I18n.getTranslation("register.usernameError", errors.username._errors[0].message),
);
} else if (errors.email) {
showError(
elements[1] as HTMLElement,
I18n.getTranslation("register.emailError", errors.email._errors[0].message),
);
} else if (errors.date_of_birth) {
showError(
elements[5] as HTMLElement,
I18n.getTranslation("register.DOBError", errors.date_of_birth._errors[0].message),
);
} else { } else {
(document.getElementById("wrong") as HTMLElement).textContent = window.location.href = "/channels/@me";
errors[Object.keys(errors)[0]]._errors[0].message;
} }
}
function showError(element: HTMLElement, message: string) {
const parent = element.parentElement!;
let errorElement = parent.getElementsByClassName("suberror")[0] as HTMLElement;
if (!errorElement) {
const div = document.createElement("div");
div.classList.add("suberror", "suberrora");
parent.append(div);
errorElement = div;
} else {
errorElement.classList.remove("suberror");
setTimeout(() => {
errorElement.classList.add("suberror");
}, 100);
} }
errorElement.textContent = message; },
} {
submitText: I18n.htmlPages.createAccount(),
method: "POST",
headers: {
"Content-type": "application/json; charset=UTF-8",
},
vsmaller: true,
},
);
const button = form.button.deref();
picker.giveButton(button);
button?.classList.add("createAccount");
async function tosLogic() { const email = form.addTextInput(I18n.htmlPages.emailField(), "email");
const user = form.addTextInput(I18n.htmlPages.userField(), "username");
const p1 = form.addTextInput(I18n.htmlPages.pwField(), "password", {password: true});
const p2 = form.addTextInput(I18n.htmlPages.pw2Field(), "password2", {password: true});
form.addDateInput(I18n.htmlPages.dobField(), "date_of_birth");
form.addPreprocessor((e) => {
if (p1.value !== p2.value) {
throw new FormError(p2, I18n.localuser.PasswordsNoMatch());
}
//@ts-expect-error it's there
delete e.password2;
if (!check.checked) throw new FormError(checkbox, I18n.register.tos());
//@ts-expect-error it's there
e.consent = check.checked;
});
const toshtml = document.createElement("div");
const md = document.createElement("span");
const check = document.createElement("input");
check.type = "checkbox";
toshtml.append(md, check);
const checkbox = form.addHTMLArea(toshtml);
form.addCaptcha();
const a = document.createElement("a");
a.onclick = () => {
dialog.hide();
makeLogin(trasparentBg);
};
a.textContent = I18n.htmlPages.alreadyHave();
form.addHTMLArea(a);
}
async function tosLogic(box: HTMLElement) {
const instanceInfo = JSON.parse(localStorage.getItem("instanceinfo") ?? "{}"); const instanceInfo = JSON.parse(localStorage.getItem("instanceinfo") ?? "{}");
const apiurl = new URL(instanceInfo.api); const apiurl = new URL(instanceInfo.api);
const urlstr = apiurl.toString(); const urlstr = apiurl.toString();
const response = await fetch(urlstr + (urlstr.endsWith("/") ? "" : "/") + "ping"); const response = await fetch(urlstr + (urlstr.endsWith("/") ? "" : "/") + "ping");
const data = await response.json(); const data = await response.json();
const tosPage = data.instance.tosPage; const tosPage = data.instance.tosPage;
if (tosPage) {
const box = document.getElementById("TOSbox");
if (!box) return; if (!box) return;
if (tosPage) {
box.innerHTML = ""; box.innerHTML = "";
box.append(new MarkDown(I18n.getTranslation("register.agreeTOS", tosPage)).makeHTML()); box.append(new MarkDown(I18n.getTranslation("register.agreeTOS", tosPage)).makeHTML());
} else { } else {
document.getElementById("TOSbox")!.textContent = I18n.getTranslation("register.noTOS"); box.textContent = I18n.getTranslation("register.noTOS");
} }
console.log(tosPage); console.log(tosPage);
} }
if (window.location.pathname.startsWith("/register")) {
tosLogic(); await I18n.done;
makeRegister();
checkInstance.alt = tosLogic; }

View file

@ -1,3 +1,10 @@
import {
checkInstance,
getInstances,
getStringURLMapPair,
instancefetch,
instanceinfo,
} from "./utils/utils.js";
import {Emoji} from "./emoji.js"; import {Emoji} from "./emoji.js";
import {I18n} from "./i18n.js"; import {I18n} from "./i18n.js";
import {Localuser} from "./localuser.js"; import {Localuser} from "./localuser.js";
@ -166,6 +173,21 @@ class TextInput implements OptionsElement<string> {
this.onSubmit(this.value); this.onSubmit(this.value);
} }
} }
class DateInput extends TextInput {
generateHTML(): HTMLDivElement {
const div = document.createElement("div");
const span = document.createElement("span");
span.textContent = this.label;
div.append(span);
const input = document.createElement("input");
input.value = this.value;
input.type = "date";
input.oninput = this.onChange.bind(this);
this.input = new WeakRef(input);
div.append(input);
return div;
}
}
const mdProm = import("./markdown.js"); const mdProm = import("./markdown.js");
class SettingsMDText implements OptionsElement<void> { class SettingsMDText implements OptionsElement<void> {
readonly onSubmit!: (str: string) => void; readonly onSubmit!: (str: string) => void;
@ -306,6 +328,7 @@ class ButtonInput implements OptionsElement<void> {
this.onClick = onClick; this.onClick = onClick;
this.textContent = textContent; this.textContent = textContent;
} }
buttonHtml?: HTMLButtonElement;
generateHTML(): HTMLDivElement { generateHTML(): HTMLDivElement {
const div = document.createElement("div"); const div = document.createElement("div");
if (this.label) { if (this.label) {
@ -317,6 +340,7 @@ class ButtonInput implements OptionsElement<void> {
const button = document.createElement("button"); const button = document.createElement("button");
button.textContent = this.textContent; button.textContent = this.textContent;
button.onclick = this.onClickEvent.bind(this); button.onclick = this.onClickEvent.bind(this);
this.buttonHtml = button;
div.append(button); div.append(button);
return div; return div;
} }
@ -712,6 +736,7 @@ class Dialog {
show(hideOnClick = true) { show(hideOnClick = true) {
const background = document.createElement("div"); const background = document.createElement("div");
background.classList.add("background"); background.classList.add("background");
if (!hideOnClick) background.classList.add("solidBackground");
const center = this.float.generateHTML(); const center = this.float.generateHTML();
center.classList.add("centeritem", "nonimagecenter"); center.classList.add("centeritem", "nonimagecenter");
center.classList.remove("titlediv"); center.classList.remove("titlediv");
@ -733,7 +758,141 @@ class Dialog {
background.remove(); background.remove();
} }
} }
class InstancePicker implements OptionsElement<instanceinfo | null> {
value: instanceinfo | null = null;
owner: Options | Form;
verify = document.createElement("p");
onchange = (_: instanceinfo) => {};
instance?: string;
watchForChange(func: (arg1: instanceinfo) => void) {
this.onchange = func;
}
constructor(
owner: Options | Form,
onchange?: InstancePicker["onchange"],
button?: HTMLButtonElement,
instance?: string,
) {
this.owner = owner;
this.instance = instance;
if (onchange) {
this.onchange = onchange;
}
this.button = button;
}
generateHTML(): HTMLElement {
const div = document.createElement("div");
const span = document.createElement("span");
span.textContent = I18n.htmlPages.instanceField();
div.append(span);
const verify = this.verify;
verify.classList.add("verify");
div.append(verify);
const input = this.input;
input.type = "search";
input.setAttribute("list", "instances");
div.append(input);
let cur = 0;
input.onkeyup = async () => {
const thiscur = ++cur;
await new Promise((res) => setTimeout(res, 500));
if (thiscur !== cur) return;
const urls = await checkInstance(input.value, verify, this.button);
if (thiscur === cur && urls) {
this.onchange(urls);
}
};
InstancePicker.picker = this;
InstancePicker.genDataList();
return div;
}
button?: HTMLButtonElement;
input = document.createElement("input");
giveButton(button: HTMLButtonElement | undefined) {
this.button = button;
}
static picker?: InstancePicker;
static genDataList() {
let datalist = document.getElementById("instances");
if (!datalist) {
datalist = document.createElement("datalist");
datalist.setAttribute("id", "instances");
document.body.append(datalist);
}
const json = getInstances();
const [stringURLMap, stringURLsMap] = getStringURLMapPair();
if (!json) {
instancefetch.then(this.genDataList.bind(this));
return;
}
if (json.length !== 0) {
let name =
this.picker?.instance || new URLSearchParams(window.location.search).get("instance");
if (!name) {
const l = localStorage.getItem("instanceinfo");
if (l) {
const json = JSON.parse(l);
if (json.value) {
name = json.value;
} else {
name = json.wellknown;
}
}
}
if (!name) {
name = json[0].name;
}
if (this.picker) {
checkInstance(
name,
this.picker.verify,
this.picker.button || document.createElement("button"),
).then((e) => {
if (e) this.picker?.onchange(e);
});
this.picker.input.value = name;
}
}
if (datalist.childElementCount !== 0) {
return;
}
for (const instance of json) {
if (instance.display === false) {
continue;
}
const option = document.createElement("option");
option.disabled = !instance.online;
option.value = instance.name;
if (instance.url) {
stringURLMap.set(option.value, instance.url);
if (instance.urls) {
stringURLsMap.set(instance.url, instance.urls);
}
} else if (instance.urls) {
stringURLsMap.set(option.value, instance.urls);
} else {
option.disabled = true;
}
if (instance.description) {
option.label = instance.description;
} else {
option.label = instance.name;
}
datalist.append(option);
}
}
submit() {}
}
setTimeout(InstancePicker.genDataList.bind(InstancePicker), 0);
export {Dialog}; export {Dialog};
class Options implements OptionsElement<void> { class Options implements OptionsElement<void> {
name: string; name: string;
@ -745,16 +904,18 @@ class Options implements OptionsElement<void> {
readonly html: WeakMap<OptionsElement<any>, WeakRef<HTMLDivElement>> = new WeakMap(); readonly html: WeakMap<OptionsElement<any>, WeakRef<HTMLDivElement>> = new WeakMap();
readonly noSubmit: boolean = false; readonly noSubmit: boolean = false;
container: WeakRef<HTMLDivElement> = new WeakRef(document.createElement("div")); container: WeakRef<HTMLDivElement> = new WeakRef(document.createElement("div"));
vsmaller = false;
constructor( constructor(
name: string, name: string,
owner: Buttons | Options | Form | Float, owner: Buttons | Options | Form | Float,
{ltr = false, noSubmit = false} = {}, {ltr = false, noSubmit = false, vsmaller = false} = {},
) { ) {
this.name = name; this.name = name;
this.options = []; this.options = [];
this.owner = owner; this.owner = owner;
this.ltr = ltr; this.ltr = ltr;
this.noSubmit = noSubmit; this.noSubmit = noSubmit;
this.vsmaller = vsmaller;
} }
removeAll() { removeAll() {
this.returnFromSub(); this.returnFromSub();
@ -837,6 +998,15 @@ class Options implements OptionsElement<void> {
this.generate(emoji); this.generate(emoji);
return emoji; return emoji;
} }
addInstancePicker(
onchange?: InstancePicker["onchange"],
{button, instance}: {button?: HTMLButtonElement; instance?: string} = {},
) {
const instacePicker = new InstancePicker(this, onchange, button, instance);
this.options.push(instacePicker);
this.generate(instacePicker);
return instacePicker;
}
returnFromSub() { returnFromSub() {
this.subOptions = undefined; this.subOptions = undefined;
this.genTop(); this.genTop();
@ -861,6 +1031,14 @@ class Options implements OptionsElement<void> {
this.generate(FI); this.generate(FI);
return FI; return FI;
} }
addDateInput(label: string, onSubmit: (str: string) => void, {initText = ""} = {}) {
const textInput = new DateInput(label, onSubmit, this, {
initText,
});
this.options.push(textInput);
this.generate(textInput);
return textInput;
}
addTextInput( addTextInput(
label: string, label: string,
onSubmit: (str: string) => void, onSubmit: (str: string) => void,
@ -938,6 +1116,7 @@ class Options implements OptionsElement<void> {
headers = {}, headers = {},
method = "POST", method = "POST",
traditionalSubmit = false, traditionalSubmit = false,
vsmaller = false,
} = {}, } = {},
) { ) {
const options = new Form(name, this, onSubmit, { const options = new Form(name, this, onSubmit, {
@ -947,6 +1126,7 @@ class Options implements OptionsElement<void> {
headers, headers,
method, method,
traditionalSubmit, traditionalSubmit,
vsmaller,
}); });
this.options.push(options); this.options.push(options);
this.generate(options); this.generate(options);
@ -969,6 +1149,7 @@ class Options implements OptionsElement<void> {
generateHTML(): HTMLElement { generateHTML(): HTMLElement {
const div = document.createElement("div"); const div = document.createElement("div");
div.classList.add("flexttb", "titlediv"); div.classList.add("flexttb", "titlediv");
if (this.vsmaller) div.classList.add("vsmaller");
if (this.owner instanceof Options) { if (this.owner instanceof Options) {
div.classList.add("optionElement"); div.classList.add("optionElement");
} }
@ -1106,6 +1287,83 @@ class Options implements OptionsElement<void> {
} }
} }
} }
class Captcha implements OptionsElement<string> {
owner: Form;
value: string = "";
constructor(owner: Form) {
this.owner = owner;
}
div?: HTMLElement;
generateHTML(): HTMLElement {
const div = document.createElement("div");
this.div = div;
return div;
}
submit() {}
onchange = (_: string) => {};
watchForChange(func: (arg1: string) => void) {
this.onchange = func;
}
static hcaptcha?: HTMLDivElement;
static async waitForCaptcha(ctype: "hcaptcha") {
switch (ctype) {
case "hcaptcha":
if (!this.hcaptcha) throw Error("no captcha found");
const hcaptcha = this.hcaptcha;
console.log(hcaptcha);
//@ts-expect-error
while (!hcaptcha.children[1].children.length || !hcaptcha.children[1].children[1].value) {
await new Promise<void>((res) => setTimeout(res, 100));
}
//@ts-expect-error
return hcaptcha.children[1].children[1].value;
}
}
async makeCaptcha({
captcha_sitekey,
captcha_service,
}: {
captcha_sitekey: string;
captcha_service: "hcaptcha";
}): Promise<string> {
if (!this.div) throw new Error("Div doesn't exist yet to give catpcha");
switch (captcha_service) {
case "hcaptcha":
if (Captcha.hcaptcha) {
this.div.append(Captcha.hcaptcha);
Captcha.hcaptcha.setAttribute("data-sitekey", captcha_sitekey);
eval("hcaptcha.reset()");
return Captcha.waitForCaptcha(captcha_service);
} else {
const capt = document.createElement("div");
const capty = document.createElement("div");
capty.classList.add("h-captcha");
capty.setAttribute("data-sitekey", captcha_sitekey);
const script = document.createElement("script");
script.src = "https://js.hcaptcha.com/1/api.js";
capt.append(script);
capt.append(capty);
Captcha.hcaptcha = capt;
this.div.append(capt);
return Captcha.waitForCaptcha(captcha_service);
}
}
}
static async makeCaptcha(json: {
captcha_sitekey: string;
captcha_service: "hcaptcha";
}): Promise<string> {
const float = new Dialog("", {noSubmit: true});
float.options.addTitle(I18n.form.captcha());
const cap = float.options.addForm("", () => {}, {traditionalSubmit: true}).addCaptcha();
float.show();
const ret = cap.makeCaptcha(json);
await ret;
float.hide();
return ret;
}
}
class FormError extends Error { class FormError extends Error {
elem: OptionsElement<any>; elem: OptionsElement<any>;
message: string; message: string;
@ -1115,6 +1373,53 @@ class FormError extends Error {
this.elem = elem; this.elem = elem;
} }
} }
async function handle2fa(json: any, api: string): Promise<false | any> {
if (json.ticket) {
return new Promise<boolean>((resolution) => {
const better = new Dialog("");
const form = better.options.addForm(
"",
(res: any) => {
if (res.message) {
throw new FormError(ti, res.message);
} else {
resolution(res);
better.hide();
}
},
{
fetchURL: api + "/auth/mfa/totp",
method: "POST",
headers: {
"Content-Type": "application/json",
},
},
);
form.addTitle(I18n.getTranslation("2faCode"));
form.addPreprocessor((e) => {
//@ts-ignore
e.ticket = json.ticket;
});
const ti = form.addTextInput("", "code");
better.show();
});
} else {
return false;
}
}
async function handleCaptcha(json: any, build: any, cap: Captcha | undefined) {
if (json.captcha_sitekey) {
let token: string;
if (cap) {
token = await cap.makeCaptcha(json);
} else {
token = await Captcha.makeCaptcha(json);
}
build.captcha_key = token;
return true;
}
return false;
}
export {FormError}; export {FormError};
class Form implements OptionsElement<object> { class Form implements OptionsElement<object> {
name: string; name: string;
@ -1141,13 +1446,14 @@ class Form implements OptionsElement<object> {
headers = {}, headers = {},
method = "POST", method = "POST",
traditionalSubmit = false, traditionalSubmit = false,
vsmaller = false,
} = {}, } = {},
) { ) {
this.traditionalSubmit = traditionalSubmit; this.traditionalSubmit = traditionalSubmit;
this.name = name; this.name = name;
this.method = method; this.method = method;
this.submitText = submitText; this.submitText = submitText;
this.options = new Options(name, this, {ltr}); this.options = new Options(name, this, {ltr, vsmaller});
this.owner = owner; this.owner = owner;
this.fetchURL = fetchURL; this.fetchURL = fetchURL;
this.headers = headers; this.headers = headers;
@ -1167,6 +1473,15 @@ class Form implements OptionsElement<object> {
addHTMLArea(html: (() => HTMLElement) | HTMLElement, onSubmit = () => {}) { addHTMLArea(html: (() => HTMLElement) | HTMLElement, onSubmit = () => {}) {
return this.options.addHTMLArea(html, onSubmit); return this.options.addHTMLArea(html, onSubmit);
} }
private captcha?: Captcha;
addCaptcha() {
if (this.captcha) throw new Error("only one captcha is allowed per form");
const cap = new Captcha(this);
this.options.options.push(cap);
this.options.generate(cap);
this.captcha = cap;
return cap;
}
addSubForm( addSubForm(
name: string, name: string,
onSubmit: (arg1: object, sent: object) => void, onSubmit: (arg1: object, sent: object) => void,
@ -1247,7 +1562,16 @@ class Form implements OptionsElement<object> {
this.names.set(formName, emoji); this.names.set(formName, emoji);
return emoji; return emoji;
} }
addDateInput(label: string, formName: string, {initText = "", required = false} = {}) {
const dateInput = this.options.addDateInput(label, (_) => {}, {
initText,
});
this.names.set(formName, dateInput);
if (required) {
this.required.add(dateInput);
}
return dateInput;
}
addTextInput( addTextInput(
label: string, label: string,
formName: string, formName: string,
@ -1377,7 +1701,7 @@ class Form implements OptionsElement<object> {
return; return;
} }
} else { } else {
(build as any)[thing] = thing; (build as any)[key] = thing;
} }
} }
console.log("middle"); console.log("middle");
@ -1444,24 +1768,7 @@ class Form implements OptionsElement<object> {
return; return;
} }
if (this.fetchURL !== "") { if (this.fetchURL !== "") {
fetch(this.fetchURL, { const onSubmit = async (json: any) => {
method: this.method,
body: JSON.stringify(build),
headers: this.headers,
})
.then((_) => {
return _.text();
})
.then((_) => {
if (_ === "") return {};
return JSON.parse(_);
})
.then(async (json) => {
if (json.errors) {
if (this.errors(json)) {
return;
}
}
try { try {
await this.onSubmit(json, build); await this.onSubmit(json, build);
} catch (e) { } catch (e) {
@ -1478,7 +1785,42 @@ class Form implements OptionsElement<object> {
} }
return; return;
} }
};
const doFetch = async () => {
fetch(this.fetchURL, {
method: this.method,
body: JSON.stringify(build),
headers: this.headers,
})
.then((_) => {
return _.text();
})
.then((_) => {
if (_ === "") return {};
return JSON.parse(_);
})
.then(async (json) => {
if (await handleCaptcha(json, build, this.captcha)) {
return await doFetch();
}
const match = this.fetchURL.match(/https?:\/\/[^\/]*\/api\/v9/gm);
if (match) {
const tried = await handle2fa(json, match[0]);
if (tried) {
return await onSubmit(tried);
}
}
if (json.ticket) {
}
if (json.errors) {
if (this.errors(json)) {
return;
}
}
onSubmit(json);
}); });
};
doFetch();
} else { } else {
try { try {
await this.onSubmit(build, build); await this.onSubmit(build, build);

View file

@ -782,6 +782,9 @@ span.instanceStatus {
#verify { #verify {
color: var(--primary-text-soft); color: var(--primary-text-soft);
} }
.verify {
color: var(--primary-text-soft);
}
#TOS { #TOS {
vertical-align: middle; vertical-align: middle;
margin-bottom: 4px; margin-bottom: 4px;
@ -790,6 +793,15 @@ span.instanceStatus {
margin: 16px 0; margin: 16px 0;
text-align: center; text-align: center;
} }
.createAccount {
width: 94%;
padding: 8px;
margin-bottom: 16px;
font-size: 1.15rem;
font-weight: bold;
text-align: center;
box-sizing: border-box;
}
#logindiv button { #logindiv button {
width: 100%; width: 100%;
padding: 8px; padding: 8px;
@ -2216,9 +2228,9 @@ img.bigembedimg {
} }
.nonimagecenter, .nonimagecenter,
.accountSwitcher { .accountSwitcher {
max-height: 80svh; max-height: 95svh;
width: 500px; width: 500px;
padding: 12px; padding: 6px;
margin: 0; margin: 0;
background: var(--secondary-bg); background: var(--secondary-bg);
border: none; border: none;
@ -2227,7 +2239,6 @@ img.bigembedimg {
0 0 24px var(--shadow), 0 0 24px var(--shadow),
0 0 1.5px var(--primary-text); 0 0 1.5px var(--primary-text);
box-sizing: border-box; box-sizing: border-box;
gap: 8px;
overflow-y: auto; overflow-y: auto;
} }
.nonimagecenter & .flexttb, .nonimagecenter & .flexttb,
@ -2484,6 +2495,11 @@ fieldset input[type="radio"] {
max-width: 4in; max-width: 4in;
height: 2in; height: 2in;
} }
.vsmaller {
.optionElement {
margin-top: 8px;
}
}
.optionElement, .optionElement,
.FormSettings > button { .FormSettings > button {
margin: 16px 16px 0 16px; margin: 16px 16px 0 16px;
@ -2501,6 +2517,7 @@ fieldset input[type="radio"] {
} }
.optionElement:has(.optionElement) { .optionElement:has(.optionElement) {
margin: 0; margin: 0;
position: relative;
} }
.optionElement:has(.Buttons) { .optionElement:has(.Buttons) {
height: 100%; height: 100%;
@ -3056,3 +3073,7 @@ fieldset input[type="radio"] {
.stickerMArea { .stickerMArea {
padding-left: 48px; padding-left: 48px;
} }
.solidBackground {
background: var(--secondary-bg);
opacity: 1;
}

View file

@ -1,5 +1,24 @@
import {I18n} from "../i18n.js"; import {I18n} from "../i18n.js";
import {Dialog} from "../settings.js"; import {Dialog} from "../settings.js";
let instances:
| {
name: string;
description?: string;
descriptionLong?: string;
image?: string;
url?: string;
display?: boolean;
online?: boolean;
uptime: {alltime: number; daytime: number; weektime: number};
urls: {
wellknown: string;
api: string;
cdn: string;
gateway: string;
login?: string;
};
}[]
| null = null;
setTheme(); setTheme();
export function setTheme() { export function setTheme() {
let name = localStorage.getItem("theme"); let name = localStorage.getItem("theme");
@ -172,6 +191,51 @@ export class Specialuser {
localStorage.setItem("userinfos", JSON.stringify(info)); localStorage.setItem("userinfos", JSON.stringify(info));
} }
} }
//this currently does not work, and need to be implemented better at some time.
if (!localStorage.getItem("SWMode")) {
localStorage.setItem("SWMode", "SWOn");
}
export function trimswitcher() {
const json = getBulkInfo();
const map = new Map();
for (const thing in json.users) {
const user = json.users[thing];
let wellknown = user.serverurls.wellknown;
if (wellknown.at(-1) !== "/") {
wellknown += "/";
}
wellknown = (user.id || user.email) + "@" + wellknown;
if (map.has(wellknown)) {
const otheruser = map.get(wellknown);
if (otheruser[1].serverurls.wellknown.at(-1) === "/") {
delete json.users[otheruser[0]];
map.set(wellknown, [thing, user]);
} else {
delete json.users[thing];
}
} else {
map.set(wellknown, [thing, user]);
}
}
for (const thing in json.users) {
if (thing.at(-1) === "/") {
const user = json.users[thing];
delete json.users[thing];
json.users[thing.slice(0, -1)] = user;
}
}
localStorage.setItem("userinfos", JSON.stringify(json));
console.log(json);
}
export function adduser(user: typeof Specialuser.prototype.json) {
user = new Specialuser(user);
const info = getBulkInfo();
info.users[user.uid] = user;
info.currentuser = user.uid;
sessionStorage.setItem("currentuser", user.uid);
localStorage.setItem("userinfos", JSON.stringify(info));
return user;
}
class Directory { class Directory {
static home = this.createHome(); static home = this.createHome();
handle: FileSystemDirectoryHandle; handle: FileSystemDirectoryHandle;
@ -257,28 +321,10 @@ export {Directory};
const mobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent); const mobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
const iOS = /iPhone|iPad|iPod/i.test(navigator.userAgent); const iOS = /iPhone|iPad|iPod/i.test(navigator.userAgent);
export {mobile, iOS}; export {mobile, iOS};
let instances:
| {
name: string;
description?: string;
descriptionLong?: string;
image?: string;
url?: string;
display?: boolean;
online?: boolean;
uptime: {alltime: number; daytime: number; weektime: number};
urls: {
wellknown: string;
api: string;
cdn: string;
gateway: string;
login?: string;
};
}[]
| null;
const datalist = document.getElementById("instances"); const datalist = document.getElementById("instances");
console.warn(datalist); console.warn(datalist);
const instancefetch = fetch("/instances.json") export const instancefetch = fetch("/instances.json")
.then((res) => res.json()) .then((res) => res.json())
.then( .then(
async ( async (
@ -302,48 +348,6 @@ const instancefetch = fetch("/instances.json")
) => { ) => {
await I18n.done; await I18n.done;
instances = json; instances = json;
if (datalist) {
console.warn(json);
const instancein = document.getElementById("instancein") as HTMLInputElement;
if (
instancein &&
instancein.value === "" &&
!new URLSearchParams(window.location.search).get("instance")
) {
instancein.value = json[0].name;
}
for (const instance of json) {
if (instance.display === false) {
continue;
}
const option = document.createElement("option");
option.disabled = !instance.online;
option.value = instance.name;
if (instance.url) {
stringURLMap.set(option.value, instance.url);
if (instance.urls) {
stringURLsMap.set(instance.url, instance.urls);
}
} else if (instance.urls) {
stringURLsMap.set(option.value, instance.urls);
} else {
option.disabled = true;
}
if (instance.description) {
option.label = instance.description;
} else {
option.label = instance.name;
}
datalist.append(option);
}
if (
json.length !== 0 &&
!localStorage.getItem("instanceinfo") &&
!new URLSearchParams(window.location.search).get("instance")
) {
checkInstance(json[0].name);
}
}
}, },
); );
const stringURLMap = new Map<string, string>(); const stringURLMap = new Map<string, string>();
@ -646,6 +650,14 @@ export function createImg(
}, },
}); });
} }
export interface instanceinfo {
wellknown: string;
api: string;
cdn: string;
gateway: string;
login: string;
value: string;
}
/** /**
* *
* This function takes in a string and checks if the string is a valid instance * This function takes in a string and checks if the string is a valid instance
@ -653,24 +665,19 @@ export function createImg(
* the alt property is something you may fire on success. * the alt property is something you may fire on success.
*/ */
const checkInstance = Object.assign( const checkInstance = Object.assign(
async function (instance: string) { async function (
await instancefetch; instance: string,
const verify = document.getElementById("verify"); verify = document.getElementById("verify"),
const loginButton = (document.getElementById("loginButton") || loginButton = (document.getElementById("loginButton") ||
document.getElementById("createAccount") || document.getElementById("createAccount") ||
document.createElement("button")) as HTMLButtonElement; document.createElement("button")) as HTMLButtonElement,
) {
await instancefetch;
try { try {
loginButton.disabled = true; loginButton.disabled = true;
verify!.textContent = I18n.getTranslation("login.checking"); verify!.textContent = I18n.getTranslation("login.checking");
const instanceValue = instance; const instanceValue = instance;
const instanceinfo = (await getapiurls(instanceValue)) as { const instanceinfo = (await getapiurls(instanceValue)) as instanceinfo;
wellknown: string;
api: string;
cdn: string;
gateway: string;
login: string;
value: string;
};
if (instanceinfo) { if (instanceinfo) {
instanceinfo.value = instanceValue; instanceinfo.value = instanceValue;
localStorage.setItem("instanceinfo", JSON.stringify(instanceinfo)); localStorage.setItem("instanceinfo", JSON.stringify(instanceinfo));
@ -683,14 +690,17 @@ const checkInstance = Object.assign(
console.log(verify!.textContent); console.log(verify!.textContent);
verify!.textContent = ""; verify!.textContent = "";
}, 3000); }, 3000);
return instanceinfo;
} else { } else {
verify!.textContent = I18n.getTranslation("login.invalid"); verify!.textContent = I18n.getTranslation("login.invalid");
loginButton.disabled = true; loginButton.disabled = true;
return;
} }
} catch { } catch {
console.log("catch"); console.log("catch");
verify!.textContent = I18n.getTranslation("login.invalid"); verify!.textContent = I18n.getTranslation("login.invalid");
loginButton.disabled = true; loginButton.disabled = true;
return;
} }
}, },
{} as { {} as {
@ -705,9 +715,7 @@ const checkInstance = Object.assign(
}, },
); );
export {checkInstance}; export {checkInstance};
export function getInstances() {
return instances;
}
export class SW { export class SW {
static worker: undefined | ServiceWorker; static worker: undefined | ServiceWorker;
static setMode(mode: "false" | "offlineOnly" | "true") { static setMode(mode: "false" | "offlineOnly" | "true") {
@ -727,7 +735,14 @@ export class SW {
} }
} }
} }
let installPrompt: Event | undefined = undefined;
window.addEventListener("beforeinstallprompt", (event) => {
event.preventDefault();
installPrompt = event;
});
export function installPGet() {
return installPrompt;
}
if ("serviceWorker" in navigator) { if ("serviceWorker" in navigator) {
navigator.serviceWorker navigator.serviceWorker
.register("/service.js", { .register("/service.js", {
@ -755,3 +770,9 @@ if ("serviceWorker" in navigator) {
} }
}); });
} }
export function getInstances() {
return instances;
}
export function getStringURLMapPair() {
return [stringURLMap, stringURLsMap] as const;
}

View file

@ -232,15 +232,20 @@
"box3title": "Contribute to Jank Client", "box3title": "Contribute to Jank Client",
"box3description": "We always appreciate some help, whether that be in the form of bug reports, or code, or even just pointing out some typos." "box3description": "We always appreciate some help, whether that be in the form of bug reports, or code, or even just pointing out some typos."
}, },
"form": {
"captcha": "Wait, are you a human?"
},
"useTemplate": "Use $1 as a template", "useTemplate": "Use $1 as a template",
"useTemplateButton": "Use Template", "useTemplateButton": "Use Template",
"register": { "register": {
"register": "Register",
"passwordError:": "Password: $1", "passwordError:": "Password: $1",
"usernameError": "Username: $1", "usernameError": "Username: $1",
"emailError": "Email: $1", "emailError": "Email: $1",
"DOBError": "Date of Birth: $1", "DOBError": "Date of Birth: $1",
"agreeTOS": "I agree to the [Terms of Service]($1):", "agreeTOS": "I agree to the [Terms of Service]($1):",
"noTOS": "This instance has no Terms of Service, accept ToS anyways:" "noTOS": "This instance has no Terms of Service, accept ToS anyways:",
"tos": "You must agree to the TOS"
}, },
"leaving": "You're leaving Spacebar", "leaving": "You're leaving Spacebar",
"goingToURL": "You're going to $1. Are you sure you want to go there?", "goingToURL": "You're going to $1. Are you sure you want to go there?",
@ -330,6 +335,9 @@
"save": "Save changes" "save": "Save changes"
}, },
"localuser": { "localuser": {
"install": "Install",
"installJank": "Install Jank Client",
"installDesc": "Installing Jank Client will allow you to open it in its own window and act like its own app! You can also just continue to use Jank Client in the web browser like you have been and it'll work the same.",
"addStatus": "Add status", "addStatus": "Add status",
"status": "Status", "status": "Status",
"statusWarn": "Spacebar has bugs with this feature and will only update after a refresh, and will often not respect it", "statusWarn": "Spacebar has bugs with this feature and will only update after a refresh, and will often not respect it",
@ -538,7 +546,8 @@
"pasteInfo": "Paste the recovery URL here:", "pasteInfo": "Paste the recovery URL here:",
"newPassword": "New password:", "newPassword": "New password:",
"enterPAgain": "Enter new password again:", "enterPAgain": "Enter new password again:",
"recovery": "Forgotten Password" "recovery": "Forgotten Password",
"login": "Login"
}, },
"member": { "member": {
"kick": "Kick $1 from $2", "kick": "Kick $1 from $2",