307 lines
7.6 KiB
Vue
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>
|