mirror of
https://github.com/hrfee/jfa-go.git
synced 2024-12-28 03:50:10 +00:00
Compare commits
3 Commits
b5dea7755b
...
6347495b5b
Author | SHA1 | Date | |
---|---|---|---|
6347495b5b | |||
02f4ba6e8e | |||
d2e5209832 |
239
'
Normal file
239
'
Normal 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)
|
23
Makefile
23
Makefile
@ -110,22 +110,18 @@ CONFIG_DEFAULT = $(DATA)/config-default.ini
|
|||||||
# -mkdir -p $(DATA)
|
# -mkdir -p $(DATA)
|
||||||
|
|
||||||
$(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)
|
$(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_MJML = $(wildcard mail/*.mjml)
|
EMAIL_SRC = $(wildcard mail/*)
|
||||||
EMAIL_SRC_TXT = $(wildcard mail/*.txt)
|
EMAIL_TARGET = $(DATA)/confirmation.html
|
||||||
EMAIL_DATA_MJML = $(EMAIL_SRC_MJML:mail/%=data/%)
|
$(EMAIL_TARGET): $(EMAIL_SRC)
|
||||||
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)
|
||||||
@ -144,7 +140,6 @@ $(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)
|
||||||
@ -172,7 +167,6 @@ 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)
|
||||||
@ -204,7 +198,6 @@ $(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)
|
||||||
@ -213,11 +206,11 @@ $(COPY_TARGET): $(INLINE_TARGET) $(STATIC_SRC) $(LANG_SRC)
|
|||||||
cp -r lang $(DATA)/
|
cp -r lang $(DATA)/
|
||||||
cp LICENSE $(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_SRC = $(shell find ./ -name "*.go")
|
||||||
GO_TARGET = build/jfa-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)
|
$(info Downloading deps)
|
||||||
$(GOBINARY) mod download
|
$(GOBINARY) mod download
|
||||||
$(info Building)
|
$(info Building)
|
||||||
|
102
ts/admin.ts
102
ts/admin.ts
@ -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 } from "./modules/tabs.js";
|
import { Tabs, Tab } 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,21 +120,41 @@ 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: { url: string, reloader: () => void }[] = [
|
const tabs: { id: string, url: string, reloader: () => void }[] = [
|
||||||
{
|
{
|
||||||
url: "invites",
|
id: "invites",
|
||||||
reloader: window.invites.reload
|
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",
|
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
|
||||||
}
|
}
|
||||||
@ -145,41 +165,20 @@ 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.url, null, tab.reloader);
|
window.tabs.addTab(tab.id, 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)) {
|
||||||
window.tabs.switch(defaultTab.url, true);
|
if (!matchedTab) {
|
||||||
}
|
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);
|
||||||
@ -189,35 +188,8 @@ 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);
|
||||||
const currentTab = window.tabs.current;
|
// Triggers pre and post funcs, even though we're already on that page
|
||||||
switch (currentTab) {
|
window.tabs.switch(window.tabs.current);
|
||||||
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();
|
||||||
|
@ -1804,10 +1804,16 @@ export class accountsList {
|
|||||||
this.focusAccount(event.detail);
|
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 = () => {
|
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);
|
this.focusAccount(userID);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,17 +64,17 @@ export class Activity implements activity, SearchableItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_genUserLink = (): string => {
|
_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 => {
|
_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 _renderInvText = (): string => { return `<span class="font-medium font-mono">${this.value || this.invite_code || "???"}</span>`; }
|
||||||
|
|
||||||
private _genInvLink = (): string => {
|
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>;
|
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;*/
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
@ -448,10 +448,15 @@ export class inviteList implements inviteList {
|
|||||||
this.focusInvite(event.detail);
|
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 = () => {
|
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"));
|
this.focusInvite(inviteCode, window.lang.notif("errorInviteNotFound"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Modal } from "../modules/modal.js";
|
import { Modal } from "../modules/modal.js";
|
||||||
import { toggleLoader, _post } from "../modules/common.js";
|
import { toggleLoader, _post, unicodeB64Encode } 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 " + btoa(username + ":" + password));
|
req.setRequestHeader("Authorization", "Basic " + unicodeB64Encode(username + ":" + password));
|
||||||
}
|
}
|
||||||
req.onreadystatechange = ((req: XMLHttpRequest, _: Event): any => {
|
req.onreadystatechange = ((req: XMLHttpRequest, _: Event): any => {
|
||||||
if (req.readyState == 4) {
|
if (req.readyState == 4) {
|
||||||
|
100
ts/modules/pages.ts
Normal file
100
ts/modules/pages.ts
Normal 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);
|
||||||
|
}
|
||||||
|
};
|
@ -636,6 +636,7 @@ 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;
|
||||||
@ -919,7 +920,11 @@ 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");
|
||||||
}
|
}
|
||||||
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-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");
|
||||||
|
@ -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 {
|
export class Tabs implements Tabs {
|
||||||
private _current: string = "";
|
private _current: string = "";
|
||||||
tabs: Array<Tab>;
|
private _baseOffset = -1;
|
||||||
|
tabs: Map<string, Tab>;
|
||||||
|
pages: PageManager;
|
||||||
|
|
||||||
constructor() {
|
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 {}) => {
|
addTab = (tabID: string, url: string, preFunc = () => void {}, postFunc = () => void {},) => {
|
||||||
let tab = {} as Tab;
|
let tab: Tab = {
|
||||||
tab.tabID = tabID;
|
page: null,
|
||||||
tab.tabEl = document.getElementById("tab-" + tabID) as HTMLDivElement;
|
tabEl: document.getElementById("tab-" + tabID) as HTMLDivElement,
|
||||||
tab.buttonEl = document.getElementById("button-tab-" + tabID) as HTMLSpanElement;
|
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); };
|
||||||
tab.preFunc = preFunc;
|
this.tabs.set(tabID, tab);
|
||||||
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, keepURL: boolean = false) => {
|
switch = (tabID: string, noRun: boolean = false) => {
|
||||||
this._current = tabID;
|
let t = this.tabs.get(tabID);
|
||||||
let baseOffset = -1;
|
if (t == undefined) {
|
||||||
for (let t of this.tabs) {
|
[t] = this.tabs.values();
|
||||||
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");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this._current = t.page.name;
|
||||||
|
|
||||||
|
if (t.preFunc && !noRun) { t.preFunc(); }
|
||||||
|
this.pages.load(tabID);
|
||||||
|
if (t.postFunc && !noRun) { t.postFunc(); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
110
ts/setup.ts
110
ts/setup.ts
@ -1,6 +1,7 @@
|
|||||||
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: {};
|
||||||
@ -18,6 +19,8 @@ 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;
|
||||||
}
|
}
|
||||||
@ -513,61 +516,15 @@ for (let section in settings) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const pageNames: string[][] = [];
|
let pages = new PageManager({
|
||||||
|
hideOthersOnPageShow: true,
|
||||||
(() => {
|
defaultName: "welcome",
|
||||||
const pushState = window.history.pushState;
|
defaultTitle: "Setup - jfa-go",
|
||||||
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];
|
||||||
@ -578,37 +535,30 @@ const changePage = (title: string, pageTitle: string) => {
|
|||||||
if (titleEl.classList.contains("welcome")) {
|
if (titleEl.classList.contains("welcome")) {
|
||||||
title = "";
|
title = "";
|
||||||
}
|
}
|
||||||
let pageTitle = titleEl.textContent + " - jfa-go";
|
pages.setPage({
|
||||||
pageNames.push([title, pageTitle]);
|
name: title,
|
||||||
if (back) { back.addEventListener("click", () => {
|
title: titleEl.textContent + " - jfa-go",
|
||||||
for (let ind = cards.length - 1; ind >= 0; ind--) {
|
url: "/#" + title,
|
||||||
if (ind < i && !(cards[ind].classList.contains("hidden"))) {
|
show: () => {
|
||||||
changePage(pageNames[ind][0], pageNames[ind][1]);
|
cards[i].classList.remove("unfocused");
|
||||||
break;
|
return true;
|
||||||
}
|
},
|
||||||
}
|
hide: () => {
|
||||||
}); }
|
cards[i].classList.add("unfocused");
|
||||||
if (next) {
|
return true;
|
||||||
const func = () => {
|
},
|
||||||
if (next.hasAttribute("disabled")) return;
|
shouldSkip: () => {
|
||||||
for (let ind = 0; ind < cards.length; ind++) {
|
return cards[i].classList.contains("hidden");
|
||||||
if (ind > i && !(cards[ind].classList.contains("hidden"))) {
|
},
|
||||||
changePage(pageNames[ind][0], pageNames[ind][1]);
|
});
|
||||||
break;
|
if (back) back.addEventListener("click", () => pages.prev(title));
|
||||||
}
|
if (next) next.addEventListener("click", () => {
|
||||||
}
|
if (next.hasAttribute("disabled")) return;
|
||||||
};
|
pages.next(title);
|
||||||
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;
|
||||||
@ -665,3 +615,5 @@ const changePage = (title: string, pageTitle: string) => {
|
|||||||
})();
|
})();
|
||||||
|
|
||||||
loadLangSelector("setup");
|
loadLangSelector("setup");
|
||||||
|
|
||||||
|
pages.load(window.location.hash.replace("#", ""));
|
||||||
|
@ -79,20 +79,10 @@ declare interface NotificationBox {
|
|||||||
|
|
||||||
declare interface Tabs {
|
declare interface Tabs {
|
||||||
current: string;
|
current: string;
|
||||||
tabs: Array<Tab>;
|
addTab: (tabID: string, url: string, preFunc?: () => void, postFunc?: () => void) => void;
|
||||||
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;
|
||||||
|
72
ts/user.ts
72
ts/user.ts
@ -5,6 +5,7 @@ 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;
|
||||||
@ -21,6 +22,8 @@ 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"));
|
||||||
@ -35,6 +38,27 @@ 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);
|
||||||
@ -49,38 +73,34 @@ window.modals = {} as Modals;
|
|||||||
}
|
}
|
||||||
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);
|
||||||
window.modals.pwr.onclose = () => {
|
pages.setPage({
|
||||||
window.history.pushState("", "", window.location.pathname.replace("/password/reset", ""));
|
name: "reset",
|
||||||
};
|
title: document.title,
|
||||||
const resetButton = document.getElementById("modal-login-pwr");
|
url: basePath+"/password/reset",
|
||||||
resetButton.onclick = () => {
|
show: () => {
|
||||||
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 usernameInput = document.getElementById("login-user") as HTMLInputElement;
|
||||||
const input = document.getElementById("pwr-address") as HTMLInputElement;
|
const input = document.getElementById("pwr-address") as HTMLInputElement;
|
||||||
input.value = usernameInput.value;
|
input.value = usernameInput.value;
|
||||||
window.modals.login.close();
|
|
||||||
window.modals.pwr.show();
|
window.modals.pwr.show();
|
||||||
} else {
|
return true;
|
||||||
|
},
|
||||||
|
hide: () => {
|
||||||
|
// Don't recursively run this through the onclose event
|
||||||
window.modals.pwr.close(null, true);
|
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);
|
window.notifications = new notificationBox(document.getElementById('notification-box') as HTMLDivElement, 5);
|
||||||
|
|
||||||
if (window.pwrEnabled && window.linkResetEnabled) {
|
if (window.pwrEnabled && window.linkResetEnabled) {
|
||||||
@ -798,8 +818,4 @@ 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);
|
|
||||||
})();
|
|
||||||
|
Loading…
Reference in New Issue
Block a user