✨ Add profile management
- Save class filter, timetable and timetable groups in profiles - Easily switch between profiles - Rename profiles - Export/Import/Duplicate profiles
This commit is contained in:
@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { history, loadingFailed, classFilter } from "@/store";
|
import { history, loadingFailed, activeProfile } from "@/store";
|
||||||
import { getSubstitutionText } from "@/util";
|
import { getSubstitutionText } from "@/util";
|
||||||
import { computed } from "vue";
|
import { computed } from "vue";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
@ -68,7 +68,8 @@ const chars = {
|
|||||||
$t(
|
$t(
|
||||||
getSubstitutionText(
|
getSubstitutionText(
|
||||||
event.change,
|
event.change,
|
||||||
!classFilter || classFilter == "none",
|
!activeProfile.classFilter ||
|
||||||
|
activeProfile.classFilter == "none",
|
||||||
),
|
),
|
||||||
{
|
{
|
||||||
subject: event.change.change.subject,
|
subject: event.change.change.subject,
|
||||||
|
76
src/components/settings/profile-card.vue
Normal file
76
src/components/settings/profile-card.vue
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, watch } from "vue";
|
||||||
|
import {
|
||||||
|
CheckIcon,
|
||||||
|
XIcon,
|
||||||
|
Edit2Icon,
|
||||||
|
TrashIcon,
|
||||||
|
AlertCircleIcon,
|
||||||
|
CopyIcon,
|
||||||
|
DownloadIcon,
|
||||||
|
} from "lucide-vue-next";
|
||||||
|
import RadioCard from "./radio-card.vue";
|
||||||
|
|
||||||
|
const props = defineProps(["profile", "canDelete", "selected"]);
|
||||||
|
defineEmits(["click", "delete", "copy", "export", "rename"]);
|
||||||
|
|
||||||
|
const deleteConfirm = ref(false);
|
||||||
|
|
||||||
|
const renameText = ref(props.profile.name);
|
||||||
|
const renameActive = ref(false);
|
||||||
|
const rerenderCard = ref(0);
|
||||||
|
function rename() {
|
||||||
|
renameActive.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(deleteConfirm, (value) => {
|
||||||
|
if (value) {
|
||||||
|
setTimeout(() => (deleteConfirm.value = false), 2000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<RadioCard
|
||||||
|
@click="$emit('click')"
|
||||||
|
@change="(event) => (renameText = event.target.innerText)"
|
||||||
|
:title="profile.name"
|
||||||
|
:subtitle="`${$t('settings.classFilter')}: ${profile.classFilter}, ${$t(
|
||||||
|
'settings.timetableGroups',
|
||||||
|
)}: ${profile.timetableGroups.join('; ')}`"
|
||||||
|
:titleEditable="renameActive"
|
||||||
|
:selected="selected"
|
||||||
|
:key="rerenderCard"
|
||||||
|
>
|
||||||
|
<CheckIcon
|
||||||
|
v-if="renameActive"
|
||||||
|
@click="
|
||||||
|
() => {
|
||||||
|
$emit('rename', renameText);
|
||||||
|
renameActive = false;
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<XIcon
|
||||||
|
v-if="renameActive"
|
||||||
|
@click="
|
||||||
|
() => {
|
||||||
|
renameActive = false;
|
||||||
|
rerenderCard++;
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<Edit2Icon v-if="!renameActive" @click="rename()" />
|
||||||
|
<DownloadIcon @click="$emit('export')" />
|
||||||
|
<CopyIcon @click="$emit('copy')" />
|
||||||
|
<TrashIcon
|
||||||
|
v-if="canDelete && !deleteConfirm"
|
||||||
|
@click="deleteConfirm = true"
|
||||||
|
/>
|
||||||
|
<AlertCircleIcon
|
||||||
|
v-if="deleteConfirm"
|
||||||
|
color="red"
|
||||||
|
@click="$emit('delete')"
|
||||||
|
/>
|
||||||
|
</RadioCard>
|
||||||
|
</template>
|
76
src/components/settings/radio-card.vue
Normal file
76
src/components/settings/radio-card.vue
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
<script setup>
|
||||||
|
import { CircleIcon, CheckCircleIcon } from "lucide-vue-next";
|
||||||
|
import { watch, ref } from "vue";
|
||||||
|
|
||||||
|
const props = defineProps(["title", "subtitle", "selected", "titleEditable"]);
|
||||||
|
defineEmits(["click", "change"]);
|
||||||
|
|
||||||
|
const titleElement = ref();
|
||||||
|
watch(
|
||||||
|
() => props.titleEditable,
|
||||||
|
() => setTimeout(() => titleElement.value.focus(), 0),
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="card">
|
||||||
|
<div class="button" @click="$emit('click')">
|
||||||
|
<CheckCircleIcon v-if="selected" />
|
||||||
|
<CircleIcon v-else />
|
||||||
|
|
||||||
|
<div class="info">
|
||||||
|
<span
|
||||||
|
:contenteditable="titleEditable"
|
||||||
|
ref="titleElement"
|
||||||
|
@input="$emit('change', $event)"
|
||||||
|
class="name"
|
||||||
|
>{{ title }}</span
|
||||||
|
>
|
||||||
|
<span class="detail">{{ subtitle }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="buttons">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.card {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr auto;
|
||||||
|
gap: 15px;
|
||||||
|
background-color: var(--element-color);
|
||||||
|
border-radius: 7px;
|
||||||
|
padding: 10px;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
gap: 15px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: auto auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 7px;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
</style>
|
@ -1,15 +1,14 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, watch } from "vue";
|
import { ref, watch } from "vue";
|
||||||
import {
|
import {
|
||||||
CircleIcon,
|
|
||||||
CheckCircleIcon,
|
|
||||||
Edit2Icon,
|
Edit2Icon,
|
||||||
TrashIcon,
|
TrashIcon,
|
||||||
AlertCircleIcon,
|
AlertCircleIcon,
|
||||||
CopyIcon,
|
CopyIcon,
|
||||||
UploadCloudIcon,
|
UploadCloudIcon,
|
||||||
|
DownloadIcon,
|
||||||
} from "lucide-vue-next";
|
} from "lucide-vue-next";
|
||||||
import { DownloadIcon } from "lucide-vue-next";
|
import RadioCard from "./radio-card.vue";
|
||||||
|
|
||||||
defineProps(["timetable", "editable", "remote", "selected", "admin"]);
|
defineProps(["timetable", "editable", "remote", "selected", "admin"]);
|
||||||
defineEmits(["click", "edit", "delete", "copy", "export", "upload"]);
|
defineEmits(["click", "edit", "delete", "copy", "export", "upload"]);
|
||||||
@ -24,75 +23,24 @@ watch(deleteConfirm, (value) => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="card">
|
<RadioCard
|
||||||
<div class="button" @click="$emit('click')">
|
@click="$emit('click')"
|
||||||
<CheckCircleIcon v-if="selected" />
|
:title="timetable.title"
|
||||||
<CircleIcon v-else />
|
:subtitle="`${$t('settings.source')}: ${timetable.source}`"
|
||||||
|
:selected="selected"
|
||||||
<div class="info">
|
>
|
||||||
<span class="name">{{ timetable.title }}</span>
|
<DownloadIcon v-if="!admin" @click="$emit('export')" />
|
||||||
<span class="detail"
|
<Edit2Icon v-if="editable && !remote" @click="$emit('edit')" />
|
||||||
>{{ $t("settings.source") }}: {{ timetable.source }}
|
<UploadCloudIcon v-if="editable && remote" @click="$emit('upload')" />
|
||||||
<span v-if="admin"
|
<CopyIcon v-if="!admin" @click="$emit('copy')" />
|
||||||
>, Class: {{ timetable.class }}, Id: {{ timetable.id }}</span
|
<TrashIcon
|
||||||
></span
|
v-if="editable && !remote && !deleteConfirm"
|
||||||
>
|
@click="deleteConfirm = true"
|
||||||
</div>
|
/>
|
||||||
</div>
|
<AlertCircleIcon
|
||||||
|
v-if="editable && deleteConfirm"
|
||||||
<div class="buttons">
|
color="red"
|
||||||
<DownloadIcon v-if="!admin" @click="$emit('export')" />
|
@click="$emit('delete')"
|
||||||
<Edit2Icon v-if="editable && !remote" @click="$emit('edit')" />
|
/>
|
||||||
<UploadCloudIcon v-if="editable && remote" @click="$emit('upload')" />
|
</RadioCard>
|
||||||
<CopyIcon v-if="!admin" @click="$emit('copy')" />
|
|
||||||
<TrashIcon
|
|
||||||
v-if="editable && !remote && !deleteConfirm"
|
|
||||||
@click="deleteConfirm = true"
|
|
||||||
/>
|
|
||||||
<AlertCircleIcon
|
|
||||||
v-if="editable && deleteConfirm"
|
|
||||||
color="red"
|
|
||||||
@click="$emit('delete')"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.card {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr auto;
|
|
||||||
gap: 15px;
|
|
||||||
background-color: var(--element-color);
|
|
||||||
border-radius: 7px;
|
|
||||||
padding: 10px;
|
|
||||||
align-items: center;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: auto 1fr;
|
|
||||||
gap: 15px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info {
|
|
||||||
display: grid;
|
|
||||||
grid-template-rows: 1fr 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.name {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail {
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.buttons {
|
|
||||||
display: flex;
|
|
||||||
gap: 7px;
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { substitutions, loadingFailed, classFilter } from "@/store";
|
import { substitutions, loadingFailed, activeProfile } from "@/store";
|
||||||
import { getSubstitutionText, getSubstitutionColor } from "@/util";
|
import { getSubstitutionText, getSubstitutionColor } from "@/util";
|
||||||
import { computed } from "vue";
|
import { computed } from "vue";
|
||||||
import InfoCard from "@/components/info-card.vue";
|
import InfoCard from "@/components/info-card.vue";
|
||||||
@ -49,7 +49,7 @@ const substitutionsForDate = computed(() => {
|
|||||||
$t(
|
$t(
|
||||||
getSubstitutionText(
|
getSubstitutionText(
|
||||||
substitution,
|
substitution,
|
||||||
!classFilter || classFilter == "none",
|
!activeProfile.classFilter || activeProfile.classFilter == "none",
|
||||||
),
|
),
|
||||||
{
|
{
|
||||||
subject: substitution.change.subject,
|
subject: substitution.change.subject,
|
||||||
|
@ -13,6 +13,7 @@ import TimetableSettings from "@/views/settings/TimetableSettings.vue";
|
|||||||
import TimetableEditor from "@/views/settings/TimetableEditor.vue";
|
import TimetableEditor from "@/views/settings/TimetableEditor.vue";
|
||||||
import TimetableGroupSettings from "@/views/settings/TimetableGroupSettings.vue";
|
import TimetableGroupSettings from "@/views/settings/TimetableGroupSettings.vue";
|
||||||
import AppearanceSettings from "@/views/settings/AppearanceSettings.vue";
|
import AppearanceSettings from "@/views/settings/AppearanceSettings.vue";
|
||||||
|
import ProfileSettings from "@/views/settings/ProfileSettings.vue";
|
||||||
import KeySettings from "@/views/settings/KeySettings.vue";
|
import KeySettings from "@/views/settings/KeySettings.vue";
|
||||||
import AdminSettings from "@/views/settings/AdminSettings.vue";
|
import AdminSettings from "@/views/settings/AdminSettings.vue";
|
||||||
import AboutPage from "@/views/settings/AboutPage.vue";
|
import AboutPage from "@/views/settings/AboutPage.vue";
|
||||||
@ -73,6 +74,11 @@ const router = createRouter({
|
|||||||
name: "title.settings.appearance",
|
name: "title.settings.appearance",
|
||||||
component: AppearanceSettings,
|
component: AppearanceSettings,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "profiles",
|
||||||
|
name: "title.settings.profiles",
|
||||||
|
component: ProfileSettings,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "keys",
|
path: "keys",
|
||||||
name: "title.settings.keys",
|
name: "title.settings.keys",
|
||||||
|
61
src/store.js
61
src/store.js
@ -9,31 +9,50 @@ export const loadingProgress = ref(0);
|
|||||||
export const loadingFailed = ref(false);
|
export const loadingFailed = ref(false);
|
||||||
|
|
||||||
/* Preferences */
|
/* Preferences */
|
||||||
export const classFilter = ref(localStorage.getItem("classFilter") || "none");
|
export const profiles = ref(
|
||||||
export const timetableId = ref(localStorage.getItem("timetableId") || "none");
|
JSON.parse(localStorage.getItem("profiles")) || [
|
||||||
export const timetableGroups = ref(
|
{
|
||||||
JSON.parse(localStorage.getItem("timetableGroups") || "[]"),
|
id: 0,
|
||||||
|
name: "Default Profile",
|
||||||
|
classFilter: "none",
|
||||||
|
timetableId: "none",
|
||||||
|
timetableGroups: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
export const activeProfile = computed(() => {
|
||||||
|
return (
|
||||||
|
profiles.value.find((e) => e.id == activeProfileId.value) ||
|
||||||
|
profiles.value[0]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
export const activeProfileId = ref(
|
||||||
|
localStorage.getItem("activeProfile") || profiles.value[0].id,
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => activeProfile.value.classFilter,
|
||||||
|
() => {
|
||||||
|
fetchData(getNextAndPrevDay(selectedDate.value), false);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
export const localTimetables = ref(
|
export const localTimetables = ref(
|
||||||
JSON.parse(localStorage.getItem("timetables")) || [],
|
JSON.parse(localStorage.getItem("timetables")) || [],
|
||||||
);
|
);
|
||||||
|
|
||||||
export const theme = ref(localStorage.getItem("theme") || "auto");
|
export const theme = ref(localStorage.getItem("theme") || "auto");
|
||||||
|
|
||||||
watch(classFilter, (newValue) => {
|
|
||||||
localStorage.setItem("classFilter", newValue);
|
|
||||||
fetchData(getNextAndPrevDay(selectedDate.value), false);
|
|
||||||
});
|
|
||||||
watch(timetableId, (newValue) => {
|
|
||||||
localStorage.setItem("timetableId", newValue);
|
|
||||||
});
|
|
||||||
watch(
|
watch(
|
||||||
timetableGroups,
|
profiles,
|
||||||
(newValue) => {
|
(newValue) => {
|
||||||
localStorage.setItem("timetableGroups", JSON.stringify(newValue));
|
localStorage.setItem("profiles", JSON.stringify(newValue));
|
||||||
},
|
},
|
||||||
{ deep: true },
|
{ deep: true },
|
||||||
);
|
);
|
||||||
|
watch(activeProfileId, (newValue) => {
|
||||||
|
localStorage.setItem("activeProfile", newValue);
|
||||||
|
});
|
||||||
watch(
|
watch(
|
||||||
localTimetables,
|
localTimetables,
|
||||||
(newValue) => {
|
(newValue) => {
|
||||||
@ -63,10 +82,10 @@ export const changeDate = ref(new Date());
|
|||||||
/* Data store */
|
/* Data store */
|
||||||
export const timetable = computed(() => {
|
export const timetable = computed(() => {
|
||||||
const localTimetable = localTimetables.value.find(
|
const localTimetable = localTimetables.value.find(
|
||||||
(e) => e.id == timetableId.value,
|
(e) => e.id == activeProfile.value.timetableId,
|
||||||
);
|
);
|
||||||
const remoteTimetable = timetables.value.find(
|
const remoteTimetable = timetables.value.find(
|
||||||
(e) => e.id == timetableId.value,
|
(e) => e.id == activeProfile.value.timetableId,
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
localTimetable || remoteTimetable || { trusted: true, source: "", data: [] }
|
localTimetable || remoteTimetable || { trusted: true, source: "", data: [] }
|
||||||
@ -154,7 +173,7 @@ export async function fetchClassList() {
|
|||||||
|
|
||||||
export async function fetchTimetables() {
|
export async function fetchTimetables() {
|
||||||
const timetableResponse = await fetch(
|
const timetableResponse = await fetch(
|
||||||
`${baseUrl}/timetable?class=${classFilter.value}`,
|
`${baseUrl}/timetable?class=${activeProfile.value.classFilter}`,
|
||||||
);
|
);
|
||||||
const timetableData = await timetableResponse.json();
|
const timetableData = await timetableResponse.json();
|
||||||
if (timetableData.error) {
|
if (timetableData.error) {
|
||||||
@ -169,9 +188,9 @@ export async function fetchTimetables() {
|
|||||||
export async function fetchSubstitutions(day) {
|
export async function fetchSubstitutions(day) {
|
||||||
const requestDate = `?date=${day}`;
|
const requestDate = `?date=${day}`;
|
||||||
const substitutionResponse = await fetch(
|
const substitutionResponse = await fetch(
|
||||||
classFilter.value == "none"
|
activeProfile.value.classFilter == "none"
|
||||||
? `${baseUrl}/substitutions${requestDate}`
|
? `${baseUrl}/substitutions${requestDate}`
|
||||||
: `${baseUrl}/substitutions${requestDate}&class=${classFilter.value}`,
|
: `${baseUrl}/substitutions${requestDate}&class=${activeProfile.value.classFilter}`,
|
||||||
);
|
);
|
||||||
const substitutionData = await substitutionResponse.json();
|
const substitutionData = await substitutionResponse.json();
|
||||||
substitutions.value[day] = substitutionData;
|
substitutions.value[day] = substitutionData;
|
||||||
@ -180,9 +199,9 @@ export async function fetchSubstitutions(day) {
|
|||||||
export async function fetchHistory(day) {
|
export async function fetchHistory(day) {
|
||||||
const requestDate = `?date=${day}`;
|
const requestDate = `?date=${day}`;
|
||||||
const historyResponse = await fetch(
|
const historyResponse = await fetch(
|
||||||
classFilter.value == "none"
|
activeProfile.value.classFilter == "none"
|
||||||
? `${baseUrl}/history${requestDate}`
|
? `${baseUrl}/history${requestDate}`
|
||||||
: `${baseUrl}/history${requestDate}&class=${classFilter.value}`,
|
: `${baseUrl}/history${requestDate}&class=${activeProfile.value.classFilter}`,
|
||||||
);
|
);
|
||||||
const historyData = await historyResponse.json();
|
const historyData = await historyResponse.json();
|
||||||
if (historyData.error) console.warn("API Error: " + historyData.error);
|
if (historyData.error) console.warn("API Error: " + historyData.error);
|
||||||
@ -201,7 +220,7 @@ export const parsedTimetable = computed(() => {
|
|||||||
// (timetable groups)
|
// (timetable groups)
|
||||||
if (Array.isArray(lesson) && lesson.length > 1) {
|
if (Array.isArray(lesson) && lesson.length > 1) {
|
||||||
let matchingLesson = lesson.find((e) =>
|
let matchingLesson = lesson.find((e) =>
|
||||||
timetableGroups.value.includes(e.group),
|
activeProfile.value.timetableGroups.includes(e.group),
|
||||||
);
|
);
|
||||||
// If no valid timetable group is configured
|
// If no valid timetable group is configured
|
||||||
// add a dummy lesson showing a notice
|
// add a dummy lesson showing a notice
|
||||||
|
@ -13,6 +13,7 @@ export const strings = {
|
|||||||
timetable: "Manage Timetables",
|
timetable: "Manage Timetables",
|
||||||
groups: "Timetable Groups",
|
groups: "Timetable Groups",
|
||||||
appearance: "Appearance",
|
appearance: "Appearance",
|
||||||
|
profiles: "Profiles",
|
||||||
keys: "Manage Keys",
|
keys: "Manage Keys",
|
||||||
admin: "Admin Settings",
|
admin: "Admin Settings",
|
||||||
about: "About",
|
about: "About",
|
||||||
@ -27,6 +28,7 @@ export const strings = {
|
|||||||
language: "Language",
|
language: "Language",
|
||||||
about: "About",
|
about: "About",
|
||||||
theme: "Theme",
|
theme: "Theme",
|
||||||
|
profiles: "Saved Profiles",
|
||||||
keys: "Manage Keys",
|
keys: "Manage Keys",
|
||||||
},
|
},
|
||||||
text: {
|
text: {
|
||||||
@ -41,6 +43,10 @@ export const strings = {
|
|||||||
"This Tool queries and parses the latest substitution-plan data every minute. The correctness of the data can in no way be guaranteed, so please check the data against the official plan if something seems wrong! Due to the format of the plan files, it it sometimes not easily possible to extract the correct data, so the plan displayed here may not be correct.",
|
"This Tool queries and parses the latest substitution-plan data every minute. The correctness of the data can in no way be guaranteed, so please check the data against the official plan if something seems wrong! Due to the format of the plan files, it it sometimes not easily possible to extract the correct data, so the plan displayed here may not be correct.",
|
||||||
theme:
|
theme:
|
||||||
"Select a Theme to change the colors of the app. The 'Auto' option selects a theme based on your system preferences.",
|
"Select a Theme to change the colors of the app. The 'Auto' option selects a theme based on your system preferences.",
|
||||||
|
profiles:
|
||||||
|
"You can create multiple profiles containing the class filter, selected timetable and configured timetable groups here and easily switch between.",
|
||||||
|
createProfile: "Create Profile",
|
||||||
|
importProfile: "Import Profile",
|
||||||
keys: "Keys are used to give you special permissions, for example editing a timetable. You can enter keys that you received here.",
|
keys: "Keys are used to give you special permissions, for example editing a timetable. You can enter keys that you received here.",
|
||||||
},
|
},
|
||||||
other: "Other",
|
other: "Other",
|
||||||
@ -48,6 +54,8 @@ export const strings = {
|
|||||||
none: "None",
|
none: "None",
|
||||||
version: "Version",
|
version: "Version",
|
||||||
source: "Source",
|
source: "Source",
|
||||||
|
classFilter: "Filter",
|
||||||
|
timetableGroups: "Timetable Groups",
|
||||||
key: "Key",
|
key: "Key",
|
||||||
invalidKey: "This key does not exist!",
|
invalidKey: "This key does not exist!",
|
||||||
theme: {
|
theme: {
|
||||||
@ -136,6 +144,7 @@ export const strings = {
|
|||||||
timetable: "Stundenpläne Verwalten",
|
timetable: "Stundenpläne Verwalten",
|
||||||
groups: "Stundenplan-Gruppen",
|
groups: "Stundenplan-Gruppen",
|
||||||
appearance: "Aussehen",
|
appearance: "Aussehen",
|
||||||
|
profiles: "Profile",
|
||||||
keys: "Schlüssel Verwalten",
|
keys: "Schlüssel Verwalten",
|
||||||
about: "Über",
|
about: "Über",
|
||||||
},
|
},
|
||||||
@ -149,6 +158,7 @@ export const strings = {
|
|||||||
language: "Sprache",
|
language: "Sprache",
|
||||||
about: "Über diese Anwendung",
|
about: "Über diese Anwendung",
|
||||||
theme: "Farbschema",
|
theme: "Farbschema",
|
||||||
|
profiles: "Gespeicherte Profile",
|
||||||
keys: "Schlüssel Verwalten",
|
keys: "Schlüssel Verwalten",
|
||||||
},
|
},
|
||||||
text: {
|
text: {
|
||||||
@ -163,6 +173,10 @@ export const strings = {
|
|||||||
"Diese Anwendung fragt jede Minute die neusten Vertretungsplan-Daten an und verarbeitet sie, um sie dir schöner und besser lesbar anzuzeigen. Die Richtigkeit dieser Daten kann nicht garantiert werden, da es manchmal kompliziert ist, die richtigen Daten zu extrahieren. Wenn etwas nicht richtig aussieht, überprüfe es bitte auf dem offiziellen Plan!",
|
"Diese Anwendung fragt jede Minute die neusten Vertretungsplan-Daten an und verarbeitet sie, um sie dir schöner und besser lesbar anzuzeigen. Die Richtigkeit dieser Daten kann nicht garantiert werden, da es manchmal kompliziert ist, die richtigen Daten zu extrahieren. Wenn etwas nicht richtig aussieht, überprüfe es bitte auf dem offiziellen Plan!",
|
||||||
theme:
|
theme:
|
||||||
"Wähle ein Farbschema aus, um die Farben dieser Anwendung anzupassen. Die Option 'Automatisch' wählt ein Farbschema basierend auf den Einstellungen deines Systems aus.",
|
"Wähle ein Farbschema aus, um die Farben dieser Anwendung anzupassen. Die Option 'Automatisch' wählt ein Farbschema basierend auf den Einstellungen deines Systems aus.",
|
||||||
|
profiles:
|
||||||
|
"Hier kannst du mehrere Profile erstellen, welche deinen ausgewählen Filter, deinen Stundenplan und deine eingestellen Stundenplan-Gruppen speichern. So kannst du ganz einfach zwischen ihnen wechseln.",
|
||||||
|
createProfile: "Profil erstellen",
|
||||||
|
importProfile: "Profil importieren",
|
||||||
keys: "Schlüssel können dir erweiterte rechte geben, zum Beispiel zum editieren eines Stundenplans. Wenn du einen Schlüssel bekommen hast, kannst du ihn hier eingeben.",
|
keys: "Schlüssel können dir erweiterte rechte geben, zum Beispiel zum editieren eines Stundenplans. Wenn du einen Schlüssel bekommen hast, kannst du ihn hier eingeben.",
|
||||||
},
|
},
|
||||||
other: "Andere",
|
other: "Andere",
|
||||||
@ -170,6 +184,8 @@ export const strings = {
|
|||||||
none: "Keine",
|
none: "Keine",
|
||||||
version: "Version",
|
version: "Version",
|
||||||
source: "Quelle",
|
source: "Quelle",
|
||||||
|
classFilter: "Filter",
|
||||||
|
timetableGroups: "Stundenplan-Gruppen",
|
||||||
key: "Schlüssel",
|
key: "Schlüssel",
|
||||||
invalidKey: "Dieser Schlüssel existiert nicht!",
|
invalidKey: "Dieser Schlüssel existiert nicht!",
|
||||||
theme: {
|
theme: {
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
import ScrollableContainer from "@/components/scrollable-container.vue";
|
import ScrollableContainer from "@/components/scrollable-container.vue";
|
||||||
import PageCard from "@/components/settings/page-card.vue";
|
import PageCard from "@/components/settings/page-card.vue";
|
||||||
import { hasPermission } from "@/permission";
|
import { hasPermission } from "@/permission";
|
||||||
|
import { BookmarkIcon } from "lucide-vue-next";
|
||||||
import {
|
import {
|
||||||
FilterIcon,
|
FilterIcon,
|
||||||
CalendarIcon,
|
CalendarIcon,
|
||||||
@ -38,6 +39,11 @@ import {
|
|||||||
:icon="PaletteIcon"
|
:icon="PaletteIcon"
|
||||||
route="settings/appearance"
|
route="settings/appearance"
|
||||||
/>
|
/>
|
||||||
|
<PageCard
|
||||||
|
:name="$t('title.settings.profiles')"
|
||||||
|
:icon="BookmarkIcon"
|
||||||
|
route="settings/profiles"
|
||||||
|
/>
|
||||||
<PageCard
|
<PageCard
|
||||||
:name="$t('title.settings.keys')"
|
:name="$t('title.settings.keys')"
|
||||||
:icon="KeyRoundIcon"
|
:icon="KeyRoundIcon"
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
import TimetableSetup from "@/components/timetable-setup.vue";
|
import TimetableSetup from "@/components/timetable-setup.vue";
|
||||||
import {
|
import {
|
||||||
classList,
|
classList,
|
||||||
classFilter,
|
activeProfile,
|
||||||
timetable,
|
timetable,
|
||||||
loadingFailed,
|
loadingFailed,
|
||||||
loading,
|
loading,
|
||||||
@ -15,13 +15,15 @@ import { ClockIcon, CloudOffIcon, CalendarOffIcon } from "lucide-vue-next";
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<TimetableSetup
|
<TimetableSetup
|
||||||
v-show="classFilter == 'none'"
|
v-show="activeProfile.classFilter == 'none'"
|
||||||
:options="classList"
|
:options="classList"
|
||||||
v-model="classFilter"
|
v-model="activeProfile.classFilter"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
class="cardContainer"
|
class="cardContainer"
|
||||||
v-show="(timetable.data || []).length == 0 && classFilter != 'none'"
|
v-show="
|
||||||
|
(timetable.data || []).length == 0 && activeProfile.classFilter != 'none'
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<InfoCard
|
<InfoCard
|
||||||
class="card"
|
class="card"
|
||||||
@ -45,7 +47,10 @@ import { ClockIcon, CloudOffIcon, CalendarOffIcon } from "lucide-vue-next";
|
|||||||
:text="$t('infoCard.texts.loadingFailed')"
|
:text="$t('infoCard.texts.loadingFailed')"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<DayCarousel v-show="classFilter != 'none'" :element="TimetableList" />
|
<DayCarousel
|
||||||
|
v-show="activeProfile.classFilter != 'none'"
|
||||||
|
:element="TimetableList"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { classList, classFilter } from "@/store";
|
import { classList, activeProfile } from "@/store";
|
||||||
import RadioButtons from "@/components/settings/radio-buttons.vue";
|
import RadioButtons from "@/components/settings/radio-buttons.vue";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -9,7 +9,7 @@ import RadioButtons from "@/components/settings/radio-buttons.vue";
|
|||||||
<RadioButtons
|
<RadioButtons
|
||||||
:options="['None', ...classList]"
|
:options="['None', ...classList]"
|
||||||
:values="['none', ...classList]"
|
:values="['none', ...classList]"
|
||||||
v-model="classFilter"
|
v-model="activeProfile.classFilter"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
134
src/views/settings/ProfileSettings.vue
Normal file
134
src/views/settings/ProfileSettings.vue
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
<script setup>
|
||||||
|
import { profiles, activeProfileId } from "@/store";
|
||||||
|
import { PlusIcon, PaperclipIcon } from "lucide-vue-next";
|
||||||
|
import { ref, toRaw } from "vue";
|
||||||
|
import download from "downloadjs";
|
||||||
|
import ProfileCard from "@/components/settings/profile-card.vue";
|
||||||
|
|
||||||
|
function createProfile() {
|
||||||
|
profiles.value.push({
|
||||||
|
id: new Date().getTime(),
|
||||||
|
name: "New Profile",
|
||||||
|
classFilter: "none",
|
||||||
|
timetableId: "none",
|
||||||
|
timetableGroups: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyProfile(profile) {
|
||||||
|
const newProfile = structuredClone(toRaw(profile));
|
||||||
|
newProfile.name = "Copy of " + profile.name;
|
||||||
|
newProfile.id = new Date().getTime();
|
||||||
|
profiles.value.push(newProfile);
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileInput = ref();
|
||||||
|
function importProfile(event) {
|
||||||
|
const file = event.target.files[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
const contents = e.target.result;
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(contents);
|
||||||
|
if (
|
||||||
|
!data.name ||
|
||||||
|
!data.classFilter ||
|
||||||
|
!data.timetableId ||
|
||||||
|
!data.timetableGroups
|
||||||
|
)
|
||||||
|
throw "Invalid data";
|
||||||
|
data.id = new Date().getTime();
|
||||||
|
profiles.value.push(data);
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e.stack);
|
||||||
|
alert("Import failed! Check your profile file!");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.readAsText(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportProfile(profile) {
|
||||||
|
download(
|
||||||
|
JSON.stringify(profile),
|
||||||
|
`profile-${profile.id}.json`,
|
||||||
|
"application/json",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="content">
|
||||||
|
<h2>{{ $t("settings.heading.profiles") }}</h2>
|
||||||
|
<p>{{ $t("settings.text.profiles") }}</p>
|
||||||
|
<div class="list">
|
||||||
|
<ProfileCard
|
||||||
|
v-for="profile in profiles"
|
||||||
|
:key="profile.id"
|
||||||
|
:profile="profile"
|
||||||
|
:canDelete="profiles.length > 1 && activeProfileId != profile.id"
|
||||||
|
:selected="activeProfileId == profile.id"
|
||||||
|
@click="activeProfileId = profile.id"
|
||||||
|
@copy="copyProfile(profile)"
|
||||||
|
@rename="(name) => (profile.name = name)"
|
||||||
|
@delete="
|
||||||
|
profiles.splice(
|
||||||
|
profiles.findIndex((e) => e.id == profile.id),
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
@export="exportProfile(profile)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="buttons">
|
||||||
|
<div class="create" @click="createProfile">
|
||||||
|
<PlusIcon /> {{ $t("settings.text.createProfile") }}
|
||||||
|
</div>
|
||||||
|
<div class="import" @click="fileInput.click()">
|
||||||
|
<PaperclipIcon /> {{ $t("settings.text.importProfile") }}
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
ref="fileInput"
|
||||||
|
style="display: none"
|
||||||
|
@change="importProfile"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
h2 {
|
||||||
|
margin: 5px 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 5px 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list {
|
||||||
|
margin-top: 15px;
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 5px;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create,
|
||||||
|
.import {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px;
|
||||||
|
gap: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { possibleTimetableGroups, timetableGroups } from "@/store";
|
import { possibleTimetableGroups, activeProfile } from "@/store";
|
||||||
import MultiselectButtons from "@/components/settings/multiselect-buttons.vue";
|
import MultiselectButtons from "@/components/settings/multiselect-buttons.vue";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -9,7 +9,7 @@ import MultiselectButtons from "@/components/settings/multiselect-buttons.vue";
|
|||||||
<MultiselectButtons
|
<MultiselectButtons
|
||||||
:options="possibleTimetableGroups"
|
:options="possibleTimetableGroups"
|
||||||
:values="possibleTimetableGroups"
|
:values="possibleTimetableGroups"
|
||||||
v-model="timetableGroups"
|
v-model="activeProfile.timetableGroups"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@ import {
|
|||||||
timetable,
|
timetable,
|
||||||
timetables,
|
timetables,
|
||||||
localTimetables,
|
localTimetables,
|
||||||
timetableId,
|
activeProfile,
|
||||||
baseUrl,
|
baseUrl,
|
||||||
fetchTimetables,
|
fetchTimetables,
|
||||||
} from "@/store";
|
} from "@/store";
|
||||||
@ -78,7 +78,7 @@ async function uploadTimetable(id) {
|
|||||||
} else {
|
} else {
|
||||||
loadingProgress.value = 0.5;
|
loadingProgress.value = 0.5;
|
||||||
await fetchTimetables();
|
await fetchTimetables();
|
||||||
timetableId.value = id;
|
activeProfile.value.timetableId = id;
|
||||||
}
|
}
|
||||||
|
|
||||||
loadingProgress.value = 1;
|
loadingProgress.value = 1;
|
||||||
@ -94,10 +94,10 @@ async function uploadTimetable(id) {
|
|||||||
v-for="timetable in localTimetables"
|
v-for="timetable in localTimetables"
|
||||||
:key="timetable.id"
|
:key="timetable.id"
|
||||||
:timetable="timetable"
|
:timetable="timetable"
|
||||||
:selected="timetableId == timetable.id"
|
:selected="activeProfile.timetableId == timetable.id"
|
||||||
:editable="true"
|
:editable="true"
|
||||||
:remote="false"
|
:remote="false"
|
||||||
@click="timetableId = timetable.id"
|
@click="activeProfile.timetableId = timetable.id"
|
||||||
@edit="$router.push('timetable/edit/' + timetable.id)"
|
@edit="$router.push('timetable/edit/' + timetable.id)"
|
||||||
@copy="copyTimetable(timetable)"
|
@copy="copyTimetable(timetable)"
|
||||||
@delete="
|
@delete="
|
||||||
@ -129,10 +129,10 @@ async function uploadTimetable(id) {
|
|||||||
v-for="timetable in timetables"
|
v-for="timetable in timetables"
|
||||||
:key="timetable.id"
|
:key="timetable.id"
|
||||||
:timetable="timetable"
|
:timetable="timetable"
|
||||||
:selected="timetableId == timetable.id"
|
:selected="activeProfile.timetableId == timetable.id"
|
||||||
:editable="canEditTimetable(timetable.id)"
|
:editable="canEditTimetable(timetable.id)"
|
||||||
:remote="true"
|
:remote="true"
|
||||||
@click="timetableId = timetable.id"
|
@click="activeProfile.timetableId = timetable.id"
|
||||||
@copy="copyTimetable(timetable)"
|
@copy="copyTimetable(timetable)"
|
||||||
@export="exportTimetable(timetable)"
|
@export="exportTimetable(timetable)"
|
||||||
@upload="uploadTimetable(timetable.id)"
|
@upload="uploadTimetable(timetable.id)"
|
||||||
|
Reference in New Issue
Block a user