Switch to vite for building and add rating modal

This commit is contained in:
Evan Fiordeliso 2025-06-27 22:28:22 -04:00
parent db676f93f2
commit 1ecf93e5da
11 changed files with 987 additions and 24 deletions

View File

@ -4,23 +4,26 @@
"description": "This is a sample plugin for Obsidian (https://obsidian.md)", "description": "This is a sample plugin for Obsidian (https://obsidian.md)",
"main": "main.js", "main": "main.js",
"scripts": { "scripts": {
"dev": "node esbuild.config.mjs", "dev": "vite build --watch --mode=development",
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production", "build": "vite build",
"version": "node version-bump.mjs && git add manifest.json versions.json", "version": "node version-bump.mjs && git add manifest.json versions.json",
"svelte-check": "svelte-check --tsconfig tsconfig.json" "svelte-check": "svelte-check --tsconfig tsconfig.json"
}, },
"keywords": [], "keywords": [],
"author": "", "author": "",
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"@popperjs/core": "^2.11.8", "@popperjs/core": "^2.11.8",
"@types/node": "^16.11.6", "@sveltejs/vite-plugin-svelte": "^5.1.0",
"@types/node": "^24.0.6",
"@typescript-eslint/eslint-plugin": "5.29.0", "@typescript-eslint/eslint-plugin": "5.29.0",
"@typescript-eslint/parser": "5.29.0", "@typescript-eslint/parser": "5.29.0",
"bits-ui": "^2.8.10",
"builtin-modules": "3.3.0", "builtin-modules": "3.3.0",
"esbuild": "0.17.3", "esbuild": "0.17.3",
"esbuild-svelte": "^0.9.3", "esbuild-svelte": "^0.9.3",
"handlebars": "^4.7.8", "handlebars": "^4.7.8",
"lucide-svelte": "^0.525.0",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"obsidian": "latest", "obsidian": "latest",
"sass": "^1.89.2", "sass": "^1.89.2",
@ -28,6 +31,7 @@
"svelte-check": "^4.2.2", "svelte-check": "^4.2.2",
"svelte-preprocess": "^6.0.3", "svelte-preprocess": "^6.0.3",
"tslib": "2.4.0", "tslib": "2.4.0",
"typescript": "5.0.4" "typescript": "5.0.4",
"vite": "^6.0.0"
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,2 +0,0 @@
onlyBuiltDependencies:
- esbuild

View File

@ -0,0 +1,57 @@
<script lang="ts">
import RatingInput from "./RatingInput.svelte";
interface Props {
onSubmit: (value: number) => void;
}
let { onSubmit }: Props = $props();
let value = $state(0);
function onsubmit(ev: SubmitEvent) {
ev.preventDefault();
onSubmit(value);
}
</script>
<div class="obt-rating">
<h2>Rating</h2>
<form {onsubmit}>
<div class="value-field">
<label for="value">Rating</label>
<RatingInput bind:value />
</div>
<button type="submit">Submit</button>
</form>
</div>
<style lang="scss">
.obt-rating {
padding-bottom: var(--size-4-4);
h2 {
text-align: center;
}
form {
display: flex;
flex-direction: column;
gap: var(--size-4-4);
.value-field {
display: flex;
flex-direction: column;
align-items: center;
label {
display: none;
}
}
button {
align-self: stretch;
}
}
}
</style>

View File

