diff --git a/src/app/admin/admin-routing.module.ts b/src/app/admin/admin-routing.module.ts
new file mode 100644
index 0000000..793ca88
--- /dev/null
+++ b/src/app/admin/admin-routing.module.ts
@@ -0,0 +1,19 @@
+import { NgModule } from "@angular/core";
+import { RouterModule, Routes } from "@angular/router";
+import { PanelComponent } from "./panel/panel.component";
+import { ItemResolver } from "../services/item-resolver.service";
+
+const routes: Routes = [
+ {path: "admin", component: PanelComponent,
+ resolve: {
+ items: ItemResolver.forResource("config/panel"),
+ config: ItemResolver.forResource("config")
+ }
+ },
+];
+
+@NgModule({
+ imports: [RouterModule.forChild(routes)],
+ exports: [RouterModule]
+})
+export class AdminRoutingModule { }
diff --git a/src/app/admin/admin.module.ts b/src/app/admin/admin.module.ts
new file mode 100644
index 0000000..47ea991
--- /dev/null
+++ b/src/app/admin/admin.module.ts
@@ -0,0 +1,30 @@
+import { NgModule } from "@angular/core";
+import { CommonModule } from "@angular/common";
+import { PanelComponent } from './panel/panel.component';
+import { RouterModule } from "@angular/router";
+import { MatSidenavModule } from "@angular/material/sidenav";
+import { MatButtonModule } from "@angular/material/button";
+import { OptionComponent } from './option/option.component';
+import { SectionComponent } from './section/section.component';
+import { MatFormFieldModule } from "@angular/material/form-field";
+import { MatInputModule } from "@angular/material/input";
+import { MatIconModule } from "@angular/material/icon";
+import { MatToolbarModule } from "@angular/material/toolbar";
+import { MatTooltipModule } from "@angular/material/tooltip";
+
+@NgModule({
+ declarations: [PanelComponent, OptionComponent, SectionComponent],
+ imports: [
+ CommonModule,
+ MatSidenavModule,
+ RouterModule,
+ MatButtonModule,
+ MatFormFieldModule,
+ MatInputModule,
+ MatIconModule,
+ MatToolbarModule,
+ MatTooltipModule
+ ]
+})
+export class AdminModule
+{ }
diff --git a/src/app/admin/models/configuration-section.ts b/src/app/admin/models/configuration-section.ts
new file mode 100644
index 0000000..baf9ea8
--- /dev/null
+++ b/src/app/admin/models/configuration-section.ts
@@ -0,0 +1,7 @@
+import { Option } from "./option";
+
+export interface ConfigurationSection
+{
+ name: string;
+ options: Option[]
+}
diff --git a/src/app/admin/models/option.ts b/src/app/admin/models/option.ts
new file mode 100644
index 0000000..f5ba219
--- /dev/null
+++ b/src/app/admin/models/option.ts
@@ -0,0 +1,8 @@
+export interface Option
+{
+ slug: string;
+ name: string;
+ description: string;
+ helpMessage: string;
+ type: number;
+}
diff --git a/src/app/admin/option/option.component.html b/src/app/admin/option/option.component.html
new file mode 100644
index 0000000..f0ce924
--- /dev/null
+++ b/src/app/admin/option/option.component.html
@@ -0,0 +1,7 @@
+
diff --git a/src/app/admin/option/option.component.scss b/src/app/admin/option/option.component.scss
new file mode 100644
index 0000000..e69de29
diff --git a/src/app/admin/option/option.component.ts b/src/app/admin/option/option.component.ts
new file mode 100644
index 0000000..acf48dd
--- /dev/null
+++ b/src/app/admin/option/option.component.ts
@@ -0,0 +1,44 @@
+import { Component, ElementRef, Input, OnDestroy, OnInit, ViewChild } from "@angular/core";
+import { Option } from "../models/option";
+import { HttpClient } from "@angular/common/http";
+
+@Component({
+ selector: "app-option",
+ templateUrl: "./option.component.html",
+ styleUrls: ["./option.component.scss"]
+})
+export class OptionComponent implements OnInit, OnDestroy
+{
+ value: string;
+ @Input() option: Option;
+ @Input() config;
+ @ViewChild("input") input: ElementRef;
+
+ constructor(private http: HttpClient)
+ {}
+
+ ngOnInit(): void
+ {
+ this.value = this.getConfig(this.option.slug);
+ }
+
+ getConfig(slug: string): string
+ {
+ const sections = slug.split(':');
+ let conf = this.config;
+
+ for (const child of sections)
+ {
+ if (conf == null)
+ throw new Error(`Invalid configuration. No resource at ${slug}`);
+ conf = conf[child];
+ }
+ return conf;
+ }
+ async ngOnDestroy()
+ {
+ if (this.value == this.input.nativeElement.value)
+ return;
+ await this.http.put(`/api/config/${this.option.slug}`, this.input.nativeElement.value);
+ }
+}
diff --git a/src/app/admin/panel/panel.component.html b/src/app/admin/panel/panel.component.html
new file mode 100644
index 0000000..e27cd03
--- /dev/null
+++ b/src/app/admin/panel/panel.component.html
@@ -0,0 +1,18 @@
+
+
+ {{item.name}}
+
+
+
+
+
+ {{getChanges()}} changes
+
+
+
+
+
diff --git a/src/app/admin/panel/panel.component.scss b/src/app/admin/panel/panel.component.scss
new file mode 100644
index 0000000..8a254ca
--- /dev/null
+++ b/src/app/admin/panel/panel.component.scss
@@ -0,0 +1,36 @@
+@import "variables";
+
+
+.admin-panel
+{
+ width: 100%;
+ height: 100%;
+}
+
+.sidenav-container
+{
+ display: flex;
+ height: 100%;
+ align-items: center;
+ justify-content: center;
+ padding: 20px;
+}
+
+.content
+{
+ height: 100%;
+ padding: 20px;
+
+ > div
+ {
+ margin-top: $nav-bar-height;
+ }
+}
+
+.toolbar
+{
+ position: fixed;
+ top: $nav-bar-height;
+ margin-left: -20px;
+ margin-right: -20px;
+}
diff --git a/src/app/admin/panel/panel.component.ts b/src/app/admin/panel/panel.component.ts
new file mode 100644
index 0000000..8f232c9
--- /dev/null
+++ b/src/app/admin/panel/panel.component.ts
@@ -0,0 +1,28 @@
+import { Component } from "@angular/core";
+import { ActivatedRoute } from "@angular/router";
+import { ConfigurationSection } from "../models/configuration-section";
+
+@Component({
+ selector: "app-panel",
+ templateUrl: "./panel.component.html",
+ styleUrls: ["./panel.component.scss"]
+})
+export class PanelComponent
+{
+ items: ConfigurationSection[];
+ config: any;
+
+ constructor(private route: ActivatedRoute)
+ {
+ this.route.data.subscribe(x =>
+ {
+ this.items = x.items;
+ this.config = x.config;
+ });
+ }
+
+ getChanges(): number
+ {
+ return 0;
+ }
+}
diff --git a/src/app/admin/section/section.component.html b/src/app/admin/section/section.component.html
new file mode 100644
index 0000000..007a125
--- /dev/null
+++ b/src/app/admin/section/section.component.html
@@ -0,0 +1,4 @@
+{{section.name}}
+
+
+
diff --git a/src/app/admin/section/section.component.scss b/src/app/admin/section/section.component.scss
new file mode 100644
index 0000000..e69de29
diff --git a/src/app/admin/section/section.component.ts b/src/app/admin/section/section.component.ts
new file mode 100644
index 0000000..adab3a5
--- /dev/null
+++ b/src/app/admin/section/section.component.ts
@@ -0,0 +1,13 @@
+import { Component, Input } from "@angular/core";
+import { ConfigurationSection } from "../models/configuration-section";
+
+@Component({
+ selector: "app-section",
+ templateUrl: "./section.component.html",
+ styleUrls: ["./section.component.scss"]
+})
+export class SectionComponent
+{
+ @Input() section: ConfigurationSection;
+ @Input() config: any;
+}
diff --git a/src/app/app.component.html b/src/app/app.component.html
index 4a226de..d7348a3 100644
--- a/src/app/app.component.html
+++ b/src/app/app.component.html
@@ -18,6 +18,10 @@
+
+ settings
+
+
account_circle
diff --git a/src/app/app.component.scss b/src/app/app.component.scss
index 09e84b6..6ea8a12 100644
--- a/src/app/app.component.scss
+++ b/src/app/app.component.scss
@@ -111,7 +111,7 @@ input::-webkit-search-cancel-button
opacity: 0.7;
outline: none;
- &:host-context(.hoverEnabled) &:hover, &:focus
+ &:host-context(.hoverEnabled) &:hover, &:focus, &.active
{
cursor: pointer;
opacity: 1;
diff --git a/src/app/app.module.ts b/src/app/app.module.ts
index 8e774f3..d64b143 100644
--- a/src/app/app.module.ts
+++ b/src/app/app.module.ts
@@ -48,6 +48,8 @@ import { ShowGridComponent } from "./components/show-grid/show-grid.component";
import { MatBadgeModule } from "@angular/material/badge";
import { StartupService } from "./services/startup.service";
import { LongPressDirective } from "./misc/long-press.directive";
+import { AdminRoutingModule } from "./admin/admin-routing.module";
+import { AdminModule } from "./admin/admin.module";
@NgModule({
@@ -77,6 +79,7 @@ import { LongPressDirective } from "./misc/long-press.directive";
BrowserModule,
HttpClientModule,
AuthRoutingModule,
+ AdminRoutingModule,
AppRoutingModule,
BrowserAnimationsModule,
MatSnackBarModule,
@@ -97,6 +100,7 @@ import { LongPressDirective } from "./misc/long-press.directive";
MatTabsModule,
MatCheckboxModule,
AuthModule,
+ AdminModule,
MatChipsModule,
MatAutocompleteModule,
MatExpansionModule,