generated from tpl/obsidian-sample-plugin
Add A to Z Challenge
This commit is contained in:
parent
ffb8cc8d9c
commit
891041c965
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "obsidian-book-tracker",
|
||||
"name": "Book Tracker",
|
||||
"version": "1.1.0",
|
||||
"version": "1.2.0",
|
||||
"minAppVersion": "0.15.0",
|
||||
"description": "Simplifies tracking your reading progress and managing your book collection in Obsidian.",
|
||||
"author": "FiFiTiDo",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "obsidian-book-tracker",
|
||||
"version": "1.1.0",
|
||||
"version": "1.2.0",
|
||||
"description": "Simplifies tracking your reading progress and managing your book collection in Obsidian.",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
|
|
|
@ -25,6 +25,7 @@ import { CreateBookFromGoodreadsUrlCommand } from "@commands/CreateBookFromGoodr
|
|||
import { registerShelfCodeBlockProcessor } from "@ui/code-blocks/ShelfCodeBlock";
|
||||
import { ReloadReadingLogCommand } from "@commands/ReloadReadingLogCommand";
|
||||
import { registerReadingCalendarCodeBlockProcessor } from "@ui/code-blocks/ReadingCalendarCodeBlock";
|
||||
import { registerAToZChallengeCodeBlockProcessor } from "@ui/code-blocks/AToZChallengeCodeBlock";
|
||||
|
||||
export default class BookTrackerPlugin extends Plugin {
|
||||
public settings: BookTrackerPluginSettings;
|
||||
|
@ -91,6 +92,7 @@ export default class BookTrackerPlugin extends Plugin {
|
|||
registerReadingStatsCodeBlockProcessor(this);
|
||||
registerShelfCodeBlockProcessor(this);
|
||||
registerReadingCalendarCodeBlockProcessor(this);
|
||||
registerAToZChallengeCodeBlockProcessor(this);
|
||||
}
|
||||
|
||||
onunload() {}
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
import { registerCodeBlockRenderer } from ".";
|
||||
import { SvelteCodeBlockRenderer } from "./SvelteCodeBlockRenderer";
|
||||
import AToZChallengeCodeBlockView from "./AToZChallengeCodeBlockView.svelte";
|
||||
import type BookTrackerPlugin from "@src/main";
|
||||
import z from "zod/v4";
|
||||
|
||||
export function registerAToZChallengeCodeBlockProcessor(
|
||||
plugin: BookTrackerPlugin
|
||||
): void {
|
||||
registerCodeBlockRenderer(
|
||||
plugin,
|
||||
"a-to-z-challenge",
|
||||
(source, el) =>
|
||||
new SvelteCodeBlockRenderer(
|
||||
AToZChallengeCodeBlockView,
|
||||
plugin,
|
||||
source,
|
||||
el
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export const AToZChallengeSettingsSchema = z.object({
|
||||
coverProperty: z.string(),
|
||||
titleProperty: z.string(),
|
||||
});
|
|
@ -0,0 +1,194 @@
|
|||
<script lang="ts">
|
||||
import {
|
||||
createSettings,
|
||||
setSettingsContext,
|
||||
} from "@ui/stores/settings.svelte";
|
||||
import type { SvelteCodeBlockProps } from "./SvelteCodeBlockRenderer";
|
||||
import { createMetadata } from "@ui/stores/metadata.svelte";
|
||||
import { STATUS_READ } from "@src/const";
|
||||
import { ALL_TIME } from "@ui/stores/date-filter.svelte";
|
||||
import { AToZChallengeSettingsSchema } from "./AToZChallengeCodeBlock";
|
||||
import { parseYaml, TFile } from "obsidian";
|
||||
import OpenFileLink from "@ui/components/OpenFileLink.svelte";
|
||||
import type { Moment } from "moment";
|
||||
|
||||
const { plugin, source }: SvelteCodeBlockProps = $props();
|
||||
|
||||
const settings = AToZChallengeSettingsSchema.parse(parseYaml(source));
|
||||
|
||||
const settingsStore = createSettings(plugin);
|
||||
setSettingsContext(settingsStore);
|
||||
|
||||
const metadataStore = createMetadata(plugin, {
|
||||
statusFilter: STATUS_READ,
|
||||
initialYear: true,
|
||||
disableMonthFilter: true,
|
||||
});
|
||||
|
||||
const getSortValue = (value: string) => {
|
||||
if (value.startsWith("A ")) {
|
||||
return value.slice(2) + ", A";
|
||||
} else if (value.startsWith("An ")) {
|
||||
return value.slice(3) + ", An";
|
||||
} else if (value.startsWith("The ")) {
|
||||
return value.slice(4) + ", The";
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
};
|
||||
|
||||
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".split("");
|
||||
|
||||
const items = $derived(
|
||||
metadataStore.metadata.reduce(
|
||||
(acc, item) => {
|
||||
const title = item.frontmatter[settings.titleProperty];
|
||||
const firstLetter = getSortValue(title).charAt(0).toUpperCase();
|
||||
|
||||
if (!firstLetter.match(/[A-Z]/)) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
if (!acc[firstLetter]) {
|
||||
const coverPath = item.frontmatter[
|
||||
settings.coverProperty
|
||||
] as string;
|
||||
const coverFile = plugin.app.vault.getFileByPath(coverPath);
|
||||
|
||||
let coverSrc: string = "";
|
||||
if (coverFile) {
|
||||
coverSrc = plugin.app.vault.getResourcePath(coverFile);
|
||||
}
|
||||
|
||||
const coverAlt = item.frontmatter[settings.titleProperty];
|
||||
|
||||
acc[firstLetter] = {
|
||||
file: item.file,
|
||||
// @ts-expect-error Moment is provided by Obsidian
|
||||
startDate: moment(
|
||||
item.frontmatter[
|
||||
settingsStore.settings.startDateProperty
|
||||
],
|
||||
),
|
||||
// @ts-expect-error Moment is provided by Obsidian
|
||||
endDate: moment(
|
||||
item.frontmatter[
|
||||
settingsStore.settings.endDateProperty
|
||||
],
|
||||
),
|
||||
coverSrc,
|
||||
coverAlt,
|
||||
};
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{} as Record<
|
||||
string,
|
||||
{
|
||||
file: TFile;
|
||||
startDate: Moment;
|
||||
endDate: Moment;
|
||||
coverSrc: string;
|
||||
coverAlt: string;
|
||||
}
|
||||
>,
|
||||
),
|
||||
);
|
||||
|
||||
const startDate = $derived(
|
||||
Object.values(items)
|
||||
.map((item) => item.startDate)
|
||||
.sort((a, b) => a.diff(b))[0],
|
||||
);
|
||||
|
||||
const endDate = $derived.by(() => {
|
||||
const dates = Object.values(items)
|
||||
.map((item) => item.endDate)
|
||||
.sort((a, b) => b.diff(a));
|
||||
|
||||
if (dates.length !== 26) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return dates[0];
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="reading-bingo">
|
||||
<div class="top-info">
|
||||
<select class="year-filter" bind:value={metadataStore.filterYear}>
|
||||
{#each metadataStore.filterYears as year}
|
||||
<option value={year}>{year}</option>
|
||||
{/each}
|
||||
</select>
|
||||
<p>Started: {startDate.format("YYYY-MM-DD")}</p>
|
||||
<p>Ended: {endDate?.format("YYYY-MM-DD") ?? "N/A"}</p>
|
||||
</div>
|
||||
<div class="bingo">
|
||||
{#each alphabet as letter}
|
||||
<div class="bingo-item">
|
||||
{#if items[letter]}
|
||||
{@const item = items[letter]}
|
||||
<OpenFileLink file={item.file}>
|
||||
<img src={item.coverSrc} alt={item.coverAlt} />
|
||||
</OpenFileLink>
|
||||
{:else}
|
||||
<div class="placeholder">{letter}</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.reading-bingo {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--size-4-6);
|
||||
|
||||
.top-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--size-4-4);
|
||||
|
||||
p {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.bingo {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: var(--size-4-4);
|
||||
|
||||
.bingo-item {
|
||||
min-width: 150px;
|
||||
max-width: 300px;
|
||||
width: calc(20% - var(--size-4-4) * 2);
|
||||
aspect-ratio: 2 / 3;
|
||||
background-color: var(--background-secondary);
|
||||
border-radius: var(--radius-l);
|
||||
|
||||
&:has(.placeholder) {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
font-size: 2rem;
|
||||
font-weight: bold;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
border-radius: var(--radius-l);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -9,7 +9,7 @@ export function registerReadingCalendarCodeBlockProcessor(
|
|||
): void {
|
||||
registerCodeBlockRenderer(
|
||||
plugin,
|
||||
"readingcalendar",
|
||||
"reading-calendar",
|
||||
(source, el) =>
|
||||
new SvelteCodeBlockRenderer(
|
||||
ReadingCalendarCodeBlockView,
|
||||
|
|
|
@ -24,7 +24,9 @@
|
|||
const settingsStore = createSettings(plugin);
|
||||
setSettingsContext(settingsStore);
|
||||
|
||||
const metadataStore = createMetadata(plugin, null);
|
||||
const metadataStore = createMetadata(plugin, {
|
||||
statusFilter: null,
|
||||
});
|
||||
setMetadataContext(metadataStore);
|
||||
|
||||
const readingLog = createReadingLog(plugin.readingLog);
|
||||
|
|
|
@ -8,7 +8,7 @@ export function registerReadingLogCodeBlockProcessor(
|
|||
): void {
|
||||
registerCodeBlockRenderer(
|
||||
plugin,
|
||||
"readinglog",
|
||||
"reading-log",
|
||||
(source, el) =>
|
||||
new SvelteCodeBlockRenderer(
|
||||
ReadingLogCodeBlockView,
|
||||
|
|
|
@ -10,7 +10,7 @@ export function registerReadingStatsCodeBlockProcessor(
|
|||
): void {
|
||||
registerCodeBlockRenderer(
|
||||
plugin,
|
||||
"readingstats",
|
||||
"reading-stats",
|
||||
(source, el) =>
|
||||
new SvelteCodeBlockRenderer(
|
||||
ReadingStatsCodeBlockView,
|
||||
|
|
|
@ -26,7 +26,10 @@
|
|||
const settingsStore = createSettings(plugin);
|
||||
setSettingsContext(settingsStore);
|
||||
|
||||
const metadataStore = createMetadata(plugin, settings.statusFilter, true);
|
||||
const metadataStore = createMetadata(plugin, {
|
||||
statusFilter: settings.statusFilter,
|
||||
initialMonth: true,
|
||||
});
|
||||
setMetadataContext(metadataStore);
|
||||
|
||||
let view = $state(settings.defaultView);
|
||||
|
|
|
@ -23,7 +23,9 @@
|
|||
const settingsStore = createSettings(plugin);
|
||||
setSettingsContext(settingsStore);
|
||||
|
||||
const metadataStore = createMetadata(plugin, null);
|
||||
const metadataStore = createMetadata(plugin, {
|
||||
statusFilter: null,
|
||||
});
|
||||
|
||||
let editMode = $derived(entry !== undefined);
|
||||
let book = $state(entry?.book ?? "");
|
||||
|
|
|
@ -9,17 +9,56 @@ export interface DateFilterStore {
|
|||
get filterMonths(): { label: string; value: number }[];
|
||||
}
|
||||
|
||||
export interface DateFilterStoreOptions {
|
||||
/**
|
||||
* If true, the filter month will be set to the current month
|
||||
* If a number is provided, the filter month will be set to the provided month
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
initialMonth?: boolean | number;
|
||||
|
||||
/**
|
||||
* If true, the filter year will be set to the current year
|
||||
* If a number is provided, the filter year will be set to the provided year
|
||||
*
|
||||
* @default true
|
||||
*/
|
||||
initialYear?: boolean | number;
|
||||
|
||||
/**
|
||||
* If true, the month filter will be disabled
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
disableMonthFilter?: boolean;
|
||||
}
|
||||
|
||||
export function createDateFilter<T>(
|
||||
data: () => T[],
|
||||
selector: (item: T) => Moment,
|
||||
initialMonth?: boolean
|
||||
{
|
||||
initialMonth = false,
|
||||
initialYear = true,
|
||||
disableMonthFilter = false,
|
||||
}: DateFilterStoreOptions
|
||||
): DateFilterStore & {
|
||||
filteredData: T[];
|
||||
} {
|
||||
const today = new Date();
|
||||
let filterYear: number | typeof ALL_TIME = $state(today.getFullYear());
|
||||
let filterYear: number | typeof ALL_TIME = $state(
|
||||
typeof initialYear === "number"
|
||||
? initialYear
|
||||
: initialYear
|
||||
? today.getFullYear()
|
||||
: ALL_TIME
|
||||
);
|
||||
let filterMonth: number | typeof ALL_TIME = $state(
|
||||
initialMonth ? today.getMonth() + 1 : ALL_TIME
|
||||
typeof initialMonth === "number"
|
||||
? initialMonth
|
||||
: initialMonth
|
||||
? today.getMonth() + 1
|
||||
: ALL_TIME
|
||||
);
|
||||
const filteredData = $derived.by(() => {
|
||||
return data()
|
||||
|
@ -30,7 +69,7 @@ export function createDateFilter<T>(
|
|||
return false;
|
||||
}
|
||||
|
||||
if (filterMonth !== ALL_TIME) {
|
||||
if (filterMonth !== ALL_TIME && !disableMonthFilter) {
|
||||
if (date.month() !== filterMonth - 1) {
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -7,7 +7,11 @@ import {
|
|||
setSettingsContext,
|
||||
} from "./settings.svelte";
|
||||
import type BookTrackerPlugin from "@src/main";
|
||||
import { createDateFilter, type DateFilterStore } from "./date-filter.svelte";
|
||||
import {
|
||||
createDateFilter,
|
||||
type DateFilterStore,
|
||||
type DateFilterStoreOptions,
|
||||
} from "./date-filter.svelte";
|
||||
import type { ReadingState } from "@src/types";
|
||||
import type { BookTrackerPluginSettings } from "@ui/settings";
|
||||
|
||||
|
@ -49,10 +53,16 @@ function getMetadata(
|
|||
return metadata;
|
||||
}
|
||||
|
||||
interface MetadataStoreOptions extends DateFilterStoreOptions {
|
||||
/**
|
||||
* The reading state to filter by
|
||||
*/
|
||||
statusFilter?: ReadingState | null;
|
||||
}
|
||||
|
||||
export function createMetadata(
|
||||
plugin: BookTrackerPlugin,
|
||||
statusFilter: ReadingState | null = STATUS_READ,
|
||||
initialMonth?: boolean
|
||||
{ statusFilter = STATUS_READ, ...dateFilterOpts }: MetadataStoreOptions = {}
|
||||
): MetadataStore {
|
||||
let settingsStore = getSettingsContext();
|
||||
if (!settingsStore) {
|
||||
|
@ -97,7 +107,7 @@ export function createMetadata(
|
|||
f.frontmatter[settingsStore.settings.endDateProperty]
|
||||
);
|
||||
},
|
||||
initialMonth
|
||||
dateFilterOpts
|
||||
);
|
||||
|
||||
return {
|
||||
|
|
|
@ -18,7 +18,7 @@ export function createReadingLog(readingLog: ReadingLog): ReadingLogStore {
|
|||
const dateFilter = createDateFilter(
|
||||
() => entries,
|
||||
(entry) => entry.createdAt,
|
||||
true
|
||||
{ initialMonth: true }
|
||||
);
|
||||
|
||||
async function addEntry(entry: ReadingLogEntry): Promise<void> {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"1.0.0": "0.15.0",
|
||||
"1.1.0": "0.15.0"
|
||||
"1.1.0": "0.15.0",
|
||||
"1.2.0": "0.15.0"
|
||||
}
|
Loading…
Reference in New Issue