@ -0,0 +1,62 @@
<script lang="ts">
import { unstable_RatingGroup as RatingGroup } from "bits-ui";
import { Star, StarHalf } from "lucide-svelte";
interface Props {
value?: number;
name?: string;
}
let { value = $bindable(), name }: Props = $props();
</script>
<div class="rating-input">
<RatingGroup.Root
{name}
bind:value
min={0}
max={5}
allowHalf
class="rating-group"
>
{#snippet children({ items })}
{#each items as item (item.index)}
<RatingGroup.Item index={item.index} class="rating-item">
{#if item.state === "inactive"}
<Star fill="var(--interactive-normal)" />
{:else if item.state === "active"}
<Star fill="var(--interactive-accent)" />
{:else if item.state === "partial"}
<Star fill="var(--interactive-normal)" />
<StarHalf fill="var(--interactive-accent)" />
{/if}
</RatingGroup.Item>
{/each}
{/snippet}
</RatingGroup.Root>
</div>
<style lang="scss">
.rating-input {
display: flex;
align-items: center;
gap: 0.5rem;
:global(svg) {
position: absolute;
width: var(--size-4-16);
height: var(--size-4-16);
}
:global(.rating-group) {
display: flex;
gap: 0.25rem;
}
:global(.rating-item) {
position: relative;
width: var(--size-4-16);
height: var(--size-4-16);
}
}
</style>

View File

@ -1,5 +1,9 @@
import { requestUrl } from "obsidian"; import { requestUrl } from "obsidian";
import { Author, Book as OutputBook, Series as OutputSeries } from "../types"; import type {
Author,
Book as OutputBook,
Series as OutputSeries,
} from "../types";
interface Ref { interface Ref {
__ref: string; __ref: string;

View File

@ -16,6 +16,7 @@ import {
} from "./const"; } from "./const";
import { ReadingLog, Storage } from "@utils/storage"; import { ReadingLog, Storage } from "@utils/storage";
import { ReadingProgressModal } from "@views/reading-progress-modal"; import { ReadingProgressModal } from "@views/reading-progress-modal";
import { RatingModal } from "@views/rating-modal";
export default class BookTrackerPlugin extends Plugin { export default class BookTrackerPlugin extends Plugin {
settings: BookTrackerPluginSettings; settings: BookTrackerPluginSettings;
@ -279,6 +280,11 @@ export default class BookTrackerPlugin extends Plugin {
return; return;
} }
if (!this.settings.ratingProperty) {
new Notice("Rating property is not set in settings.");
return;
}
const pageLength = const pageLength =
(this.app.metadataCache.getFileCache(activeFile)?.frontmatter?.[ (this.app.metadataCache.getFileCache(activeFile)?.frontmatter?.[
this.settings.pageLengthProperty this.settings.pageLengthProperty
@ -291,6 +297,8 @@ export default class BookTrackerPlugin extends Plugin {
return; return;
} }
const rating = await RatingModal.createAndOpen(this.app);
await this.readingLog.addEntry(activeFile.basename, pageLength); await this.readingLog.addEntry(activeFile.basename, pageLength);
// @ts-expect-error Moment is provided by Obsidian // @ts-expect-error Moment is provided by Obsidian
@ -299,6 +307,7 @@ export default class BookTrackerPlugin extends Plugin {
this.app.fileManager.processFrontMatter(activeFile, (frontMatter) => { this.app.fileManager.processFrontMatter(activeFile, (frontMatter) => {
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;
}); });
new Notice("Reading finished for " + activeFile.name); new Notice("Reading finished for " + activeFile.name);

View File

@ -1,7 +1,7 @@
// Credits go to Liam's Periodic Notes Plugin: https://github.com/liamcain/obsidian-periodic-notes // Credits go to Liam's Periodic Notes Plugin: https://github.com/liamcain/obsidian-periodic-notes
import { App, ISuggestOwner, Scope } from "obsidian"; import { App, type ISuggestOwner, Scope } from "obsidian";
import { createPopper, Instance as PopperInstance } from "@popperjs/core"; import { createPopper, type Instance as PopperInstance } from "@popperjs/core";
const wrapAround = (value: number, size: number): number => { const wrapAround = (value: number, size: number): number => {
return ((value % size) + size) % size; return ((value % size) + size) % size;

39
src/views/rating-modal.ts Normal file
View File

@ -0,0 +1,39 @@
import Rating from "@components/Rating.svelte";
import { App, Modal } from "obsidian";
import { mount, unmount } from "svelte";
export class RatingModal extends Modal {
private component: ReturnType<typeof Rating> | undefined;
constructor(app: App, private readonly onSubmit: (rating: number) => void) {
super(app);
}
onOpen(): void {
this.component = mount(Rating, {
target: this.contentEl,
props: {
onSubmit: (rating) => {
this.onSubmit(rating);
this.close();
},
},
});
}
onClose(): void {
if (this.component) {
unmount(this.component);
this.component = undefined;
}
}
static createAndOpen(app: App): Promise<number> {
return new Promise((resolve) => {
const modal = new RatingModal(app, (rating) => {
resolve(rating);
});
modal.open();
});
}
}

View File

@ -1,3 +1 @@
.obt-settings .search-input-container { .obt-goodreads-search.svelte-11kqgr4{padding-bottom:var(--size-4-4)}.obt-goodreads-search.svelte-11kqgr4 input:where(.svelte-11kqgr4){width:100%}.obt-goodreads-search-suggestion.svelte-1kq4sbn{display:flex;align-items:center}.obt-goodreads-search-suggestion.svelte-1kq4sbn img.cover:where(.svelte-1kq4sbn){max-width:100px;max-height:100px;margin-right:var(--size-4-2);object-fit:cover;border-radius:var(--radius-s)}.obt-goodreads-search-suggestion.svelte-1kq4sbn .details:where(.svelte-1kq4sbn){flex-grow:1}.obt-goodreads-search-suggestion.svelte-1kq4sbn .details:where(.svelte-1kq4sbn) .title:where(.svelte-1kq4sbn){color:var(--text-normal);font-size:var(--font-ui-medium)}.obt-goodreads-search-suggestion.svelte-1kq4sbn .details:where(.svelte-1kq4sbn) .extra-details:where(.svelte-1kq4sbn){color:var(--text-muted);font-size:var(--font-ui-small);display:flex;gap:var(--size-4-1)}.obt-reading-progress.svelte-paogvq{padding-bottom:var(--size-4-4)}.obt-reading-progress.svelte-paogvq h2:where(.svelte-paogvq){margin-bottom:var(--size-4-6)}.obt-reading-progress.svelte-paogvq form:where(.svelte-paogvq){display:flex;flex-direction:column;gap:var(--size-4-4)}.obt-reading-progress.svelte-paogvq form:where(.svelte-paogvq) .value-field:where(.svelte-paogvq){display:flex;flex-direction:column;align-items:stretch;gap:var(--size-4-2);width:100%}.obt-reading-progress.svelte-paogvq form:where(.svelte-paogvq) .mode-field:where(.svelte-paogvq){width:100%;display:grid;grid-template-columns:1fr 1fr}.obt-reading-progress.svelte-paogvq form:where(.svelte-paogvq) .mode-field:where(.svelte-paogvq) .mode-field-option:where(.svelte-paogvq){text-align:center;padding:var(--size-4-2);background-color:var(--interactive-normal);border:var(--border-width) solid var(--background-modifier-border);border-radius:var(--radius-m)}.obt-reading-progress.svelte-paogvq form:where(.svelte-paogvq) .mode-field:where(.svelte-paogvq) .mode-field-option:where(.svelte-paogvq):has(input:where(.svelte-paogvq):checked){background-color:var(--interactive-accent)}.obt-reading-progress.svelte-paogvq form:where(.svelte-paogvq) .mode-field:where(.svelte-paogvq) .mode-field-option:where(.svelte-paogvq):hover{background-color:var(--interactive-hover)}.obt-reading-progress.svelte-paogvq form:where(.svelte-paogvq) .mode-field:where(.svelte-paogvq) .mode-field-option:where(.svelte-paogvq) input:where(.svelte-paogvq){display:none}.obt-reading-progress.svelte-paogvq form:where(.svelte-paogvq) .mode-field:where(.svelte-paogvq) .mode-field-option:where(.svelte-paogvq).page-number{border-top-right-radius:0;border-bottom-right-radius:0}.obt-reading-progress.svelte-paogvq form:where(.svelte-paogvq) .mode-field:where(.svelte-paogvq) .mode-field-option:where(.svelte-paogvq).percentage{border-top-left-radius:0;border-bottom-left-radius:0}.rating-input.svelte-19sa8ca{display:flex;align-items:center;gap:.5rem}.rating-input.svelte-19sa8ca svg{position:absolute;width:var(--size-4-16);height:var(--size-4-16)}.rating-input.svelte-19sa8ca .rating-group{display:flex;gap:.25rem}.rating-input.svelte-19sa8ca .rating-item{position:relative;width:var(--size-4-16);height:var(--size-4-16)}.obt-rating.svelte-badw3i{padding-bottom:var(--size-4-4)}.obt-rating.svelte-badw3i h2:where(.svelte-badw3i){text-align:center}.obt-rating.svelte-badw3i form:where(.svelte-badw3i){display:flex;flex-direction:column;gap:var(--size-4-4)}.obt-rating.svelte-badw3i form:where(.svelte-badw3i) .value-field:where(.svelte-badw3i){display:flex;flex-direction:column;align-items:center}.obt-rating.svelte-badw3i form:where(.svelte-badw3i) .value-field:where(.svelte-badw3i) label:where(.svelte-badw3i){display:none}.obt-rating.svelte-badw3i form:where(.svelte-badw3i) button:where(.svelte-badw3i){align-self:stretch}
width: 100%;
}

61
vite.config.mts Normal file
View File

@ -0,0 +1,61 @@
import { UserConfig, defineConfig } from "vite";
import { svelte } from "@sveltejs/vite-plugin-svelte";
import path from "path";
import builtins from "builtin-modules";
export default defineConfig(async ({ mode }) => {
const { resolve } = path;
const prod = mode === "production";
return {
plugins: [svelte()],
resolve: {
alias: {
"@components": path.resolve(__dirname, "./src/components"),
"@data-sources": path.resolve(__dirname, "./src/data-sources"),
"@settings": path.resolve(__dirname, "./src/settings"),
"@utils": path.resolve(__dirname, "./src/utils"),
"@views": path.resolve(__dirname, "./src/views"),
"@src": path.resolve(__dirname, "./src"),
},
},
build: {
lib: {
entry: resolve(__dirname, "src/main.ts"),
name: "main",
fileName: () => "main.js",
formats: ["cjs"],
},
minify: prod,
sourcemap: prod ? false : "inline",
cssCodeSplit: false,
emptyOutDir: false,
outDir: "",
rollupOptions: {
input: {
main: resolve(__dirname, "src/main.ts"),
},
output: {
entryFileNames: "main.js",
assetFileNames: "styles.css",
},
external: [
"obsidian",
"electron",
"@codemirror/autocomplete",
"@codemirror/collab",
"@codemirror/commands",
"@codemirror/language",
"@codemirror/lint",
"@codemirror/search",
"@codemirror/state",
"@codemirror/view",
"@lezer/common",
"@lezer/highlight",
"@lezer/lr",
...builtins,
],
},
},
} as UserConfig;
});