v3 init push
This commit is contained in:
+306
@@ -0,0 +1,306 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user