1
0
mirror of https://github.com/hrfee/jfa-go.git synced 2024-12-26 19:10:10 +00:00

Compare commits

...

3 Commits

Author SHA1 Message Date
6347495b5b
auth: use unicode b64 encoding on browser
brought over unicodeB64Encode/Decode from my other filaments project.
Fixes #364.
2024-08-28 14:29:36 +01:00
02f4ba6e8e
ts: use pages modules in admin (kinda), change pseudo-links
pseudo-links are now just links, because i'm lazy and it's easier than
fixing an issue. They now take the form `/?invite=code` and
`/accounts/?user=userid`.

ts/modules.tabs.ts is now a wrapper for ts/modules/pages.ts.

Also, fixed no section appearing when visiting the settings tab.
2024-08-28 14:18:52 +01:00
d2e5209832
ts: move "page" stuff to module
not the happiest with it, but it works alright. PageManager is
instantiated, you pass is Page{} objects, which have a (code)name, page
title, and url, and a show, hide, and shouldSkip function, each
returning a bool. The first two are self explanatory, the last tells you
if the page is disabled for some reason (like on setup some are disabled
    if messages are). You can then call load(<(code)name>), or
        prev/next(<name>).
2024-08-27 18:55:28 +01:00
14 changed files with 565 additions and 241 deletions

239
' Normal file
View File

@ -0,0 +1,239 @@
.PHONY: configuration email typescript swagger copy compile compress inline-css variants-html install clean npm config-description config-default precompile
all: compile
GOESBUILD ?= off
ifeq ($(GOESBUILD), on)
ESBUILD := esbuild
else
ESBUILD := npx esbuild
endif
GOBINARY ?= go
CSSVERSION ?= v3
CSS_BUNDLE = $(DATA)/web/css/$(CSSVERSION)bundle.css
VERSION ?= $(shell git describe --exact-match HEAD 2> /dev/null || echo vgit)
VERSION := $(shell echo $(VERSION) | sed 's/v//g')
COMMIT ?= $(shell git rev-parse --short HEAD || echo unknown)
BUILDTIME ?= $(shell date +%s)
UPDATER ?= off
LDFLAGS := -X main.version=$(VERSION) -X main.commit=$(COMMIT) -X main.cssVersion=$(CSSVERSION) -X main.buildTimeUnix=$(BUILDTIME) $(if $(BUILTBY),-X 'main.builtBy=$(BUILTBY)',)
ifeq ($(UPDATER), on)
LDFLAGS := $(LDFLAGS) -X main.updater=binary
else ifneq ($(UPDATER), off)
LDFLAGS := $(LDFLAGS) -X main.updater=$(UPDATER)
endif
INTERNAL ?= on
TRAY ?= off
E2EE ?= on
TAGS := -tags "
ifeq ($(INTERNAL), on)
DATA := data
else
DATA := build/data
TAGS := $(TAGS) external
endif
ifeq ($(TRAY), on)
TAGS := $(TAGS) tray
endif
ifeq ($(E2EE), on)
TAGS := $(TAGS) e2ee
endif
TAGS := $(TAGS)"
OS := $(shell go env GOOS)
ifeq ($(TRAY)$(OS), onwindows)
LDFLAGS := $(LDFLAGS) -H=windowsgui
endif
DEBUG ?= off
ifeq ($(DEBUG), on)
SOURCEMAP := --sourcemap
MINIFY :=
TYPECHECK := npx tsc -noEmit --project ts/tsconfig.json
# jank
COPYTS := rm -r $(DATA)/web/js/ts; cp -r tempts $(DATA)/web/js/ts
UNCSS := cp $(CSS_BUNDLE) $(DATA)/bundle.css
# TAILWIND := --content ""
else
LDFLAGS := -s -w $(LDFLAGS)
SOURCEMAP :=
MINIFY := --minify
COPYTS :=
TYPECHECK :=
UNCSS := npx tailwindcss -i $(CSS_BUNDLE) -o $(DATA)/bundle.css --content "html/crash.html"
# UNCSS := npx uncss $(DATA)/crash.html --csspath web/css --output $(DATA)/bundle.css
TAILWIND :=
endif
RACE ?= off
ifeq ($(RACE), on)
RACEDETECTOR := -race
else
RACEDETECTOR :=
endif
ifeq (, $(shell which esbuild))
ESBUILDINSTALL := go install github.com/evanw/esbuild/cmd/esbuild@latest
else
ESBUILDINSTALL :=
endif
ifeq ($(GOESBUILD), on)
NPMIGNOREOPTIONAL := --no-optional
NPMOPTS := $(NPMIGNOREOPTIONAL); $(ESBUILDINSTALL)
else
NPMOPTS :=
endif
ifeq (, $(shell which swag))
SWAGINSTALL := $(GOBINARY) install github.com/swaggo/swag/cmd/swag@latest
else
SWAGINSTALL :=
endif
CONFIG_BASE = config/config-base.yaml
# CONFIG_DESCRIPTION = $(DATA)/config-base.json
CONFIG_DEFAULT = $(DATA)/config-default.ini
# $(CONFIG_DESCRIPTION) &: $(CONFIG_BASE)
# $(info Fixing config-base)
# -mkdir -p $(DATA)
$(DATA):
mkdir -p $(DATA)
$(CONFIG_DEFAULT): $(DATA) $(CONFIG_BASE)
$(info Generating config-default.ini)
go run scripts/ini/main.go -in $(CONFIG_BASE) -out $(DATA)/config-default.ini
configuration: $(CONFIG_DEFAULT)
EMAIL_SRC = $(wildcard mail/*)
EMAIL_TARGET = mail/confirmation.html
$(EMAIL_TARGET): $(EMAIL_SRC)
$(info Generating email html)
npx mjml mail/*.mjml -o $(DATA)/
$(info Copying plaintext mail)
cp mail/*.txt $(DATA)/
TYPESCRIPT_FULLSRC = $(shell find ts/ -type f -name "*.ts")
TYPESCRIPT_SRC = $(wildcard ts/*.ts)
TYPESCRIPT_TEMPSRC = $(TYPESCRIPT_SRC:ts/%=tempts/%)
# TYPESCRIPT_TARGET = $(patsubst %.ts,%.js,$(subst tempts/,./$(DATA)/web/js/,$(TYPESCRIPT_TEMPSRC)))
TYPESCRIPT_TARGET = $(DATA)/web/js/admin.js
$(TYPESCRIPT_TARGET): $(TYPESCRIPT_FULLSRC) ts/tsconfig.json
$(TYPECHECK)
rm -rf tempts
cp -r ts tempts
$(adding dark variants to typescript)
scripts/dark-variant.sh tempts
scripts/dark-variant.sh tempts/modules
$(info compiling typescript)
mkdir -p $(DATA)/web/js
$(foreach tempsrc,$(TYPESCRIPT_TEMPSRC),$(ESBUILD) --target=es6 --bundle $(tempsrc) $(SOURCEMAP) --outfile=$(patsubst %.ts,%.js,$(subst tempts/,./$(DATA)/web/js/,$(tempsrc))) $(MINIFY);)
mv $(DATA)/web/js/crash.js $(DATA)/
$(COPYTS)
SWAGGER_SRC = $(wildcard api*.go) $(wildcard *auth.go) views.go
SWAGGER_TARGET = docs/docs.go
$(SWAGGER_TARGET): $(SWAGGER_SRC)
$(SWAGINSTALL)
swag init -g main.go
VARIANTS_SRC = $(wildcard html/*.html)
VARIANTS_TARGET = $(DATA)/html/admin.html
$(VARIANTS_TARGET): $(VARIANTS_SRC)
$(info copying html)
cp -r html $(DATA)/
$(info adding dark variants to html)
node scripts/missing-colors.js html $(DATA)/html
ICON_SRC = node_modules/remixicon/fonts/remixicon.css node_modules/remixicon/fonts/remixicon.woff2
ICON_TARGET = $(ICON_SRC:node_modules/remixicon/fonts/%=$(DATA)/web/css/%)
CSS_SRC = $(wildcard css/*.css)
CSS_TARGET = $(DATA)/web/css/part-bundle.css
CSS_FULLTARGET = $(CSS_BUNDLE)
ALL_CSS_SRC = $(ICON_SRC) $(CSS_SRC)
ALL_CSS_TARGET = $(ICON_TARGET)
$(CSS_FULLTARGET): $(TYPESCRIPT_TARGET) $(VARIANTS_TARGET) $(ALL_CSS_SRC) $(wildcard html/*.html)
mkdir -p $(DATA)/web/css
$(info copying fonts)
cp -r node_modules/remixicon/fonts/remixicon.css node_modules/remixicon/fonts/remixicon.woff2 $(DATA)/web/css/
$(info bundling css)
$(ESBUILD) --bundle css/base.css --outfile=$(CSS_TARGET) --external:remixicon.css --external:../fonts/hanken* --minify
npx tailwindcss -i $(CSS_TARGET) -o $(CSS_FULLTARGET) $(TAILWIND)
rm $(CSS_TARGET)
# mv $(CSS_BUNDLE) $(DATA)/web/css/$(CSSVERSION)bundle.css
# npx postcss -o $(CSS_TARGET) $(CSS_TARGET)
INLINE_SRC = html/crash.html
INLINE_TARGET = $(DATA)/crash.html
$(INLINE_TARGET): $(CSS_FULLTARGET) $(INLINE_SRC)
cp html/crash.html $(DATA)/crash.html
$(UNCSS) # generates $(DATA)/bundle.css for us
node scripts/inline.js root $(DATA) $(DATA)/crash.html $(DATA)/crash.html
rm $(DATA)/bundle.css
LANG_SRC = $(shell find ./lang)
LANG_TARGET = $(LANG_SRC:lang/%=$(DATA)/lang/%)
STATIC_SRC = $(wildcard static/*)
STATIC_TARGET = $(STATIC_SRC:static/%=$(DATA)/web/%)
COPY_SRC = images/banner.svg jfa-go.service LICENSE $(LANG_SRC) $(STATIC_SRC)
COPY_TARGET = $(DATA)/jfa-go.service
# $(DATA)/LICENSE $(LANG_TARGET) $(STATIC_TARGET) $(DATA)/web/css/$(CSSVERSION)bundle.css
$(COPY_TARGET): $(INLINE_TARGET) $(STATIC_SRC) $(LANG_SRC)
$(info copying $(CONFIG_BASE))
cp $(CONFIG_BASE) $(DATA)/
$(info copying crash page)
cp $(DATA)/crash.html $(DATA)/html/
$(info copying static data)
mkdir -p $(DATA)/web
cp images/banner.svg static/banner.svg
cp -r static/* $(DATA)/web/
$(info copying systemd service)
cp jfa-go.service $(DATA)/
$(info copying language files)
cp -r lang $(DATA)/
cp LICENSE $(DATA)/
precompile: $(CONFIG_DEFAULT) $(EMAIL_TARGET) $(COPY_TARGET) $(SWAGGER_TARGET)
GO_SRC = $(shell find ./ -name "*.go")
GO_TARGET = build/jfa-go
$(GO_TARGET): $(CONFIG_DEFAULT) $(EMAIL_TARGET) $(COPY_TARGET) $(SWAGGER_TARGET) $(GO_SRC) go.mod go.sum
$(info Downloading deps)
$(GOBINARY) mod download
$(info Building)
mkdir -p build
$(GOBINARY) build $(RACEDETECTOR) -ldflags="$(LDFLAGS)" $(TAGS) -o $(GO_TARGET)
compile: $(GO_TARGET)
compress:
upx --lzma build/jfa-go
install:
cp -r build $(DESTDIR)/jfa-go
clean:
-rm -r $(DATA)
-rm -r build
-rm mail/*.html
-rm docs/docs.go docs/swagger.json docs/swagger.yaml
go clean
npm:
$(info installing npm dependencies)
npm install $(NPMOPTS)

View File

@ -110,22 +110,18 @@ CONFIG_DEFAULT = $(DATA)/config-default.ini
# -mkdir -p $(DATA)
$(DATA):
mkdir -p $(DATA)
mkdir -p $(DATA)/web/js
mkdir -p $(DATA)/web/css
$(CONFIG_DEFAULT): $(DATA) $(CONFIG_BASE)
$(CONFIG_DEFAULT): $(CONFIG_BASE)
$(info Generating config-default.ini)
go run scripts/ini/main.go -in $(CONFIG_BASE) -out $(DATA)/config-default.ini
configuration: $(CONFIG_DEFAULT)
EMAIL_SRC_MJML = $(wildcard mail/*.mjml)
EMAIL_SRC_TXT = $(wildcard mail/*.txt)
EMAIL_DATA_MJML = $(EMAIL_SRC_MJML:mail/%=data/%)
EMAIL_HTML = $(EMAIL_DATA_MJML:.mjml=.html)
EMAIL_TXT = $(EMAIL_SRC_TXT:mail/%=data/%)
EMAIL_ALL = $(EMAIL_HTML) $(EMAIL_TXT)
EMAIL_TARGET = mail/confirmation.html
$(EMAIL_TARGET): $(EMAIL_SRC_MJML) $(EMAIL_SRC_TXT)
EMAIL_SRC = $(wildcard mail/*)
EMAIL_TARGET = $(DATA)/confirmation.html
$(EMAIL_TARGET): $(EMAIL_SRC)
$(info Generating email html)
npx mjml mail/*.mjml -o $(DATA)/
$(info Copying plaintext mail)
@ -144,7 +140,6 @@ $(TYPESCRIPT_TARGET): $(TYPESCRIPT_FULLSRC) ts/tsconfig.json
scripts/dark-variant.sh tempts
scripts/dark-variant.sh tempts/modules
$(info compiling typescript)
mkdir -p $(DATA)/web/js
$(foreach tempsrc,$(TYPESCRIPT_TEMPSRC),$(ESBUILD) --target=es6 --bundle $(tempsrc) $(SOURCEMAP) --outfile=$(patsubst %.ts,%.js,$(subst tempts/,./$(DATA)/web/js/,$(tempsrc))) $(MINIFY);)
mv $(DATA)/web/js/crash.js $(DATA)/
$(COPYTS)
@ -172,7 +167,6 @@ ALL_CSS_SRC = $(ICON_SRC) $(CSS_SRC)
ALL_CSS_TARGET = $(ICON_TARGET)
$(CSS_FULLTARGET): $(TYPESCRIPT_TARGET) $(VARIANTS_TARGET) $(ALL_CSS_SRC) $(wildcard html/*.html)
mkdir -p $(DATA)/web/css
$(info copying fonts)
cp -r node_modules/remixicon/fonts/remixicon.css node_modules/remixicon/fonts/remixicon.woff2 $(DATA)/web/css/
$(info bundling css)
@ -204,7 +198,6 @@ $(COPY_TARGET): $(INLINE_TARGET) $(STATIC_SRC) $(LANG_SRC)
$(info copying crash page)
cp $(DATA)/crash.html $(DATA)/html/
$(info copying static data)
mkdir -p $(DATA)/web
cp images/banner.svg static/banner.svg
cp -r static/* $(DATA)/web/
$(info copying systemd service)
@ -213,11 +206,11 @@ $(COPY_TARGET): $(INLINE_TARGET) $(STATIC_SRC) $(LANG_SRC)
cp -r lang $(DATA)/
cp LICENSE $(DATA)/
precompile: $(CONFIG_DEFAULT) $(EMAIL_TARGET) $(COPY_TARGET) $(SWAGGER_TARGET)
precompile: $(DATA) $(CONFIG_DEFAULT) $(EMAIL_TARGET) $(COPY_TARGET) $(SWAGGER_TARGET)
GO_SRC = $(shell find ./ -name "*.go")
GO_TARGET = build/jfa-go
$(GO_TARGET): $(CONFIG_DEFAULT) $(EMAIL_TARGET) $(COPY_TARGET) $(SWAGGER_TARGET) $(GO_SRC) go.mod go.sum
$(GO_TARGET): $(DATA) $(CONFIG_DEFAULT) $(EMAIL_TARGET) $(COPY_TARGET) $(SWAGGER_TARGET) $(GO_SRC) go.mod go.sum
$(info Downloading deps)
$(GOBINARY) mod download
$(info Building)

View File

@ -1,7 +1,7 @@
import { ThemeManager } from "./modules/theme.js";
import { lang, LangFile, loadLangSelector } from "./modules/lang.js";
import { Modal } from "./modules/modal.js";
import { Tabs } from "./modules/tabs.js";
import { Tabs, Tab } from "./modules/tabs.js";
import { inviteList, createInvite } from "./modules/invites.js";
import { accountsList } from "./modules/accounts.js";
import { settingsList } from "./modules/settings.js";
@ -120,21 +120,41 @@ window.notifications = new notificationBox(document.getElementById('notification
userSelect.classList.toggle('unfocused');
}*/
// Determine if url references an invite or account
let isInviteURL = window.invites.isInviteURL();
let isAccountURL = accounts.isAccountURL();
// load tabs
const tabs: { url: string, reloader: () => void }[] = [
const tabs: { id: string, url: string, reloader: () => void }[] = [
{
url: "invites",
reloader: window.invites.reload
id: "invites",
url: "",
reloader: () => window.invites.reload(() => {
if (isInviteURL) {
window.invites.loadInviteURL();
// Don't keep loading the same item on every tab refresh
isInviteURL = false;
}
}),
},
{
id: "accounts",
url: "accounts",
reloader: accounts.reload
reloader: () => accounts.reload(() => {
if (isAccountURL) {
accounts.loadAccountURL();
// Don't keep loading the same item on every tab refresh
isAccountURL = false;
}
}),
},
{
id: "activity",
url: "activity",
reloader: activity.reload
},
{
id: "settings",
url: "settings",
reloader: settings.reload
}
@ -145,41 +165,20 @@ const defaultTab = tabs[0];
window.tabs = new Tabs();
for (let tab of tabs) {
window.tabs.addTab(tab.url, null, tab.reloader);
if (window.location.pathname == window.URLBase + "/" + tab.url) {
window.tabs.addTab(tab.id, tab.url, null, tab.reloader);
}
let matchedTab = false
for (let tab of tabs) {
if (window.location.pathname.startsWith(window.URLBase + "/" + tab.url)) {
window.tabs.switch(tab.url, true);
matchedTab = true;
}
}
let isInviteURL = window.invites.isInviteURL();
let isAccountURL = accounts.isAccountURL();
// Default tab
if ((window.URLBase + "/").includes(window.location.pathname)) {
window.tabs.switch(defaultTab.url, true);
}
document.addEventListener("tab-change", (event: CustomEvent) => {
const urlParams = new URLSearchParams(window.location.search);
const lang = urlParams.get('lang');
let tab = window.URLBase + "/" + event.detail;
if (event.detail == "") {
tab = window.location.pathname;
} else if (tab == window.URLBase + "/invites") {
if (window.location.pathname == window.URLBase + "/") {
tab = window.URLBase + "/";
} else if (window.URLBase) { tab = window.URLBase; }
else { tab = "../"; }
}
if (lang) {
tab += "?lang=" + lang
}
window.history.pushState(event.detail, "Admin - jfa-go", tab);
});
window.onpopstate = (event: PopStateEvent) => {
console.log(event.state);
window.tabs.switch(event.state);
// if ((window.URLBase + "/").includes(window.location.pathname)) {
if (!matchedTab) {
window.tabs.switch("", true);
}
const login = new Login(window.modals.login as Modal, "/", window.loginAppearance);
@ -189,35 +188,8 @@ login.onLogin = () => {
// FIXME: Decide whether to autoload activity or not
reloadProfileNames();
setInterval(() => { window.invites.reload(); accounts.reload(); }, 30*1000);
const currentTab = window.tabs.current;
switch (currentTab) {
case "invites":
window.invites.reload();
break;
case "accounts":
accounts.reload();
break;
case "settings":
settings.reload();
break;
case "activity": // FIXME: fix URL clash with route
activity.reload();
break;
default:
console.log(isAccountURL, isInviteURL);
if (isInviteURL) {
window.invites.reload(() => {
window.invites.loadInviteURL();
window.tabs.switch("invites", false, true);
});
} else if (isAccountURL) {
accounts.reload(() => {
accounts.loadAccountURL();
window.tabs.switch("accounts", false, true);
});
}
break;
}
// Triggers pre and post funcs, even though we're already on that page
window.tabs.switch(window.tabs.current);
}
bindManualDropdowns();

View File

@ -1804,10 +1804,16 @@ export class accountsList {
this.focusAccount(event.detail);
});
isAccountURL = () => { return window.location.pathname.startsWith(window.URLBase + "/accounts/user/"); }
// FIXME: Use Query Param! so it doesn't get cleared by pages.ts.
isAccountURL = () => {
const urlParams = new URLSearchParams(window.location.search);
const userID = urlParams.get("user");
return Boolean(userID);
}
loadAccountURL = () => {
let userID = window.location.pathname.split(window.URLBase + "/accounts/user/")[1].split("?lang")[0];
const urlParams = new URLSearchParams(window.location.search);
const userID = urlParams.get("user");
this.focusAccount(userID);
}

View File

@ -64,17 +64,17 @@ export class Activity implements activity, SearchableItem {
}
_genUserLink = (): string => {
return `<span role="link" tabindex="0" class="hover:underline cursor-pointer activity-pseudo-link-user" data-id="${this._act.user_id}" data-href="${this._urlBase}accounts/user/${this._act.user_id}">${this._genUserText()}</span>`;
return `<a role="link" tabindex="0" class="hover:underline cursor-pointer activity-pseudo-link-user" data-id="${this._act.user_id}" href="${this._urlBase}accounts?user=${this._act.user_id}">${this._genUserText()}</a>`;
}
_genSrcUserLink = (): string => {
return `<span role="link" tabindex="0" class="hover:underline cursor-pointer activity-pseudo-link-user" data-id="${this._act.user_id}" data-href="${this._urlBase}accounts/user/${this._act.source}">${this._genSrcUserText()}</span>`;
return `<a role="link" tabindex="0" class="hover:underline cursor-pointer activity-pseudo-link-user" data-id="${this._act.user_id}" href="${this._urlBase}accounts?user=${this._act.source}">${this._genSrcUserText()}</a>`;
}
private _renderInvText = (): string => { return `<span class="font-medium font-mono">${this.value || this.invite_code || "???"}</span>`; }
private _genInvLink = (): string => {
return `<span role="link" tabindex="0" class="hover:underline cursor-pointer activity-pseudo-link-invite" data-id="${this.invite_code}" data-href="${this._urlBase}invites/${this.invite_code}">${this._renderInvText()}</span>`;
return `<a role="link" tabindex="0" class="hover:underline cursor-pointer activity-pseudo-link-invite" data-id="${this.invite_code}" href="${this._urlBase}?invite=${this.invite_code}">${this._renderInvText()}</a>`;
}
@ -307,17 +307,17 @@ export class Activity implements activity, SearchableItem {
const pseudoInvites = this._card.getElementsByClassName("activity-pseudo-link-invite") as HTMLCollectionOf<HTMLAnchorElement>;
for (let i = 0; i < pseudoUsers.length; i++) {
const navigate = (event: Event) => {
/*const navigate = (event: Event) => {
event.preventDefault()
window.tabs.switch("accounts");
document.dispatchEvent(accountURLEvent(pseudoUsers[i].getAttribute("data-id")));
window.history.pushState(null, document.title, pseudoUsers[i].getAttribute("data-href"));
}
pseudoUsers[i].onclick = navigate;
pseudoUsers[i].onkeydown = navigate;
pseudoUsers[i].onkeydown = navigate;*/
}
for (let i = 0; i < pseudoInvites.length; i++) {
const navigate = (event: Event) => {
/*const navigate = (event: Event) => {
event.preventDefault();
window.invites.reload(() => {
window.tabs.switch("invites");
@ -326,7 +326,7 @@ export class Activity implements activity, SearchableItem {
});
}
pseudoInvites[i].onclick = navigate;
pseudoInvites[i].onkeydown = navigate;
pseudoInvites[i].onkeydown = navigate;*/
}
}

View File

@ -291,3 +291,16 @@ export function bindManualDropdowns() {
};
}
}
export function unicodeB64Decode(s: string): string {
const decoded = atob(s);
const byteArray = Uint8Array.from(decoded, (m) => m.codePointAt(0));
const toUnicode = new TextDecoder().decode(byteArray);
return toUnicode;
}
export function unicodeB64Encode(s: string): string {
const encoded = new TextEncoder().encode(s);
const bin = String.fromCodePoint(...encoded);
return btoa(bin);
}

View File

@ -448,10 +448,15 @@ export class inviteList implements inviteList {
this.focusInvite(event.detail);
})
isInviteURL = () => { return window.location.pathname.startsWith(window.URLBase + "/invites/"); }
isInviteURL = () => {
const urlParams = new URLSearchParams(window.location.search);
const inviteCode = urlParams.get("invite");
return Boolean(inviteCode);
}
loadInviteURL = () => {
let inviteCode = window.location.pathname.split(window.URLBase + "/invites/")[1].split("?lang")[0];
const urlParams = new URLSearchParams(window.location.search);
const inviteCode = urlParams.get("invite");
this.focusInvite(inviteCode, window.lang.notif("errorInviteNotFound"));
}

View File

@ -1,5 +1,5 @@
import { Modal } from "../modules/modal.js";
import { toggleLoader, _post } from "../modules/common.js";
import { toggleLoader, _post, unicodeB64Encode } from "../modules/common.js";
export class Login {
loggedIn: boolean = false;
@ -68,7 +68,7 @@ export class Login {
const refresh = (username == "" && password == "");
req.open("GET", this._url + (refresh ? "token/refresh" : "token/login"), true);
if (!refresh) {
req.setRequestHeader("Authorization", "Basic " + btoa(username + ":" + password));
req.setRequestHeader("Authorization", "Basic " + unicodeB64Encode(username + ":" + password));
}
req.onreadystatechange = ((req: XMLHttpRequest, _: Event): any => {
if (req.readyState == 4) {

100
ts/modules/pages.ts Normal file
View File

@ -0,0 +1,100 @@
export interface Page {
name: string;
title: string;
url: string;
show: () => boolean;
hide: () => boolean;
shouldSkip: () => boolean;
index?: number;
};
export interface PageConfig {
hideOthersOnPageShow: boolean;
defaultName: string;
defaultTitle: string;
}
export class PageManager {
pages: Map<string, Page>;
pageList: string[];
hideOthers: boolean;
defaultName: string = "";
defaultTitle: string = "";
private _overridePushState = () => {
const pushState = window.history.pushState;
window.history.pushState = function (data: any, __: string, _: string | URL) {
pushState.apply(window.history, arguments);
let ev = { state: data as string } as PopStateEvent;
window.onpopstate(ev);
};
}
private _onpopstate = (event: PopStateEvent) => {
let name = event.state;
if (!this.pages.has(event.state)) {
name = this.pageList[0]
}
let success = this.pages.get(name).show();
if (!success) {
return;
}
if (!(this.hideOthers)) {
return;
}
for (let k of this.pageList) {
if (name != k) {
this.pages.get(k).hide();
}
}
}
constructor(c: PageConfig) {
this.pages = new Map<string, Page>;
this.pageList = [];
this.hideOthers = c.hideOthersOnPageShow;
this.defaultName = c.defaultName;
this.defaultTitle = c.defaultTitle;
this._overridePushState();
window.onpopstate = this._onpopstate;
}
setPage(p: Page) {
p.index = this.pageList.length;
this.pages.set(p.name, p);
this.pageList.push(p.name);
}
load(name: string = "") {
if (!this.pages.has(name)) return window.history.pushState(name || this.defaultName, this.defaultTitle, "")
const p = this.pages.get(name);
this.loadPage(p);
}
loadPage (p: Page) {
window.history.pushState(p.name || this.defaultName, p.title, p.url + window.location.search);
}
prev(name: string = "") {
if (!this.pages.has(name)) return console.error(`previous page ${name} not found`);
let p = this.pages.get(name);
let shouldSkip = true;
while (shouldSkip && p.index > 0) {
p = this.pages.get(this.pageList[p.index-1]);
shouldSkip = p.shouldSkip();
}
this.loadPage(p);
}
next(name: string = "") {
if (!this.pages.has(name)) return console.error(`previous page ${name} not found`);
let p = this.pages.get(name);
let shouldSkip = true;
while (shouldSkip && p.index < this.pageList.length) {
p = this.pages.get(this.pageList[p.index+1]);
shouldSkip = p.shouldSkip();
}
this.loadPage(p);
}
};

View File

@ -636,6 +636,7 @@ export class settingsList {
}
private _showPanel = (name: string) => {
console.log("showing", name);
for (let n in this._sections) {
if (n == name) {
this._sections[name].visible = true;
@ -919,7 +920,11 @@ export class settingsList {
for (let i = 0; i < this._loader.children.length; i++) {
this._loader.children[i].classList.remove("invisible");
}
this._showPanel(this._settings.sections[0].section);
for (let s of this._settings.sections) {
if (s.meta.disabled) continue;
this._showPanel(s.section);
break;
}
document.dispatchEvent(new CustomEvent("settings-loaded"));
document.dispatchEvent(new CustomEvent("settings-advancedState", { detail: false }));
this._saveButton.classList.add("unfocused");

View File

@ -1,44 +1,77 @@
import { PageManager, Page } from "../modules/pages.js";
export interface Tab {
page: Page;
tabEl: HTMLDivElement;
buttonEl: HTMLSpanElement;
preFunc?: () => void;
postFunc?: () => void;
}
export class Tabs implements Tabs {
private _current: string = "";
tabs: Array<Tab>;
private _baseOffset = -1;
tabs: Map<string, Tab>;
pages: PageManager;
constructor() {
this.tabs = [];
this.tabs = new Map<string, Tab>;
this.pages = new PageManager({
hideOthersOnPageShow: true,
defaultName: "invites",
defaultTitle: document.title,
});
}
addTab = (tabID: string, preFunc = () => void {}, postFunc = () => void {}) => {
let tab = {} as Tab;
tab.tabID = tabID;
tab.tabEl = document.getElementById("tab-" + tabID) as HTMLDivElement;
tab.buttonEl = document.getElementById("button-tab-" + tabID) as HTMLSpanElement;
addTab = (tabID: string, url: string, preFunc = () => void {}, postFunc = () => void {},) => {
let tab: Tab = {
page: null,
tabEl: document.getElementById("tab-" + tabID) as HTMLDivElement,
buttonEl: document.getElementById("button-tab-" + tabID) as HTMLSpanElement,
preFunc: preFunc,
postFunc: postFunc,
};
if (this._baseOffset == -1) {
this._baseOffset = tab.buttonEl.offsetLeft;
}
tab.page = {
name: tabID,
title: document.title, /*FIXME: Get actual names from translations*/
url: window.URLBase + "/" + url,
show: () => {
tab.buttonEl.classList.add("active", "~urge");
tab.tabEl.classList.remove("unfocused");
tab.buttonEl.parentElement.scrollTo(tab.buttonEl.offsetLeft-this._baseOffset, 0);
document.dispatchEvent(new CustomEvent("tab-change", { detail: tabID }));
return true;
},
hide: () => {
tab.buttonEl.classList.remove("active");
tab.buttonEl.classList.remove("~urge");
tab.tabEl.classList.add("unfocused");
return true;
},
shouldSkip: () => false,
};
this.pages.setPage(tab.page);
tab.buttonEl.onclick = () => { this.switch(tabID); };
tab.preFunc = preFunc;
tab.postFunc = postFunc;
this.tabs.push(tab);
this.tabs.set(tabID, tab);
}
get current(): string { return this._current; }
set current(tabID: string) { this.switch(tabID); }
switch = (tabID: string, noRun: boolean = false, keepURL: boolean = false) => {
this._current = tabID;
let baseOffset = -1;
for (let t of this.tabs) {
if (baseOffset == -1) baseOffset = t.buttonEl.offsetLeft;
if (t.tabID == tabID) {
t.buttonEl.classList.add("active", "~urge");
if (t.preFunc && !noRun) { t.preFunc(); }
t.tabEl.classList.remove("unfocused");
if (t.postFunc && !noRun) { t.postFunc(); }
document.dispatchEvent(new CustomEvent("tab-change", { detail: keepURL ? "" : tabID }));
// t.buttonEl.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'start' })
t.buttonEl.parentElement.scrollTo(t.buttonEl.offsetLeft-baseOffset, 0);
} else {
t.buttonEl.classList.remove("active");
t.buttonEl.classList.remove("~urge");
t.tabEl.classList.add("unfocused");
}
switch = (tabID: string, noRun: boolean = false) => {
let t = this.tabs.get(tabID);
if (t == undefined) {
[t] = this.tabs.values();
}
this._current = t.page.name;
if (t.preFunc && !noRun) { t.preFunc(); }
this.pages.load(tabID);
if (t.postFunc && !noRun) { t.postFunc(); }
}
}

View File

@ -1,6 +1,7 @@
import { _get, _post, toggleLoader, notificationBox } from "./modules/common.js";
import { lang, LangFile, loadLangSelector } from "./modules/lang.js";
import { ThemeManager } from "./modules/theme.js";
import { PageManager } from "./modules/pages.js";
interface sWindow extends Window {
messages: {};
@ -18,6 +19,8 @@ const get = (id: string): HTMLElement => document.getElementById(id);
const text = (id: string, val: string) => { document.getElementById(id).textContent = val; };
const html = (id: string, val: string) => { document.getElementById(id).innerHTML = val; };
// FIXME: Reuse setting types from ts/modules/settings.ts
interface boolEvent extends Event {
detail: boolean;
}
@ -513,61 +516,15 @@ for (let section in settings) {
}
}
const pageNames: string[][] = [];
(() => {
const pushState = window.history.pushState;
window.history.pushState = function (data: any, __: string, _: string | URL) {
pushState.apply(window.history, arguments);
let ev = { state: data as string } as PopStateEvent;
window.onpopstate(ev);
};
})();
window.onpopstate = (event: PopStateEvent) => {
if (event.state === "welcome") {
cards[0].classList.remove("unfocused");
for (let i = 1; i < cards.length; i++) { cards[i].classList.add("unfocused"); }
return;
}
for (let i = 0; i < cards.length; i++) {
if (event.state === pageNames[i][0]) {
cards[i].classList.remove("unfocused");
} else {
cards[i].classList.add("unfocused");
}
}
};
let pages = new PageManager({
hideOthersOnPageShow: true,
defaultName: "welcome",
defaultTitle: "Setup - jfa-go",
});
const cards = Array.from(document.getElementsByClassName("page-container")[0].querySelectorAll(".card.sectioned")) as Array<HTMLDivElement>;
(window as any).cards = cards;
const changePageToIndex = (title: string, pageTitle: string, i: number) => {
cards[i].classList.remove("unfocused");
const urlParams = new URLSearchParams(window.location.search);
const lang = urlParams.get("lang");
let page = "/#" + title;
if (lang) { page += "?lang=" + lang; }
console.log("pushing", title, pageTitle, page);
window.history.pushState(title || "welcome", pageTitle, page);
};
const changePage = (title: string, pageTitle: string) => {
let found = false;
for (let i = 0; i < cards.length; i++) {
if (!found && pageNames[i][0] == title && !(cards[i].classList.contains("hidden"))) {
found = true;
changePageToIndex(title, pageTitle, i);
} else {
cards[i].classList.add("unfocused");
}
}
if (!found) {
changePageToIndex(title, pageTitle, 0);
}
window.scrollTo(0, 0);
};
(() => {
for (let i = 0; i < cards.length; i++) {
const card = cards[i];
@ -578,37 +535,30 @@ const changePage = (title: string, pageTitle: string) => {
if (titleEl.classList.contains("welcome")) {
title = "";
}
let pageTitle = titleEl.textContent + " - jfa-go";
pageNames.push([title, pageTitle]);
if (back) { back.addEventListener("click", () => {
for (let ind = cards.length - 1; ind >= 0; ind--) {
if (ind < i && !(cards[ind].classList.contains("hidden"))) {
changePage(pageNames[ind][0], pageNames[ind][1]);
break;
}
}
}); }
if (next) {
const func = () => {
if (next.hasAttribute("disabled")) return;
for (let ind = 0; ind < cards.length; ind++) {
if (ind > i && !(cards[ind].classList.contains("hidden"))) {
changePage(pageNames[ind][0], pageNames[ind][1]);
break;
}
}
};
next.addEventListener("click", func)
}
pages.setPage({
name: title,
title: titleEl.textContent + " - jfa-go",
url: "/#" + title,
show: () => {
cards[i].classList.remove("unfocused");
return true;
},
hide: () => {
cards[i].classList.add("unfocused");
return true;
},
shouldSkip: () => {
return cards[i].classList.contains("hidden");
},
});
if (back) back.addEventListener("click", () => pages.prev(title));
if (next) next.addEventListener("click", () => {
if (next.hasAttribute("disabled")) return;
pages.next(title);
});
}
})();
(() => {
let initialLocation = window.location.hash.replace("#", "") || "welcome";
changePage(initialLocation, "Setup - jfa-go");
})();
// window.history.replaceState("welcome", "Setup - jfa-go",);
(() => {
const button = document.getElementById("jellyfin-test-connection") as HTMLSpanElement;
const ogText = button.textContent;
@ -665,3 +615,5 @@ const changePage = (title: string, pageTitle: string) => {
})();
loadLangSelector("setup");
pages.load(window.location.hash.replace("#", ""));

View File

@ -79,20 +79,10 @@ declare interface NotificationBox {
declare interface Tabs {
current: string;
tabs: Array<Tab>;
addTab: (tabID: string, preFunc?: () => void, postFunc?: () => void) => void;
addTab: (tabID: string, url: string, preFunc?: () => void, postFunc?: () => void) => void;
switch: (tabID: string, noRun?: boolean, keepURL?: boolean) => void;
}
declare interface Tab {
tabID: string;
tabEl: HTMLDivElement;
buttonEl: HTMLSpanElement;
preFunc?: () => void;
postFunc?: () => void;
}
declare interface Modals {
about: Modal;
login: Modal;

View File

@ -5,6 +5,7 @@ import { _get, _post, _delete, notificationBox, whichAnimationEvent, toDateStrin
import { Login } from "./modules/login.js";
import { Discord, Telegram, Matrix, ServiceConfiguration, MatrixConfiguration } from "./modules/account-linking.js";
import { Validator, ValidatorConf, ValidatorRespDTO } from "./modules/validator.js";
import { PageManager } from "./modules/pages.js";
interface userWindow extends Window {
jellyfinID: string;
@ -21,6 +22,8 @@ interface userWindow extends Window {
referralsEnabled: boolean;
}
const basePath = window.location.pathname.replace("/password/reset", "");
declare var window: userWindow;
const theme = new ThemeManager(document.getElementById("button-theme"));
@ -35,6 +38,27 @@ window.token = "";
window.modals = {} as Modals;
let pages = new PageManager({
hideOthersOnPageShow: true,
defaultName: "",
defaultTitle: document.title,
});
pages.setPage({
name: "",
title: document.title,
url: basePath,
show: () => {
if (!login.loggedIn) login.login("", "");
return true;
},
hide: () => {
window.modals.login.close();
return true;
},
shouldSkip: () => false,
});
(() => {
window.modals.login = new Modal(document.getElementById("modal-login"), true);
window.modals.email = new Modal(document.getElementById("modal-email"), false);
@ -49,38 +73,34 @@ window.modals = {} as Modals;
}
if (window.pwrEnabled) {
window.modals.pwr = new Modal(document.getElementById("modal-pwr"), false);
window.modals.pwr.onclose = () => {
window.history.pushState("", "", window.location.pathname.replace("/password/reset", ""));
};
const resetButton = document.getElementById("modal-login-pwr");
resetButton.onclick = () => {
window.history.pushState("reset", "", window.location.pathname+"/password/reset");
}
window.onpopstate = (event: PopStateEvent) => {
if ((event.state == "reset" || window.location.pathname.includes("/password/reset")) && window.pwrEnabled) {
pages.setPage({
name: "reset",
title: document.title,
url: basePath+"/password/reset",
show: () => {
const usernameInput = document.getElementById("login-user") as HTMLInputElement;
const input = document.getElementById("pwr-address") as HTMLInputElement;
input.value = usernameInput.value;
window.modals.login.close();
window.modals.pwr.show();
} else {
return true;
},
hide: () => {
// Don't recursively run this through the onclose event
window.modals.pwr.close(null, true);
if (!login.loggedIn) login.login("", "");
}
return true;
},
shouldSkip: () => false,
});
window.modals.pwr.onclose = () => {
pages.load("");
};
const resetButton = document.getElementById("modal-login-pwr");
resetButton.onclick = () => {
pages.load("reset");
}
}
})();
(() => {
const pushState = window.history.pushState;
window.history.pushState = function (data: any, __: string, _: string | URL) {
pushState.apply(window.history, arguments);
let ev = { state: data as string } as PopStateEvent;
window.onpopstate(ev);
};
})();
window.notifications = new notificationBox(document.getElementById('notification-box') as HTMLDivElement, 5);
if (window.pwrEnabled && window.linkResetEnabled) {
@ -798,8 +818,4 @@ const generatePermutations = (xs: number[]): [number[], number[]][] => {
login.bindLogout(document.getElementById("logout-button"));
(() => {
let data = "";
if (window.location.pathname.endsWith("/password/reset")) data = "reset";
window.history.pushState(data, "", window.location.pathname);
})();
pages.load(window.location.pathname.endsWith("/password/reset") ? "reset" : "");