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:
2023-08-26 21:54:02 +02:00
parent 70238b4151
commit 21b2e68198
14 changed files with 401 additions and 114 deletions

View File

@@ -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,

View 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>

View 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>

View File

@@ -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>

View File

@@ -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,