✨ 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,
|
||||
|
||||
Reference in New Issue
Block a user