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",
|
"id": "obsidian-book-tracker",
|
||||||
"name": "Book Tracker",
|
"name": "Book Tracker",
|
||||||
"version": "1.1.0",
|
"version": "1.2.0",
|
||||||
"minAppVersion": "0.15.0",
|
"minAppVersion": "0.15.0",
|
||||||
"description": "Simplifies tracking your reading progress and managing your book collection in Obsidian.",
|
"description": "Simplifies tracking your reading progress and managing your book collection in Obsidian.",
|
||||||
"author": "FiFiTiDo",
|
"author": "FiFiTiDo",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "obsidian-book-tracker",
|
"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.",
|
"description": "Simplifies tracking your reading progress and managing your book collection in Obsidian.",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
|
@ -25,6 +25,7 @@ import { CreateBookFromGoodreadsUrlCommand } from "@commands/CreateBookFromGoodr
|
||||||
import { registerShelfCodeBlockProcessor } from "@ui/code-blocks/ShelfCodeBlock";
|
import { registerShelfCodeBlockProcessor } from "@ui/code-blocks/ShelfCodeBlock";
|
||||||
import { ReloadReadingLogCommand } from "@commands/ReloadReadingLogCommand";
|
import { ReloadReadingLogCommand } from "@commands/ReloadReadingLogCommand";
|
||||||
import { registerReadingCalendarCodeBlockProcessor } from "@ui/code-blocks/ReadingCalendarCodeBlock";
|
import { registerReadingCalendarCodeBlockProcessor } from "@ui/code-blocks/ReadingCalendarCodeBlock";
|
||||||
|
import { registerAToZChallengeCodeBlockProcessor } from "@ui/code-blocks/AToZChallengeCodeBlock";
|
||||||
|
|
||||||
export default class BookTrackerPlugin extends Plugin {
|
export default class BookTrackerPlugin extends Plugin {
|
||||||
public settings: BookTrackerPluginSettings;
|
public settings: BookTrackerPluginSettings;
|
||||||
|
@ -91,6 +92,7 @@ export default class BookTrackerPlugin extends Plugin {
|
||||||
registerReadingStatsCodeBlockProcessor(this);
|
registerReadingStatsCodeBlockProcessor(this);
|
||||||
registerShelfCodeBlockProcessor(this);
|
registerShelfCodeBlockProcessor(this);
|
||||||
registerReadingCalendarCodeBlockProcessor(this);
|
registerReadingCalendarCodeBlockProcessor(this);
|
||||||
|
registerAToZChallengeCodeBlockProcessor(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
onunload() {}
|
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 {
|
): void {
|
||||||
registerCodeBlockRenderer(
|
registerCodeBlockRenderer(
|
||||||
plugin,
|
plugin,
|
||||||
"readingcalendar",
|
"reading-calendar",
|
||||||
(source, el) =>
|
(source, el) =>
|
||||||
new SvelteCodeBlockRenderer(
|
new SvelteCodeBlockRenderer(
|
||||||
ReadingCalendarCodeBlockView,
|
ReadingCalendarCodeBlockView,
|
||||||
|
|
|
@ -24,7 +24,9 @@
|
||||||
const settingsStore = createSettings(plugin);
|
const settingsStore = createSettings(plugin);
|
||||||
setSettingsContext(settingsStore);
|
setSettingsContext(settingsStore);
|
||||||
|
|
||||||
const metadataStore = createMetadata(plugin, null);
|
const metadataStore = createMetadata(plugin, {
|
||||||
|
statusFilter: null,
|
||||||
|
});
|
||||||
setMetadataContext(metadataStore);
|
setMetadataContext(metadataStore);
|
||||||
|
|
||||||
const readingLog = createReadingLog(plugin.readingLog);
|
const readingLog = createReadingLog(plugin.readingLog);
|
||||||
|
|
|
@ -8,7 +8,7 @@ export function registerReadingLogCodeBlockProcessor(
|
||||||
): void {
|
): void {
|
||||||
registerCodeBlockRenderer(
|
registerCodeBlockRenderer(
|
||||||
plugin,
|
plugin,
|
||||||
"readinglog",
|
"reading-log",
|
||||||
(source, el) =>
|
(source, el) =>
|
||||||
new SvelteCodeBlockRenderer(
|
new SvelteCodeBlockRenderer(
|
||||||
ReadingLogCodeBlockView,
|
ReadingLogCodeBlockView,
|
||||||
|
|
|
@ -10,7 +10,7 @@ export function registerReadingStatsCodeBlockProcessor(
|
||||||
): void {
|
): void {
|
||||||
registerCodeBlockRenderer(
|
registerCodeBlockRenderer(
|
||||||
plugin,
|
plugin,
|
||||||
"readingstats",
|
"reading-stats",
|
||||||
(source, el) =>
|
(source, el) =>
|
||||||
new SvelteCodeBlockRenderer(
|
new SvelteCodeBlockRenderer(
|
||||||
ReadingStatsCodeBlockView,
|
ReadingStatsCodeBlockView,
|
||||||
|
|
|
@ -26,7 +26,10 @@
|
||||||
const settingsStore = createSettings(plugin);
|
const settingsStore = createSettings(plugin);
|
||||||
setSettingsContext(settingsStore);
|
setSettingsContext(settingsStore);
|
||||||
|
|
||||||
const metadataStore = createMetadata(plugin, settings.statusFilter, true);
|
const metadataStore = createMetadata(plugin, {
|
||||||
|
statusFilter: settings.statusFilter,
|
||||||
|
initialMonth: true,
|
||||||
|
});
|
||||||
setMetadataContext(metadataStore);
|
setMetadataContext(metadataStore);
|
||||||
|
|
||||||
let view = $state(settings.defaultView);
|
let view = $state(settings.defaultView);
|
||||||
|
|
|
@ -23,7 +23,9 @@
|
||||||
const settingsStore = createSettings(plugin);
|
const settingsStore = createSettings(plugin);
|
||||||
setSettingsContext(settingsStore);
|
setSettingsContext(settingsStore);
|
||||||
|
|
||||||
const metadataStore = createMetadata(plugin, null);
|
const metadataStore = createMetadata(plugin, {
|
||||||
|
statusFilter: null,
|
||||||
|
});
|
||||||
|
|
||||||
let editMode = $derived(entry !== undefined);
|
let editMode = $derived(entry !== undefined);
|
||||||
let book = $state(entry?.book ?? "");
|
let book = $state(entry?.book ?? "");
|
||||||
|
|
|
@ -9,17 +9,56 @@ export interface DateFilterStore {
|
||||||
get filterMonths(): { label: string; value: number }[];
|
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>(
|
export function createDateFilter<T>(
|
||||||
data: () => T[],
|
data: () => T[],
|
||||||
selector: (item: T) => Moment,
|
selector: (item: T) => Moment,
|
||||||
initialMonth?: boolean
|
{
|
||||||
|
initialMonth = false,
|
||||||
|
initialYear = true,
|
||||||
|
disableMonthFilter = false,
|
||||||
|
}: DateFilterStoreOptions
|
||||||
): DateFilterStore & {
|
): DateFilterStore & {
|
||||||
filteredData: T[];
|
filteredData: T[];
|
||||||
} {
|
} {
|
||||||
const today = new Date();
|
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(
|
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(() => {
|
const filteredData = $derived.by(() => {
|
||||||
return data()
|
return data()
|
||||||
|
@ -30,7 +69,7 @@ export function createDateFilter<T>(
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filterMonth !== ALL_TIME) {
|
if (filterMonth !== ALL_TIME && !disableMonthFilter) {
|
||||||
if (date.month() !== filterMonth - 1) {
|
if (date.month() !== filterMonth - 1) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,11 @@ import {
|
||||||
setSettingsContext,
|
setSettingsContext,
|
||||||
} from "./settings.svelte";
|
} from "./settings.svelte";
|
||||||
import type BookTrackerPlugin from "@src/main";
|
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 { ReadingState } from "@src/types";
|
||||||
import type { BookTrackerPluginSettings } from "@ui/settings";
|
import type { BookTrackerPluginSettings } from "@ui/settings";
|
||||||
|
|
||||||
|
@ -49,10 +53,16 @@ function getMetadata(
|
||||||
return metadata;
|
return metadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface MetadataStoreOptions extends DateFilterStoreOptions {
|
||||||
|
/**
|
||||||
|
* The reading state to filter by
|
||||||
|
*/
|
||||||
|
statusFilter?: ReadingState | null;
|
||||||
|
}
|
||||||
|
|
||||||
export function createMetadata(
|
export function createMetadata(
|
||||||
plugin: BookTrackerPlugin,
|
plugin: BookTrackerPlugin,
|
||||||
statusFilter: ReadingState | null = STATUS_READ,
|
{ statusFilter = STATUS_READ, ...dateFilterOpts }: MetadataStoreOptions = {}
|
||||||
initialMonth?: boolean
|
|
||||||
): MetadataStore {
|
): MetadataStore {
|
||||||
let settingsStore = getSettingsContext();
|
let settingsStore = getSettingsContext();
|
||||||
if (!settingsStore) {
|
if (!settingsStore) {
|
||||||
|
@ -97,7 +107,7 @@ export function createMetadata(
|
||||||
f.frontmatter[settingsStore.settings.endDateProperty]
|
f.frontmatter[settingsStore.settings.endDateProperty]
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
initialMonth
|
dateFilterOpts
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -18,7 +18,7 @@ export function createReadingLog(readingLog: ReadingLog): ReadingLogStore {
|
||||||
const dateFilter = createDateFilter(
|
const dateFilter = createDateFilter(
|
||||||
() => entries,
|
() => entries,
|
||||||
(entry) => entry.createdAt,
|
(entry) => entry.createdAt,
|
||||||
true
|
{ initialMonth: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
async function addEntry(entry: ReadingLogEntry): Promise<void> {
|
async function addEntry(entry: ReadingLogEntry): Promise<void> {
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
{
|
{
|
||||||
"1.0.0": "0.15.0",
|
"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