Add spice field handling

This commit is contained in:
Evan Fiordeliso 2025-06-30 14:20:25 -04:00
parent 20ffc78e8e
commit 113ddefac7
6 changed files with 119 additions and 27 deletions

View File

@ -302,7 +302,10 @@ export default class BookTrackerPlugin extends Plugin {
return; return;
} }
const rating = await RatingModal.createAndOpen(this.app); const rating = await RatingModal.createAndOpen(
this.app,
this.settings.spiceProperty !== ""
);
await this.readingLog.addEntry( await this.readingLog.addEntry(
activeFile.basename, activeFile.basename,
@ -317,6 +320,9 @@ export default class BookTrackerPlugin extends Plugin {
frontMatter[this.settings.statusProperty] = READ_STATE; frontMatter[this.settings.statusProperty] = READ_STATE;
frontMatter[this.settings.endDateProperty] = endDate; frontMatter[this.settings.endDateProperty] = endDate;
frontMatter[this.settings.ratingProperty] = rating; frontMatter[this.settings.ratingProperty] = rating;
if (this.settings.spiceProperty !== "") {
frontMatter[this.settings.spiceProperty] = rating;
}
}); });
new Notice("Reading finished for " + activeFile.name); new Notice("Reading finished for " + activeFile.name);

View File

