Files
daydream/src/App.vue
T
2026-09-03 18:28:33 +02:00

307 lines
7.6 KiB
Vue

<script>
import appIcon from '@components/AppIcon';
import authPanel from '@components/AuthPanel';
import book from '@components/Book';
import bookmarkRail from '@components/BookmarkRail';
import calendarWidget from '@components/CalendarWidget';
import saveIndicator from '@components/SaveIndicator';
import settingsPanel from '@components/SettingsPanel';
import sLogo from '@images/logo.png';
/**
* The desk: the book, the tabs down its side, and the few controls that are not
* part of the book itself.
*
* This also owns the one piece of lifecycle that cannot live in a component:
* sealing the open entry when the page goes away. It has to be a beacon - a
* fetch issued during unload is cancelled along with the document - and it
* closes the entry only, never the login.
*/
export default {
components: {
appIcon,
authPanel,
book,
bookmarkRail,
calendarWidget,
saveIndicator,
settingsPanel
},
inject: ['api', 'consts', 'journal', 'lang', 'user'],
data() {
return {
sLogo,
bLoaded: false,
bCalendarOpen: false,
bSettingsOpen: false,
bMenuOpen: false,
bRailOpen: false,
iCurrentId: 0,
sCurrentDay: '',
sNotice: '',
bNoticeBad: false
};
},
computed: {
signedIn() {
return (this.user.id > 0);
},
days() {
return this.journal.days;
}
},
watch: {
signedIn: {
immediate: true,
handler(bSignedIn) {
if(bSignedIn) this.openBook();
}
}
},
mounted() {
//pagehide is the one that fires reliably on mobile; beforeunload does
//not on iOS, and visibilitychange fires on every tab switch.
window.addEventListener('pagehide', this.onLeave);
window.addEventListener('beforeunload', this.onLeave);
document.addEventListener('visibilitychange', this.onVisibilityChange);
document.addEventListener('keydown', this.onKeydown);
},
beforeUnmount() {
window.removeEventListener('pagehide', this.onLeave);
window.removeEventListener('beforeunload', this.onLeave);
document.removeEventListener('visibilitychange', this.onVisibilityChange);
document.removeEventListener('keydown', this.onKeydown);
},
methods: {
async openBook() {
try {
await this.journal.load();
//Arriving at the book should mean you can just write - so the
//page being written on is claimed up front. An entry nothing was
//written in is discarded when it closes, so this costs nothing.
if(this.journal.openId === 0) await this.journal.startWriting();
this.bLoaded = true;
this.$nextTick(() => this.$refs.book?.goToWriting());
}
catch(oError) {
this.notify(oError.desc_lang_text || oError.message, true);
}
},
onReading({id, day}) {
this.iCurrentId = id;
this.sCurrentDay = day;
},
/* Getting about */
async onPickBookmark(iEntryId) {
this.bRailOpen = false;
await this.$refs.book?.goToEntry(iEntryId);
},
async onPickDate(sDay) {
this.bCalendarOpen = false;
try {
const iEntryId = await this.journal.findEntryAtDate(sDay);
if(iEntryId > 0) await this.$refs.book?.goToEntry(iEntryId);
}
catch(oError) {
this.notify(oError.desc_lang_text || oError.message, true);
}
},
async onLoadOlder() {
try {
await this.$refs.book?.extendBackwards();
}
catch(oError) {
this.notify(oError.desc_lang_text || oError.message, true);
}
},
goToWriting() {
this.$refs.book?.goToWriting();
},
/* Account */
onSignedIn(asUser) {
Object.assign(this.user, asUser);
},
async onSignedOut() {
this.bSettingsOpen = false;
this.bMenuOpen = false;
//Everything about the previous book has to go, or the next reader
//would be handed its pages.
this.journal.entries = [];
this.journal.bookmarks = [];
this.journal.openId = 0;
this.bLoaded = false;
Object.assign(this.user, {id: 0, name: '', email: ''});
},
/* Leaving */
onLeave() {
//Whatever is on the page stays as an entry; the login is untouched.
this.journal.closeOnUnload();
},
onVisibilityChange() {
//A backgrounded tab may never come back, so flush before it goes.
if(document.visibilityState === 'hidden' && this.journal.isDirty) this.journal.save();
},
onKeydown(oEvent) {
if(oEvent.key !== 'Escape') return;
if(this.bCalendarOpen) this.bCalendarOpen = false;
else if(this.bMenuOpen) this.bMenuOpen = false;
else if(this.bRailOpen) this.bRailOpen = false;
},
notify(sMessage, bBad = false) {
this.sNotice = sMessage;
this.bNoticeBad = bBad;
},
closeMenus() {
this.bCalendarOpen = false;
this.bMenuOpen = false;
}
}
};
</script>
<template>
<div class="app">
<header class="app__header">
<h1 class="app__brand">
<img class="app__logo" :src="sLogo" :alt="consts.title" />
<span class="app__tagline">{{ lang.get('book.tagline') }}</span>
</h1>
<div class="app__tools">
<saveIndicator v-if="signedIn && bLoaded" />
<template v-if="signedIn">
<button
type="button"
class="desk-button"
:title="lang.get('book.go_to_writing')"
@click="goToWriting"
>
<appIcon icon="pen" />
</button>
<div class="app__popover-anchor">
<button
type="button"
class="desk-button"
:aria-expanded="bCalendarOpen"
:title="lang.get('action.calendar')"
@click="bCalendarOpen = !bCalendarOpen; bMenuOpen = false"
>
<appIcon icon="calendar" />
</button>
<div v-if="bCalendarOpen" class="app__popover">
<calendarWidget
:days="days"
:current-day="sCurrentDay"
@pick="onPickDate"
/>
</div>
</div>
<button
type="button"
class="desk-button desk-button--icon app__rail-toggle"
:title="lang.get('book.bookmarks')"
@click="bRailOpen = !bRailOpen"
>
<appIcon icon="bookmark" />
</button>
<div class="app__popover-anchor">
<button
type="button"
class="desk-button"
:aria-expanded="bMenuOpen"
:title="lang.get('account.settings')"
@click="bMenuOpen = !bMenuOpen; bCalendarOpen = false"
>
<appIcon icon="user" />
</button>
<div v-if="bMenuOpen" class="app__popover account-menu">
<div class="account-menu__who">
<div class="account-menu__name">{{ user.name }}</div>
<div class="account-menu__email">{{ user.email }}</div>
</div>
<button
type="button"
class="account-menu__item"
@click="bSettingsOpen = true; bMenuOpen = false"
>
<appIcon icon="settings" />
<span>{{ lang.get('account.settings') }}</span>
</button>
</div>
</div>
</template>
</div>
</header>
<main class="app__desk" @click="closeMenus">
<div class="app__book">
<book
v-if="signedIn && bLoaded"
ref="book"
@reading="onReading"
/>
</div>
<div
v-if="signedIn && bLoaded"
class="app__rail"
:class="bRailOpen ? 'app__rail--open' : null"
>
<bookmarkRail
:bookmarks="journal.bookmarks"
:current-id="iCurrentId"
:open-id="journal.openId"
:has-older="journal.hasOlder"
:loading="journal.loading"
@pick="onPickBookmark"
@load-older="onLoadOlder"
/>
</div>
</main>
<div v-if="sNotice" class="app__notice" :class="bNoticeBad ? 'app__notice--bad' : null" role="status">
<appIcon :icon="bNoticeBad ? 'alert' : 'check'" />
<span>{{ sNotice }}</span>
<button type="button" class="app__notice-close" @click="sNotice = ''">
<appIcon icon="close" size="0.9em" />
</button>
</div>
<authPanel v-if="!signedIn" @done="onSignedIn" />
<settingsPanel
v-if="bSettingsOpen"
@close="bSettingsOpen = false"
@signed-out="onSignedOut"
/>
</div>
</template>