diff --git a/package.json b/package.json
index cf00920..4ed00ea 100644
--- a/package.json
+++ b/package.json
@@ -13,7 +13,7 @@
"build-only": "vite build",
"type-check": "vue-tsc --build",
"format": "prettier --write --experimental-cli src/",
- "fetch-wasm": "curl -L -o src/assets/core.wasm https://github.com/git-calendar/core/releases/download/v0.1.5/core.wasm",
+ "fetch-wasm": "curl -L -o src/assets/core.wasm https://github.com/git-calendar/core/releases/download/v0.1.6/core.wasm",
"fetch-wasm-latest": "curl -L -o src/assets/core.wasm https://github.com/git-calendar/core/releases/latest/download/core.wasm"
},
"dependencies": {
diff --git a/src/App.vue b/src/App.vue
index 1c1afc8..0114d31 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -5,9 +5,10 @@ import { onBeforeMount, ref } from 'vue';
import AlertModal from '@/components/modals/AlertModal.vue';
import { createCalendarOnce } from '@/utils';
import { syncAllWrapper } from '@/services/gitSync';
-import { settings } from './services/settings';
-import { useAlertModal } from './composables/modals/useAlertModal';
-import { notifyEventsChanged } from './composables/useEventsRefresh';
+import { settings } from '@/services/settings';
+import { useAlertModal } from '@/composables/modals/useAlertModal';
+import { notifyEventsChanged } from '@/composables/useEventsRefresh';
+import { loadCalendars } from '@/services/calendarCache';
const { alert } = useAlertModal();
@@ -29,6 +30,8 @@ onBeforeMount(async () => {
await CalendarCore.loadCalendars(); // load local first, they might change after with network pull
sync();
+ await loadCalendars();
+
coreReady.value = true;
} catch (err) {
alert(String(err));
diff --git a/src/assets/locales/cs.json b/src/assets/locales/cs.json
index f202942..aa697ac 100644
--- a/src/assets/locales/cs.json
+++ b/src/assets/locales/cs.json
@@ -23,6 +23,7 @@
"to": "Do",
"entireDay": "Celý den",
"calendar": "Kalendář",
+ "tag": "Štítek",
"repeat": {
"repeat": "Opakování",
"end": "Konec",
@@ -41,7 +42,13 @@
"username": "Uživatelské jméno",
"password": "Heslo/Token",
"encrypted": "Šifrování",
- "decryptionKey": "Šifrovací heslo"
+ "encryptionKey": "Šifrovací heslo",
+ "readonly": "Pouze čtení"
+ },
+ "tag": {
+ "name": "Jméno",
+ "color": "Barva",
+ "notag": "Bez štítku"
},
"views": {
"4days": "4 dny",
@@ -85,5 +92,15 @@
"message": {
"confirmCalendarDelete": "Chcete opravdu smazat celý kalenář?",
"errorRemoteUrlEndDotGit": "Remote URL musí končit \".git\"."
+ },
+ "colors": {
+ "blue": "Modrá",
+ "green": "Zelená",
+ "red": "Červená",
+ "purple": "Fialová",
+ "yellow": "Žlutá",
+ "orange": "Oranžová",
+ "pink": "Růžová",
+ "gray": "Šedá"
}
}
diff --git a/src/assets/locales/en.json b/src/assets/locales/en.json
index d2350c1..3d6ea86 100644
--- a/src/assets/locales/en.json
+++ b/src/assets/locales/en.json
@@ -23,6 +23,7 @@
"to": "Till",
"entireDay": "Entire day",
"calendar": "Calendar",
+ "tag": "Tag",
"repeat": {
"repeat": "Repeat",
"end": "End",
@@ -41,7 +42,13 @@
"username": "Username",
"password": "Password/Token",
"encrypted": "Encrypted",
- "decryptionKey": "Encryption key/password"
+ "encryptionKey": "Encryption key/password",
+ "readonly": "Read-only"
+ },
+ "tag": {
+ "name": "Name",
+ "color": "Color",
+ "notag": "Untagged"
},
"views": {
"4days": "4 days",
@@ -85,5 +92,15 @@
"message": {
"confirmCalendarDelete": "Do you really want to delete the whole calendar?",
"errorRemoteUrlEndDotGit": "Remote URL must end with \".git\"."
+ },
+ "colors": {
+ "blue": "Blue",
+ "green": "Green",
+ "red": "Red",
+ "purple": "Purple",
+ "yellow": "Yellow",
+ "orange": "Orange",
+ "pink": "Pink",
+ "gray": "Gray"
}
}
diff --git a/src/assets/locales/git.json b/src/assets/locales/git.json
index 92933e0..63d8048 100644
--- a/src/assets/locales/git.json
+++ b/src/assets/locales/git.json
@@ -19,5 +19,15 @@
"compiling": "Building...",
"starting": "Running...",
"done": "Ready!"
+ },
+ "colors": {
+ "blue": "Commit Blue",
+ "green": "Addition Green (+)",
+ "red": "Deletion Red (-)",
+ "purple": "Merged Purple",
+ "yellow": "Stash Yellow",
+ "orange": "Rebase Orange",
+ "pink": "Fetch Pink",
+ "gray": "Docs Gray"
}
}
diff --git a/src/assets/styles.css b/src/assets/styles.css
index a773a18..293fb6a 100644
--- a/src/assets/styles.css
+++ b/src/assets/styles.css
@@ -120,7 +120,6 @@ input[type='checkbox'] {
width: 1rem;
height: 1rem;
padding: 0;
- margin-bottom: 2px;
vertical-align: middle;
cursor: pointer;
}
@@ -130,7 +129,7 @@ input[type='checkbox']::before {
position: absolute;
inset: 0;
border-radius: var(--small-border-radius);
- background-color: var(--btn-bg-color);
+ background-color: var(--sidebar-btn-bg-color);
}
input[type='checkbox']:checked::before {
@@ -182,6 +181,33 @@ input.red,
resize: vertical;
min-height: 2rem;
}
+
+ fieldset {
+ display: contents;
+ border: 0;
+ padding: 0;
+ margin: 0;
+
+ &:disabled label,
+ &:disabled .dates > span,
+ &:disabled > #git-credentials {
+ opacity: 0.5;
+ }
+
+ &:disabled select {
+ opacity: 1; /* it was dimmed 2 times idk */
+ }
+ }
+
+ label:has(select) {
+ display: flex;
+ align-items: center;
+ gap: 0.8rem;
+
+ select {
+ flex: 1 1 auto;
+ }
+ }
}
.bottom-btns {
@@ -189,6 +215,15 @@ input.red,
display: flex;
gap: 1rem;
+
+ .delete-btn {
+ border: 1px solid var(--git-color);
+ background-color: var(--btn-red-bg-color);
+
+ &:hover:not(:focus):not(:disabled) {
+ background-color: var(--btn-red-bg-color-hover);
+ }
+ }
}
@media (max-width: 768px) {
diff --git a/src/assets/themes.css b/src/assets/themes.css
index 825a41a..02105d3 100644
--- a/src/assets/themes.css
+++ b/src/assets/themes.css
@@ -7,10 +7,11 @@
[data-theme='dark'] {
--bg-color: #1a1a1a;
--text-color: #c1c1c1;
+ --text-color-harder: #d9d9d9;
--text-color-hard: #fff;
--btn-bg-color: #2b2b2b;
- --btn-bg-color-hover: color-mix(in srgb, var(--btn-bg-color), white 3%);
+ --btn-bg-color-hover: color-mix(in srgb, var(--btn-bg-color), white 5%);
--btn-bg-color-checked: color-mix(in srgb, var(--btn-bg-color), white 10%);
--btn-red-bg-color: color-mix(in srgb, var(--git-color), black 50%);
--btn-red-bg-color-hover: color-mix(in srgb, var(--git-color), black 40%);
@@ -19,14 +20,41 @@
--git-bg-color: color-mix(in srgb, var(--git-color-real), black 30%);
--sidebar-color: #242424;
+ --sidebar-btn-bg-color: #333;
+ --sidebar-btn-bg-hover-color: color-mix(in srgb, var(--sidebar-btn-bg-color), white 5%);
--sidebar-hover-color: color-mix(in srgb, transparent, white 10%);
--grid-color: #262626;
+
+ /* old:
+ --event-color-blue: #6495ed;
+ --event-color-green: #2ea043;
+ --event-color-red: #ee3a49;
+ --event-color-purple: #8250df;
+ --event-color-yellow: #d9a520;
+ --event-color-orange: #f66a0a;
+ --event-color-pink: #e84393;
+ --event-color-teal: #009688;
+ --event-color-gray: #9d9c9d;*/
+
+ --event-color-blue: #00579f;
+ --event-color-green: #0d7b00;
+ --event-color-red: #af0800;
+ --event-color-purple: #7a218f;
+ --event-color-yellow: #fdc700;
+ --event-color-orange: #e85d04;
+ --event-color-pink: #ff5ba1;
+ --event-color-teal: #00a8ad;
+ --event-color-gray: #777777;
+ --event-color-brown: #755020;
+
+ --event-color-git: #f03c2e;
}
[data-theme='light'] {
--bg-color: #f2f2f2;
- --text-color: #101010;
+ --text-color: #1a1a1a;
+ --text-color-harder: #101010;
--text-color-hard: #000;
--btn-bg-color: #d5d5d5;
@@ -39,6 +67,7 @@
--git-bg-color: color-mix(in srgb, var(--git-color-real), white 10%);
--sidebar-color: #eaeaea;
+ --sidebar-btn-bg-color: #ddd;
--sidebar-hover-color: color-mix(in srgb, transparent, black 10%);
--grid-color: #d1d1d1;
diff --git a/src/colors.ts b/src/colors.ts
new file mode 100644
index 0000000..f600e98
--- /dev/null
+++ b/src/colors.ts
@@ -0,0 +1,30 @@
+import { settings } from './services/settings';
+
+export const colorsList = [
+ 'blue',
+ 'green',
+ 'red',
+ 'purple',
+ 'yellow',
+ 'orange',
+ 'pink',
+ 'teal',
+ 'brown',
+ 'gray',
+] as const;
+
+export type ColorId = (typeof colorsList)[number];
+
+// Just returns the CSS variable name
+export function getEventColorCSSVariable(colorId: ColorId): string {
+ return `var(--event-color-${colorId})`;
+}
+
+export function getColorI18nKey(id: ColorId): string {
+ return `colors.${id}`;
+}
+
+export function toColorId(color?: string): ColorId {
+ if (colorsList.includes(color as ColorId)) return color as ColorId;
+ else return settings.value.defaultEventColor;
+}
diff --git a/src/components/CalendarList.vue b/src/components/CalendarList.vue
index 1188eb9..d929d2a 100644
--- a/src/components/CalendarList.vue
+++ b/src/components/CalendarList.vue
@@ -1,34 +1,35 @@
@@ -39,20 +40,49 @@ watch(
-
-
-
+
+
+
+
+
+
+
+
+
+
+
@@ -60,27 +90,46 @@ watch(
diff --git a/src/components/ColorPicker.vue b/src/components/ColorPicker.vue
new file mode 100644
index 0000000..be991af
--- /dev/null
+++ b/src/components/ColorPicker.vue
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/components/MonthSideMap.vue b/src/components/MonthSideMap.vue
index 1bb3a37..04ec96f 100644
--- a/src/components/MonthSideMap.vue
+++ b/src/components/MonthSideMap.vue
@@ -241,6 +241,10 @@ const hoverInterval = computed(() => {
font-weight: 900;
position: relative;
+ &:not(.in-range) {
+ color: var(--git-color);
+ }
+
/* the dot */
&::after {
content: '';
diff --git a/src/components/SideBar.vue b/src/components/SideBar.vue
index 30c302f..bf1bf92 100644
--- a/src/components/SideBar.vue
+++ b/src/components/SideBar.vue
@@ -34,6 +34,7 @@ async function syncAndReload() {
+
@@ -57,19 +58,30 @@ aside {
display: flex;
flex-direction: column;
align-items: center;
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ overflow: hidden;
+ min-height: 0;
padding: 0.5rem;
background-color: var(--sidebar-color);
+
+ button {
+ background-color: var(--sidebar-btn-bg-color);
+
+ &:hover:not(:focus):not(:disabled) {
+ background-color: var(--sidebar-btn-bg-hover-color);
+ }
+ }
}
#links {
display: flex;
align-items: center;
gap: 0.5rem;
-
- position: absolute;
- bottom: 0.8rem;
+ padding: 0.3rem;
> * {
display: flex;
@@ -92,7 +104,7 @@ hr {
width: 98%;
border: 0.5px solid white;
opacity: 0.2;
- margin: 0.5rem 0 0.8rem 0;
+ margin: 0.5rem 0;
}
#top-btns {
diff --git a/src/components/modals/CalendarModal.vue b/src/components/modals/CalendarModal.vue
index 7f62f57..ea28527 100644
--- a/src/components/modals/CalendarModal.vue
+++ b/src/components/modals/CalendarModal.vue
@@ -7,7 +7,7 @@ import type { Calendar } from '@/types/core';
import { useAlertModal } from '@/composables/modals/useAlertModal';
import { useI18n } from 'vue-i18n';
import { syncAllWrapper } from '@/services/gitSync';
-import { notifyEventsChanged } from '@/composables/useEventsRefresh';
+import { refreshCalendars } from '@/services/calendarCache';
const { t } = useI18n();
const thisModal = useCalendarModal();
@@ -22,11 +22,12 @@ const howOptions = ['Init', 'Clone'];
const form = reactive({
how: 'Init',
name: '',
- url: '',
+ remoteURL: '',
username: '',
password: '',
encrypted: false,
- decryptionKey: '',
+ encryptionKey: '',
+ readonly: false,
});
const errors = reactive({
missingName: false,
@@ -58,11 +59,12 @@ function updateFormFromCalendar(calendar: Calendar) {
form.how = '';
form.name = calendar.name ?? '';
form.encrypted = calendar.encrypted;
- form.decryptionKey = ''; // secret
+ form.encryptionKey = ''; // secret
+ form.readonly = calendar.readonly;
if (calendar.remoteUrl) {
const remoteUrl = authFromUrl(calendar.remoteUrl);
- form.url = remoteUrl.url;
+ form.remoteURL = remoteUrl.url;
form.username = remoteUrl.username;
form.password = remoteUrl.password;
}
@@ -84,7 +86,7 @@ async function saveCalendar(e: Event) {
}
await CalendarCore.loadCalendars();
- notifyEventsChanged();
+ refreshCalendars();
thisModal.close();
} catch (err) {
alert(String(err));
@@ -94,20 +96,25 @@ async function saveCalendar(e: Event) {
}
async function createCalendar() {
- console.log('creating calendar', form.name, form.url);
+ console.log('creating calendar', form.name, form.remoteURL);
switch (form.how) {
case 'Init':
- const name = form.name.trim();
- await CalendarCore.createCalendar(name, form.encrypted ? form.decryptionKey : '');
- if (form.url !== '')
- await CalendarCore.updateRemote(name, urlWithAuth(form.url.trim(), form.username, form.password));
+ await CalendarCore.createCalendar(form.name.trim(), form.encrypted ? form.encryptionKey : '');
+ if (form.remoteURL !== '') {
+ await CalendarCore.updateRemote(
+ form.name.trim(),
+ urlWithAuth(form.remoteURL.trim(), form.username, form.password),
+ form.readonly,
+ );
+ }
break;
case 'Clone':
await CalendarCore.cloneCalendar(
- urlWithAuth(form.url.trim(), form.username, form.password),
- form.encrypted ? form.decryptionKey : '',
+ urlWithAuth(form.remoteURL.trim(), form.username, form.password),
+ form.encrypted ? form.encryptionKey : '',
+ form.readonly,
);
break;
}
@@ -127,11 +134,11 @@ async function updateCalendar() {
calendarName = form.name;
}
- const rawUrl = form.url.trim();
+ const rawUrl = form.remoteURL.trim();
if (rawUrl !== '') {
const newRemoteUrl = urlWithAuth(rawUrl, form.username, form.password);
- if (calendar.remoteUrl !== newRemoteUrl) {
- await CalendarCore.updateRemote(calendarName, newRemoteUrl);
+ if (calendar.remoteUrl !== newRemoteUrl || originalCalendar.value?.readonly !== form.readonly) {
+ await CalendarCore.updateRemote(calendarName, newRemoteUrl, form.readonly);
await syncAllWrapper(); // merge/pull or whatever
}
}
@@ -151,7 +158,7 @@ async function deleteCal() {
await CalendarCore.removeCalendar(originalCalendar.value.name);
await CalendarCore.loadCalendars();
- notifyEventsChanged();
+ refreshCalendars();
thisModal.close();
} catch (err) {
alert(String(err));
@@ -172,19 +179,19 @@ function validate(): boolean {
return false;
}
- form.url = form.url.trim();
- if (needsURL && form.url === '') {
+ form.remoteURL = form.remoteURL.trim();
+ if (needsURL && form.remoteURL === '') {
errors.badURL = true;
return false;
}
- if (form.url !== '') {
- if (!isValidUrl(form.url)) {
+ if (form.remoteURL !== '') {
+ if (!isValidUrl(form.remoteURL)) {
errors.badURL = true;
return false;
}
- if (!form.url.endsWith('.git')) {
+ if (!form.remoteURL.endsWith('.git')) {
errors.badURL = true;
alert(t('message.errorRemoteUrlEndDotGit'));
return false;
@@ -206,11 +213,12 @@ function isValidUrl(value: string): boolean {
function resetForm() {
form.how = howOptions[0];
form.name = '';
- form.url = '';
+ form.remoteURL = '';
form.username = '';
form.password = '';
form.encrypted = false;
- form.decryptionKey = '';
+ form.encryptionKey = '';
+ form.readonly = false;
resetErrors();
}
@@ -289,7 +297,7 @@ onMounted(() => {
name="url"
:placeholder="`Remote URL ${form.how == 'Init' ? '(' + $t('optionalText') + ')' : ''}`"
autocomplete="none"
- v-model="form.url"
+ v-model="form.remoteURL"
:class="{ red: errors.badURL }"
@input="errors.badURL = false"
/>
@@ -314,20 +322,25 @@ onMounted(() => {
+
+
@@ -364,24 +377,4 @@ onMounted(() => {
}
}
}
-
-fieldset {
- display: contents;
- border: 0;
- padding: 0;
- margin: 0;
-
- &:disabled > #git-credentials {
- opacity: 0.5;
- }
-}
-
-.delete-btn {
- border: 1px solid var(--git-color);
- background-color: var(--btn-red-bg-color);
-
- &:hover:not(:focus):not(:disabled) {
- background-color: var(--btn-red-bg-color-hover);
- }
-}
diff --git a/src/components/modals/EventModal.vue b/src/components/modals/EventModal.vue
index 0b16ec0..ad3e2fc 100644
--- a/src/components/modals/EventModal.vue
+++ b/src/components/modals/EventModal.vue
@@ -10,6 +10,7 @@ import { useAlertModal } from '@/composables/modals/useAlertModal';
import { syncAllWrapper } from '@/services/gitSync';
import cloneDeep from 'lodash-es/cloneDeep';
import { notifyEventsChanged } from '@/composables/useEventsRefresh';
+import { cachedCalendars, getDefaultCalendar, loadCalendars } from '@/services/calendarCache';
const repeatEndOptions = [
{ value: 'on', label: 'On' },
@@ -42,6 +43,7 @@ const form = reactive({
toTime: '',
calendar: '',
entireDay: false,
+ tagId: '',
repeatFreq: Freq.Invalid,
repeatEnd: 'after',
@@ -49,31 +51,16 @@ const form = reactive({
repeatEndAfter: 5,
});
const errors = reactive({
- missingName: false,
+ missingTitle: false,
badToDate: false,
badFromDate: false,
badUntilDate: false,
});
-const calendarNames = ref([]);
-const calendarNamesLoaded = ref(false);
-
-void loadCalendarNames();
-
-async function loadCalendarNames() {
- try {
- calendarNames.value = (await CalendarCore.listCalendars()).map((cal) => cal.name);
- } catch (err) {
- alert(String(err));
- } finally {
- calendarNamesLoaded.value = true;
- }
-}
-
-let originalEvent: CalendarEvent | undefined = undefined;
+let originalEvent: CalendarEvent | undefined;
watch(
- [() => thisModal.event.value, calendarNames],
- ([newEvent]) => {
+ () => thisModal.event.value,
+ (newEvent) => {
if (!newEvent) {
originalEvent = undefined;
return;
@@ -83,16 +70,13 @@ watch(
const eventForForm = cloneDeep(originalEvent);
if (eventForForm.calendar === '') {
- if (!calendarNamesLoaded.value) return;
- console.log(calendarNames.value);
- // calendar unset -> choose one
- originalEvent.calendar = eventForForm.calendar = calendarNames.value.includes('default')
- ? 'default'
- : (calendarNames.value.at(0) ?? '');
+ const defaultCal = getDefaultCalendar();
+ originalEvent.calendar = eventForForm.calendar = defaultCal?.name ?? '';
}
+
updateFormFromEvent(originalEvent);
},
- { immediate: true }, // fire right onMounted, not wait till first change
+ { immediate: true },
);
function updateFormFromEvent(event: CalendarEvent | undefined) {
@@ -129,6 +113,7 @@ function updateFormFromEvent(event: CalendarEvent | undefined) {
}
form.calendar = event.calendar;
+ form.tagId = event.tagId ?? '';
}
function reconstructEvent(): CalendarEvent {
@@ -138,7 +123,7 @@ function reconstructEvent(): CalendarEvent {
location: form.location,
description: form.description,
calendar: form.calendar,
- tag: originalEvent!.tag, // TODO
+ tagId: form.tagId === '' ? undefined : form.tagId,
from: form.entireDay
? DateTime.fromISO(`${form.fromDate}T00:00`, { zone: originalEvent?.from.zone })
: DateTime.fromISO(`${form.fromDate}T${form.fromTime}`, { zone: originalEvent?.from.zone }),
@@ -267,7 +252,7 @@ async function deleteRepeatingEvent(strategy: UpdateStrategy) {
function validate(event: CalendarEvent): boolean {
if (event.title == '') {
- errors.missingName = true;
+ errors.missingTitle = true;
return false;
}
@@ -299,13 +284,19 @@ function validate(event: CalendarEvent): boolean {
return true; // ok
}
-const nameInputField = useTemplateRef('name-input-field');
+const titleInputField = useTemplateRef('title-input-field');
onMounted(async () => {
- nameInputField.value?.focus(); // focus name field
-});
+ titleInputField.value?.focus(); // focus title field
+
+ await loadCalendars();
+
+ if (form.calendar === '') {
+ const calendar = getDefaultCalendar();
-// const exampleTags = ref(['School', 'Work', 'Birthday']); // TODO
-// const selectedTags = ref([]);
+ form.calendar = calendar?.name ?? '';
+ form.tagId = calendar?.tags[0]?.id ?? '';
+ }
+});
@@ -318,9 +309,9 @@ onMounted(async () => {
:placeholder="$t('event.title')"
autocomplete="none"
v-model="form.title"
- ref="name-input-field"
- :class="{ red: errors.missingName }"
- @input="errors.missingName = false"
+ ref="title-input-field"
+ :class="{ red: errors.missingTitle }"
+ @input="errors.missingTitle = false"
/>
@@ -385,8 +376,22 @@ onMounted(async () => {
+
+
@@ -399,16 +404,6 @@ onMounted(async () => {
v-model="form.location"
/>
-
-
@@ -440,16 +435,6 @@ label:has(select[name='end']) {
}
}
-label:has(select) {
- display: flex;
- align-items: center;
- gap: 0.8rem;
-
- select {
- flex: 1 1 auto;
- }
-}
-
.dates {
display: grid;
grid-template-columns: auto 1fr;
@@ -467,31 +452,6 @@ label:has(select) {
gap: 0.7rem;
}
-.delete-btn {
- border: 1px solid var(--git-color);
- background-color: var(--btn-red-bg-color);
-
- &:hover:not(:focus):not(:disabled) {
- background-color: var(--btn-red-bg-color-hover);
- }
-}
-
-fieldset {
- display: contents;
- border: 0;
- padding: 0;
- margin: 0;
-
- &:disabled label,
- &:disabled .dates > span {
- opacity: 0.5;
- }
-
- &:disabled select {
- opacity: 1; /* it was dimmed 2 times idk */
- }
-}
-
@media (max-width: 450px) {
label:has(select[name='end']) {
margin-left: 1.5rem;
diff --git a/src/components/modals/TagModal.vue b/src/components/modals/TagModal.vue
new file mode 100644
index 0000000..639cf8e
--- /dev/null
+++ b/src/components/modals/TagModal.vue
@@ -0,0 +1,178 @@
+
+
+
+
+
+
+
diff --git a/src/components/timeline/BaseEvent.vue b/src/components/timeline/BaseEvent.vue
index e835880..e78a744 100644
--- a/src/components/timeline/BaseEvent.vue
+++ b/src/components/timeline/BaseEvent.vue
@@ -1,25 +1,33 @@
@@ -67,7 +75,7 @@ const dynamicStyles = computed(() => ({
background-size: 3px 100%;
background-repeat: repeat-y;
- background-color: color-mix(in srgb, var(--event-color), transparent 85%); /* light */
+ background-color: color-mix(in srgb, var(--event-color), transparent 80%); /* light */
}
/* hover only for non-temporary events */
diff --git a/src/components/timeline/DayTimeline.vue b/src/components/timeline/DayTimeline.vue
index 642ddd6..e28d6b3 100644
--- a/src/components/timeline/DayTimeline.vue
+++ b/src/components/timeline/DayTimeline.vue
@@ -308,6 +308,7 @@ const { drag, placeholderTop, placeholderHeight, placeholderSubtitle, dragStart
:title="$t('event.new')"
:subtitle="placeholderSubtitle"
:temporary="true"
+ :color="'git-real'"
/>
diff --git a/src/components/timeline/XDaysView.vue b/src/components/timeline/XDaysView.vue
index 8fa61b2..94f286f 100644
--- a/src/components/timeline/XDaysView.vue
+++ b/src/components/timeline/XDaysView.vue
@@ -71,6 +71,7 @@ async function updateData() {
const resultTimeline: CalendarEvent[][] = Array.from({ length: props.numOfDays }, () => []);
const resultWholeDay: CalendarEvent[] = [];
const events = await CalendarCore.getEvents(startDate.value, startDate.value.plus({ day: props.numOfDays }));
+ console.log('Client got events:', events);
for (const event of events) {
if (isWholeDay(event) || event.from.day != event.to.day) {
diff --git a/src/composables/modals/useEventModal.ts b/src/composables/modals/useEventModal.ts
index 1021854..d3febd1 100644
--- a/src/composables/modals/useEventModal.ts
+++ b/src/composables/modals/useEventModal.ts
@@ -22,7 +22,7 @@ export function useEventModal() {
from: DateTime.now(),
to: DateTime.now().plus({ hour: 2 }),
calendar: '',
- tag: '',
+ tagId: '',
};
}
diff --git a/src/composables/modals/useTagModal.ts b/src/composables/modals/useTagModal.ts
new file mode 100644
index 0000000..788419a
--- /dev/null
+++ b/src/composables/modals/useTagModal.ts
@@ -0,0 +1,36 @@
+import { ref, shallowRef, readonly, computed } from 'vue';
+import type { Tag } from '@/types/core';
+
+const isOpen = ref(false);
+const editingTag = shallowRef
(undefined);
+const editingTagCalendarName = shallowRef(undefined); // ughhh idk this feels stupid
+
+export function useTagModal() {
+ return {
+ isOpen: readonly(isOpen),
+ tag: readonly(editingTag),
+ calendarName: readonly(editingTagCalendarName),
+ isNew: computed(() => editingTag.value && editingTag.value.id === undefined),
+
+ open(calendar: string, tag?: Tag) {
+ if (tag) {
+ editingTag.value = tag;
+ editingTagCalendarName.value = calendar;
+ } else {
+ editingTag.value = {
+ name: '',
+ color: 'blue',
+ };
+ editingTagCalendarName.value = calendar;
+ }
+
+ isOpen.value = true;
+ },
+
+ close() {
+ isOpen.value = false;
+ editingTag.value = undefined;
+ editingTagCalendarName.value = undefined;
+ },
+ };
+}
diff --git a/src/composables/useDraggingEvent.ts b/src/composables/useDraggingEvent.ts
index 00b2bbb..e4c785f 100644
--- a/src/composables/useDraggingEvent.ts
+++ b/src/composables/useDraggingEvent.ts
@@ -122,7 +122,7 @@ export function useDraggingEvent(timelineRef: Ref, date: Ref
from: startTime,
to: endTime,
calendar: '',
- tag: '',
+ tagId: '',
};
eventModal.open(event);
diff --git a/src/composables/useKeyboard.ts b/src/composables/useKeyboard.ts
index 51c3116..ed653b3 100644
--- a/src/composables/useKeyboard.ts
+++ b/src/composables/useKeyboard.ts
@@ -6,8 +6,12 @@ import { DateTime } from 'luxon';
import { useEventModal } from '@/composables/modals/useEventModal';
import { useCalendarModal } from '@/composables/modals/useCalendarModal';
import { useStrategyModal } from '@/composables/modals/useStrategyModal';
+import { useTagModal } from './modals/useTagModal';
+const calendarModal = useCalendarModal();
+const tagModal = useTagModal();
const eventModal = useEventModal();
+const strategyModal = useStrategyModal();
export function useKeyboard() {
function inputNeededElsewhere(): boolean {
@@ -22,7 +26,7 @@ export function useKeyboard() {
onKeyStroke('n', (e) => {
if (inputNeededElsewhere()) return;
e.preventDefault();
- eventModal.open();
+ if (!tagModal.isOpen.value && !calendarModal.isOpen.value) eventModal.open();
});
// T -> go to today
@@ -75,12 +79,10 @@ export function useKeyboard() {
onKeyStroke('Escape', (e) => {
e.preventDefault();
- const calendarModal = useCalendarModal();
- const eventModal = useEventModal();
- const strategyModal = useStrategyModal();
-
if (calendarModal.isOpen.value) {
calendarModal.close();
+ } else if (tagModal.isOpen.value) {
+ tagModal.close();
} else if (eventModal.isOpen.value && !strategyModal.isOpen.value) {
eventModal.close();
} else if (eventModal.isOpen.value && strategyModal.isOpen.value) {
diff --git a/src/router.ts b/src/router.ts
index 0071be7..a0e4e1e 100644
--- a/src/router.ts
+++ b/src/router.ts
@@ -1,10 +1,11 @@
import { createRouter, createWebHistory, createWebHashHistory } from 'vue-router';
import CalendarView from '@/views/CalendarView.vue';
import SettingsView from '@/views/SettingsView.vue';
+import FileExplorer from '@/views/FileExplorer.vue';
import { DateTime } from 'luxon';
import { getWeekAlignedRedirect } from '@/utils';
-import { settings } from './services/settings';
+import { settings } from '@/services/settings';
const isGitHubPages = import.meta.env.VITE_GITHUB_PAGES == 'true'; // needs to have VITE_ prefix
@@ -14,10 +15,11 @@ const router = createRouter({
routes: [
{
name: 'calendar',
- path: '/:view(month|week|4days)/:year(\\d+)/:month(\\d+)/:day(\\d*)', // for example: /m/2026/3 -> shows March 2026
+ path: '/:view(month|week|4days)/:year(\\d+)/:month(\\d+)/:day(\\d*)', // for example: /month/2026/3 -> shows March 2026
component: CalendarView,
},
{ path: '/settings', component: SettingsView },
+ { path: '/files', component: FileExplorer },
{
path: '/:pathMatch(.*)*', // anything
redirect: () => {
diff --git a/src/services/calendarCache.ts b/src/services/calendarCache.ts
new file mode 100644
index 0000000..9c39038
--- /dev/null
+++ b/src/services/calendarCache.ts
@@ -0,0 +1,63 @@
+import { computed, readonly, shallowRef } from 'vue';
+
+import type { Calendar, Tag } from '@/types/core';
+import { CalendarCore } from '@/wasm/core-wrapper';
+
+const calendars = shallowRef([]);
+const loaded = shallowRef(false);
+
+let loading: Promise | null = null;
+
+const calendarMap = computed(() => {
+ const map = new Map();
+
+ for (const calendar of calendars.value) {
+ map.set(calendar.name, calendar);
+ }
+
+ return map;
+});
+
+export const cachedCalendars = readonly(calendars);
+export const calendarsLoaded = readonly(loaded);
+
+export async function loadCalendars(): Promise {
+ if (loaded.value) {
+ return;
+ }
+
+ if (!loading) {
+ loading = CalendarCore.listCalendars()
+ .then((items) => {
+ calendars.value = items;
+ loaded.value = true;
+ })
+ .finally(() => {
+ loading = null;
+ });
+ }
+
+ return loading;
+}
+
+export async function refreshCalendars(): Promise {
+ loaded.value = false;
+ await loadCalendars();
+}
+
+export function clearCalendarCache(): void {
+ calendars.value = [];
+ loaded.value = false;
+}
+
+export function getCalendar(calendarName: string): Calendar | undefined {
+ return calendarMap.value.get(calendarName);
+}
+
+export function getDefaultCalendar(): Calendar | undefined {
+ return calendarMap.value.get('default') ?? calendars.value[0];
+}
+
+export function getTag(calendarName: string, tagId: string): Tag | undefined {
+ return getCalendar(calendarName)?.tags?.find((tag) => tag.id === tagId);
+}
diff --git a/src/services/settings.ts b/src/services/settings.ts
index 28fe6cc..d82541e 100644
--- a/src/services/settings.ts
+++ b/src/services/settings.ts
@@ -2,6 +2,7 @@ import { useStorage } from '@vueuse/core';
import { watch } from 'vue';
import { CALENDAR_VIEWS, LANGUAGES, THEMES } from '@/constants.ts';
import { type HourNumbers, type WeekdayNumbers, Settings as LuxonSettings } from 'luxon';
+import { type ColorId } from '@/colors';
export type CalendarView = (typeof CALENDAR_VIEWS)[number];
@@ -18,6 +19,7 @@ type UserSettings = {
dayViewStartHour: HourNumbers;
dayViewEndHour: HourNumbers;
dragPrecisionMinutes: number;
+ defaultEventColor: ColorId;
corsProxyURL: string;
};
@@ -33,6 +35,7 @@ export const settings = useStorage(
dayViewStartHour: 6,
dayViewEndHour: 0,
dragPrecisionMinutes: 30,
+ defaultEventColor: 'gray',
corsProxyURL: 'https://cors-proxy.firu.dev',
},
localStorage,
diff --git a/src/types/core.ts b/src/types/core.ts
index 8c63ec9..a250179 100644
--- a/src/types/core.ts
+++ b/src/types/core.ts
@@ -8,7 +8,7 @@ export interface CalendarEvent {
from: DateTime;
to: DateTime;
calendar: string;
- tag: string;
+ tagId?: string;
parentId?: string;
repeat?: Repetition;
}
@@ -21,16 +21,30 @@ export interface Repetition {
exceptions: readonly string[];
}
+export interface Calendar {
+ name: string;
+ tags: Tag[];
+ remoteUrl: string;
+ encrypted: boolean;
+ readonly: boolean;
+}
+
+export interface Tag {
+ id?: string;
+ name: string;
+ color: string;
+}
+
// Interface showing all the methods of CalendarCore.
// The response types are all made async (Promise) using Asyncify type.
export interface CalendarApi {
createCalendar(name: string, password: string): void;
- cloneCalendar(url: string, password: string): void;
+ cloneCalendar(url: string, password: string, readonly: boolean): void;
removeCalendar(name: string): void;
renameCalendar(oldName: string, newName: string): void;
listCalendars(): Calendar[];
loadCalendars(): void;
- updateRemote(calendar: string, remoteUrl: string): void;
+ updateRemote(calendar: string, remoteUrl: string, readonly: boolean): void;
createEvent(event: CalendarEvent): CalendarEvent;
updateEvent(event: CalendarEvent): CalendarEvent;
@@ -40,6 +54,10 @@ export interface CalendarApi {
getEvent(id: string): CalendarEvent;
getEvents(from: DateTime, to: DateTime): CalendarEvent[];
+ createTag(calendar: string, tag: Tag): Tag;
+ updateTag(calendar: string, tag: Tag): Tag;
+ removeTag(calendar: string, id: string): void;
+
setCorsProxy(url: string): void;
syncAll(): void;
exportZip(calendar: string): ArrayBuffer;
@@ -58,15 +76,3 @@ export enum UpdateStrategy {
Following,
All,
}
-
-export interface Calendar {
- name: string;
- tags: Tag[];
- encrypted: boolean;
- remoteUrl: string;
-}
-
-export interface Tag {
- name: string;
- color: string;
-}
diff --git a/src/views/CalendarView.vue b/src/views/CalendarView.vue
index d8ca411..43d06dc 100644
--- a/src/views/CalendarView.vue
+++ b/src/views/CalendarView.vue
@@ -15,10 +15,13 @@ import { useEventModal } from '@/composables/modals/useEventModal';
import { useSidebar } from '@/composables/useSidebar';
import { CALENDAR_VIEWS } from '@/constants';
import { settings, type CalendarView } from '@/services/settings';
+import TagModal from '@/components/modals/TagModal.vue';
+import { useTagModal } from '@/composables/modals/useTagModal';
useKeyboard();
const calendarModal = useCalendarModal();
const eventModal = useEventModal();
+const tagModal = useTagModal();
const route = useRoute();
const sidebar = useSidebar();
@@ -43,7 +46,7 @@ const views = {
-
+
@@ -53,8 +56,9 @@ const views = {
+
-
+
Alpha version
diff --git a/src/views/FileExplorer.vue b/src/views/FileExplorer.vue
new file mode 100644
index 0000000..548b89f
--- /dev/null
+++ b/src/views/FileExplorer.vue
@@ -0,0 +1,799 @@
+
+
+
+
+
+
+ {{ error }}
+
+
+
+
+
+
+
+
+
+
Select a file from the tree.
+
Loading preview…
+
Empty file.
+
Binary file. Preview skipped.
+
{{ previewError }}
+
{{ preview }}
+
+
+
+
+
+
+
diff --git a/src/views/SettingsView.vue b/src/views/SettingsView.vue
index e434dd4..9b57333 100644
--- a/src/views/SettingsView.vue
+++ b/src/views/SettingsView.vue
@@ -5,6 +5,7 @@ import { DRAG_PRECISIONS, LANGUAGES, THEMES } from '@/constants.ts';
import { CalendarCore } from '@/wasm/core-wrapper';
import { useAlertModal } from '@/composables/modals/useAlertModal';
import { settings } from '@/services/settings';
+import { colorsList, getColorI18nKey } from '@/colors';
const { dayNameLong } = useTranslation();
const { alert } = useAlertModal();
@@ -84,6 +85,16 @@ function formatHour(hour: number): string {
+
+
+