✨ 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>
|
||||
import { history, loadingFailed, classFilter } from "@/store";
|
||||
import { history, loadingFailed, activeProfile } from "@/store";
|
||||
import { getSubstitutionText } from "@/util";
|
||||
import { computed } from "vue";
|
||||
import dayjs from "dayjs";
|
||||
@ -68,7 +68,8 @@ const chars = {
|
||||
$t(
|
||||
getSubstitutionText(
|
||||
event.change,
|
||||
!classFilter || classFilter == "none",
|
||||
!activeProfile.classFilter ||
|
||||
activeProfile.classFilter == "none",
|
||||
),
|
||||
{
|
||||
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>
|
||||
import { ref, watch } from "vue";
|
||||
import {
|
||||
CircleIcon,
|
||||
CheckCircleIcon,
|
||||
Edit2Icon,
|
||||
TrashIcon,
|
||||
AlertCircleIcon,
|
||||
CopyIcon,
|
||||
UploadCloudIcon,
|
||||
DownloadIcon,
|
||||
} from "lucide-vue-next";
|
||||
import { DownloadIcon } from "lucide-vue-next";
|
||||
import RadioCard from "./radio-card.vue";
|
||||
|
||||
defineProps(["timetable", "editable", "remote", "selected", "admin"]);
|
||||
defineEmits(["click", "edit", "delete", "copy", "export", "upload"]);
|
||||
@ -24,75 +23,24 @@ watch(deleteConfirm, (value) => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="card">
|
||||
<div class="button" @click="$emit('click')">
|
||||
<CheckCircleIcon v-if="selected" />
|
||||
<CircleIcon v-else />
|
||||
|
||||
<div class="info">
|
||||
<span class="name">{{ timetable.title }}</span>
|
||||
<span class="detail"
|
||||
>{{ $t("settings.source") }}: {{ timetable.source }}
|
||||
<span v-if="admin"
|
||||
>, Class: {{ timetable.class }}, Id: {{ timetable.id }}</span
|
||||
></span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="buttons">
|
||||
<DownloadIcon v-if="!admin" @click="$emit('export')" />
|
||||
<Edit2Icon v-if="editable && !remote" @click="$emit('edit')" />
|
||||
<UploadCloudIcon v-if="editable && remote" @click="$emit('upload')" />
|
||||
<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>
|
||||
<RadioCard
|
||||
@click="$emit('click')"
|
||||
:title="timetable.title"
|
||||
:subtitle="`${$t('settings.source')}: ${timetable.source}`"
|
||||
:selected="selected"
|
||||
>
|
||||
<DownloadIcon v-if="!admin" @click="$emit('export')" />
|
||||
<Edit2Icon v-if="editable && !remote" @click="$emit('edit')" />
|
||||
<UploadCloudIcon v-if="editable && remote" @click="$emit('upload')" />
|
||||
<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')"
|
||||
/>
|
||||
</RadioCard>
|
||||
</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>
|
||||
import { substitutions, loadingFailed, classFilter } from "@/store";
|
||||
import { substitutions, loadingFailed, activeProfile } from "@/store";
|
||||
import { getSubstitutionText, getSubstitutionColor } from "@/util";
|
||||
import { computed } from "vue";
|
||||
import InfoCard from "@/components/info-card.vue";
|
||||
@ -49,7 +49,7 @@ const substitutionsForDate = computed(() => {
|
||||
$t(
|
||||
getSubstitutionText(
|
||||
substitution,
|
||||
!classFilter || classFilter == "none",
|
||||
!activeProfile.classFilter || activeProfile.classFilter == "none",
|
||||
),
|
||||
{
|
||||
subject: substitution.change.subject,
|
||||
|
@ -13,6 +13,7 @@ import TimetableSettings from "@/views/settings/TimetableSettings.vue";
|
||||
import TimetableEditor from "@/views/settings/TimetableEditor.vue";
|
||||
import TimetableGroupSettings from "@/views/settings/TimetableGroupSettings.vue";
|
||||
import AppearanceSettings from "@/views/settings/AppearanceSettings.vue";
|
||||
import ProfileSettings from "@/views/settings/ProfileSettings.vue";
|
||||
import KeySettings from "@/views/settings/KeySettings.vue";
|
||||
import AdminSettings from "@/views/settings/AdminSettings.vue";
|
||||
import AboutPage from "@/views/settings/AboutPage.vue";
|
||||
@ -73,6 +74,11 @@ const router = createRouter({
|
||||
name: "title.settings.appearance",
|
||||
component: AppearanceSettings,
|
||||
},
|
||||
{
|
||||
path: "profiles",
|
||||
name: "title.settings.profiles",
|
||||
component: ProfileSettings,
|
||||
},
|
||||
{
|
||||
path: "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);
|
||||
|
||||
/* Preferences */
|
||||
export const classFilter = ref(localStorage.getItem("classFilter") || "none");
|
||||
export const timetableId = ref(localStorage.getItem("timetableId") || "none");
|
||||
export const timetableGroups = ref(
|
||||
JSON.parse(localStorage.getItem("timetableGroups") || "[]"),
|
||||
export const profiles = ref(
|
||||
JSON.parse(localStorage.getItem("profiles")) || [
|
||||
{
|
||||
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(
|
||||
JSON.parse(localStorage.getItem("timetables")) || [],
|
||||
);
|
||||
|
||||
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(
|
||||
timetableGroups,
|
||||
profiles,
|
||||
(newValue) => {
|
||||
localStorage.setItem("timetableGroups", JSON.stringify(newValue));
|
||||
localStorage.setItem("profiles", JSON.stringify(newValue));
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
watch(activeProfileId, (newValue) => {
|
||||
localStorage.setItem("activeProfile", newValue);
|
||||
});
|
||||
watch(
|
||||
localTimetables,
|
||||
(newValue) => {
|
||||
@ -63,10 +82,10 @@ export const changeDate = ref(new Date());
|
||||
/* Data store */
|
||||
export const timetable = computed(() => {
|
||||
const localTimetable = localTimetables.value.find(
|
||||
(e) => e.id == timetableId.value,
|
||||
(e) => e.id == activeProfile.value.timetableId,
|
||||
);
|
||||
const remoteTimetable = timetables.value.find(
|
||||
(e) => e.id == timetableId.value,
|
||||
(e) => e.id == activeProfile.value.timetableId,
|
||||
);
|
||||
return (
|
||||
localTimetable || remoteTimetable || { trusted: true, source: "", data: [] }
|
||||
@ -154,7 +173,7 @@ export async function fetchClassList() {
|
||||
|
||||
export async function fetchTimetables() {
|
||||
const timetableResponse = await fetch(
|
||||
`${baseUrl}/timetable?class=${classFilter.value}`,
|
||||
`${baseUrl}/timetable?class=${activeProfile.value.classFilter}`,
|
||||
);
|
||||
const timetableData = await timetableResponse.json();
|
||||
if (timetableData.error) {
|
||||
@ -169,9 +188,9 @@ export async function fetchTimetables() {
|
||||
export async function fetchSubstitutions(day) {
|
||||
const requestDate = `?date=${day}`;
|
||||
const substitutionResponse = await fetch(
|
||||
classFilter.value == "none"
|
||||
activeProfile.value.classFilter == "none"
|
||||
? `${baseUrl}/substitutions${requestDate}`
|
||||
: `${baseUrl}/substitutions${requestDate}&class=${classFilter.value}`,
|
||||
: `${baseUrl}/substitutions${requestDate}&class=${activeProfile.value.classFilter}`,
|
||||
);
|
||||
const substitutionData = await substitutionResponse.json();
|
||||
substitutions.value[day] = substitutionData;
|
||||
@ -180,9 +199,9 @@ export async function fetchSubstitutions(day) {
|
||||
export async function fetchHistory(day) {
|
||||
const requestDate = `?date=${day}`;
|
||||
const historyResponse = await fetch(
|
||||
classFilter.value == "none"
|
||||
activeProfile.value.classFilter == "none"
|
||||
? `${baseUrl}/history${requestDate}`
|
||||
: `${baseUrl}/history${requestDate}&class=${classFilter.value}`,
|
||||
: `${baseUrl}/history${requestDate}&class=${activeProfile.value.classFilter}`,
|
||||
);
|
||||
const historyData = await historyResponse.json();
|
||||
if (historyData.error) console.warn("API Error: " + historyData.error);
|
||||
@ -201,7 +220,7 @@ export const parsedTimetable = computed(() => {
|
||||
// (timetable groups)
|
||||
if (Array.isArray(lesson) && lesson.length > 1) {
|
||||
let matchingLesson = lesson.find((e) =>
|
||||
timetableGroups.value.includes(e.group),
|
||||
activeProfile.value.timetableGroups.includes(e.group),
|
||||
);
|
||||
// If no valid timetable group is configured
|
||||
// add a dummy lesson showing a notice
|
||||
|
@ -13,6 +13,7 @@ export const strings = {
|
||||
timetable: "Manage Timetables",
|
||||
groups: "Timetable Groups",
|
||||
appearance: "Appearance",
|
||||
profiles: "Profiles",
|
||||
keys: "Manage Keys",
|
||||
admin: "Admin Settings",
|
||||
about: "About",
|
||||
@ -27,6 +28,7 @@ export const strings = {
|
||||
language: "Language",
|
||||
about: "About",
|
||||
theme: "Theme",
|
||||
profiles: "Saved Profiles",
|
||||
keys: "Manage Keys",
|
||||
},
|
||||
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.",
|
||||
theme:
|
||||
"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.",
|
||||
},
|
||||
other: "Other",
|
||||
@ -48,6 +54,8 @@ export const strings = {
|
||||
none: "None",
|
||||
version: "Version",
|
||||
source: "Source",
|
||||
classFilter: "Filter",
|
||||
timetableGroups: "Timetable Groups",
|
||||
key: "Key",
|
||||
invalidKey: "This key does not exist!",
|
||||
theme: {
|
||||
@ -136,6 +144,7 @@ export const strings = {
|
||||
timetable: "Stundenpläne Verwalten",
|
||||
groups: "Stundenplan-Gruppen",
|
||||
appearance: "Aussehen",
|
||||
profiles: "Profile",
|
||||
keys: "Schlüssel Verwalten",
|
||||
about: "Über",
|
||||
},
|
||||
@ -149,6 +158,7 @@ export const strings = {
|
||||
language: "Sprache",
|
||||
about: "Über diese Anwendung",
|
||||
theme: "Farbschema",
|
||||
profiles: "Gespeicherte Profile",
|
||||
keys: "Schlüssel Verwalten",
|
||||
},
|
||||
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!",
|
||||
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.",
|
||||
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.",
|
||||
},
|
||||
other: "Andere",
|
||||
@ -170,6 +184,8 @@ export const strings = {
|
||||
none: "Keine",
|
||||
version: "Version",
|
||||
source: "Quelle",
|
||||
classFilter: "Filter",
|
||||
timetableGroups: "Stundenplan-Gruppen",
|
||||
key: "Schlüssel",
|
||||
invalidKey: "Dieser Schlüssel existiert nicht!",
|
||||
theme: {
|
||||
|
@ -2,6 +2,7 @@
|
||||
import ScrollableContainer from "@/components/scrollable-container.vue";
|
||||
import PageCard from "@/components/settings/page-card.vue";
|
||||
import { hasPermission } from "@/permission";
|
||||
import { BookmarkIcon } from "lucide-vue-next";
|
||||
import {
|
||||
FilterIcon,
|
||||
CalendarIcon,
|
||||
@ -38,6 +39,11 @@ import {
|
||||
:icon="PaletteIcon"
|
||||
route="settings/appearance"
|
||||
/>
|
||||
<PageCard
|
||||
:name="$t('title.settings.profiles')"
|
||||
:icon="BookmarkIcon"
|
||||
route="settings/profiles"
|
||||
/>
|
||||
<PageCard
|
||||
:name="$t('title.settings.keys')"
|
||||
:icon="KeyRoundIcon"
|
||||
|
@ -2,7 +2,7 @@
|
||||
import TimetableSetup from "@/components/timetable-setup.vue";
|
||||
import {
|
||||
classList,
|
||||
classFilter,
|
||||
activeProfile,
|
||||
timetable,
|
||||
loadingFailed,
|
||||
loading,
|
||||
@ -15,13 +15,15 @@ import { ClockIcon, CloudOffIcon, CalendarOffIcon } from "lucide-vue-next";
|
||||
|
||||
<template>
|
||||
<TimetableSetup
|
||||
v-show="classFilter == 'none'"
|
||||
v-show="activeProfile.classFilter == 'none'"
|
||||
:options="classList"
|
||||
v-model="classFilter"
|
||||
v-model="activeProfile.classFilter"
|
||||
/>
|
||||
<div
|
||||
class="cardContainer"
|
||||
v-show="(timetable.data || []).length == 0 && classFilter != 'none'"
|
||||
v-show="
|
||||
(timetable.data || []).length == 0 && activeProfile.classFilter != 'none'
|
||||
"
|
||||
>
|
||||
<InfoCard
|
||||
class="card"
|
||||
@ -45,7 +47,10 @@ import { ClockIcon, CloudOffIcon, CalendarOffIcon } from "lucide-vue-next";
|
||||
:text="$t('infoCard.texts.loadingFailed')"
|
||||
/>
|
||||
</div>
|
||||
<DayCarousel v-show="classFilter != 'none'" :element="TimetableList" />
|
||||
<DayCarousel
|
||||
v-show="activeProfile.classFilter != 'none'"
|
||||
:element="TimetableList"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { classList, classFilter } from "@/store";
|
||||
import { classList, activeProfile } from "@/store";
|
||||
import RadioButtons from "@/components/settings/radio-buttons.vue";
|
||||
</script>
|
||||
|
||||
@ -9,7 +9,7 @@ import RadioButtons from "@/components/settings/radio-buttons.vue";
|
||||
<RadioButtons
|
||||
:options="['None', ...classList]"
|
||||
:values="['none', ...classList]"
|
||||
v-model="classFilter"
|
||||
v-model="activeProfile.classFilter"
|
||||
/>
|
||||
</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>
|
||||
import { possibleTimetableGroups, timetableGroups } from "@/store";
|
||||
import { possibleTimetableGroups, activeProfile } from "@/store";
|
||||
import MultiselectButtons from "@/components/settings/multiselect-buttons.vue";
|
||||
</script>
|
||||
|
||||
@ -9,7 +9,7 @@ import MultiselectButtons from "@/components/settings/multiselect-buttons.vue";
|
||||
<MultiselectButtons
|
||||
:options="possibleTimetableGroups"
|
||||
:values="possibleTimetableGroups"
|
||||
v-model="timetableGroups"
|
||||
v-model="activeProfile.timetableGroups"
|
||||
/>
|
||||
</template>
|
||||
|
||||
|
@ -4,7 +4,7 @@ import {
|
||||
timetable,
|
||||
timetables,
|
||||
localTimetables,
|
||||
timetableId,
|
||||
activeProfile,
|
||||
baseUrl,
|
||||
fetchTimetables,
|
||||
} from "@/store";
|
||||
@ -78,7 +78,7 @@ async function uploadTimetable(id) {
|
||||
} else {
|
||||
loadingProgress.value = 0.5;
|
||||
await fetchTimetables();
|
||||
timetableId.value = id;
|
||||
activeProfile.value.timetableId = id;
|
||||
}
|
||||
|
||||
loadingProgress.value = 1;
|
||||
@ -94,10 +94,10 @@ async function uploadTimetable(id) {
|
||||
v-for="timetable in localTimetables"
|
||||
:key="timetable.id"
|
||||
:timetable="timetable"
|
||||
:selected="timetableId == timetable.id"
|
||||
:selected="activeProfile.timetableId == timetable.id"
|
||||
:editable="true"
|
||||
:remote="false"
|
||||
@click="timetableId = timetable.id"
|
||||
@click="activeProfile.timetableId = timetable.id"
|
||||
@edit="$router.push('timetable/edit/' + timetable.id)"
|
||||
@copy="copyTimetable(timetable)"
|
||||
@delete="
|
||||
@ -129,10 +129,10 @@ async function uploadTimetable(id) {
|
||||
v-for="timetable in timetables"
|
||||
:key="timetable.id"
|
||||
:timetable="timetable"
|
||||
:selected="timetableId == timetable.id"
|
||||
:selected="activeProfile.timetableId == timetable.id"
|
||||
:editable="canEditTimetable(timetable.id)"
|
||||
:remote="true"
|
||||
@click="timetableId = timetable.id"
|
||||
@click="activeProfile.timetableId = timetable.id"
|
||||
@copy="copyTimetable(timetable)"
|
||||
@export="exportTimetable(timetable)"
|
||||
@upload="uploadTimetable(timetable.id)"
|
||||
|
Reference in New Issue
Block a user