@ -1,14 +1,26 @@
<script lang="ts"> <script lang="ts">
import { Star, StarHalf } from "lucide-svelte"; import { Star, StarHalf } from "lucide-svelte";
import type { Snippet } from "svelte";
interface Props { interface Props {
value?: number; value?: number;
name?: string; name?: string;
max?: number;
half?: boolean;
inactive?: Snippet;
active?: Snippet;
partial?: Snippet;
} }
const maxV = 5; let {
value = $bindable(),
let { value = $bindable(), name }: Props = $props(); name = "",
max = 5,
half = false,
inactive,
active,
partial,
}: Props = $props();
let ctrl: HTMLElement | null = $state(null); let ctrl: HTMLElement | null = $state(null);
let w = $state(0); let w = $state(0);
@ -27,7 +39,7 @@
let displayVal = $derived(hovering ? valueHover : (value ?? 0)); let displayVal = $derived(hovering ? valueHover : (value ?? 0));
let items = $derived.by(() => { let items = $derived.by(() => {
const full = Number.isInteger(displayVal); const full = Number.isInteger(displayVal);
return Array.from({ length: maxV }, (_, i) => i + 1).map((index) => ({ return Array.from({ length: max }, (_, i) => i + 1).map((index) => ({
index, index,
state: state:
index <= Math.ceil(displayVal) index <= Math.ceil(displayVal)
@ -39,7 +51,7 @@
}); });
function calcSliderPos(e: MouseEvent) { function calcSliderPos(e: MouseEvent) {
return (e.offsetX / (e.target as HTMLElement).clientWidth) * maxV; return (e.offsetX / (e.target as HTMLElement).clientWidth) * max;
} }
function onclick() { function onclick() {
@ -53,7 +65,14 @@
function onmousemove(e: MouseEvent) { function onmousemove(e: MouseEvent) {
hovering = true; hovering = true;
valueHover = Math.ceil(calcSliderPos(e) * 2) / 2;
const newValue = calcSliderPos(e);
if (half) {
valueHover = Math.ceil(newValue * 2) / 2;
} else {
valueHover = Math.ceil(newValue);
}
} }
</script> </script>
@ -62,18 +81,37 @@
<!-- svelte-ignore a11y_no_static_element_interactions --> <!-- svelte-ignore a11y_no_static_element_interactions -->
<!-- svelte-ignore a11y_mouse_events_have_key_events --> <!-- svelte-ignore a11y_mouse_events_have_key_events -->
<div class="rating-input" {onclick} {onmouseout}> <div class="rating-input" {onclick} {onmouseout}>
<input type="number" {name} bind:value />
<!-- svelte-ignore a11y_no_static_element_interactions --> <!-- svelte-ignore a11y_no_static_element_interactions -->
<div class="ctrl" {onmousemove} bind:this={ctrl}></div> <div class="ctrl" {onmousemove} bind:this={ctrl}></div>
<div class="cont m-1" bind:clientWidth={w} bind:clientHeight={h}> <div class="cont m-1" bind:clientWidth={w} bind:clientHeight={h}>
{#each items as item (item.index)} {#each items as item (item.index)}
<div class="rating-item"> <div class="rating-item">
{#if item.state === "inactive"} {#if item.state === "inactive"}
<Star fill="var(--interactive-normal)" /> {#if inactive}
{@render inactive()}
{:else}
<Star color="var(--background-modifier-border)" />
{/if}
{:else if item.state === "active"} {:else if item.state === "active"}
<Star fill="var(--interactive-accent)" /> {#if active}
{@render active()}
{:else}
<Star
color="var(--color-yellow)"
fill="rgba(var(--color-yellow-rgb), 0.2)"
/>
{/if}
{:else if item.state === "partial"} {:else if item.state === "partial"}
<Star fill="var(--interactive-normal)" /> {#if partial}
<StarHalf fill="var(--interactive-accent)" /> {@render partial()}
{:else}
<Star color="var(--background-modifier-border)" />
<StarHalf
color="var(--color-yellow)"
fill="rgba(var(--color-yellow-rgb), 0.2)"
/>
{/if}
{/if} {/if}
</div> </div>
{/each} {/each}
@ -84,6 +122,10 @@
.rating-input { .rating-input {
cursor: pointer; cursor: pointer;
input {
display: none;
}
.ctrl { .ctrl {
position: absolute; position: absolute;
z-index: 2; z-index: 2;

View File

@ -3,16 +3,27 @@ import { App } from "obsidian";
import { SvelteModal } from "./SvelteModal"; import { SvelteModal } from "./SvelteModal";
export class RatingModal extends SvelteModal<typeof RatingModalView> { export class RatingModal extends SvelteModal<typeof RatingModalView> {
constructor(app: App, onSubmit: (rating: number) => void = () => {}) { constructor(
super(app, RatingModalView, { props: { onSubmit } }); app: App,
spiceConfigured: boolean,
onSubmit: (rating: number, spice: number) => void = () => {}
) {
super(app, RatingModalView, { props: { spiceConfigured, onSubmit } });
} }
static createAndOpen(app: App): Promise<number> { static createAndOpen(
app: App,
spiceConfigured: boolean
): Promise<{ rating: number; spice: number }> {
return new Promise((resolve) => { return new Promise((resolve) => {
const modal = new RatingModal(app, (rating) => { const modal = new RatingModal(
modal.close(); app,
resolve(rating); spiceConfigured,
}); (rating, spice) => {
modal.close();
resolve({ rating, spice });
}
);
modal.open(); modal.open();
}); });
} }

View File

@ -1,32 +1,53 @@
<script lang="ts"> <script lang="ts">
import RatingInput from "@ui/components/RatingInput.svelte"; import RatingInput from "@ui/components/RatingInput.svelte";
import { Flame } from "lucide-svelte";
interface Props { interface Props {
onSubmit: (value: number) => void; spiceConfigured?: boolean;
onSubmit: (rating: number, spice: number) => void;
} }
let { onSubmit }: Props = $props(); let { spiceConfigured = false, onSubmit }: Props = $props();
let value = $state(0); let rating = $state(0);
let spice = $state(0);
function onsubmit(ev: SubmitEvent) { function onsubmit(ev: SubmitEvent) {
ev.preventDefault(); ev.preventDefault();
onSubmit(value); onSubmit(rating, spice);
} }
</script> </script>
<div class="obt-rating"> <div class="obt-rating">
<h2>Rating</h2> <h2>Rating</h2>
<form {onsubmit}> <form {onsubmit}>
<div class="value-field"> <div class="field">
<label for="value">Rating</label> <label for="rating">Rating</label>
<RatingInput bind:value /> <RatingInput name="rating" half bind:value={rating} />
</div> </div>
{#if spiceConfigured}
<div class="field">
<label for="spice">Spice</label>
<RatingInput name="spice" bind:value={spice}>
{#snippet inactive()}
<Flame color="var(--background-modifier-border)" />
{/snippet}
{#snippet active()}
<Flame
color="var(--color-red)"
fill="rgba(var(--color-red-rgb), 0.2)"
/>
{/snippet}
</RatingInput>
</div>
{/if}
<button type="submit">Submit</button> <button type="submit">Submit</button>
</form> </form>
</div> </div>
<style lang="scss"> <style lang="scss">
@use "../styles/utils";
.obt-rating { .obt-rating {
padding-bottom: var(--size-4-4); padding-bottom: var(--size-4-4);
@ -39,13 +60,13 @@
flex-direction: column; flex-direction: column;
gap: var(--size-4-4); gap: var(--size-4-4);
.value-field { .field {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
label { label {
display: none; @include utils.visually-hidden;
} }
} }

View File

@ -8,6 +8,7 @@
import type BookTrackerPlugin from "@src/main"; import type BookTrackerPlugin from "@src/main";
import { createSettingsStore } from "./store"; import { createSettingsStore } from "./store";
import { onMount } from "svelte"; import { onMount } from "svelte";
import FieldSuggest from "@ui/components/suggesters/FieldSuggest.svelte";
type Props = { type Props = {
plugin: BookTrackerPlugin; plugin: BookTrackerPlugin;
@ -121,6 +122,15 @@
bind:value={$settings.ratingProperty} bind:value={$settings.ratingProperty}
accepts={["number"]} accepts={["number"]}
/> />
<FieldSuggestItem
{app}
id="spice-field"
name="Spice Field"
description={`Select the field to use for spice rating.
Set to empty to disable.`}
bind:value={$settings.spiceProperty}
accepts={["number"]}
/>
<FieldSuggestItem <FieldSuggestItem
{app} {app}
id="page-count-field" id="page-count-field"

View File

@ -12,6 +12,7 @@ export interface BookTrackerSettings {
startDateProperty: string; startDateProperty: string;
endDateProperty: string; endDateProperty: string;
ratingProperty: string; ratingProperty: string;
spiceProperty: string;
pageCountProperty: string; pageCountProperty: string;
} }
@ -29,5 +30,6 @@ export const DEFAULT_SETTINGS: BookTrackerSettings = {
startDateProperty: "startDate", startDateProperty: "startDate",
endDateProperty: "endDate", endDateProperty: "endDate",
ratingProperty: "rating", ratingProperty: "rating",
spiceProperty: "",
pageCountProperty: "pageCount", pageCountProperty: "pageCount",
}; };