mirror of
https://github.com/ebullient/obsidian-deck-notes.git
synced 2026-07-22 06:40:43 +00:00
Compare commits
41 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5f0adac25b | ||
|
|
c782933af5 | ||
|
|
8c3379b349 | ||
|
|
83f0a61f28 | ||
|
|
5ab47a954b | ||
|
|
10aaaf2ed9 | ||
|
|
9c1c2c36a7 | ||
|
|
f279ae3297 | ||
|
|
990113e1e6 | ||
|
|
ce408c1a80 | ||
|
|
5db632cfb0 | ||
|
|
96f9f35f70 | ||
|
|
cee3c8a993 | ||
|
|
eeafa09f38 | ||
|
|
7b5826c211 | ||
|
|
7e419d4028 | ||
|
|
5f526e098f | ||
|
|
77e3a30086 | ||
|
|
8247f3a92c | ||
|
|
39dfce8020 | ||
|
|
8ea3cd02cf | ||
|
|
c3d22818d4 | ||
|
|
622aa168fe | ||
|
|
b3af69c936 | ||
|
|
bd20a736ca | ||
|
|
484ff7758a | ||
|
|
e09ae9c922 | ||
|
|
0b3cd95bce | ||
|
|
f68d9b9f9d | ||
|
|
51c201da95 | ||
|
|
7d2f43e752 | ||
|
|
dc49a6ccf3 | ||
|
|
e57ad0a696 | ||
|
|
99da810b1a | ||
|
|
1a249c742b | ||
|
|
57b2c43195 | ||
|
|
9e073e9610 | ||
|
|
1e10f70f57 | ||
|
|
68de05b5d2 | ||
|
|
c7df00f53d | ||
|
|
3921ebf361 |
19 changed files with 936 additions and 1164 deletions
7
.github/workflows/build.yml
vendored
7
.github/workflows/build.yml
vendored
|
|
@ -1,6 +1,7 @@
|
|||
name: CI Test
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [ main ]
|
||||
paths:
|
||||
|
|
@ -23,12 +24,12 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version: '20'
|
||||
node-version: '24'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Build and Test
|
||||
|
|
|
|||
96
.github/workflows/release.yml
vendored
96
.github/workflows/release.yml
vendored
|
|
@ -18,11 +18,6 @@ on:
|
|||
default: true
|
||||
required: true
|
||||
type: boolean
|
||||
retry:
|
||||
description: "Retry release (clear created tag)"
|
||||
default: false
|
||||
required: true
|
||||
type: boolean
|
||||
|
||||
env:
|
||||
GH_BOT_EMAIL: "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
|
|
@ -32,19 +27,22 @@ jobs:
|
|||
build:
|
||||
permissions:
|
||||
contents: write
|
||||
actions: write
|
||||
id-token: write
|
||||
attestations: write
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
version: ${{ steps.build.outputs.version }}
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
# persist-credentials required: this step pushes commits and tags
|
||||
# zizmor: ignore[artipacked]
|
||||
with:
|
||||
fetch-depth: 0 # otherwise, you will failed to push refs to dest repo
|
||||
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version: '20'
|
||||
node-version: '24'
|
||||
cache: 'npm'
|
||||
|
||||
# Build the plugin
|
||||
|
|
@ -52,37 +50,33 @@ jobs:
|
|||
id: build
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
INPUT_VERSION: ${{ github.event.inputs.version }}
|
||||
INPUT_UPDATE_MANIFEST: ${{ github.event.inputs.update_manifest }}
|
||||
INPUT_UPDATE_BRAT: ${{ github.event.inputs.update_brat }}
|
||||
run: |
|
||||
echo "version: ${{ github.event.inputs.version }}"
|
||||
echo "retry: ${{ github.event.inputs.retry }}"
|
||||
echo "update_manifest: ${{ github.event.inputs.update_manifest }}"
|
||||
echo "update_brat: ${{ github.event.inputs.update_brat }}"
|
||||
echo "version: $INPUT_VERSION"
|
||||
echo "update_manifest: $INPUT_UPDATE_MANIFEST"
|
||||
echo "update_brat: $INPUT_UPDATE_BRAT"
|
||||
|
||||
git config user.name ${{ env.GH_BOT_NAME }}
|
||||
git config user.email ${{ env.GH_BOT_EMAIL }}
|
||||
git config user.name "$GH_BOT_NAME"
|
||||
git config user.email "$GH_BOT_EMAIL"
|
||||
|
||||
npm ci
|
||||
if [[ "${{ github.event.inputs.retry }}" = "true" ]]; then
|
||||
npm run preversion
|
||||
else
|
||||
npm version ${{ github.event.inputs.version }} --no-git-tag-version
|
||||
fi
|
||||
npm version "$INPUT_VERSION" --no-git-tag-version
|
||||
VERSION=$(grep '^ "version"' package.json | cut -d'"' -f4)
|
||||
echo $VERSION
|
||||
echo "$VERSION"
|
||||
|
||||
if git rev-parse "refs/tags/$VERSION" > /dev/null 2>&1; then
|
||||
if [[ "${{ github.event.inputs.retry }}" = "true" ]]; then
|
||||
gh release delete $VERSION --cleanup-tag --yes
|
||||
else
|
||||
echo "🛑 Tag $VERSION already exists"
|
||||
exit 1
|
||||
fi
|
||||
echo "🛑 Tag $VERSION already exists. Delete the release and tag manually, then re-run."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "${{ github.event.inputs.update_manifest }}" = "true" ]; then
|
||||
if [ "$INPUT_UPDATE_MANIFEST" = "true" ]; then
|
||||
# shellcheck disable=SC2086
|
||||
sed -i 's|\(version":\) "[0-9\.]*"|\1 "'$VERSION'"|' manifest.json
|
||||
fi
|
||||
if [ "${{ github.event.inputs.update_brat }}" = "true" ]; then
|
||||
if [ "$INPUT_UPDATE_BRAT" = "true" ]; then
|
||||
# shellcheck disable=SC2086
|
||||
sed -i 's|\(version":\) "[0-9\.]*"|\1 "'$VERSION'"|' manifest-beta.json
|
||||
fi
|
||||
|
||||
|
|
@ -91,38 +85,54 @@ jobs:
|
|||
git commit -m "🔖 $VERSION"
|
||||
git push
|
||||
|
||||
git tag $VERSION
|
||||
git tag "$VERSION"
|
||||
git push --tags
|
||||
|
||||
npm run brat-notes -- ${VERSION}
|
||||
echo "version=${VERSION}" >> $GITHUB_OUTPUT
|
||||
npm run brat-notes -- "${VERSION}"
|
||||
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# Package the required files into a zip
|
||||
- name: Package
|
||||
env:
|
||||
REPO_NAME: ${{ github.event.repository.name }}
|
||||
RELEASE_VERSION: ${{ steps.build.outputs.version }}
|
||||
run: |
|
||||
mkdir ${{ github.event.repository.name }}
|
||||
cp ./build/* ${{ github.event.repository.name }}
|
||||
zip -r ${{ github.event.repository.name }}-${{ steps.build.outputs.version }}.zip ${{ github.event.repository.name }}
|
||||
mv build "$REPO_NAME"
|
||||
zip -r "${REPO_NAME}-${RELEASE_VERSION}.zip" "$REPO_NAME"
|
||||
unzip -j "${REPO_NAME}-${RELEASE_VERSION}.zip" \
|
||||
"$REPO_NAME/main.js" \
|
||||
"$REPO_NAME/styles.css"
|
||||
|
||||
# Attest build provenance for release artifacts
|
||||
- name: Attest build artifacts
|
||||
uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4
|
||||
with:
|
||||
subject-path: |
|
||||
${{ github.event.repository.name }}-${{ steps.build.outputs.version }}.zip
|
||||
./main.js
|
||||
./styles.css
|
||||
|
||||
# Create the release on github
|
||||
- name: Create Release
|
||||
id: create_release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
INPUT_UPDATE_MANIFEST: ${{ github.event.inputs.update_manifest }}
|
||||
REPO_NAME: ${{ github.event.repository.name }}
|
||||
RELEASE_VERSION: ${{ steps.build.outputs.version }}
|
||||
run: |
|
||||
prerelease=true
|
||||
if [ ${{ github.event.inputs.update_manifest }} ]; then
|
||||
if [ "$INPUT_UPDATE_MANIFEST" = "true" ]; then
|
||||
prerelease=false
|
||||
fi
|
||||
|
||||
gh release create "${{ steps.build.outputs.version }}" \
|
||||
gh release create "$RELEASE_VERSION" \
|
||||
-F ./release-notes.md \
|
||||
--title "Release ${{ steps.build.outputs.version }}" \
|
||||
--title "$RELEASE_VERSION" \
|
||||
--verify-tag \
|
||||
--prerelease=${prerelease}
|
||||
|
||||
gh release upload "${{ steps.build.outputs.version }}" --clobber \
|
||||
${{ github.event.repository.name }}-${{ steps.build.outputs.version }}.zip \
|
||||
'./build/main.js#main.js' \
|
||||
--prerelease=${prerelease} \
|
||||
"${REPO_NAME}-${RELEASE_VERSION}.zip" \
|
||||
'./main.js' \
|
||||
'./styles.css' \
|
||||
'./manifest.json' \
|
||||
'./manifest-beta.json'
|
||||
|
|
|
|||
52
CHANGELOG.md
52
CHANGELOG.md
|
|
@ -4,8 +4,60 @@ All notable changes to this project will be documented in this file. Dates are d
|
|||
|
||||
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
||||
|
||||
#### [0.8.0](https://github.com/ebullient/obsidian-deck-notes/compare/0.7.6...0.8.0)
|
||||
|
||||
- 🎨 Use 1.13.0 Settings API [`f279ae3`](https://github.com/ebullient/obsidian-deck-notes/commit/f279ae32970cad72040ba6ce1b59b073443c46f9)
|
||||
- ⬆️ obsidian 1.12.3 / obsidian eslint 0.3.0 [`ce408c1`](https://github.com/ebullient/obsidian-deck-notes/commit/ce408c1a80743030a487a95a8a685dbf70719d22)
|
||||
- 👷 node 24 [`5db632c`](https://github.com/ebullient/obsidian-deck-notes/commit/5db632cfb09c2683ae88f131c7a594f5dde4487b)
|
||||
- 🔧 globalIgnores first [`990113e`](https://github.com/ebullient/obsidian-deck-notes/commit/990113e1e650adf88daeac3a917fc5c81fe69edc)
|
||||
|
||||
#### [0.7.6](https://github.com/ebullient/obsidian-deck-notes/compare/0.7.5...0.7.6)
|
||||
|
||||
> 11 May 2026
|
||||
|
||||
- Bump esbuild from 0.27.7 to 0.28.0 [`#33`](https://github.com/ebullient/obsidian-deck-notes/pull/33)
|
||||
- Bump @biomejs/biome from 2.4.9 to 2.4.11 [`#34`](https://github.com/ebullient/obsidian-deck-notes/pull/34)
|
||||
- ⬆️ update dependencies [`7e419d4`](https://github.com/ebullient/obsidian-deck-notes/commit/7e419d40287d9d1244987cada01e6389af05900c)
|
||||
|
||||
#### [0.7.5](https://github.com/ebullient/obsidian-deck-notes/compare/0.7.4...0.7.5)
|
||||
|
||||
> 30 April 2026
|
||||
|
||||
- Bump eslint from 9.39.4 to 10.1.0 [`c3d2281`](https://github.com/ebullient/obsidian-deck-notes/commit/c3d22818d4489bed4b00b56e56cc2107676aaf27)
|
||||
- 🔧 🎨 eslint updates + fixes [`622aa16`](https://github.com/ebullient/obsidian-deck-notes/commit/622aa168fe4ed55c01d81cbc5680e49033b87d48)
|
||||
- Bump @biomejs/biome from 2.4.7 to 2.4.9 [`39dfce8`](https://github.com/ebullient/obsidian-deck-notes/commit/39dfce8020a4a2b74197a7a0a3aa20e3c7afaca3)
|
||||
- ⬆️ resolve dependencies [`8247f3a`](https://github.com/ebullient/obsidian-deck-notes/commit/8247f3a92c7238f0abb08889bc7c5867dcd28d87)
|
||||
- Bump typescript from 5.9.3 to 6.0.2 [`8ea3cd0`](https://github.com/ebullient/obsidian-deck-notes/commit/8ea3cd02cfa789c91de4e8313ca52abea11d8608)
|
||||
|
||||
#### [0.7.4](https://github.com/ebullient/obsidian-deck-notes/compare/0.7.3...0.7.4)
|
||||
|
||||
> 15 April 2026
|
||||
|
||||
- Bump @types/node from 24.10.4 to 25.0.3 [`#25`](https://github.com/ebullient/obsidian-deck-notes/pull/25)
|
||||
- Bump esbuild from 0.25.12 to 0.27.2 [`#24`](https://github.com/ebullient/obsidian-deck-notes/pull/24)
|
||||
- Bump @biomejs/biome from 2.2.6 to 2.3.10 [`#26`](https://github.com/ebullient/obsidian-deck-notes/pull/26)
|
||||
- ⬆️ update minimatch, audit fix [`7d2f43e`](https://github.com/ebullient/obsidian-deck-notes/commit/7d2f43e75236c0f89dde3e315c0c880bcc553323)
|
||||
|
||||
#### [0.7.3](https://github.com/ebullient/obsidian-deck-notes/compare/0.7.2...0.7.3)
|
||||
|
||||
> 3 January 2026
|
||||
|
||||
- ⬆️ dependency bump [`99da810`](https://github.com/ebullient/obsidian-deck-notes/commit/99da810b1a1ffe96e83f0d665bc3ffb9039a43cd)
|
||||
- ♻️ Review comments [`1a249c7`](https://github.com/ebullient/obsidian-deck-notes/commit/1a249c742b8181be46331e2337f78dcc928301dd)
|
||||
- 🎨 Add settings icon [`e57ad0a`](https://github.com/ebullient/obsidian-deck-notes/commit/e57ad0a696a475d33162a8fd855217a772ec9223)
|
||||
|
||||
#### [0.7.2](https://github.com/ebullient/obsidian-deck-notes/compare/0.7.1...0.7.2)
|
||||
|
||||
> 22 November 2025
|
||||
|
||||
- 🎨 Simpilify tracking last seen [`c7df00f`](https://github.com/ebullient/obsidian-deck-notes/commit/c7df00f53d5ab0a88b9293b6dd0e4cfa62cc9db1)
|
||||
- 🐛 debounce over Timer [`68de05b`](https://github.com/ebullient/obsidian-deck-notes/commit/68de05b5d28f55789dfc85e76cd78182e09f7884)
|
||||
- ☕️ coffee [`1e10f70`](https://github.com/ebullient/obsidian-deck-notes/commit/1e10f70f57d3944f4e34be26ed06f665f0048516)
|
||||
|
||||
#### 0.7.1
|
||||
|
||||
> 18 November 2025
|
||||
|
||||
- 🎨 Plugin lint [`cedca21`](https://github.com/ebullient/obsidian-deck-notes/commit/cedca2173da68711e43ca1ec2298721271ff9a89)
|
||||
- ✨ Initial pass at simple flashcards [`2d5695e`](https://github.com/ebullient/obsidian-deck-notes/commit/2d5695e5b123e21c6a8df022b873569403cc7da8)
|
||||
- 📝 human docs to README/CONTRIBUTING [`63c230a`](https://github.com/ebullient/obsidian-deck-notes/commit/63c230a198991e24f6589aacab60c7ba0bd9d69b)
|
||||
|
|
|
|||
|
|
@ -63,6 +63,11 @@ npm run format # Format code
|
|||
|
||||
- **Error handling**: `try/catch` with user-friendly `Notice` messages
|
||||
- **Async**: Use `async/await` consistently
|
||||
- **UI text (enforced by `obsidianmd/ui/sentence-case`)**:
|
||||
- Use sentence case (capitalize first word only, except proper nouns)
|
||||
- Setting descriptions must be complete sentences ending with periods
|
||||
- Use semicolons to separate related clauses in a single sentence
|
||||
- Avoid parenthetical notes; use semicolons instead
|
||||
|
||||
## Development Patterns
|
||||
|
||||
|
|
|
|||
|
|
@ -220,7 +220,7 @@ npm run format # Format code
|
|||
```txt
|
||||
src/
|
||||
├── @types/
|
||||
│ └── settings.d.ts # TypeScript interfaces
|
||||
│ └── settings.d.ts # TypeScript interfaces
|
||||
├── dn-Plugin.ts # Main plugin class
|
||||
├── dn-CardParser.ts # Parse files into cards
|
||||
├── dn-Modal.ts # Card display modal
|
||||
|
|
|
|||
35
SECURITY.md
35
SECURITY.md
|
|
@ -1,35 +0,0 @@
|
|||
# Security Policy
|
||||
|
||||
I only support the latest version of this plugin. Given the rapid
|
||||
evolution of the MCP protocol and dependencies, running/maintaining older
|
||||
versions isn't feasible.
|
||||
|
||||
## Security Considerations
|
||||
|
||||
This plugin runs a local HTTP server on your machine to expose vault
|
||||
access via the MCP protocol:
|
||||
|
||||
- **Network exposure**: The server listens on localhost by default but
|
||||
supports CORS for remote access (e.g., via Tailscale)
|
||||
- **Authentication**: Currently there is no authentication mechanism - any
|
||||
client that can reach the configured port can access your vault
|
||||
- **Data privacy**: All data stays on your machine; the plugin does not
|
||||
connect to external services or collect telemetry
|
||||
- **Bridge script**: The `mcp-bridge.js` script runs as a Node.js process
|
||||
and bridges stdio to HTTP
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Please report suspected security issues privately using GitHub's "Report a
|
||||
vulnerability" link in the repository sidebar.
|
||||
|
||||
Do **not** open a public issue for suspected vulnerabilities.
|
||||
|
||||
When reporting, please include:
|
||||
|
||||
- A description of the issue and affected versions
|
||||
- Steps to reproduce (ideally a minimal proof-of-concept)
|
||||
- Assessment of potential impact (network exposure, data access, etc.)
|
||||
- Your contact information
|
||||
- Any specific requests, such as anonymity for you and/or the
|
||||
organization you represent
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import esbuild from "esbuild";
|
||||
import process from "node:process";
|
||||
import builtins from 'builtin-modules';
|
||||
import { builtinModules } from "node:module";
|
||||
|
||||
const banner = `/*
|
||||
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
|
||||
|
|
@ -15,7 +15,7 @@ const parameters = {
|
|||
banner: {
|
||||
js: banner,
|
||||
},
|
||||
entryPoints: ['src/main.ts'],
|
||||
entryPoints: ['src/main.ts', 'src/styles.css'],
|
||||
bundle: true,
|
||||
external: [
|
||||
'obsidian',
|
||||
|
|
@ -31,7 +31,7 @@ const parameters = {
|
|||
'@lezer/common',
|
||||
'@lezer/highlight',
|
||||
'@lezer/lr',
|
||||
...builtins
|
||||
...builtinModules
|
||||
],
|
||||
format: 'cjs',
|
||||
logLevel: 'info',
|
||||
|
|
@ -54,4 +54,4 @@ if (prod) {
|
|||
} else {
|
||||
const ctx = await esbuild.context(parameters);
|
||||
await ctx.watch()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,12 +5,12 @@ import { defineConfig, globalIgnores } from "eslint/config";
|
|||
import obsidianmd from "eslint-plugin-obsidianmd";
|
||||
|
||||
export default defineConfig([
|
||||
...obsidianmd.configs.recommended,
|
||||
globalIgnores([
|
||||
"tests/",
|
||||
"*.mjs",
|
||||
"package.json"
|
||||
]),
|
||||
...obsidianmd.configs.recommended,
|
||||
{
|
||||
files: ["src/**/*.ts"],
|
||||
languageOptions: {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
{
|
||||
"id": "deck-notes",
|
||||
"name": "Deck Notes",
|
||||
"version": "0.7.1",
|
||||
"minAppVersion": "1.9.0",
|
||||
"version": "0.8.0",
|
||||
"minAppVersion": "1.13.0",
|
||||
"description": "Define card decks for activities, strategies, or any content you want to rotate through. Embed cards in your notes or browse them in a modal. Filter by tag hierarchies to narrow your selection.",
|
||||
"author": "ebullient",
|
||||
"authorUrl": "https://github.com/ebullient",
|
||||
"fundingUrl": "https://buymeacoffee.com/ebullient",
|
||||
"isDesktopOnly": false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
{
|
||||
"id": "deck-notes",
|
||||
"name": "Deck Notes",
|
||||
"version": "0.7.1",
|
||||
"minAppVersion": "1.9.0",
|
||||
"version": "0.8.0",
|
||||
"minAppVersion": "1.13.0",
|
||||
"description": "Define card decks for activities, strategies, or any content you want to rotate through. Embed cards in your notes or browse them in a modal. Filter by tag hierarchies to narrow your selection.",
|
||||
"author": "ebullient",
|
||||
"authorUrl": "https://github.com/ebullient",
|
||||
"fundingUrl": "https://buymeacoffee.com/ebullient",
|
||||
"isDesktopOnly": false
|
||||
}
|
||||
|
|
|
|||
1467
package-lock.json
generated
1467
package-lock.json
generated
File diff suppressed because it is too large
Load diff
29
package.json
29
package.json
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "deck-notes",
|
||||
"version": "0.7.1",
|
||||
"version": "0.8.0",
|
||||
"private": true,
|
||||
"description": "Define card decks for activities, strategies, or any content you want to rotate through. Embed cards in your notes or browse them in a modal. Filter by tag hierarchies to narrow your selection.",
|
||||
"main": "main.js",
|
||||
|
|
@ -25,25 +25,30 @@
|
|||
"flashcards"
|
||||
],
|
||||
"author": "ebullient",
|
||||
"repository": "github.com:ebullient/obsidian-flashcards",
|
||||
"repository": "github.com:ebullient/obsidian-deck-notes",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "2.2.6",
|
||||
"@types/node": "^24.8.1",
|
||||
"@biomejs/biome": "2.4.14",
|
||||
"@types/node": "^25.5.0",
|
||||
"@typescript-eslint/parser": "^8.46.4",
|
||||
"auto-changelog": "^2.5.0",
|
||||
"builtin-modules": "^5.0.0",
|
||||
"esbuild": "^0.25.9",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-plugin-obsidianmd": "^0.1.9",
|
||||
"moment": "^2.30.1",
|
||||
"obsidian": "^1.10.2",
|
||||
"esbuild": "^0.28.0",
|
||||
"eslint-plugin-obsidianmd": "^0.3.0",
|
||||
"obsidian": "^1.13.0",
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "5.9.3"
|
||||
"typescript": "6.0.3"
|
||||
},
|
||||
"overrides": {
|
||||
"glob": "13.0.6",
|
||||
"minimatch": "10.2.3"
|
||||
},
|
||||
"auto-changelog": {
|
||||
"backfillLimit": false,
|
||||
"commitLimit": false,
|
||||
"ignoreCommitPattern": "(🔖|🔨|🧹|changelog|release|Update README).*"
|
||||
"ignoreCommitPattern": "(🔖|🔨|🧹|changelog|release|Update README).*",
|
||||
"replaceText": {
|
||||
"([;.,] [rR]esolves) #\\d+\\.?\\s*$": "$1",
|
||||
"([;.,] [Ff]ixes) #\\d+\\.?\\s*$": "$1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
5
src/@types/settings.d.ts
vendored
5
src/@types/settings.d.ts
vendored
|
|
@ -9,10 +9,9 @@ export interface Card {
|
|||
|
||||
export interface DeckNotesSettings {
|
||||
cardPaths: string[]; // ["Journal/Coping", "Activities"]
|
||||
defaultDeckTag: string; // Default tag for deck selection (e.g., "activities")
|
||||
trackViews: boolean; // Enable view tracking
|
||||
defaultDeckTag: string; // Default tag for deck selection
|
||||
selectionMode: "random" | "least-recent";
|
||||
calloutType: string; // Callout type for embedded cards (e.g., "readaloud")
|
||||
calloutType: string; // Callout type for embedded cards
|
||||
}
|
||||
|
||||
export interface DeckNotesData {
|
||||
|
|
|
|||
|
|
@ -19,7 +19,8 @@ export class DeckNotesApi {
|
|||
|
||||
/**
|
||||
* Get all unique deck tags discovered in cards
|
||||
* @returns Array of normalized tag strings (e.g., ["activities", "activities/morning"])
|
||||
* @returns Array of normalized tag strings; for example,
|
||||
* ["activities", "activities/morning"]
|
||||
*/
|
||||
getTags(): string[] {
|
||||
const tagsSet = new Set<string>();
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import type { DeckNotesSettings } from "./@types/settings";
|
|||
export const DEFAULT_SETTINGS: DeckNotesSettings = {
|
||||
cardPaths: [],
|
||||
defaultDeckTag: "",
|
||||
trackViews: true,
|
||||
selectionMode: "least-recent",
|
||||
calloutType: "tip",
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,4 +1,10 @@
|
|||
import { type App, ButtonComponent, MarkdownRenderer, Modal } from "obsidian";
|
||||
import {
|
||||
type App,
|
||||
ButtonComponent,
|
||||
MarkdownRenderChild,
|
||||
MarkdownRenderer,
|
||||
Modal,
|
||||
} from "obsidian";
|
||||
import type { Card } from "./@types/settings";
|
||||
import type DeckNotesPlugin from "./dn-Plugin";
|
||||
|
||||
|
|
@ -6,6 +12,7 @@ export class CardModal extends Modal {
|
|||
plugin: DeckNotesPlugin;
|
||||
card: Card | null;
|
||||
deckTag: string | undefined;
|
||||
renderer: MarkdownRenderChild;
|
||||
|
||||
constructor(
|
||||
app: App,
|
||||
|
|
@ -17,6 +24,7 @@ export class CardModal extends Modal {
|
|||
this.plugin = plugin;
|
||||
this.card = card;
|
||||
this.deckTag = deckTag;
|
||||
this.renderer = new MarkdownRenderChild(this.containerEl);
|
||||
}
|
||||
|
||||
onOpen() {
|
||||
|
|
@ -39,7 +47,7 @@ export class CardModal extends Modal {
|
|||
contentEl.createEl("h2", { text: this.card.heading });
|
||||
|
||||
// Render card content as markdown
|
||||
const contentDiv = contentEl.createEl("div", {
|
||||
const contentDiv = contentEl.createDiv({
|
||||
cls: "card-content",
|
||||
});
|
||||
|
||||
|
|
@ -49,8 +57,7 @@ export class CardModal extends Modal {
|
|||
this.card.content,
|
||||
contentDiv,
|
||||
this.card.filePath,
|
||||
// eslint-disable-next-line obsidianmd/no-plugin-as-component
|
||||
this.plugin,
|
||||
this.renderer,
|
||||
);
|
||||
|
||||
// Add button controls
|
||||
|
|
@ -59,7 +66,7 @@ export class CardModal extends Modal {
|
|||
|
||||
private addControls() {
|
||||
const { contentEl } = this;
|
||||
const buttonContainer = contentEl.createEl("div", {
|
||||
const buttonContainer = contentEl.createDiv({
|
||||
cls: "modal-button-container",
|
||||
});
|
||||
|
||||
|
|
@ -91,7 +98,7 @@ export class CardModal extends Modal {
|
|||
|
||||
private addCloseButton() {
|
||||
const { contentEl } = this;
|
||||
const buttonContainer = contentEl.createEl("div", {
|
||||
const buttonContainer = contentEl.createDiv({
|
||||
cls: "modal-button-container",
|
||||
});
|
||||
|
||||
|
|
@ -120,7 +127,7 @@ export class CardModal extends Modal {
|
|||
|
||||
private showNextCard() {
|
||||
// Record view for current card
|
||||
if (this.card && this.plugin.settings.trackViews) {
|
||||
if (this.card) {
|
||||
this.plugin.recordView(this.card.key);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,11 @@
|
|||
import { type Editor, Notice, Plugin, TFile, TFolder } from "obsidian";
|
||||
import {
|
||||
debounce,
|
||||
type Editor,
|
||||
Notice,
|
||||
Plugin,
|
||||
TFile,
|
||||
TFolder,
|
||||
} from "obsidian";
|
||||
import type { Card, DeckNotesData, DeckNotesSettings } from "./@types/settings";
|
||||
import { DeckNotesApi } from "./dn-Api";
|
||||
import { CardParser } from "./dn-CardParser";
|
||||
|
|
@ -21,7 +28,8 @@ export default class DeckNotesPlugin extends Plugin {
|
|||
|
||||
this.addSettingTab(new DeckNotesSettingsTab(this.app, this));
|
||||
|
||||
// Defer initial card scan, API, and command registration to avoid blocking startup
|
||||
// Defer initial card scan, API, and command registration
|
||||
// to avoid blocking startup
|
||||
this.app.workspace.onLayoutReady(async () => {
|
||||
await this.scanCards();
|
||||
|
||||
|
|
@ -65,6 +73,10 @@ export default class DeckNotesPlugin extends Plugin {
|
|||
onunload() {
|
||||
console.debug("Unloading Deck Notes plugin");
|
||||
|
||||
// Flush any pending view saves
|
||||
this.debouncedSaveViews.cancel();
|
||||
void this.saveViews();
|
||||
|
||||
// Clear API reference
|
||||
if (window.deckNotes) {
|
||||
window.deckNotes.api = undefined;
|
||||
|
|
@ -85,6 +97,21 @@ export default class DeckNotesPlugin extends Plugin {
|
|||
await this.scanCards();
|
||||
}
|
||||
|
||||
debouncedSaveViews = debounce(
|
||||
async () => {
|
||||
await this.saveViews();
|
||||
},
|
||||
2000,
|
||||
true, // resetTimer: save only after 2s of no new views
|
||||
);
|
||||
|
||||
async saveViews() {
|
||||
await this.saveData({
|
||||
...this.settings,
|
||||
cardViews: this.data.cardViews,
|
||||
});
|
||||
}
|
||||
|
||||
async scanCards() {
|
||||
this.cachedCards = [];
|
||||
|
||||
|
|
@ -151,10 +178,7 @@ export default class DeckNotesPlugin extends Plugin {
|
|||
|
||||
recordView(cardKey: string) {
|
||||
this.data.cardViews[cardKey] = Date.now();
|
||||
void this.saveData({
|
||||
...this.settings,
|
||||
cardViews: this.data.cardViews,
|
||||
});
|
||||
this.debouncedSaveViews();
|
||||
}
|
||||
|
||||
showCard() {
|
||||
|
|
@ -188,9 +212,7 @@ export default class DeckNotesPlugin extends Plugin {
|
|||
].join("\n");
|
||||
|
||||
// Record as viewed
|
||||
if (this.settings.trackViews) {
|
||||
this.recordView(card.key);
|
||||
}
|
||||
this.recordView(card.key);
|
||||
|
||||
return embedText;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,160 +1,187 @@
|
|||
import { type App, PluginSettingTab, Setting } from "obsidian";
|
||||
import type { DeckNotesSettings } from "./@types/settings";
|
||||
import {
|
||||
type App,
|
||||
Modal,
|
||||
PluginSettingTab,
|
||||
Setting,
|
||||
type SettingDefinitionItem,
|
||||
type Setting as SettingType,
|
||||
} from "obsidian";
|
||||
import type DeckNotesPlugin from "./dn-Plugin";
|
||||
|
||||
export class DeckNotesSettingsTab extends PluginSettingTab {
|
||||
plugin: DeckNotesPlugin;
|
||||
newSettings!: DeckNotesSettings;
|
||||
|
||||
constructor(app: App, plugin: DeckNotesPlugin) {
|
||||
super(app, plugin);
|
||||
this.plugin = plugin;
|
||||
this.icon = "gallery-thumbnails";
|
||||
}
|
||||
|
||||
async save() {
|
||||
this.plugin.settings = this.newSettings;
|
||||
async setControlValue(key: string, value: unknown): Promise<void> {
|
||||
(this.plugin.settings as unknown as Record<string, unknown>)[key] =
|
||||
value;
|
||||
await this.plugin.saveSettings();
|
||||
}
|
||||
|
||||
private cloneSettings(): DeckNotesSettings {
|
||||
return JSON.parse(
|
||||
JSON.stringify(this.plugin.settings),
|
||||
) as DeckNotesSettings;
|
||||
}
|
||||
getSettingDefinitions(): SettingDefinitionItem[] {
|
||||
return [
|
||||
{
|
||||
name: "Usage",
|
||||
render: (setting: SettingType) => {
|
||||
setting.descEl.appendChild(
|
||||
createFragment((f) => {
|
||||
const p = f.createEl("p");
|
||||
p.appendText(
|
||||
"Cards are created from markdown files with ",
|
||||
);
|
||||
p.createEl("code", { text: "##" });
|
||||
p.appendText(" headings.");
|
||||
|
||||
async reset() {
|
||||
this.newSettings = this.cloneSettings();
|
||||
this.display();
|
||||
}
|
||||
const ul = f.createEl("ul");
|
||||
|
||||
display(): void {
|
||||
if (!this.newSettings) {
|
||||
this.newSettings = this.cloneSettings();
|
||||
}
|
||||
const li1 = ul.createEl("li");
|
||||
li1.appendText("Use ");
|
||||
li1.createEl("code", { text: "---" });
|
||||
li1.appendText(
|
||||
" (horizontal rule) to mark the end of card content.",
|
||||
);
|
||||
|
||||
const { containerEl } = this;
|
||||
containerEl.empty();
|
||||
|
||||
new Setting(containerEl).setHeading().setName("Deck Notes");
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Save settings")
|
||||
.setClass("decknotes-save-reset")
|
||||
.addButton((button) =>
|
||||
button
|
||||
.setButtonText("Reset")
|
||||
.setTooltip("Reset to current saved settings")
|
||||
.onClick(async () => {
|
||||
await this.reset();
|
||||
}),
|
||||
)
|
||||
.addButton((button) =>
|
||||
button
|
||||
.setButtonText("Save")
|
||||
.setCta()
|
||||
.setTooltip("Save all changes")
|
||||
.onClick(async () => {
|
||||
await this.save();
|
||||
}),
|
||||
);
|
||||
|
||||
containerEl.createEl("p", {
|
||||
text: "Configure activity card decks for random selection.",
|
||||
});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Card paths")
|
||||
.setDesc(
|
||||
"Paths to folders containing card files (one per line, relative to vault root)",
|
||||
)
|
||||
.addTextArea((text) =>
|
||||
text
|
||||
.setPlaceholder("journal/coping\nactivities/morning")
|
||||
.setValue(this.newSettings.cardPaths.join("\n"))
|
||||
.onChange((value) => {
|
||||
this.newSettings.cardPaths = value
|
||||
.split("\n")
|
||||
.map((p) => p.trim())
|
||||
.filter((p) => p.length > 0);
|
||||
}),
|
||||
)
|
||||
.then((setting) => {
|
||||
setting.controlEl
|
||||
.querySelector("textarea")
|
||||
?.setAttribute("rows", "4");
|
||||
});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Default deck tag")
|
||||
.setDesc(
|
||||
"Default tag for 'Show Random Activity Card' command (e.g., 'activities' or 'activities/morning'). Leave empty for all cards.",
|
||||
)
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder("Activities")
|
||||
.setValue(this.newSettings.defaultDeckTag)
|
||||
.onChange((value) => {
|
||||
this.newSettings.defaultDeckTag = value.trim();
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Track views")
|
||||
.setDesc(
|
||||
"Track when cards were last viewed (enables least-recent selection)",
|
||||
)
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(this.newSettings.trackViews)
|
||||
.onChange((value) => {
|
||||
this.newSettings.trackViews = value;
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Selection mode")
|
||||
.setDesc("How to select cards to display")
|
||||
.addDropdown((dropdown) =>
|
||||
dropdown
|
||||
.addOption("random", "Random")
|
||||
.addOption("least-recent", "Least recently viewed")
|
||||
.setValue(this.newSettings.selectionMode)
|
||||
.onChange((value) => {
|
||||
this.newSettings.selectionMode = value as
|
||||
| "random"
|
||||
| "least-recent";
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Callout type")
|
||||
.setDesc(
|
||||
"Callout type for embedded cards (e.g., note, tip, warning)",
|
||||
)
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder("example")
|
||||
.setValue(this.newSettings.calloutType)
|
||||
.onChange((value) => {
|
||||
this.newSettings.calloutType = value.trim();
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(containerEl).setHeading().setName("Usage");
|
||||
|
||||
containerEl.createEl("p", {
|
||||
text: "Cards are created from markdown files with H2 headings (##). Each heading becomes one card.",
|
||||
});
|
||||
containerEl.createEl("p", {
|
||||
text: "Use --- (horizontal rule) to mark the end of card content. Anything after --- will be ignored.",
|
||||
});
|
||||
containerEl.createEl("p", {
|
||||
text: "Tag cards with #flashcards/deck-name (e.g., #flashcards/activities/morning). Tags can be in frontmatter or inline before each H2. Lines starting with #flashcards are stripped from display.",
|
||||
});
|
||||
}
|
||||
|
||||
/** Save on exit */
|
||||
hide(): void {
|
||||
void this.save();
|
||||
const li2 = ul.createEl("li");
|
||||
li2.appendText("Use ");
|
||||
li2.createEl("code", {
|
||||
text: "#flashcards",
|
||||
});
|
||||
li2.appendText(
|
||||
" tags to identify decks. For example, ",
|
||||
);
|
||||
li2.createEl("code", {
|
||||
text: "#flashcards/activities",
|
||||
});
|
||||
li2.appendText(" or ");
|
||||
li2.createEl("code", {
|
||||
text: "#flashcards/meditation",
|
||||
});
|
||||
li2.appendText(
|
||||
". Tags can be added to the whole file (in frontmatter properties), or defined inline before each ",
|
||||
);
|
||||
li2.createEl("code", { text: "##" });
|
||||
li2.appendText(". See the ");
|
||||
li2.createEl("a", {
|
||||
text: "README",
|
||||
href: "https://github.com/ebullient/obsidian-deck-notes#card-format",
|
||||
attr: { target: "_blank" },
|
||||
});
|
||||
li2.appendText(" for examples.");
|
||||
}),
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Default deck tag",
|
||||
desc:
|
||||
"Optional tag to narrow the selection of cards available " +
|
||||
"for 'Show Card' command; for example, 'activities' or " +
|
||||
"'activities/morning'.",
|
||||
control: {
|
||||
type: "text",
|
||||
key: "defaultDeckTag",
|
||||
placeholder: "activities",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Selection mode",
|
||||
desc: "Cards can be selected at random or based on when they were last viewed.",
|
||||
control: {
|
||||
type: "dropdown",
|
||||
key: "selectionMode",
|
||||
options: {
|
||||
random: "Random",
|
||||
"least-recent": "Least recently viewed",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Callout type",
|
||||
desc: "Callout type for embedded cards; for example, note, tip, or warning.",
|
||||
control: {
|
||||
type: "text",
|
||||
key: "calloutType",
|
||||
placeholder: "example",
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "list",
|
||||
heading: "Card paths",
|
||||
desc: "Paths to folders containing card decks, relative to vault root.",
|
||||
emptyState: "No card paths configured.",
|
||||
addItem: {
|
||||
name: "Add path",
|
||||
action: () =>
|
||||
new AddCardPathModal(this.app, (path) => {
|
||||
this.plugin.settings.cardPaths.push(path);
|
||||
void this.plugin.saveSettings();
|
||||
this.update();
|
||||
}).open(),
|
||||
},
|
||||
onDelete: async (idx: number) => {
|
||||
this.plugin.settings.cardPaths.splice(idx, 1);
|
||||
await this.plugin.saveSettings();
|
||||
this.update();
|
||||
},
|
||||
items: this.plugin.settings.cardPaths.map((path) => ({
|
||||
name: path,
|
||||
})),
|
||||
},
|
||||
{
|
||||
name: "",
|
||||
render: (setting: SettingType) => {
|
||||
setting.descEl.addClass("deck-cards-coffee");
|
||||
setting.descEl
|
||||
.createEl("a", {
|
||||
href: "https://www.buymeacoffee.com/ebullient",
|
||||
})
|
||||
.createEl("img", {
|
||||
attr: {
|
||||
src: "https://img.buymeacoffee.com/button-api/?text=Buy me a coffee&emoji=☕&slug=ebullient&button_colour=8e6787&font_colour=ebebeb&font_family=Inter&outline_colour=392a37&coffee_colour=ecc986",
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
class AddCardPathModal extends Modal {
|
||||
private onSubmit: (path: string) => void;
|
||||
|
||||
constructor(app: App, onSubmit: (path: string) => void) {
|
||||
super(app);
|
||||
this.onSubmit = onSubmit;
|
||||
}
|
||||
|
||||
onOpen(): void {
|
||||
const { contentEl } = this;
|
||||
contentEl.createEl("h2", { text: "Add card path" });
|
||||
let input = "";
|
||||
new Setting(contentEl).setName("Path").addText((t) =>
|
||||
t.setPlaceholder("journal/coping").onChange((v) => {
|
||||
input = v.trim();
|
||||
}),
|
||||
);
|
||||
new Setting(contentEl).addButton((b) =>
|
||||
b
|
||||
.setButtonText("Add")
|
||||
.setCta()
|
||||
.onClick(() => {
|
||||
if (input) {
|
||||
this.onSubmit(input);
|
||||
this.close();
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
onClose(): void {
|
||||
this.contentEl.empty();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
6
src/styles.css
Normal file
6
src/styles.css
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
.deck-cards-coffee {
|
||||
text-align: center;
|
||||
img {
|
||||
height: 30px;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue