1
0
mirror of https://github.com/hrfee/jfa-go.git synced 2024-12-29 12:30:11 +00:00

Compare commits

..

No commits in common. "6347495b5b69150dae1b6dc46ce38486b7e32bd4" and "b5dea7755b5d801528e244e11a906d64b2c6c2a2" have entirely different histories.

14 changed files with 242 additions and 566 deletions

239
'
View File

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

View File

@ -1,7 +1,7 @@
import { ThemeManager } from "./modules/theme.js"; import { ThemeManager } from "./modules/theme.js";
import { lang, LangFile, loadLangSelector } from "./modules/lang.js"; import { lang, LangFile, loadLangSelector } from "./modules/lang.js";
import { Modal } from "./modules/modal.js"; import { Modal } from "./modules/modal.js";
import { Tabs, Tab } from "./modules/tabs.js"; import { Tabs } from "./modules/tabs.js";
import { inviteList, createInvite } from "./modules/invites.js"; import { inviteList, createInvite } from "./modules/invites.js";
import { accountsList } from "./modules/accounts.js"; import { accountsList } from "./modules/accounts.js";
import { settingsList } from "./modules/settings.js"; import { settingsList } from "./modules/settings.js";
@ -120,41 +120,21 @@ window.notifications = new notificationBox(document.getElementById('notification
userSelect.classList.toggle('unfocused'); userSelect.classList.toggle('unfocused');
}*/ }*/
// Determine if url references an invite or account
let isInviteURL = window.invites.isInviteURL();
let isAccountURL = accounts.isAccountURL();
// load tabs // load tabs
const tabs: { id: string, url: string, reloader: () => void }[] = [ const tabs: { url: string, reloader: () => void }[] = [
{ {
id: "invites", url: "invites",
url: "", reloader: window.invites.reload
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", 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", url: "activity",
reloader: activity.reload reloader: activity.reload
}, },
{ {
id: "settings",
url: "settings", url: "settings",
reloader: settings.reload reloader: settings.reload
} }
@ -165,20 +145,41 @@ const defaultTab = tabs[0];
window.tabs = new Tabs(); window.tabs = new Tabs();
for (let tab of tabs) { for (let tab of tabs) {
window.tabs.addTab(tab.id, tab.url, null, tab.reloader); window.tabs.addTab(tab.url, null, tab.reloader);
} if (window.location.pathname == window.URLBase + "/" + tab.url) {
let matchedTab = false
for (let tab of tabs) {
if (window.location.pathname.startsWith(window.URLBase + "/" + tab.url)) {
window.tabs.switch(tab.url, true); window.tabs.switch(tab.url, true);
matchedTab = true;
} }
} }
let isInviteURL = window.invites.isInviteURL();
let isAccountURL = accounts.isAccountURL();
// Default tab // Default tab
// if ((window.URLBase + "/").includes(window.location.pathname)) { if ((window.URLBase + "/").includes(window.location.pathname)) {
if (!matchedTab) { window.tabs.switch(defaultTab.url, true);
window.tabs.switch("", 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);
} }
const login = new Login(window.modals.login as Modal, "/", window.loginAppearance); const login = new Login(window.modals.login as Modal, "/", window.loginAppearance);
@ -188,8 +189,35 @@ login.onLogin = () => {
// FIXME: Decide whether to autoload activity or not // FIXME: Decide whether to autoload activity or not
reloadProfileNames(); reloadProfileNames();
setInterval(() => { window.invites.reload(); accounts.reload(); }, 30*1000); setInterval(() => { window.invites.reload(); accounts.reload(); }, 30*1000);
// Triggers pre and post funcs, even though we're already on that page const currentTab = window.tabs.current;
window.tabs.switch(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;
}
} }
bindManualDropdowns(); bindManualDropdowns();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,77 +1,44 @@
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 { export class Tabs implements Tabs {
private _current: string = ""; private _current: string = "";
private _baseOffset = -1; tabs: Array<Tab>;
tabs: Map<string, Tab>;
pages: PageManager;
constructor() { constructor() {
this.tabs = new Map<string, Tab>; this.tabs = [];
this.pages = new PageManager({
hideOthersOnPageShow: true,
defaultName: "invites",
defaultTitle: document.title,
});
} }
addTab = (tabID: string, url: string, preFunc = () => void {}, postFunc = () => void {},) => { addTab = (tabID: string, preFunc = () => void {}, postFunc = () => void {}) => {
let tab: Tab = { let tab = {} as Tab;
page: null, tab.tabID = tabID;
tabEl: document.getElementById("tab-" + tabID) as HTMLDivElement, tab.tabEl = document.getElementById("tab-" + tabID) as HTMLDivElement;
buttonEl: document.getElementById("button-tab-" + tabID) as HTMLSpanElement, tab.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.buttonEl.onclick = () => { this.switch(tabID); };
this.tabs.set(tabID, tab); tab.preFunc = preFunc;
tab.postFunc = postFunc;
this.tabs.push(tab);
} }
get current(): string { return this._current; } get current(): string { return this._current; }
set current(tabID: string) { this.switch(tabID); } set current(tabID: string) { this.switch(tabID); }
switch = (tabID: string, noRun: boolean = false) => { switch = (tabID: string, noRun: boolean = false, keepURL: boolean = false) => {
let t = this.tabs.get(tabID); this._current = tabID;
if (t == undefined) { let baseOffset = -1;
[t] = this.tabs.values(); for (let t of this.tabs) {
} if (baseOffset == -1) baseOffset = t.buttonEl.offsetLeft;
if (t.tabID == tabID) {
this._current = t.page.name; t.buttonEl.classList.add("active", "~urge");
if (t.preFunc && !noRun) { t.preFunc(); } if (t.preFunc && !noRun) { t.preFunc(); }
this.pages.load(tabID); t.tabEl.classList.remove("unfocused");
if (t.postFunc && !noRun) { t.postFunc(); } 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");
}
}
} }
} }

View File

@ -1,7 +1,6 @@
import { _get, _post, toggleLoader, notificationBox } from "./modules/common.js"; import { _get, _post, toggleLoader, notificationBox } from "./modules/common.js";
import { lang, LangFile, loadLangSelector } from "./modules/lang.js"; import { lang, LangFile, loadLangSelector } from "./modules/lang.js";
import { ThemeManager } from "./modules/theme.js"; import { ThemeManager } from "./modules/theme.js";
import { PageManager } from "./modules/pages.js";
interface sWindow extends Window { interface sWindow extends Window {
messages: {}; messages: {};
@ -19,8 +18,6 @@ const get = (id: string): HTMLElement => document.getElementById(id);
const text = (id: string, val: string) => { document.getElementById(id).textContent = val; }; const text = (id: string, val: string) => { document.getElementById(id).textContent = val; };
const html = (id: string, val: string) => { document.getElementById(id).innerHTML = 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 { interface boolEvent extends Event {
detail: boolean; detail: boolean;
} }
@ -516,15 +513,61 @@ for (let section in settings) {
} }
} }
let pages = new PageManager({ const pageNames: string[][] = [];
hideOthersOnPageShow: true,
defaultName: "welcome", (() => {
defaultTitle: "Setup - jfa-go", 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");
}
}
};
const cards = Array.from(document.getElementsByClassName("page-container")[0].querySelectorAll(".card.sectioned")) as Array<HTMLDivElement>; const cards = Array.from(document.getElementsByClassName("page-container")[0].querySelectorAll(".card.sectioned")) as Array<HTMLDivElement>;
(window as any).cards = cards; (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++) { for (let i = 0; i < cards.length; i++) {
const card = cards[i]; const card = cards[i];
@ -535,30 +578,37 @@ const cards = Array.from(document.getElementsByClassName("page-container")[0].qu
if (titleEl.classList.contains("welcome")) { if (titleEl.classList.contains("welcome")) {
title = ""; title = "";
} }
pages.setPage({ let pageTitle = titleEl.textContent + " - jfa-go";
name: title, pageNames.push([title, pageTitle]);
title: titleEl.textContent + " - jfa-go", if (back) { back.addEventListener("click", () => {
url: "/#" + title, for (let ind = cards.length - 1; ind >= 0; ind--) {
show: () => { if (ind < i && !(cards[ind].classList.contains("hidden"))) {
cards[i].classList.remove("unfocused"); changePage(pageNames[ind][0], pageNames[ind][1]);
return true; break;
}, }
hide: () => { }
cards[i].classList.add("unfocused"); }); }
return true; if (next) {
}, const func = () => {
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; if (next.hasAttribute("disabled")) return;
pages.next(title); 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)
}
} }
})(); })();
(() => {
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 button = document.getElementById("jellyfin-test-connection") as HTMLSpanElement;
const ogText = button.textContent; const ogText = button.textContent;
@ -615,5 +665,3 @@ const cards = Array.from(document.getElementsByClassName("page-container")[0].qu
})(); })();
loadLangSelector("setup"); loadLangSelector("setup");
pages.load(window.location.hash.replace("#", ""));

