From 6591acf3e429c91cc6695a669c8c6c6f7b861e82 Mon Sep 17 00:00:00 2001 From: SongshGeo Date: Sun, 19 Apr 2026 12:40:18 +0200 Subject: [PATCH 1/2] release: 2.2.0-beta.1 --- manifest.json | 2 +- package-lock.json | 4 ++-- package.json | 2 +- versions.json | 3 ++- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/manifest.json b/manifest.json index 230610c..9e0fea0 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "longform", "name": "Longform", - "version": "2.1.0", + "version": "2.2.0-beta.1", "minAppVersion": "1.0", "description": "Write novels, screenplays, and other long projects in Obsidian.", "author": "Kevin Barrett", diff --git a/package-lock.json b/package-lock.json index ed4cebf..35fab03 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "longform", - "version": "2.1.0", + "version": "2.2.0-beta.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "longform", - "version": "2.1.0", + "version": "2.2.0-beta.1", "license": "SEE LICENSE IN LICENSE.md", "dependencies": { "@popperjs/core": "^2.11.2", diff --git a/package.json b/package.json index e497f78..106edba 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "longform", - "version": "2.1.0", + "version": "2.2.0-beta.1", "description": "Write novels, screenplays, and other long projects in Obsidian (https://obsidian.md).", "main": "main.js", "scripts": { diff --git a/versions.json b/versions.json index 0d81ce1..5b29f74 100644 --- a/versions.json +++ b/versions.json @@ -12,5 +12,6 @@ "1.0.3": "0.12.11", "1.0.2": "0.12.11", "1.0.1": "0.12.0", - "1.0.0": "0.12.0" + "1.0.0": "0.12.0", + "2.2.0-beta.1": "1.0" } From 7919755481904cd0497aaa6b348ad94a87f827ec Mon Sep 17 00:00:00 2001 From: SongshGeo Date: Sun, 3 May 2026 11:17:10 +0200 Subject: [PATCH 2/2] feat: :sparkles: implement scene compile numbering and ignore functionality - Updated LongformAPI to include scenesWithCompileNumberings method for handling scene numbering during compile. - Enhanced draft-utils with scenesForCompileNumbering to filter out ignored scenes. - Modified compile function to utilize new scene numbering logic. - Added UI elements in SceneList and ExplorerPane to toggle scene ignore status, affecting compile output. - Bumped version to 2.1.0 in package-lock.json. --- docs/COMPILE.md | 64 ++ .../steps/add-zenodo-frontmatter-utils.ts | 155 +++++ src/compile/steps/add-zenodo-frontmatter.ts | 110 ++++ src/compile/steps/index.ts | 4 + .../steps/replace-json-placeholders-utils.ts | 61 ++ .../steps/replace-json-placeholders.ts | 135 ++++ src/view/explorer/ExplorerPane.ts | 11 + src/view/explorer/ProjectDetails.svelte | 55 ++ src/view/metadata-modal/MetadataModal.svelte | 586 ++++++++++++++++++ src/view/metadata-modal/index.ts | 43 ++ .../.obsidian/core-plugins.json | 4 +- .../projects 2.0/A Novel/metadata.json | 30 + .../projects 2.0/A Novel/results.json | 8 + test-longform-vault/simple-project/3.md | 3 + .../simple-project/manuscript.md | 195 +++++- .../simple-project/metadata.json | 86 +++ .../steps/add-zenodo-frontmatter.test.ts | 162 +++++ .../steps/replace-json-placeholders.test.ts | 82 +++ 18 files changed, 1787 insertions(+), 7 deletions(-) create mode 100644 src/compile/steps/add-zenodo-frontmatter-utils.ts create mode 100644 src/compile/steps/add-zenodo-frontmatter.ts create mode 100644 src/compile/steps/replace-json-placeholders-utils.ts create mode 100644 src/compile/steps/replace-json-placeholders.ts create mode 100644 src/view/metadata-modal/MetadataModal.svelte create mode 100644 src/view/metadata-modal/index.ts create mode 100644 test-longform-vault/projects 2.0/A Novel/metadata.json create mode 100644 test-longform-vault/projects 2.0/A Novel/results.json create mode 100644 test-longform-vault/simple-project/metadata.json create mode 100644 test/compile/steps/add-zenodo-frontmatter.test.ts create mode 100644 test/compile/steps/replace-json-placeholders.test.ts diff --git a/docs/COMPILE.md b/docs/COMPILE.md index 53e20bb..ced561b 100644 --- a/docs/COMPILE.md +++ b/docs/COMPILE.md @@ -88,6 +88,70 @@ Saves the manuscript as Markdown note in your vault. Options: | Output Path | Text | manuscript.md | Path relative to your project at which to save your compiled manuscript. $1, if present, will be replaced with your project’s title. | | Open Compiled Manuscript | Boolean | true | If checked, open the compiled manuscript in a new pane. | +#### Add Zenodo Frontmatter + +_Manuscript_ + +Reads a [Zenodo deposition](https://developers.zenodo.org/#representation)–style metadata JSON from your project folder (or its `source/` subfolder) and prepends a Pandoc-compatible YAML frontmatter to the manuscript. Keeping the metadata in Zenodo's schema means the same file can be uploaded to Zenodo when archiving your work. Options: + +| Name | Type | Default | Description | +| --------------------- | ------- | ------------- | ------------------------------------------------------------------------------------------------------------ | +| Metadata file | Text | metadata.json | Filename of the Zenodo metadata JSON in your project folder. Trailing `.json` is optional. | +| Error on missing file | Boolean | true | If checked, throw when the metadata file is not found. Otherwise pass the manuscript through unchanged. | + +The metadata file follows Zenodo's deposition schema for fields like `title`, `publication_date`, `description`, `creators[]`, `contributors[]`, `keywords[]`, `journal_title`, and `version`. Plugin-specific keys (Pandoc template, citation style, line numbering, multiple affiliations per author, corresponding-author flags, free-form extra YAML) live under a `_longform` namespace that Zenodo will ignore on upload. Example: + +```json +{ + "title": "A Study", + "publication_date": "2026-05-03", + "description": "An abstract.", + "creators": [ + { "name": "Doe, Jane", "affiliation": "Org A", "orcid": "0000-0000-0000-0000" }, + { "name": "Roe, Rick", "affiliation": "Org B" } + ], + "keywords": ["alpha", "beta"], + "journal_title": "Nature", + "version": "v1.0", + "_longform": { + "acronym": "STUDY", + "csl": "nature", + "template": "default", + "lineno": false, + "figures_at_end": false, + "author_affiliations": { "Doe, Jane": ["Org A", "Org C"] }, + "corresponding": ["Roe, Rick"], + "extra_yaml": "numbersections: true\n" + } +} +``` + +The step derives Pandoc's indexed `affiliations:` table from `creators[].affiliation` (or `_longform.author_affiliations[name]` when an author belongs to more than one institution), in order of first appearance. `title` and `creators` are required; the step throws if either is missing. + +#### Replace JSON Placeholders + +_Manuscript_ + +Replaces `{{ path.to.value }}` placeholders in your manuscript with values resolved from a JSON file in your project folder (or its `source/` subfolder). Useful for injecting computed numerical results, dates, or any other values produced outside Obsidian. Options: + +| Name | Type | Default | Description | +| ---------------- | ------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------ | +| JSON file | Text | results.json | Filename of the JSON data file. Trailing `.json` is optional. | +| Start delimiter | Text | `{{` | Left delimiter of placeholders. | +| End delimiter | Text | `}}` | Right delimiter of placeholders. | +| Error on missing | Boolean | false | If checked, throw when a placeholder path is not found in the JSON file. Otherwise the placeholder is left unchanged in the output. | + +Path expressions support dot and bracket notation in any combination (`a.b.c`, `a.b[0].c`). Object values are stringified as JSON, `null` becomes the empty string. Example `results.json`: + +```json +{ + "summary": { "n": 42, "mean": 3.14 }, + "samples": [{ "id": "S-01" }, { "id": "S-02" }] +} +``` + +The manuscript text `We collected {{ summary.n }} samples (first: {{ samples[0].id }}).` becomes `We collected 42 samples (first: S-01).`. + ### User Script Steps In addition to the built-in steps above, Longform also supports user script steps, which are arbitrary JavaScript scripts that can be loaded and used like any other step. diff --git a/src/compile/steps/add-zenodo-frontmatter-utils.ts b/src/compile/steps/add-zenodo-frontmatter-utils.ts new file mode 100644 index 0000000..ce3616e --- /dev/null +++ b/src/compile/steps/add-zenodo-frontmatter-utils.ts @@ -0,0 +1,155 @@ +export interface ZenodoCreator { + name: string; + affiliation?: string; + orcid?: string; + gnd?: string; +} + +export interface ZenodoContributor extends ZenodoCreator { + type?: string; +} + +export interface LongformExtras { + acronym?: string; + csl?: string; + template?: string; + lineno?: boolean; + figures_at_end?: boolean; + author_affiliations?: Record; + corresponding?: string[]; + extra_yaml?: string; +} + +export interface ZenodoMetadata { + title?: string; + publication_date?: string; + description?: string; + creators?: ZenodoCreator[]; + contributors?: ZenodoContributor[]; + keywords?: string[]; + journal_title?: string; + version?: string; + _longform?: LongformExtras; +} + +/** + * Build a Pandoc-style YAML frontmatter from a Zenodo deposition metadata + * object. Returns the body of the frontmatter (no surrounding `---` lines) + * and always ends with a newline. + */ +export function buildPandocYaml(metadata: ZenodoMetadata): string { + if (!metadata || typeof metadata !== "object") { + throw new Error("[Add Zenodo Frontmatter] Metadata must be a JSON object."); + } + if (!metadata.title || typeof metadata.title !== "string") { + throw new Error( + "[Add Zenodo Frontmatter] Metadata is missing required field 'title'." + ); + } + if ( + !Array.isArray(metadata.creators) || + metadata.creators.length === 0 || + metadata.creators.some((c) => !c || typeof c.name !== "string" || !c.name) + ) { + throw new Error( + "[Add Zenodo Frontmatter] Metadata is missing required field 'creators' (non-empty array of {name, ...})." + ); + } + + const ext = metadata._longform ?? {}; + const date = + metadata.publication_date && metadata.publication_date.length > 0 + ? metadata.publication_date + : new Date().toISOString().slice(0, 10); + + const correspondingSet = new Set(ext.corresponding ?? []); + const authorAffiliations = ext.author_affiliations ?? {}; + + const affiliationIndex: string[] = []; + const indexFor = (name: string): number => { + const i = affiliationIndex.indexOf(name); + if (i >= 0) return i + 1; + affiliationIndex.push(name); + return affiliationIndex.length; + }; + + type AuthorOut = { + name: string; + affiliationIndices: number[]; + corresponding: boolean; + }; + const authorsOut: AuthorOut[] = metadata.creators.map((creator) => { + const explicit = authorAffiliations[creator.name]; + const affilNames = + explicit && explicit.length > 0 + ? explicit + : creator.affiliation + ? [creator.affiliation] + : []; + return { + name: creator.name, + affiliationIndices: affilNames.map(indexFor), + corresponding: correspondingSet.has(creator.name), + }; + }); + + const lines: string[] = []; + lines.push(`title: ${yamlString(metadata.title)}`); + lines.push(`date: ${yamlString(date)}`); + + lines.push("authors:"); + for (const a of authorsOut) { + lines.push(` - name: ${yamlString(a.name)}`); + if (a.affiliationIndices.length > 0) { + lines.push(` affiliation: [${a.affiliationIndices.join(", ")}]`); + } + if (a.corresponding) { + lines.push(` corresponding: ${yamlString("yes")}`); + } + } + + if (affiliationIndex.length > 0) { + lines.push("affiliations:"); + affiliationIndex.forEach((name, i) => { + lines.push(` - index: ${i + 1}`); + lines.push(` name: ${yamlString(name)}`); + }); + } + + lines.push(`abstract: ${yamlString(metadata.description ?? "")}`); + + if (Array.isArray(metadata.keywords) && metadata.keywords.length > 0) { + lines.push("keywords:"); + for (const k of metadata.keywords) { + lines.push(` - ${yamlString(String(k))}`); + } + } + + lines.push(`target: ${yamlString(metadata.journal_title ?? "")}`); + lines.push(`acronym: ${yamlString(ext.acronym ?? "")}`); + lines.push(`csl: ${yamlString(ext.csl ?? "")}`); + + if (ext.template) { + lines.push(`template: ${yamlString(ext.template)}`); + } + if (ext.lineno) { + lines.push(`lineno: ${yamlString("true")}`); + } + if (ext.figures_at_end) { + lines.push(`figures-at-end: ${yamlString("true")}`); + } + + let body = lines.join("\n") + "\n"; + if (ext.extra_yaml && ext.extra_yaml.length > 0) { + const extra = ext.extra_yaml.endsWith("\n") + ? ext.extra_yaml + : ext.extra_yaml + "\n"; + body += extra; + } + return body; +} + +/** Quote a value as a YAML double-quoted string, escaping `\` and `"`. */ +function yamlString(s: string): string { + return `"${String(s).replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`; +} diff --git a/src/compile/steps/add-zenodo-frontmatter.ts b/src/compile/steps/add-zenodo-frontmatter.ts new file mode 100644 index 0000000..dbe491a --- /dev/null +++ b/src/compile/steps/add-zenodo-frontmatter.ts @@ -0,0 +1,110 @@ +import { TFile } from "obsidian"; +import type { CompileContext, CompileManuscriptInput } from ".."; +import { + CompileStepKind, + CompileStepOptionType, + makeBuiltinStep, +} from "./abstract-compile-step"; +import { + buildPandocYaml, + type ZenodoMetadata, +} from "./add-zenodo-frontmatter-utils"; + +export const AddZenodoFrontmatterStep = makeBuiltinStep({ + id: "add-zenodo-frontmatter", + description: { + name: "Add Zenodo Frontmatter", + description: + "Reads a Zenodo-style metadata JSON from your project folder and prepends a Pandoc-compatible YAML frontmatter to the manuscript.", + availableKinds: [CompileStepKind.Manuscript], + options: [ + { + id: "metadata-file", + name: "Metadata file", + description: + "Filename of the Zenodo deposition metadata JSON in your project folder (or its 'source/' subfolder). Trailing '.json' is optional.", + type: CompileStepOptionType.Text, + default: "metadata.json", + }, + { + id: "error-on-missing-file", + name: "Error on missing file", + description: + "If checked, throw an error when the metadata file is not found. Otherwise pass the manuscript through unchanged.", + type: CompileStepOptionType.Boolean, + default: true, + }, + ], + }, + async compile( + input: CompileManuscriptInput, + context: CompileContext + ): Promise { + if (context.kind !== CompileStepKind.Manuscript) { + throw new Error("Cannot add frontmatter to non-manuscript."); + } + + const metaFileName = String( + context.optionValues["metadata-file"] ?? "metadata.json" + ).trim(); + const errorOnMissingFile = Boolean( + context.optionValues["error-on-missing-file"] ?? true + ); + + const baseName = metaFileName.endsWith(".json") + ? metaFileName + : `${metaFileName}.json`; + + const candidatePaths = [ + `${context.projectPath}/${baseName}`, + `${context.projectPath}/source/${baseName}`, + ]; + + let file: TFile | null = null; + let foundPath = ""; + for (const path of candidatePaths) { + const f = context.app.vault.getAbstractFileByPath(path); + if (f instanceof TFile) { + file = f; + foundPath = path; + break; + } + } + + if (!file) { + if (errorOnMissingFile) { + throw new Error( + `[Add Zenodo Frontmatter] Metadata file not found at ${candidatePaths.join( + " or " + )}` + ); + } + return input; + } + + const raw = await context.app.vault.cachedRead(file); + let metadata: ZenodoMetadata; + try { + metadata = JSON.parse(raw) as ZenodoMetadata; + } catch (e) { + throw new Error( + `[Add Zenodo Frontmatter] Invalid JSON in ${foundPath}: ${ + (e as Error).message + }` + ); + } + + const yaml = buildPandocYaml(metadata); + return { + contents: `---\n${yaml}---\n\n${input.contents}`, + }; + }, +}); + +export { + buildPandocYaml, + type ZenodoMetadata, + type ZenodoCreator, + type ZenodoContributor, + type LongformExtras, +} from "./add-zenodo-frontmatter-utils"; diff --git a/src/compile/steps/index.ts b/src/compile/steps/index.ts index f716c78..423a7d3 100644 --- a/src/compile/steps/index.ts +++ b/src/compile/steps/index.ts @@ -6,14 +6,18 @@ import { RemoveStrikethroughsStep } from "./remove-strikethroughs"; import { StripFrontmatterStep } from "./strip-frontmatter"; import { WriteToNoteStep } from "./write-to-note"; import { AddFrontmatterStep } from "./add-frontmatter"; +import { AddZenodoFrontmatterStep } from "./add-zenodo-frontmatter"; +import { ReplaceJsonPlaceholdersStep } from "./replace-json-placeholders"; export const BUILTIN_STEPS = [ AddFrontmatterStep, + AddZenodoFrontmatterStep, ConcatenateTextStep, PrependTitleStep, RemoveCommentsStep, RemoveLinksStep, RemoveStrikethroughsStep, + ReplaceJsonPlaceholdersStep, StripFrontmatterStep, WriteToNoteStep, ]; diff --git a/src/compile/steps/replace-json-placeholders-utils.ts b/src/compile/steps/replace-json-placeholders-utils.ts new file mode 100644 index 0000000..4e976ee --- /dev/null +++ b/src/compile/steps/replace-json-placeholders-utils.ts @@ -0,0 +1,61 @@ +/** + * Resolve a dot/bracket path expression against a value. + * Supports `a.b.c` and `a.b[0].c` mixed notation. Returns `undefined` + * for any segment that does not resolve. + */ +export function getByPath(root: unknown, pathExpr: string): unknown { + const tokens: string[] = []; + let buf = ""; + for (let i = 0; i < pathExpr.length; i++) { + const ch = pathExpr[i]; + if (ch === ".") { + if (buf) { + tokens.push(buf); + buf = ""; + } + } else if (ch === "[") { + if (buf) { + tokens.push(buf); + buf = ""; + } + let j = i + 1; + let idx = ""; + while (j < pathExpr.length && pathExpr[j] !== "]") { + idx += pathExpr[j++]; + } + i = j; + tokens.push(idx.trim()); + } else { + buf += ch; + } + } + if (buf) tokens.push(buf); + + let cur: unknown = root; + for (const t of tokens) { + if (cur === null || cur === undefined) return undefined; + if (/^\d+$/.test(t)) { + const idx = parseInt(t, 10); + if (!Array.isArray(cur) || idx < 0 || idx >= cur.length) return undefined; + cur = cur[idx]; + } else if (typeof cur === "object") { + const obj = cur as Record; + cur = Object.prototype.hasOwnProperty.call(obj, t) ? obj[t] : undefined; + } else { + return undefined; + } + } + return cur; +} + +/** Build a global regex matching ` path ` placeholders. */ +export function buildPlaceholderRegex( + startDelim: string, + endDelim: string +): RegExp { + const esc = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + return new RegExp( + `${esc(startDelim)}\\s*([a-zA-Z0-9_.$\\[\\]-]+)\\s*${esc(endDelim)}`, + "g" + ); +} diff --git a/src/compile/steps/replace-json-placeholders.ts b/src/compile/steps/replace-json-placeholders.ts new file mode 100644 index 0000000..01ccf85 --- /dev/null +++ b/src/compile/steps/replace-json-placeholders.ts @@ -0,0 +1,135 @@ +import { TFile } from "obsidian"; +import type { CompileContext, CompileManuscriptInput } from ".."; +import { + CompileStepKind, + CompileStepOptionType, + makeBuiltinStep, +} from "./abstract-compile-step"; +import { + buildPlaceholderRegex, + getByPath, +} from "./replace-json-placeholders-utils"; + +export const ReplaceJsonPlaceholdersStep = makeBuiltinStep({ + id: "replace-json-placeholders", + description: { + name: "Replace JSON Placeholders", + description: + "Replaces {{path.to.value}} placeholders in your manuscript with values from a JSON file in your project folder.", + availableKinds: [CompileStepKind.Manuscript], + options: [ + { + id: "json-file", + name: "JSON file", + description: + "Filename of the JSON data file in your project folder (or its 'source/' subfolder). Trailing '.json' is optional.", + type: CompileStepOptionType.Text, + default: "results.json", + }, + { + id: "start-delim", + name: "Start delimiter", + description: "Left delimiter of placeholders.", + type: CompileStepOptionType.Text, + default: "{{", + }, + { + id: "end-delim", + name: "End delimiter", + description: "Right delimiter of placeholders.", + type: CompileStepOptionType.Text, + default: "}}", + }, + { + id: "error-on-missing", + name: "Error on missing", + description: + "If checked, throw an error when a placeholder path is not found in the JSON file. Otherwise leave the placeholder unchanged.", + type: CompileStepOptionType.Boolean, + default: false, + }, + ], + }, + async compile( + input: CompileManuscriptInput, + context: CompileContext + ): Promise { + if (context.kind !== CompileStepKind.Manuscript) { + throw new Error("Cannot replace placeholders on non-manuscript."); + } + + const jsonFileName = String( + context.optionValues["json-file"] ?? "results.json" + ).trim(); + const startDelim = String(context.optionValues["start-delim"] ?? "{{"); + const endDelim = String(context.optionValues["end-delim"] ?? "}}"); + const errorOnMissing = Boolean(context.optionValues["error-on-missing"]); + + const baseName = jsonFileName.endsWith(".json") + ? jsonFileName + : `${jsonFileName}.json`; + + const candidatePaths = [ + `${context.projectPath}/${baseName}`, + `${context.projectPath}/source/${baseName}`, + ]; + + let file: TFile | null = null; + let foundPath = ""; + for (const path of candidatePaths) { + const f = context.app.vault.getAbstractFileByPath(path); + if (f instanceof TFile) { + file = f; + foundPath = path; + break; + } + } + + if (!file) { + throw new Error( + `[Replace JSON Placeholders] JSON file not found at ${candidatePaths.join( + " or " + )}` + ); + } + + const raw = await context.app.vault.cachedRead(file); + let data: unknown; + try { + data = JSON.parse(raw); + } catch (e) { + throw new Error( + `[Replace JSON Placeholders] Invalid JSON in ${foundPath}: ${ + (e as Error).message + }` + ); + } + + const pattern = buildPlaceholderRegex(startDelim, endDelim); + const replaced = input.contents.replace(pattern, (match, rawPath) => { + const pathExpr = String(rawPath).trim(); + const value = getByPath(data, pathExpr); + if (value === undefined) { + if (errorOnMissing) { + throw new Error( + `[Replace JSON Placeholders] Missing value for placeholder path: ${pathExpr}` + ); + } + return match; + } + if (value === null) return ""; + if (typeof value === "object") { + try { + return JSON.stringify(value); + } catch { + return String(value); + } + } + return String(value); + }); + + return { contents: replaced }; + }, +}); + +export { buildPlaceholderRegex, getByPath }; diff --git a/src/view/explorer/ExplorerPane.ts b/src/view/explorer/ExplorerPane.ts index af342e9..39fa16c 100644 --- a/src/view/explorer/ExplorerPane.ts +++ b/src/view/explorer/ExplorerPane.ts @@ -21,6 +21,7 @@ import { get } from "svelte/store"; import { drafts, pluginSettings, selectedDraft } from "src/model/stores"; import { insertScene } from "src/model/draft-utils"; import NewDraftModal from "src/view/project-lifecycle/new-draft-modal"; +import MetadataModal from "src/view/metadata-modal"; import { UndoManager } from "../undo/undo-manager"; import { ignoreScene } from "./scene-menu-items"; import { appContext } from "../utils"; @@ -314,6 +315,16 @@ export class ExplorerPane extends ItemView { new NewDraftModal(this.app).open(); }); + context.set("showMetadataModal", () => { + const draft = get(selectedDraft); + if (!draft) return; + const projectPath = draft.vaultPath + .split("/") + .slice(0, -1) + .join("/"); + new MetadataModal(this.app, projectPath, draft.title).open(); + }); + this.explorerView = new ExplorerView({ target: this.contentEl, context, diff --git a/src/view/explorer/ProjectDetails.svelte b/src/view/explorer/ProjectDetails.svelte index 0345496..181e8e3 100644 --- a/src/view/explorer/ProjectDetails.svelte +++ b/src/view/explorer/ProjectDetails.svelte @@ -173,6 +173,12 @@ function onNewDraft() { showNewDraftModal(); } + + const showMetadataModal: () => void = getContext("showMetadataModal"); + function onEditMetadata() { + showMetadataModal(); + } + let showManuscriptMetadata = true;
@@ -230,6 +236,40 @@ {/if}
{/if} + {#if $selectedDraft} +
+
{ + showManuscriptMetadata = !showManuscriptMetadata; + }} + > + +

Manuscript Metadata

+
+ {#if showManuscriptMetadata} +
+ + +
+ {/if} +
+ {/if}
diff --git a/src/view/metadata-modal/MetadataModal.svelte b/src/view/metadata-modal/MetadataModal.svelte new file mode 100644 index 0000000..a5c2498 --- /dev/null +++ b/src/view/metadata-modal/MetadataModal.svelte @@ -0,0 +1,586 @@ + + +