Refactor stats, add derived catalog stores, improve timer
This commit is contained in:
@@ -1,4 +1,30 @@
|
|||||||
import { db } from '$lib/catalog/db';
|
import { page } from '$app/stores';
|
||||||
|
import { db, type Catalog } from '$lib/catalog/db';
|
||||||
|
import type { Volume } from '$lib/types';
|
||||||
import { liveQuery } from 'dexie';
|
import { liveQuery } from 'dexie';
|
||||||
|
import { derived, type Readable } from 'svelte/store';
|
||||||
|
|
||||||
export const catalog = liveQuery(() => db.catalog.toArray());
|
export const catalog = liveQuery(() => db.catalog.toArray());
|
||||||
|
|
||||||
|
function sortManga(a: Volume, b: Volume) {
|
||||||
|
if (a.volumeName < b.volumeName) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (a.volumeName > b.volumeName) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const manga = derived([page, catalog as unknown as Readable<Catalog[]>], ([$page, $catalog]) => {
|
||||||
|
if ($page && $catalog) {
|
||||||
|
return $catalog.find((item) => item.id === $page.params.manga)?.manga.sort(sortManga)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const volume = derived(([page, manga]), ([$page, $manga]) => {
|
||||||
|
if ($page && $manga) {
|
||||||
|
return $manga.find((item) => item.mokuroData.volume_uuid === $page.params.volume)
|
||||||
|
}
|
||||||
|
})
|
||||||
12
src/lib/components/Reader/Timer.svelte
Normal file
12
src/lib/components/Reader/Timer.svelte
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { volumeStats } from '$lib/settings';
|
||||||
|
|
||||||
|
export let active = true;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class:text-primary-700={!active}
|
||||||
|
class="mix-blend-difference z-10 fixed opacity-50 right-20 top-5 p-10 m-[-2.5rem]"
|
||||||
|
>
|
||||||
|
<p>{active ? 'Timer active' : 'Timer idle'} | Minutes read: {$volumeStats?.timeReadInMinutes}</p>
|
||||||
|
</div>
|
||||||
@@ -9,7 +9,8 @@
|
|||||||
{ key: 'boldFont', text: 'Bold font', value: $settings.boldFont },
|
{ key: 'boldFont', text: 'Bold font', value: $settings.boldFont },
|
||||||
{ key: 'pageNum', text: 'Show page number', value: $settings.pageNum },
|
{ key: 'pageNum', text: 'Show page number', value: $settings.pageNum },
|
||||||
{ key: 'charCount', text: 'Show character count', value: $settings.charCount },
|
{ key: 'charCount', text: 'Show character count', value: $settings.charCount },
|
||||||
{ key: 'mobile', text: 'Mobile', value: $settings.mobile }
|
{ key: 'mobile', text: 'Mobile', value: $settings.mobile },
|
||||||
|
{ key: 'showTimer', text: 'Show timer', value: $settings.showTimer }
|
||||||
] as { key: SettingsKey; text: string; value: any }[];
|
] as { key: SettingsKey; text: string; value: any }[];
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,44 +1,14 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { volumes } from '$lib/settings';
|
import { totalStats } from '$lib/settings';
|
||||||
import { AccordionItem, P } from 'flowbite-svelte';
|
import { AccordionItem } from 'flowbite-svelte';
|
||||||
|
|
||||||
$: completed = $volumes
|
|
||||||
? Object.values($volumes).reduce((total: number, { completed }) => {
|
|
||||||
if (completed) {
|
|
||||||
total++;
|
|
||||||
}
|
|
||||||
return total;
|
|
||||||
}, 0)
|
|
||||||
: 0;
|
|
||||||
|
|
||||||
$: pagesRead = $volumes
|
|
||||||
? Object.values($volumes).reduce((total: number, { progress }) => {
|
|
||||||
total += progress;
|
|
||||||
return total;
|
|
||||||
}, 0)
|
|
||||||
: 0;
|
|
||||||
|
|
||||||
$: charsRead = $volumes
|
|
||||||
? Object.values($volumes).reduce((total: number, { chars }) => {
|
|
||||||
total += chars;
|
|
||||||
return total;
|
|
||||||
}, 0)
|
|
||||||
: 0;
|
|
||||||
|
|
||||||
$: minutesRead = $volumes
|
|
||||||
? Object.values($volumes).reduce((total: number, { timeReadInMinutes }) => {
|
|
||||||
total += timeReadInMinutes;
|
|
||||||
return total;
|
|
||||||
}, 0)
|
|
||||||
: 0;
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<AccordionItem>
|
<AccordionItem>
|
||||||
<span slot="header">Stats</span>
|
<span slot="header">Stats</span>
|
||||||
<div>
|
<div>
|
||||||
<p>Completed volumes: {completed}</p>
|
<p>Completed volumes: {$totalStats?.completed || 0}</p>
|
||||||
<p>Pages read: {pagesRead}</p>
|
<p>Pages read: {$totalStats?.pagesRead || 0}</p>
|
||||||
<p>Characters read: {charsRead}</p>
|
<p>Characters read: {$totalStats?.charsRead || 0}</p>
|
||||||
<p>Minutes read: {minutesRead}</p>
|
<p>Minutes read: {$totalStats?.minutesRead || 0}</p>
|
||||||
</div>
|
</div>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ export type Settings = {
|
|||||||
mobile: boolean;
|
mobile: boolean;
|
||||||
backgroundColor: string;
|
backgroundColor: string;
|
||||||
swipeThreshold: number;
|
swipeThreshold: number;
|
||||||
|
showTimer: boolean;
|
||||||
fontSize: FontSize;
|
fontSize: FontSize;
|
||||||
zoomDefault: ZoomModes;
|
zoomDefault: ZoomModes;
|
||||||
volumeDefaults: VolumeDefaults;
|
volumeDefaults: VolumeDefaults;
|
||||||
@@ -71,6 +72,7 @@ const defaultSettings: Settings = {
|
|||||||
mobile: false,
|
mobile: false,
|
||||||
backgroundColor: '#030712',
|
backgroundColor: '#030712',
|
||||||
swipeThreshold: 50,
|
swipeThreshold: 50,
|
||||||
|
showTimer: false,
|
||||||
fontSize: 'auto',
|
fontSize: 'auto',
|
||||||
zoomDefault: 'zoomFitToScreen',
|
zoomDefault: 'zoomFitToScreen',
|
||||||
volumeDefaults: {
|
volumeDefaults: {
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import { browser } from '$app/environment';
|
|||||||
import { derived, get, writable } from 'svelte/store';
|
import { derived, get, writable } from 'svelte/store';
|
||||||
import { settings } from './settings';
|
import { settings } from './settings';
|
||||||
import { zoomDefault } from '$lib/panzoom';
|
import { zoomDefault } from '$lib/panzoom';
|
||||||
|
import { page } from '$app/stores';
|
||||||
|
import { manga, volume } from '$lib/catalog';
|
||||||
|
|
||||||
export type VolumeSettings = {
|
export type VolumeSettings = {
|
||||||
rightToLeft: boolean;
|
rightToLeft: boolean;
|
||||||
@@ -21,6 +23,13 @@ type VolumeData = {
|
|||||||
settings: VolumeSettings;
|
settings: VolumeSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TotalStats = {
|
||||||
|
completed: number;
|
||||||
|
pagesRead: number;
|
||||||
|
charsRead: number;
|
||||||
|
minutesRead: number;
|
||||||
|
}
|
||||||
|
|
||||||
type Volumes = Record<string, VolumeData>;
|
type Volumes = Record<string, VolumeData>;
|
||||||
|
|
||||||
|
|
||||||
@@ -132,4 +141,51 @@ export function updateVolumeSetting(volume: string, key: VolumeSettingsKey, valu
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
zoomDefault();
|
zoomDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const totalStats = derived([volumes, page], ([$volumes, $page]) => {
|
||||||
|
if ($page && $volumes) {
|
||||||
|
return Object.values($volumes).reduce<TotalStats>((stats, { chars, completed, timeReadInMinutes, progress }) => {
|
||||||
|
if (completed) {
|
||||||
|
stats.completed++;
|
||||||
|
}
|
||||||
|
|
||||||
|
stats.pagesRead += progress;
|
||||||
|
stats.minutesRead += timeReadInMinutes;
|
||||||
|
stats.charsRead += chars
|
||||||
|
|
||||||
|
return stats;
|
||||||
|
}, {
|
||||||
|
charsRead: 0,
|
||||||
|
completed: 0,
|
||||||
|
pagesRead: 0,
|
||||||
|
minutesRead: 0
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export const mangaStats = derived([manga, volumes], ([$manga, $volumes]) => {
|
||||||
|
if ($manga && $volumes) {
|
||||||
|
return $manga.map((vol) => vol.mokuroData.volume_uuid).reduce(
|
||||||
|
(stats: any, volumeId) => {
|
||||||
|
const timeReadInMinutes = $volumes[volumeId]?.timeReadInMinutes || 0;
|
||||||
|
const chars = $volumes[volumeId]?.chars || 0;
|
||||||
|
const completed = $volumes[volumeId]?.completed || 0;
|
||||||
|
|
||||||
|
stats.timeReadInMinutes = stats.timeReadInMinutes + timeReadInMinutes;
|
||||||
|
stats.chars = stats.chars + chars;
|
||||||
|
stats.completed = stats.completed + completed;
|
||||||
|
|
||||||
|
return stats;
|
||||||
|
},
|
||||||
|
{ timeReadInMinutes: 0, chars: 0, completed: 0 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const volumeStats = derived([volume, volumes], ([$volume, $volumes]) => {
|
||||||
|
if ($volume && $volumes) {
|
||||||
|
const { chars, completed, timeReadInMinutes, progress } = $volumes[$volume.mokuroData.volume_uuid]
|
||||||
|
return { chars, completed, timeReadInMinutes, progress }
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
import { promptConfirmation, zipManga } from '$lib/util';
|
import { promptConfirmation, zipManga } from '$lib/util';
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
import type { Volume } from '$lib/types';
|
import type { Volume } from '$lib/types';
|
||||||
import { deleteVolume, volumes } from '$lib/settings';
|
import { deleteVolume, mangaStats, volumes } from '$lib/settings';
|
||||||
|
|
||||||
function sortManga(a: Volume, b: Volume) {
|
function sortManga(a: Volume, b: Volume) {
|
||||||
if (a.volumeName < b.volumeName) {
|
if (a.volumeName < b.volumeName) {
|
||||||
@@ -66,15 +66,15 @@
|
|||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>{manga?.[0].mokuroData.title || 'Manga'}</title>
|
<title>{manga?.[0].mokuroData.title || 'Manga'}</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
{#if manga}
|
{#if manga && $mangaStats}
|
||||||
<div class="p-2 flex flex-col gap-5">
|
<div class="p-2 flex flex-col gap-5">
|
||||||
<div class="flex flex-row justify-between">
|
<div class="flex flex-row justify-between">
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<h3 class="font-bold">{manga[0].mokuroData.title}</h3>
|
<h3 class="font-bold">{manga[0].mokuroData.title}</h3>
|
||||||
<div class="flex flex-col gap-0 sm:flex-row sm:gap-5">
|
<div class="flex flex-col gap-0 sm:flex-row sm:gap-5">
|
||||||
<p>Volumes: {stats.completed} / {manga.length}</p>
|
<p>Volumes: {$mangaStats.completed} / {manga.length}</p>
|
||||||
<p>Characters read: {stats.chars}</p>
|
<p>Characters read: {$mangaStats.chars}</p>
|
||||||
<p>Minutes read: {stats.timeReadInMinutes}</p>
|
<p>Minutes read: {$mangaStats.timeReadInMinutes}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="sm:block flex-col flex gap-2">
|
<div class="sm:block flex-col flex gap-2">
|
||||||
|
|||||||
@@ -1,24 +1,73 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
import Reader from '$lib/components/Reader/Reader.svelte';
|
import Reader from '$lib/components/Reader/Reader.svelte';
|
||||||
import { initializeVolume, startCount, volumeSettings, volumes } from '$lib/settings';
|
import Timer from '$lib/components/Reader/Timer.svelte';
|
||||||
|
import { initializeVolume, settings, startCount, volumeSettings, volumes } from '$lib/settings';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
const volumeId = $page.params.volume;
|
const volumeId = $page.params.volume;
|
||||||
|
let count: undefined | number = undefined;
|
||||||
|
let inactiveTimer: undefined | number = undefined;
|
||||||
|
let inactive = false;
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if (!$volumes?.[volumeId]) {
|
if (!$volumes?.[volumeId]) {
|
||||||
initializeVolume(volumeId);
|
initializeVolume(volumeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
const count = startCount(volumeId);
|
count = startCount(volumeId);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
clearInterval(count);
|
clearInterval(count);
|
||||||
|
count = undefined;
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function onBlur() {
|
||||||
|
// This is an attempt to pause the timer when the page loses focus, but
|
||||||
|
// keep it going if focus is given to an extension such as yomitan
|
||||||
|
if (
|
||||||
|
document.activeElement?.innerHTML.includes('moz-extension') ||
|
||||||
|
!Boolean(document.activeElement?.innerHTML)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearInterval(count);
|
||||||
|
count = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onFocus() {
|
||||||
|
count = startCount(volumeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetInactiveTimer() {
|
||||||
|
if (inactive && !count) {
|
||||||
|
count = startCount(volumeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
clearTimeout(inactiveTimer);
|
||||||
|
inactive = false;
|
||||||
|
|
||||||
|
inactiveTimer = setTimeout(() => {
|
||||||
|
clearInterval(count);
|
||||||
|
count = undefined;
|
||||||
|
inactive = true;
|
||||||
|
}, 15 * 1000);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<svelte:window
|
||||||
|
on:blur={onBlur}
|
||||||
|
on:focus={onFocus}
|
||||||
|
on:load={resetInactiveTimer}
|
||||||
|
on:mousemove={resetInactiveTimer}
|
||||||
|
on:keydown={resetInactiveTimer}
|
||||||
|
/>
|
||||||
|
|
||||||
{#if $volumeSettings[volumeId]}
|
{#if $volumeSettings[volumeId]}
|
||||||
|
{#if $settings.showTimer}
|
||||||
|
<Timer active={Boolean(count)} />
|
||||||
|
{/if}
|
||||||
<Reader volumeSettings={$volumeSettings[volumeId]} />
|
<Reader volumeSettings={$volumeSettings[volumeId]} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
Reference in New Issue
Block a user