Compare commits
10 Commits
21b2e68198
...
5638a8ecb5
Author | SHA1 | Date | |
---|---|---|---|
5638a8ecb5 | |||
c9e48fe8b4 | |||
607e304c50 | |||
cce7bd55da | |||
38246db16b | |||
7705a299a0 | |||
0cb55eaf68 | |||
2895efc43b | |||
f11fe89835 | |||
493a6ee05b |
@ -177,6 +177,7 @@ export async function getSubstitutions(req, res) {
|
|||||||
id: element.id,
|
id: element.id,
|
||||||
class: element.class,
|
class: element.class,
|
||||||
type: element.type,
|
type: element.type,
|
||||||
|
rawType: element.rawType,
|
||||||
lesson: element.lesson,
|
lesson: element.lesson,
|
||||||
date: new Date(element.date).getTime(),
|
date: new Date(element.date).getTime(),
|
||||||
notes: element.notes,
|
notes: element.notes,
|
||||||
|
24
server/parser/filesystem.js
Normal file
24
server/parser/filesystem.js
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import fs from "fs";
|
||||||
|
|
||||||
|
/*
|
||||||
|
This file provider allows the application to use a local folder containing
|
||||||
|
parsable (.html) files, instead of downloading them from the internet.
|
||||||
|
|
||||||
|
This can be especially useful for development.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class FileSystemClient {
|
||||||
|
constructor(path) {
|
||||||
|
this.path = path;
|
||||||
|
}
|
||||||
|
|
||||||
|
getFiles() {
|
||||||
|
const contents = [];
|
||||||
|
const files = fs.readdirSync(this.path);
|
||||||
|
for (const file of files) {
|
||||||
|
const data = fs.readFileSync(this.path + "/" + file).toString();
|
||||||
|
contents.push(data);
|
||||||
|
}
|
||||||
|
return contents;
|
||||||
|
}
|
||||||
|
}
|
@ -39,8 +39,32 @@ export class Parser {
|
|||||||
const dayPlans = [];
|
const dayPlans = [];
|
||||||
for (const plan of plans) {
|
for (const plan of plans) {
|
||||||
const foundPlan = dayPlans.find((e) => e.date == plan.date);
|
const foundPlan = dayPlans.find((e) => e.date == plan.date);
|
||||||
if (!foundPlan) dayPlans.push(plan);
|
if (!foundPlan) {
|
||||||
else foundPlan.changes.push(...plan.changes);
|
// Make sure to not insert duplicate substitutions within a file
|
||||||
|
const changes = structuredClone(plan.changes);
|
||||||
|
const cleanedChanges = [];
|
||||||
|
for (const change of changes) {
|
||||||
|
const changeExists = cleanedChanges.find(
|
||||||
|
(e) => JSON.stringify(e) == JSON.stringify(change),
|
||||||
|
);
|
||||||
|
if (!changeExists) {
|
||||||
|
cleanedChanges.push(change);
|
||||||
|
}
|
||||||
|
// Use the new array of changes
|
||||||
|
plan.changes = cleanedChanges;
|
||||||
|
}
|
||||||
|
dayPlans.push(plan);
|
||||||
|
} else {
|
||||||
|
for (const change of plan.changes) {
|
||||||
|
// Make sure to not insert a substitution that already exists in the changes
|
||||||
|
const changeExists = foundPlan.changes.find(
|
||||||
|
(e) => JSON.stringify(e) == JSON.stringify(change),
|
||||||
|
);
|
||||||
|
if (!changeExists) {
|
||||||
|
foundPlan.changes.push(change);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Insert substitutions of all substitution plans
|
// Insert substitutions of all substitution plans
|
||||||
for (const plan of dayPlans) {
|
for (const plan of dayPlans) {
|
||||||
@ -96,15 +120,15 @@ export class Parser {
|
|||||||
// (Date, Type, Lesson, Classes and Subject need to be the same)
|
// (Date, Type, Lesson, Classes and Subject need to be the same)
|
||||||
const matchingSubstitutionId = knownSubstitutions.findIndex(
|
const matchingSubstitutionId = knownSubstitutions.findIndex(
|
||||||
(substitution) => {
|
(substitution) => {
|
||||||
return substitution.date == new Date(date).setUTCHours(0, 0, 0, 0) &&
|
return (
|
||||||
substitution.type == (change.type == "Entfall")
|
substitution.date.getTime() ==
|
||||||
? "cancellation"
|
new Date(date).setUTCHours(0, 0, 0, 0) &&
|
||||||
: "change" &&
|
substitution.rawType == change.type &&
|
||||||
substitution.lesson == change.lesson &&
|
substitution.lesson == change.lesson &&
|
||||||
classes.sort().join(",") ==
|
classes.sort().join(",") == substitution.class.sort().join(",") &&
|
||||||
substitution.class.sort().join(",") &&
|
substitution.changedSubject == change.subject &&
|
||||||
substitution.changedSubject == change.subject &&
|
substitution.teacher == (change.teacher || "")
|
||||||
substitution.teacher == (change.teacher || "");
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
const matchingSubstitution = knownSubstitutions[matchingSubstitutionId];
|
const matchingSubstitution = knownSubstitutions[matchingSubstitutionId];
|
||||||
@ -115,7 +139,12 @@ export class Parser {
|
|||||||
data: {
|
data: {
|
||||||
class: classes,
|
class: classes,
|
||||||
date: new Date(date),
|
date: new Date(date),
|
||||||
type: change.type == "Entfall" ? "cancellation" : "change",
|
type:
|
||||||
|
change.type == "Entfall" ||
|
||||||
|
change.type == "eigenverantwortliches Arbeiten"
|
||||||
|
? "cancellation"
|
||||||
|
: "change",
|
||||||
|
rawType: change.type,
|
||||||
lesson: parseInt(change.lesson),
|
lesson: parseInt(change.lesson),
|
||||||
teacher: change.teacher || "",
|
teacher: change.teacher || "",
|
||||||
changedTeacher: change.changedTeacher,
|
changedTeacher: change.changedTeacher,
|
||||||
@ -133,6 +162,7 @@ export class Parser {
|
|||||||
changes: {
|
changes: {
|
||||||
class: classes,
|
class: classes,
|
||||||
type: change.type == "Entfall" ? "cancellation" : "change",
|
type: change.type == "Entfall" ? "cancellation" : "change",
|
||||||
|
rawType: change.type,
|
||||||
lesson: parseInt(change.lesson),
|
lesson: parseInt(change.lesson),
|
||||||
date: new Date(date),
|
date: new Date(date),
|
||||||
notes: change.notes,
|
notes: change.notes,
|
||||||
@ -204,6 +234,7 @@ export class Parser {
|
|||||||
changes: {
|
changes: {
|
||||||
class: remainingSubstitution.class,
|
class: remainingSubstitution.class,
|
||||||
type: remainingSubstitution.type,
|
type: remainingSubstitution.type,
|
||||||
|
rawType: remainingSubstitution.rawType,
|
||||||
lesson: remainingSubstitution.lesson,
|
lesson: remainingSubstitution.lesson,
|
||||||
date: remainingSubstitution.date.getTime(),
|
date: remainingSubstitution.date.getTime(),
|
||||||
notes: remainingSubstitution.notes,
|
notes: remainingSubstitution.notes,
|
||||||
|
@ -27,6 +27,7 @@ model Substitution {
|
|||||||
class String[]
|
class String[]
|
||||||
date DateTime
|
date DateTime
|
||||||
type String
|
type String
|
||||||
|
rawType String @default("unknown")
|
||||||
lesson Int
|
lesson Int
|
||||||
teacher String
|
teacher String
|
||||||
changedTeacher String?
|
changedTeacher String?
|
||||||
|
@ -16,7 +16,7 @@ import {
|
|||||||
} from "@/store";
|
} from "@/store";
|
||||||
import { computed, ref } from "vue";
|
import { computed, ref } from "vue";
|
||||||
|
|
||||||
const autoThemes = { true: "dark", false: "light" };
|
const autoThemes = { true: "darker", false: "light" };
|
||||||
const autoTheme = ref("dark");
|
const autoTheme = ref("dark");
|
||||||
const colorSchemeMedia = window.matchMedia("(prefers-color-scheme: dark)");
|
const colorSchemeMedia = window.matchMedia("(prefers-color-scheme: dark)");
|
||||||
autoTheme.value = autoThemes[colorSchemeMedia.matches];
|
autoTheme.value = autoThemes[colorSchemeMedia.matches];
|
||||||
|
@ -3,7 +3,6 @@
|
|||||||
--element-color: #212121;
|
--element-color: #212121;
|
||||||
--element-color-hover: #1b1b1b;
|
--element-color-hover: #1b1b1b;
|
||||||
--element-border-input: #4e7a3a;
|
--element-border-input: #4e7a3a;
|
||||||
--element-border-focus: #9ac982;
|
|
||||||
--element-border-action: #1f5b63;
|
--element-border-action: #1f5b63;
|
||||||
--text-color: #bdbdbd;
|
--text-color: #bdbdbd;
|
||||||
--font-family: "sourcesanspro";
|
--font-family: "sourcesanspro";
|
||||||
@ -31,7 +30,6 @@
|
|||||||
--element-color: #bdbdbd;
|
--element-color: #bdbdbd;
|
||||||
--element-color-hover: #ada9a9;
|
--element-color-hover: #ada9a9;
|
||||||
--element-border-input: #4caf50;
|
--element-border-input: #4caf50;
|
||||||
--element-border-focus: #7ba764;
|
|
||||||
--element-border-action: #3f51b5;
|
--element-border-action: #3f51b5;
|
||||||
--text-color: #353131;
|
--text-color: #353131;
|
||||||
--font-family: "sourcesanspro";
|
--font-family: "sourcesanspro";
|
||||||
@ -53,3 +51,30 @@
|
|||||||
--timetable-trust-warning-border: #b71c1c;
|
--timetable-trust-warning-border: #b71c1c;
|
||||||
--timetable-trust-warning-background: #dabcbc;
|
--timetable-trust-warning-background: #dabcbc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.app.theme-darker {
|
||||||
|
--bg-color: #111111;
|
||||||
|
--element-color: #252525;
|
||||||
|
--element-color-hover: #1d1d1d;
|
||||||
|
--element-border-input: #4e7a3a;
|
||||||
|
--element-border-action: #1f5b63;
|
||||||
|
--text-color: #bdbdbd;
|
||||||
|
--font-family: "sourcesanspro";
|
||||||
|
--titlebar-color: #1d1d1d;
|
||||||
|
--titlebar-element-active-color: #44573b;
|
||||||
|
--titlebar-shadow: 0px 0px 10px 5px rgba(0, 0, 0, 0.2);
|
||||||
|
--bottomnav-color: hsl(137, 8%, 17%);
|
||||||
|
--bottomnav-icon-color: #899c8b;
|
||||||
|
--bottomnav-icon-active-color: #1e271f;
|
||||||
|
--bottomnav-active-color: #7c8670;
|
||||||
|
--bottomnav-shadow: 5px 7px 19px 0px rgba(0, 0, 0, 0.25);
|
||||||
|
--loader-color: #7ca74b;
|
||||||
|
--loader-error-color: #a54a4a;
|
||||||
|
--substitution-background-change: #095079;
|
||||||
|
--substitution-background-cancellation: #7d2b2d;
|
||||||
|
--substitution-background-addition: #326430;
|
||||||
|
--substitution-background-deletion: #7d2b2d;
|
||||||
|
--substitution-background-unchanged: #1c1e1d;
|
||||||
|
--timetable-trust-warning-border: #b71c1c;
|
||||||
|
--timetable-trust-warning-background: #412727;
|
||||||
|
}
|
||||||
|
@ -79,8 +79,11 @@ const chars = {
|
|||||||
room: event.change.change.room,
|
room: event.change.change.room,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}}<span class="notes" v-if="event.change.notes">
|
}}<span class="notes">
|
||||||
{{ $t("timetable.notes") }} {{ event.change.notes }}
|
<i>{{ event.change.rawType }}</i>
|
||||||
|
<span v-if="event.change.notes">
|
||||||
|
/ {{ $t("timetable.notes") }} {{ event.change.notes }}</span
|
||||||
|
>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="notes">
|
<span class="notes">
|
||||||
|
@ -130,6 +130,9 @@ function getTime(index) {
|
|||||||
>, {{ $t("timetable.notes") }} {{ getNotes(lesson.substitution) }}
|
>, {{ $t("timetable.notes") }} {{ getNotes(lesson.substitution) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<span class="info type" v-if="lesson.substitution">
|
||||||
|
<i>{{ lesson.substitution.rawType }}</i>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="times" v-if="getTime(index).start && !edit">
|
<div class="times" v-if="getTime(index).start && !edit">
|
||||||
<span>{{ getTime(index).start }} -</span>
|
<span>{{ getTime(index).start }} -</span>
|
||||||
|
@ -60,8 +60,11 @@ const substitutionsForDate = computed(() => {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
}}</span>
|
}}</span>
|
||||||
<span class="notes" v-if="substitution.notes">
|
<span class="detail">
|
||||||
{{ $t("timetable.notes") }} {{ substitution.notes }}
|
<i>{{ substitution.rawType }}</i>
|
||||||
|
<span v-if="substitution.notes">
|
||||||
|
/ {{ $t("timetable.notes") }} {{ substitution.notes }}</span
|
||||||
|
>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -98,7 +101,7 @@ const substitutionsForDate = computed(() => {
|
|||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
.notes {
|
.detail {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: 100;
|
font-weight: 100;
|
||||||
}
|
}
|
||||||
|
@ -24,7 +24,7 @@ const linkedTimetable = computed(() => {
|
|||||||
return (
|
return (
|
||||||
entry.lesson == index + 1 &&
|
entry.lesson == index + 1 &&
|
||||||
entryDay == props.date.getTime() &&
|
entryDay == props.date.getTime() &&
|
||||||
(entry.teacher == e.teacher || !entry.teacher || !e.teacher)
|
(entry.teacher == e.teacher || !entry.teacher)
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -89,8 +89,4 @@ select {
|
|||||||
select:hover {
|
select:hover {
|
||||||
background-color: var(--element-color-hover);
|
background-color: var(--element-color-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
select:focus {
|
|
||||||
border-color: var(--element-border-focus);
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
51
src/store.js
51
src/store.js
@ -30,19 +30,6 @@ export const activeProfileId = ref(
|
|||||||
localStorage.getItem("activeProfile") || profiles.value[0].id,
|
localStorage.getItem("activeProfile") || profiles.value[0].id,
|
||||||
);
|
);
|
||||||
|
|
||||||
watch(
|
|
||||||
() => activeProfile.value.classFilter,
|
|
||||||
() => {
|
|
||||||
fetchData(getNextAndPrevDay(selectedDate.value), false);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
export const localTimetables = ref(
|
|
||||||
JSON.parse(localStorage.getItem("timetables")) || [],
|
|
||||||
);
|
|
||||||
|
|
||||||
export const theme = ref(localStorage.getItem("theme") || "auto");
|
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
profiles,
|
profiles,
|
||||||
(newValue) => {
|
(newValue) => {
|
||||||
@ -53,6 +40,27 @@ watch(
|
|||||||
watch(activeProfileId, (newValue) => {
|
watch(activeProfileId, (newValue) => {
|
||||||
localStorage.setItem("activeProfile", newValue);
|
localStorage.setItem("activeProfile", newValue);
|
||||||
});
|
});
|
||||||
|
watch(
|
||||||
|
() => activeProfile.value.classFilter,
|
||||||
|
() => {
|
||||||
|
fetchData(getNextAndPrevDay(selectedDate.value), false);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export const cachedTimetables = ref(
|
||||||
|
JSON.parse(localStorage.getItem("cachedTimetables")) || {},
|
||||||
|
);
|
||||||
|
export const localTimetables = ref(
|
||||||
|
JSON.parse(localStorage.getItem("timetables")) || [],
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
cachedTimetables,
|
||||||
|
(newValue) => {
|
||||||
|
localStorage.setItem("cachedTimetables", JSON.stringify(newValue));
|
||||||
|
},
|
||||||
|
{ deep: true },
|
||||||
|
);
|
||||||
watch(
|
watch(
|
||||||
localTimetables,
|
localTimetables,
|
||||||
(newValue) => {
|
(newValue) => {
|
||||||
@ -60,6 +68,8 @@ watch(
|
|||||||
},
|
},
|
||||||
{ deep: true },
|
{ deep: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const theme = ref(localStorage.getItem("theme") || "auto");
|
||||||
watch(theme, (newValue) => {
|
watch(theme, (newValue) => {
|
||||||
localStorage.setItem("theme", newValue);
|
localStorage.setItem("theme", newValue);
|
||||||
});
|
});
|
||||||
@ -92,8 +102,12 @@ export const timetable = computed(() => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
export const sessionInfo = ref({});
|
export const sessionInfo = ref({});
|
||||||
export const timetables = ref([]);
|
export const timetables = ref(
|
||||||
export const times = ref([]);
|
(cachedTimetables.value[activeProfileId.value] || {}).timetables || [],
|
||||||
|
);
|
||||||
|
export const times = ref(
|
||||||
|
(cachedTimetables.value[activeProfileId.value] || {}).times || [],
|
||||||
|
);
|
||||||
export const substitutions = ref({});
|
export const substitutions = ref({});
|
||||||
export const history = ref({});
|
export const history = ref({});
|
||||||
export const classList = ref([]);
|
export const classList = ref([]);
|
||||||
@ -182,6 +196,13 @@ export async function fetchTimetables() {
|
|||||||
} else {
|
} else {
|
||||||
timetables.value = timetableData.timetables;
|
timetables.value = timetableData.timetables;
|
||||||
times.value = timetableData.times;
|
times.value = timetableData.times;
|
||||||
|
|
||||||
|
cachedTimetables.value[activeProfileId.value] =
|
||||||
|
structuredClone(timetableData);
|
||||||
|
for (const timetable of cachedTimetables.value[activeProfileId.value]
|
||||||
|
.timetables) {
|
||||||
|
timetable.fromCache = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -62,6 +62,7 @@ export const strings = {
|
|||||||
auto: "Auto",
|
auto: "Auto",
|
||||||
dark: "Dark",
|
dark: "Dark",
|
||||||
light: "Light",
|
light: "Light",
|
||||||
|
darker: "Darker",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
timetable: {
|
timetable: {
|
||||||
@ -192,6 +193,7 @@ export const strings = {
|
|||||||
auto: "Automatisch",
|
auto: "Automatisch",
|
||||||
dark: "Dunkel",
|
dark: "Dunkel",
|
||||||
light: "Hell",
|
light: "Hell",
|
||||||
|
darker: "Darker",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
timetable: {
|
timetable: {
|
||||||
|
@ -1,21 +1,24 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="login">
|
<div class="container">
|
||||||
<h1>Timetable V2</h1>
|
<div class="login">
|
||||||
<form action="/auth/login" method="POST">
|
<h1>Timetable V2</h1>
|
||||||
<input
|
<form action="/auth/login" method="POST">
|
||||||
type="password"
|
<input
|
||||||
name="password"
|
type="password"
|
||||||
autocomplete="current-password"
|
name="password"
|
||||||
placeholder="Password"
|
autocomplete="current-password"
|
||||||
/>
|
placeholder="Password"
|
||||||
<button type="submit">Login</button>
|
/>
|
||||||
</form>
|
<button type="submit">Login</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.login {
|
.container {
|
||||||
padding: 50px 10px;
|
height: 100%;
|
||||||
|
overflow-y: scroll;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login {
|
.login {
|
||||||
@ -25,6 +28,7 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
font-family: Arial, Helvetica, sans-serif;
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
|
padding: 30px 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
form {
|
form {
|
||||||
|
@ -1,9 +1,16 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import ExpandSection from "@/components/settings/expand-section.vue";
|
import ExpandSection from "@/components/settings/expand-section.vue";
|
||||||
import KeyCard from "@/components/settings/key-card.vue";
|
import KeyCard from "@/components/settings/key-card.vue";
|
||||||
import TimetableCard from "@/components/settings/timetable-card.vue";
|
import RadioCard from "@/components/settings/radio-card.vue";
|
||||||
import { baseUrl } from "@/store";
|
import { baseUrl } from "@/store";
|
||||||
import { PlusIcon, SaveIcon, XIcon, RefreshCwIcon } from "lucide-vue-next";
|
import {
|
||||||
|
PlusIcon,
|
||||||
|
SaveIcon,
|
||||||
|
XIcon,
|
||||||
|
RefreshCwIcon,
|
||||||
|
Edit2Icon,
|
||||||
|
TrashIcon,
|
||||||
|
} from "lucide-vue-next";
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
|
|
||||||
function confirm(message) {
|
function confirm(message) {
|
||||||
@ -146,24 +153,33 @@ updateData();
|
|||||||
<span>Cancel edit</span>
|
<span>Cancel edit</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<TimetableCard
|
<RadioCard
|
||||||
v-for="timetable in timetables"
|
v-for="timetable in timetables"
|
||||||
:key="timetable"
|
:key="timetable"
|
||||||
:timetable="timetable"
|
:title="timetable.title"
|
||||||
:editable="true"
|
:subtitle="`${$t('settings.source')}: ${timetable.source}, Class: ${
|
||||||
|
timetable.class
|
||||||
|
}, ID: ${timetable.id}`"
|
||||||
:selected="timetable.id == timetableEditId"
|
:selected="timetable.id == timetableEditId"
|
||||||
:admin="true"
|
>
|
||||||
@delete="deleteObject('timetable', timetable.id)"
|
<Edit2Icon
|
||||||
@edit="
|
@click="
|
||||||
() => {
|
() => {
|
||||||
timetableEditId = timetable.id;
|
timetableEditId = timetable.id;
|
||||||
timetableName = timetable.title;
|
timetableName = timetable.title;
|
||||||
timetableClass = timetable.class;
|
timetableClass = timetable.class;
|
||||||
timetableSource = timetable.source;
|
timetableSource = timetable.source;
|
||||||
timetableTrusted = timetable.trusted;
|
timetableTrusted = timetable.trusted;
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
|
<TrashIcon
|
||||||
|
@click="
|
||||||
|
if (confirm('Delete this timetable?'))
|
||||||
|
deleteObject('timetable', timetable.id);
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</RadioCard>
|
||||||
</div>
|
</div>
|
||||||
</ExpandSection>
|
</ExpandSection>
|
||||||
<ExpandSection title="Keys">
|
<ExpandSection title="Keys">
|
||||||
|
@ -12,8 +12,9 @@ import i18n, { language, localeNames } from "@/i18n";
|
|||||||
$t('settings.theme.auto'),
|
$t('settings.theme.auto'),
|
||||||
$t('settings.theme.light'),
|
$t('settings.theme.light'),
|
||||||
$t('settings.theme.dark'),
|
$t('settings.theme.dark'),
|
||||||
|
$t('settings.theme.darker'),
|
||||||
]"
|
]"
|
||||||
:values="['auto', 'light', 'dark']"
|
:values="['auto', 'light', 'dark', 'darker']"
|
||||||
v-model="theme"
|
v-model="theme"
|
||||||
/>
|
/>
|
||||||
<div class="spacer" />
|
<div class="spacer" />
|
||||||
|
@ -51,7 +51,7 @@ function importProfile(event) {
|
|||||||
|
|
||||||
function exportProfile(profile) {
|
function exportProfile(profile) {
|
||||||
download(
|
download(
|
||||||
JSON.stringify(profile),
|
new Blob(["\ufeff", JSON.stringify(profile)]),
|
||||||
`profile-${profile.id}.json`,
|
`profile-${profile.id}.json`,
|
||||||
"application/json",
|
"application/json",
|
||||||
);
|
);
|
||||||
|
@ -51,7 +51,7 @@ function importTimetable(event) {
|
|||||||
|
|
||||||
function exportTimetable(timetable) {
|
function exportTimetable(timetable) {
|
||||||
download(
|
download(
|
||||||
JSON.stringify(timetable),
|
new Blob(["\ufeff", JSON.stringify(timetable)]),
|
||||||
`timetable-${timetable.id}.json`,
|
`timetable-${timetable.id}.json`,
|
||||||
"application/json",
|
"application/json",
|
||||||
);
|
);
|
||||||
|
Reference in New Issue
Block a user