View File

@ -79,10 +79,20 @@ declare interface NotificationBox {
declare interface Tabs { declare interface Tabs {
current: string; current: string;
addTab: (tabID: string, url: string, preFunc?: () => void, postFunc?: () => void) => void; tabs: Array<Tab>;
addTab: (tabID: string, preFunc?: () => void, postFunc?: () => void) => void;
switch: (tabID: string, noRun?: boolean, keepURL?: boolean) => 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 { declare interface Modals {
about: Modal; about: Modal;
login: Modal; login: Modal;

View File

@ -5,7 +5,6 @@ import { _get, _post, _delete, notificationBox, whichAnimationEvent, toDateStrin
import { Login } from "./modules/login.js"; import { Login } from "./modules/login.js";
import { Discord, Telegram, Matrix, ServiceConfiguration, MatrixConfiguration } from "./modules/account-linking.js"; import { Discord, Telegram, Matrix, ServiceConfiguration, MatrixConfiguration } from "./modules/account-linking.js";
import { Validator, ValidatorConf, ValidatorRespDTO } from "./modules/validator.js"; import { Validator, ValidatorConf, ValidatorRespDTO } from "./modules/validator.js";
import { PageManager } from "./modules/pages.js";
interface userWindow extends Window { interface userWindow extends Window {
jellyfinID: string; jellyfinID: string;
@ -22,8 +21,6 @@ interface userWindow extends Window {
referralsEnabled: boolean; referralsEnabled: boolean;
} }
const basePath = window.location.pathname.replace("/password/reset", "");
declare var window: userWindow; declare var window: userWindow;
const theme = new ThemeManager(document.getElementById("button-theme")); const theme = new ThemeManager(document.getElementById("button-theme"));
@ -38,27 +35,6 @@ window.token = "";
window.modals = {} as Modals; 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.login = new Modal(document.getElementById("modal-login"), true);
window.modals.email = new Modal(document.getElementById("modal-email"), false); window.modals.email = new Modal(document.getElementById("modal-email"), false);
@ -73,32 +49,36 @@ pages.setPage({
} }
if (window.pwrEnabled) { if (window.pwrEnabled) {
window.modals.pwr = new Modal(document.getElementById("modal-pwr"), false); window.modals.pwr = new Modal(document.getElementById("modal-pwr"), false);
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.pwr.show();
return true;
},
hide: () => {
// Don't recursively run this through the onclose event
window.modals.pwr.close(null, true);
return true;
},
shouldSkip: () => false,
});
window.modals.pwr.onclose = () => { window.modals.pwr.onclose = () => {
pages.load(""); window.history.pushState("", "", window.location.pathname.replace("/password/reset", ""));
}; };
const resetButton = document.getElementById("modal-login-pwr"); const resetButton = document.getElementById("modal-login-pwr");
resetButton.onclick = () => { resetButton.onclick = () => {
pages.load("reset"); 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) {
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 {
window.modals.pwr.close(null, true);
if (!login.loggedIn) login.login("", "");
} }
};
}
})();
(() => {
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); window.notifications = new notificationBox(document.getElementById('notification-box') as HTMLDivElement, 5);
@ -818,4 +798,8 @@ const generatePermutations = (xs: number[]): [number[], number[]][] => {
login.bindLogout(document.getElementById("logout-button")); login.bindLogout(document.getElementById("logout-button"));
pages.load(window.location.pathname.endsWith("/password/reset") ? "reset" : ""); (() => {
let data = "";
if (window.location.pathname.endsWith("/password/reset")) data = "reset";
window.history.pushState(data, "", window.location.pathname);
})();