mirror of
https://github.com/mfarr/obsidian-archive.git
synced 2026-07-22 12:10:27 +00:00
Compare commits
No commits in common. "main" and "0.5.2" have entirely different histories.
20 changed files with 351 additions and 2352 deletions
|
|
@ -1,15 +1,10 @@
|
|||
# top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
indent_style = tab
|
||||
indent_size = 4
|
||||
tab_width = 4
|
||||
|
||||
[*.{yml,yaml}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
tab_width = 2
|
||||
# top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
indent_style = tab
|
||||
indent_size = 4
|
||||
tab_width = 4
|
||||
|
|
|
|||
43
.github/workflows/claude-code-review.yml
vendored
43
.github/workflows/claude-code-review.yml
vendored
|
|
@ -1,43 +0,0 @@
|
|||
name: Claude Code Review
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, ready_for_review, reopened]
|
||||
# Optional: Only run on specific file changes
|
||||
# paths:
|
||||
# - "src/**/*.ts"
|
||||
# - "src/**/*.tsx"
|
||||
# - "src/**/*.js"
|
||||
# - "src/**/*.jsx"
|
||||
|
||||
jobs:
|
||||
claude-review:
|
||||
# Optional: Filter by PR author
|
||||
# if: |
|
||||
# github.event.pull_request.user.login == 'external-contributor' ||
|
||||
# github.event.pull_request.user.login == 'new-developer' ||
|
||||
# github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR'
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
issues: read
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Run Claude Code Review
|
||||
id: claude-review
|
||||
uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
plugin_marketplaces: "https://github.com/anthropics/claude-code.git"
|
||||
plugins: "code-review@claude-code-plugins"
|
||||
prompt: "/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }}"
|
||||
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
|
||||
# or https://code.claude.com/docs/en/cli-reference for available options
|
||||
50
.github/workflows/claude.yml
vendored
50
.github/workflows/claude.yml
vendored
|
|
@ -1,50 +0,0 @@
|
|||
name: Claude Code
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
pull_request_review_comment:
|
||||
types: [created]
|
||||
issues:
|
||||
types: [opened, assigned]
|
||||
pull_request_review:
|
||||
types: [submitted]
|
||||
|
||||
jobs:
|
||||
claude:
|
||||
if: |
|
||||
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
|
||||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
|
||||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
|
||||
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
issues: read
|
||||
id-token: write
|
||||
actions: read # Required for Claude to read CI results on PRs
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Run Claude Code
|
||||
id: claude
|
||||
uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
|
||||
# This is an optional setting that allows Claude to read CI results on PRs
|
||||
additional_permissions: |
|
||||
actions: read
|
||||
|
||||
# Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it.
|
||||
# prompt: 'Update the pull request description to include a summary of changes.'
|
||||
|
||||
# Optional: Add claude_args to customize behavior and configuration
|
||||
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
|
||||
# or https://code.claude.com/docs/en/cli-reference for available options
|
||||
# claude_args: '--allowed-tools Bash(gh pr:*)'
|
||||
|
||||
65
.github/workflows/release.yml
vendored
65
.github/workflows/release.yml
vendored
|
|
@ -1,48 +1,35 @@
|
|||
name: Release Obsidian plugin
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "*"
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags:
|
||||
- "*"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
attestations: write
|
||||
artifact-metadata: write
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "24"
|
||||
cache: "npm"
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "18.x"
|
||||
|
||||
- name: Build plugin
|
||||
run: |
|
||||
npm install
|
||||
npm run build
|
||||
- name: Build plugin
|
||||
run: |
|
||||
npm install
|
||||
npm run build
|
||||
|
||||
- name: Attest
|
||||
uses: actions/attest@v4
|
||||
with:
|
||||
subject-path: |
|
||||
main.js
|
||||
manifest.json
|
||||
styles.css
|
||||
- name: Create release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
tag="${GITHUB_REF#refs/tags/}"
|
||||
|
||||
- name: Create release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
tag="${GITHUB_REF#refs/tags/}"
|
||||
|
||||
gh release create "$tag" \
|
||||
--title="$tag" \
|
||||
--draft \
|
||||
main.js manifest.json styles.css
|
||||
gh release create "$tag" \
|
||||
--title="$tag" \
|
||||
--draft \
|
||||
main.js manifest.json styles.css
|
||||
|
|
|
|||
57
CLAUDE.md
57
CLAUDE.md
|
|
@ -1,57 +0,0 @@
|
|||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
npm run dev # Development build with watch mode (esbuild)
|
||||
npm run build # Type-check (tsc --noEmit) + production bundle
|
||||
npm run version # Bump version, updates manifest.json and versions.json
|
||||
```
|
||||
|
||||
No test runner is configured. Linting is via ESLint (`npx eslint .`).
|
||||
|
||||
## Architecture
|
||||
|
||||
This is an **Obsidian plugin** ("Simple Archiver") that moves notes and folders to a configurable archive folder, with an optional auto-archive feature that runs rules on a schedule.
|
||||
|
||||
### Core Modules
|
||||
|
||||
**`main.ts`** — Plugin entry point (`SimpleArchiverPlugin extends Plugin`). Owns:
|
||||
- Settings load/save via Obsidian's `loadData()`/`saveData()`
|
||||
- Command registration (`move-to-archive`, `move-out-of-archive`)
|
||||
- Context menu items for files and folders
|
||||
- Core `archiveFile()` / `unarchiveFile()` logic (conflict resolution, path normalization)
|
||||
|
||||
**`autoarchive/`** — Auto-archive subsystem:
|
||||
- `AutoArchiveService.ts` — Rule evaluation engine and scheduler. Uses `setInterval()` for periodic runs, `setTimeout()` for startup delay. Evaluates conditions (file age, filename regex) with AND/OR logic. Rate-limited to 500 files/cycle. Regex patterns validated with 100ms timeout to prevent ReDoS.
|
||||
- `AutoArchiveTypes.ts` — Type definitions (`AutoArchiveRule`, `AutoArchiveCondition`, `SimpleArchiverSettings`, `ArchiveResult`)
|
||||
- `AutoArchiveSettings.ts` — Default settings and backward-compatibility migrations
|
||||
- `AutoArchiveLifecycle.ts` — Factory functions for creating/controlling the service
|
||||
- `index.ts` — Barrel export
|
||||
|
||||
**`SettingsTab.ts`** — Tabbed settings UI (General tab: archive folder config; Auto-Archive tab: schedule and rule management).
|
||||
|
||||
**`modals.ts`** — `SimpleArchiverPromptModal` (yes/no confirmations) and `AutoArchiveRuleModal` (rule editor with validation).
|
||||
|
||||
### Data Flow
|
||||
|
||||
```
|
||||
User command/menu → archiveFile() → conflict check → Obsidian vault.rename() → saveSettings()
|
||||
|
||||
Scheduler → AutoArchiveService.processAutoArchiveRules()
|
||||
→ enabled rules → matching folders → evaluate conditions → archiveFile()
|
||||
```
|
||||
|
||||
### Build Output
|
||||
|
||||
esbuild bundles `main.ts` → `main.js`. Obsidian, Electron, and all `@codemirror/*` / `@lezer/*` packages are marked external (provided by Obsidian at runtime).
|
||||
|
||||
### Plugin Manifest
|
||||
|
||||
`manifest.json` and `versions.json` track plugin versioning for the Obsidian plugin marketplace. The release workflow (`.github/workflows/release.yml`) auto-creates GitHub releases when version tags are pushed.
|
||||
|
||||
## Community Plugin Listing
|
||||
|
||||
https://community.obsidian.md/plugins/simple-archiver
|
||||
39
README.md
39
README.md
|
|
@ -15,36 +15,6 @@ Unarchiving can be done via:
|
|||
- `Move out of archive` file menu item
|
||||
- `Move all out of archive` multi-file menu item
|
||||
|
||||
## Auto-Archive
|
||||
|
||||
Auto-archive lets you define rules that periodically move matching files into your archive folder. Each rule targets a folder (optionally by regex), can include subfolders, and requires at least one condition. Conditions can be based on file age (days since last modified) and/or a file name regex. When a rule matches, files are archived to the same relative path under your archive folder.
|
||||
|
||||
How it works:
|
||||
|
||||
- Rules are evaluated on a schedule (default every 60 minutes).
|
||||
- The last auto-archive run time is saved, so short Obsidian sessions can still trigger catch-up runs on startup.
|
||||
- On startup, auto-archive waits for a configurable delay (default 30 seconds) before checking whether a run is due.
|
||||
- Multiple conditions can be combined with AND or OR.
|
||||
- Files already in the archive folder are skipped.
|
||||
|
||||
How to use it:
|
||||
|
||||
- Open Settings -> Simple Archiver -> Auto-Archive.
|
||||
- Set the auto-archive frequency and startup delay, and optionally click "Auto Archive Now" to run immediately.
|
||||
- Click "Add Rule", choose a folder path (or enable regex), decide whether to apply recursively, and add one or more conditions.
|
||||
- Optional: Right-click a folder and choose "Auto-archive" -> "Add rule" to prefill the folder path.
|
||||
- Optional: Right-click a folder and choose "Auto-archive" -> "Edit rule" to edit an existing rule.
|
||||
|
||||
Example rule:
|
||||
|
||||
```text
|
||||
Folder: Notes/Daily
|
||||
Apply recursively: No
|
||||
Conditions (AND):
|
||||
- File age >= 30 days
|
||||
- File name regex: ^\d{4}-\d{2}-\d{2}.*\.md$
|
||||
```
|
||||
|
||||
## Planned Improvements
|
||||
|
||||
- Archiving a folder that already exists in the archive merges the contents
|
||||
|
|
@ -62,18 +32,9 @@ AI-assisted contributions will be considered with discretion. Obviously vibe-cod
|
|||
|
||||
- [nicholaslck](https://github.com/nicholaslck)
|
||||
- [ggfevans](https://github.com/ggfevans)
|
||||
- [albertsj1](https://github.com/albertsj1)
|
||||
|
||||
## Release Notes
|
||||
|
||||
### v0.6.2
|
||||
|
||||
- **Fix**: Address issues identified on Obsidian community plugin
|
||||
|
||||
### v0.6.1
|
||||
|
||||
- **New**: Add auto-archiving functionality. Thanks to [albertsj1](https://github.com/albertsj1)!
|
||||
|
||||
### v0.5.2
|
||||
|
||||
- **Fix**: Resolve issue when archiving files from folders with leading 0s. Thanks [ggfevans](https://github.com/ggfevans)!
|
||||
|
|
|
|||
316
SettingsTab.ts
316
SettingsTab.ts
|
|
@ -1,316 +0,0 @@
|
|||
import {
|
||||
App,
|
||||
normalizePath,
|
||||
Notice,
|
||||
PluginSettingTab,
|
||||
Setting,
|
||||
} from "obsidian";
|
||||
|
||||
import { AutoArchiveService } from "./autoarchive/AutoArchiveService";
|
||||
import type { AutoArchiveRule } from "./autoarchive/AutoArchiveTypes";
|
||||
import type SimpleArchiver from "./main";
|
||||
import { AutoArchiveRuleModal } from "./modals";
|
||||
|
||||
export class SimpleArchiverSettingsTab extends PluginSettingTab {
|
||||
plugin: SimpleArchiver;
|
||||
autoArchiveService: AutoArchiveService;
|
||||
activeTab: "general" | "autoArchive" = "general";
|
||||
|
||||
constructor(
|
||||
app: App,
|
||||
plugin: SimpleArchiver,
|
||||
autoArchiveService: AutoArchiveService,
|
||||
) {
|
||||
super(app, plugin);
|
||||
this.plugin = plugin;
|
||||
this.autoArchiveService = autoArchiveService;
|
||||
}
|
||||
|
||||
display(): void {
|
||||
const { containerEl } = this;
|
||||
|
||||
containerEl.empty();
|
||||
|
||||
const tabContainer = containerEl.createDiv({
|
||||
cls: "setting-tab-container",
|
||||
});
|
||||
const tabButtonContainer = tabContainer.createDiv({
|
||||
cls: "setting-tab-buttons",
|
||||
});
|
||||
|
||||
const generalTabButton = tabButtonContainer.createEl("button", {
|
||||
text: "General",
|
||||
cls:
|
||||
this.activeTab === "general"
|
||||
? "setting-tab-button active"
|
||||
: "setting-tab-button",
|
||||
});
|
||||
generalTabButton.addEventListener("click", () => {
|
||||
this.activeTab = "general";
|
||||
this.display();
|
||||
});
|
||||
|
||||
const autoArchiveTabButton = tabButtonContainer.createEl("button", {
|
||||
text: "Auto-Archive",
|
||||
cls:
|
||||
this.activeTab === "autoArchive"
|
||||
? "setting-tab-button active"
|
||||
: "setting-tab-button",
|
||||
});
|
||||
autoArchiveTabButton.addEventListener("click", () => {
|
||||
this.activeTab = "autoArchive";
|
||||
this.display();
|
||||
});
|
||||
|
||||
const tabContent = containerEl.createDiv({
|
||||
cls: "setting-tab-content",
|
||||
});
|
||||
|
||||
if (this.activeTab === "general") {
|
||||
this.displayGeneralSettings(tabContent);
|
||||
} else {
|
||||
this.displayAutoArchiveSettings(tabContent);
|
||||
}
|
||||
}
|
||||
|
||||
private displayGeneralSettings(containerEl: HTMLElement): void {
|
||||
new Setting(containerEl)
|
||||
.setName("Archive folder")
|
||||
.setDesc(
|
||||
"The folder to use as the Archive. If the folder doesn't exist, it will be created when archiving a " +
|
||||
'note. Folder names must not contain "\\", "/" or ":" and must not start with ".".',
|
||||
)
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder("Archive folder")
|
||||
.setValue(normalizePath(this.plugin.settings.archiveFolder))
|
||||
.onChange(async (value) => {
|
||||
if (this.setArchiveFolder(value)) {
|
||||
await this.plugin.saveSettings();
|
||||
} else {
|
||||
text.setValue(this.plugin.settings.archiveFolder);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private displayAutoArchiveSettings(containerEl: HTMLElement): void {
|
||||
const frequencySetting = new Setting(containerEl)
|
||||
.setName("Auto-archive frequency")
|
||||
.setDesc("How often to check and process auto-archive rules");
|
||||
|
||||
const lastRunLineEl = frequencySetting.descEl.createDiv({
|
||||
cls: "setting-item-description",
|
||||
});
|
||||
const refreshLastRunLine = () => {
|
||||
lastRunLineEl.setText(
|
||||
`Last auto-archive run: ${this.getLastAutoArchiveRunText()}`,
|
||||
);
|
||||
};
|
||||
refreshLastRunLine();
|
||||
|
||||
frequencySetting
|
||||
.addButton((button) =>
|
||||
button
|
||||
.setButtonText("Auto Archive Now")
|
||||
.setTooltip("Process auto-archive rules immediately")
|
||||
.onClick(async () => {
|
||||
button.setDisabled(true);
|
||||
button.setButtonText("Processing...");
|
||||
await this.autoArchiveService.processAutoArchiveRules();
|
||||
refreshLastRunLine();
|
||||
this.autoArchiveService.scheduleAutoArchive();
|
||||
button.setButtonText("Auto Archive Now");
|
||||
button.setDisabled(false);
|
||||
new Notice("Auto-archive rules processed");
|
||||
}),
|
||||
)
|
||||
.addDropdown((dropdown) =>
|
||||
dropdown
|
||||
.addOption("5", "Every 5 minutes")
|
||||
.addOption("15", "Every 15 minutes")
|
||||
.addOption("30", "Every 30 minutes")
|
||||
.addOption("60", "Every 60 minutes")
|
||||
.addOption("360", "Every 6 hours")
|
||||
.addOption("720", "Every 12 hours")
|
||||
.addOption("1440", "Every 24 hours")
|
||||
.addOption("2880", "Every 48 hours")
|
||||
.setValue(
|
||||
this.plugin.settings.autoArchiveFrequency.toString(),
|
||||
)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.autoArchiveFrequency = parseInt(
|
||||
value,
|
||||
10,
|
||||
);
|
||||
await this.plugin.saveSettings();
|
||||
this.autoArchiveService.scheduleAutoArchive();
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Auto-archive startup delay")
|
||||
.setDesc(
|
||||
"How long to wait after Obsidian starts before checking if auto-archive should run",
|
||||
)
|
||||
.addDropdown((dropdown) =>
|
||||
dropdown
|
||||
.addOption("5", "5 seconds")
|
||||
.addOption("10", "10 seconds")
|
||||
.addOption("30", "30 seconds")
|
||||
.addOption("60", "60 seconds")
|
||||
.addOption("120", "2 minutes")
|
||||
.addOption("300", "5 minutes")
|
||||
.setValue(
|
||||
this.plugin.settings.autoArchiveStartupDelaySeconds.toString(),
|
||||
)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.autoArchiveStartupDelaySeconds =
|
||||
parseInt(value, 10);
|
||||
await this.plugin.saveSettings();
|
||||
this.autoArchiveService.scheduleStartupAutoArchiveCheck();
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Auto-archive rules")
|
||||
.setDesc(
|
||||
"Define rules for automatically archiving files in specific folders",
|
||||
)
|
||||
.addButton((button) =>
|
||||
button.setButtonText("Add Rule").onClick(() => {
|
||||
this.addAutoArchiveRule();
|
||||
}),
|
||||
);
|
||||
|
||||
const rulesContainer = containerEl.createDiv({
|
||||
cls: "auto-archive-rules-container",
|
||||
});
|
||||
this.displayAutoArchiveRules(rulesContainer);
|
||||
}
|
||||
|
||||
private displayAutoArchiveRules(containerEl: HTMLElement): void {
|
||||
if (this.plugin.settings.autoArchiveRules.length === 0) {
|
||||
containerEl.createEl("p", {
|
||||
text: "No auto-archive rules configured yet.",
|
||||
cls: "setting-item-description",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
for (const rule of this.plugin.settings.autoArchiveRules) {
|
||||
this.displayAutoArchiveRule(containerEl, rule);
|
||||
}
|
||||
}
|
||||
|
||||
private displayAutoArchiveRule(
|
||||
containerEl: HTMLElement,
|
||||
rule: AutoArchiveRule,
|
||||
): void {
|
||||
const ruleContainer = containerEl.createDiv({
|
||||
cls: "auto-archive-rule",
|
||||
});
|
||||
|
||||
new Setting(ruleContainer)
|
||||
.setName(`Folder: ${rule.folderPath || "(not set)"}`)
|
||||
.setClass("auto-archive-rule-header")
|
||||
.addToggle((toggle) =>
|
||||
toggle.setValue(rule.enabled).onChange(async (value) => {
|
||||
rule.enabled = value;
|
||||
await this.plugin.saveSettings();
|
||||
}),
|
||||
)
|
||||
.addButton((button) =>
|
||||
button
|
||||
.setButtonText("Edit")
|
||||
.setClass("mod-cta")
|
||||
.onClick(() => {
|
||||
this.editAutoArchiveRule(rule);
|
||||
}),
|
||||
)
|
||||
.addButton((button) =>
|
||||
button
|
||||
.setButtonText("Delete")
|
||||
.setWarning()
|
||||
.onClick(async () => {
|
||||
this.plugin.settings.autoArchiveRules =
|
||||
this.plugin.settings.autoArchiveRules.filter(
|
||||
(r) => r.id !== rule.id,
|
||||
);
|
||||
await this.plugin.saveSettings();
|
||||
this.display();
|
||||
}),
|
||||
);
|
||||
|
||||
const conditionsEl = ruleContainer.createDiv({
|
||||
cls: "auto-archive-rule-conditions",
|
||||
});
|
||||
if (rule.conditions.length === 0) {
|
||||
conditionsEl.createSpan({
|
||||
text: "No conditions set",
|
||||
cls: "setting-item-description",
|
||||
});
|
||||
} else {
|
||||
if (rule.conditions.length > 1) {
|
||||
conditionsEl.createDiv({
|
||||
text: `Logic: ${rule.logicOperator || "AND"}`,
|
||||
cls: "auto-archive-rule-logic",
|
||||
});
|
||||
}
|
||||
for (const condition of rule.conditions) {
|
||||
const conditionText =
|
||||
this.autoArchiveService.getConditionText(condition);
|
||||
conditionsEl.createDiv({
|
||||
text: `• ${conditionText}`,
|
||||
cls: "auto-archive-rule-condition",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private addAutoArchiveRule(): void {
|
||||
const newRule: AutoArchiveRule = {
|
||||
id: crypto.randomUUID(),
|
||||
enabled: true,
|
||||
folderPath: "",
|
||||
useFolderRegex: false,
|
||||
applyRecursively: false,
|
||||
conditions: [],
|
||||
logicOperator: "AND",
|
||||
};
|
||||
|
||||
this.plugin.settings.autoArchiveRules.push(newRule);
|
||||
this.editAutoArchiveRule(newRule);
|
||||
}
|
||||
|
||||
private editAutoArchiveRule(rule: AutoArchiveRule): void {
|
||||
new AutoArchiveRuleModal(this.app, this.plugin, rule, async () => {
|
||||
await this.plugin.saveSettings();
|
||||
this.display();
|
||||
}).open();
|
||||
}
|
||||
|
||||
private validateArchiveFolderName(value: string): boolean {
|
||||
return !/^\.|[:/\\]\.|:/.test(value);
|
||||
}
|
||||
|
||||
private setArchiveFolder(value: string): boolean {
|
||||
if (!this.validateArchiveFolderName(value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.plugin.settings.archiveFolder = value;
|
||||
return true;
|
||||
}
|
||||
|
||||
private getLastAutoArchiveRunText(): string {
|
||||
const lastRunAt =
|
||||
this.plugin.autoArchiveRuntimeData.lastAutoArchiveRunAt;
|
||||
|
||||
if (!Number.isFinite(lastRunAt) || lastRunAt <= 0) {
|
||||
return "never";
|
||||
}
|
||||
|
||||
return new Date(lastRunAt).toLocaleString();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
import { Workspace } from "obsidian";
|
||||
|
||||
import { AutoArchiveService } from "./AutoArchiveService";
|
||||
import type SimpleArchiver from "../main";
|
||||
|
||||
/**
|
||||
* Bootstraps the auto-archive context menu UI.
|
||||
*
|
||||
* Registers folder context menu items that allow users to add, edit,
|
||||
* and manage auto-archive rules at the folder level.
|
||||
*
|
||||
* @param service The AutoArchiveService instance
|
||||
* @param workspace The Obsidian Workspace instance
|
||||
* @param plugin The SimpleArchiver plugin instance
|
||||
*/
|
||||
export function setupAutoArchiveContextMenu(
|
||||
service: AutoArchiveService,
|
||||
workspace: Workspace,
|
||||
plugin: SimpleArchiver,
|
||||
): void {
|
||||
service.setupContextMenu(workspace, plugin);
|
||||
}
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
import { App, TAbstractFile } from "obsidian";
|
||||
|
||||
import { AutoArchiveService } from "./AutoArchiveService";
|
||||
import type {
|
||||
ArchiveResult,
|
||||
AutoArchiveRuntimeData,
|
||||
SimpleArchiverSettings,
|
||||
} from "./AutoArchiveTypes";
|
||||
|
||||
/**
|
||||
* Factory function to create and configure an AutoArchiveService instance.
|
||||
*
|
||||
* Encapsulates the complex wiring of dependencies (settings getter, archive callbacks, etc.)
|
||||
* that the service needs to function.
|
||||
*
|
||||
* @param app Obsidian App instance
|
||||
* @param getSettings Getter function to retrieve current plugin settings
|
||||
* @param archiveFile Callback to archive a file
|
||||
* @param isFileArchived Callback to check if a file is already archived
|
||||
* @param persistLastRunAt Callback to persist the last auto-archive run timestamp
|
||||
* @returns Configured AutoArchiveService instance
|
||||
*/
|
||||
export function createAutoArchiveService(
|
||||
app: App,
|
||||
getSettings: () => SimpleArchiverSettings & AutoArchiveRuntimeData,
|
||||
archiveFile: (file: TAbstractFile) => Promise<ArchiveResult>,
|
||||
isFileArchived: (file: TAbstractFile) => boolean,
|
||||
persistLastRunAt: (lastRunAt: number) => Promise<void>,
|
||||
): AutoArchiveService {
|
||||
return new AutoArchiveService(
|
||||
app,
|
||||
getSettings,
|
||||
archiveFile,
|
||||
isFileArchived,
|
||||
persistLastRunAt,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the auto-archive scheduler.
|
||||
*
|
||||
* Invokes both the startup-delay check (which may run immediately if conditions are met)
|
||||
* and the recurring interval schedule.
|
||||
*
|
||||
* @param service The AutoArchiveService instance
|
||||
*/
|
||||
export function startAutoArchive(service: AutoArchiveService): void {
|
||||
service.scheduleStartupAutoArchiveCheck();
|
||||
service.scheduleAutoArchive();
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the auto-archive scheduler.
|
||||
*
|
||||
* Clears all pending timers and intervals.
|
||||
*
|
||||
* @param service The AutoArchiveService instance
|
||||
*/
|
||||
export function stopAutoArchiveScheduler(service: AutoArchiveService): void {
|
||||
service.stopAutoArchive();
|
||||
}
|
||||
|
|
@ -1,611 +0,0 @@
|
|||
import {
|
||||
App,
|
||||
normalizePath,
|
||||
PluginSettingTab,
|
||||
TAbstractFile,
|
||||
TFile,
|
||||
TFolder,
|
||||
Workspace,
|
||||
} from "obsidian";
|
||||
|
||||
import type {
|
||||
AutoArchiveRuntimeData,
|
||||
AutoArchiveCondition,
|
||||
AutoArchiveRule,
|
||||
SimpleArchiverSettings,
|
||||
ArchiveResult,
|
||||
ObsidianInternalApis,
|
||||
} from "./AutoArchiveTypes";
|
||||
import type SimpleArchiver from "../main";
|
||||
import { AutoArchiveRuleModal } from "../modals";
|
||||
import { SimpleArchiverSettingsTab } from "../SettingsTab";
|
||||
|
||||
// Constants extracted from magic numbers
|
||||
const SETTINGS_TAB_RENDER_DELAY_MS = 200;
|
||||
const MAX_FILES_PER_CYCLE = 500;
|
||||
const DEFAULT_REGEX_TIMEOUT_MS = 100;
|
||||
const MAX_REGEX_PATTERN_LENGTH = 512;
|
||||
const MAX_REGEX_VALIDATION_INPUT_LENGTH = 1024;
|
||||
|
||||
type MenuEntry = {
|
||||
setTitle(title: string): MenuEntry;
|
||||
setIcon(icon: string): MenuEntry;
|
||||
onClick(callback: () => void): MenuEntry;
|
||||
setSubmenu?: () => MenuGroup;
|
||||
};
|
||||
|
||||
type MenuGroup = {
|
||||
addItem(callback: (item: MenuEntry) => void): void;
|
||||
};
|
||||
|
||||
export class AutoArchiveService {
|
||||
private app: App;
|
||||
private getSettings: () => SimpleArchiverSettings & AutoArchiveRuntimeData;
|
||||
private archiveFile: (file: TAbstractFile) => Promise<ArchiveResult>;
|
||||
private isFileArchived: (file: TAbstractFile) => boolean;
|
||||
private persistLastAutoArchiveRunAt: (lastRunAt: number) => Promise<void>;
|
||||
private autoArchiveInterval: number | null = null;
|
||||
private startupAutoArchiveTimeout: number | null = null;
|
||||
private filesProcessedInCycle = 0;
|
||||
private isProcessingAutoArchiveRules = false;
|
||||
|
||||
constructor(
|
||||
app: App,
|
||||
getSettings: () => SimpleArchiverSettings & AutoArchiveRuntimeData,
|
||||
archiveFile: (file: TAbstractFile) => Promise<ArchiveResult>,
|
||||
isFileArchived: (file: TAbstractFile) => boolean,
|
||||
persistLastAutoArchiveRunAt: (lastRunAt: number) => Promise<void>,
|
||||
) {
|
||||
this.app = app;
|
||||
this.getSettings = getSettings;
|
||||
this.archiveFile = archiveFile;
|
||||
this.isFileArchived = isFileArchived;
|
||||
this.persistLastAutoArchiveRunAt = persistLastAutoArchiveRunAt;
|
||||
}
|
||||
|
||||
scheduleStartupAutoArchiveCheck(): void {
|
||||
if (this.startupAutoArchiveTimeout !== null) {
|
||||
window.clearTimeout(this.startupAutoArchiveTimeout);
|
||||
}
|
||||
|
||||
const settings = this.getSettings();
|
||||
const startupDelayMs = settings.autoArchiveStartupDelaySeconds * 1000;
|
||||
|
||||
this.startupAutoArchiveTimeout = window.setTimeout(() => {
|
||||
this.startupAutoArchiveTimeout = null;
|
||||
void this.runStartupAutoArchiveCheck();
|
||||
}, startupDelayMs);
|
||||
}
|
||||
|
||||
private async runStartupAutoArchiveCheck(): Promise<void> {
|
||||
const settings = this.getSettings();
|
||||
const intervalMs = settings.autoArchiveFrequency * 60 * 1000;
|
||||
const elapsedSinceLastRun = Date.now() - settings.lastAutoArchiveRunAt;
|
||||
|
||||
if (elapsedSinceLastRun >= intervalMs) {
|
||||
await this.processAutoArchiveRules();
|
||||
}
|
||||
}
|
||||
|
||||
private async persistLastRunTimestamp(timestamp: number): Promise<void> {
|
||||
try {
|
||||
await this.persistLastAutoArchiveRunAt(timestamp);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"Failed to persist last auto-archive run time:",
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely compiles and validates a regex pattern.
|
||||
* This uses conservative static checks and adversarial test input to reduce
|
||||
* ReDoS risk. It intentionally rejects patterns that look unsafe.
|
||||
*/
|
||||
private validateRegexPattern(pattern: string): RegExp | null {
|
||||
if (pattern.length > MAX_REGEX_PATTERN_LENGTH) {
|
||||
console.error(
|
||||
`Regex pattern is too long (${pattern.length} chars): ${pattern}`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (this.isLikelyUnsafeRegex(pattern)) {
|
||||
console.error(`Unsafe regex pattern rejected: ${pattern}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const regex = new RegExp(pattern);
|
||||
for (const testInput of this.getRegexValidationInputs()) {
|
||||
const elapsedMs = this.measureRegexTestTime(regex, testInput);
|
||||
if (
|
||||
elapsedMs === null ||
|
||||
elapsedMs > DEFAULT_REGEX_TIMEOUT_MS
|
||||
) {
|
||||
console.error(
|
||||
`Regex pattern failed performance validation on adversarial input: ${pattern}`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return regex;
|
||||
} catch (error) {
|
||||
console.error(`Invalid regex pattern: ${pattern}`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects common high-risk regex constructs associated with catastrophic
|
||||
* backtracking. This is intentionally conservative.
|
||||
*/
|
||||
private isLikelyUnsafeRegex(pattern: string): boolean {
|
||||
const hasBackreferences = /(^|[^\\])\\[1-9]/.test(pattern);
|
||||
if (hasBackreferences) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const nestedQuantifierPatterns = [
|
||||
/\((?:[^()\\]|\\.)*[+*](?:[^()\\]|\\.)*\)\s*[+*{]/,
|
||||
/\((?:[^()\\]|\\.)*\{\d+,?\d*\}(?:[^()\\]|\\.)*\)\s*[+*{]/,
|
||||
/\((?:[^()\\]|\\.)*\.[+*](?:[^()\\]|\\.)*\)\s*[+*{]/,
|
||||
];
|
||||
|
||||
return nestedQuantifierPatterns.some((regex) => regex.test(pattern));
|
||||
}
|
||||
|
||||
private getRegexValidationInputs(): string[] {
|
||||
const repeatedA = "a".repeat(MAX_REGEX_VALIDATION_INPUT_LENGTH);
|
||||
const repeatedPathSegment = "segment/".repeat(
|
||||
Math.floor(MAX_REGEX_VALIDATION_INPUT_LENGTH / 8),
|
||||
);
|
||||
|
||||
return [
|
||||
`${repeatedA}!`,
|
||||
`/${repeatedPathSegment}end`,
|
||||
`${repeatedA}.md`,
|
||||
"test_file_name.md",
|
||||
];
|
||||
}
|
||||
|
||||
private measureRegexTestTime(regex: RegExp, input: string): number | null {
|
||||
try {
|
||||
const start = performance.now();
|
||||
regex.test(input);
|
||||
return performance.now() - start;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
scheduleAutoArchive(): void {
|
||||
if (this.autoArchiveInterval !== null) {
|
||||
window.clearInterval(this.autoArchiveInterval);
|
||||
}
|
||||
|
||||
const settings = this.getSettings();
|
||||
const intervalMs = settings.autoArchiveFrequency * 60 * 1000;
|
||||
this.autoArchiveInterval = window.setInterval(() => {
|
||||
void this.processAutoArchiveRules().catch((error) => {
|
||||
console.error("Auto-archive cycle failed:", error);
|
||||
});
|
||||
}, intervalMs);
|
||||
}
|
||||
|
||||
stopAutoArchive(): void {
|
||||
if (this.autoArchiveInterval !== null) {
|
||||
window.clearInterval(this.autoArchiveInterval);
|
||||
this.autoArchiveInterval = null;
|
||||
}
|
||||
|
||||
if (this.startupAutoArchiveTimeout !== null) {
|
||||
window.clearTimeout(this.startupAutoArchiveTimeout);
|
||||
this.startupAutoArchiveTimeout = null;
|
||||
}
|
||||
}
|
||||
|
||||
async processAutoArchiveRules(): Promise<void> {
|
||||
if (this.isProcessingAutoArchiveRules) {
|
||||
console.warn(
|
||||
"Auto-archive cycle skipped because a previous cycle is still running.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.isProcessingAutoArchiveRules = true;
|
||||
|
||||
try {
|
||||
const settings = this.getSettings();
|
||||
const enabledRules = settings.autoArchiveRules.filter(
|
||||
(rule) => rule.enabled,
|
||||
);
|
||||
|
||||
if (enabledRules.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.filesProcessedInCycle = 0;
|
||||
|
||||
for (const rule of enabledRules) {
|
||||
// Check if we've hit the rate limit
|
||||
if (this.filesProcessedInCycle >= MAX_FILES_PER_CYCLE) {
|
||||
console.warn(
|
||||
`Auto-archive cycle reached maximum file limit (${MAX_FILES_PER_CYCLE}). Remaining rules skipped to prevent performance issues.`,
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
let foldersToProcess: TFolder[] = [];
|
||||
|
||||
if (rule.useFolderRegex) {
|
||||
const regex = this.validateRegexPattern(rule.folderPath);
|
||||
if (!regex) {
|
||||
console.error(
|
||||
`Skipping rule with invalid regex pattern: ${rule.folderPath}`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const allFolders = this.app.vault.getAllFolders();
|
||||
foldersToProcess = allFolders.filter((folder) =>
|
||||
regex.test(folder.path),
|
||||
);
|
||||
} else {
|
||||
const folder = this.app.vault.getFolderByPath(
|
||||
normalizePath(rule.folderPath),
|
||||
);
|
||||
|
||||
if (!folder) {
|
||||
console.warn(
|
||||
`Auto-archive rule references non-existent folder: ${rule.folderPath}`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
foldersToProcess = [folder];
|
||||
}
|
||||
|
||||
const filesToArchive = [];
|
||||
|
||||
for (const folder of foldersToProcess) {
|
||||
if (!folder) continue; // Additional safety check
|
||||
|
||||
const files = this.getFilesFromFolder(
|
||||
folder,
|
||||
rule.applyRecursively || false,
|
||||
);
|
||||
|
||||
for (const file of files) {
|
||||
if (this.filesProcessedInCycle >= MAX_FILES_PER_CYCLE) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (await this.evaluateAutoArchiveRule(file, rule)) {
|
||||
filesToArchive.push(file);
|
||||
this.filesProcessedInCycle++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const file of filesToArchive) {
|
||||
try {
|
||||
await this.archiveFile(file);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Auto-archive failed for file: ${file.path}`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error("Auto-archive cycle failed:", error);
|
||||
} finally {
|
||||
await this.persistLastRunTimestamp(Date.now());
|
||||
this.isProcessingAutoArchiveRules = false;
|
||||
}
|
||||
}
|
||||
|
||||
private getFilesFromFolder(folder: TFolder, recursive: boolean): TFile[] {
|
||||
const files: TFile[] = [];
|
||||
|
||||
if (!folder || !folder.children) {
|
||||
return files;
|
||||
}
|
||||
|
||||
for (const child of folder.children) {
|
||||
if (child instanceof TFile) {
|
||||
files.push(child);
|
||||
} else if (recursive && child instanceof TFolder) {
|
||||
files.push(...this.getFilesFromFolder(child, recursive));
|
||||
}
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
private async evaluateAutoArchiveRule(
|
||||
file: TAbstractFile,
|
||||
rule: AutoArchiveRule,
|
||||
): Promise<boolean> {
|
||||
if (this.isFileArchived(file)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (rule.conditions.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const operator = rule.logicOperator || "AND";
|
||||
if (operator === "OR") {
|
||||
for (const condition of rule.conditions) {
|
||||
if (await this.evaluateCondition(file, condition)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
} else {
|
||||
for (const condition of rule.conditions) {
|
||||
if (!(await this.evaluateCondition(file, condition))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private async evaluateCondition(
|
||||
file: TAbstractFile,
|
||||
condition: AutoArchiveCondition,
|
||||
): Promise<boolean> {
|
||||
if (condition.type === "fileAge") {
|
||||
const ageInDays = parseInt(condition.value);
|
||||
if (isNaN(ageInDays)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const stats = await this.app.vault.adapter.stat(file.path);
|
||||
if (!stats) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const fileAgeMs = Date.now() - stats.mtime;
|
||||
const fileAgeDays = fileAgeMs / (1000 * 60 * 60 * 24);
|
||||
return fileAgeDays >= ageInDays;
|
||||
} else if (condition.type === "regexPattern") {
|
||||
const regex = this.validateRegexPattern(condition.value);
|
||||
if (!regex) {
|
||||
console.error(
|
||||
`Invalid regex pattern in auto-archive rule: ${condition.value}`,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
return regex.test(file.name);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
getRulesForFolder(folderPath: string): AutoArchiveRule[] {
|
||||
const settings = this.getSettings();
|
||||
return settings.autoArchiveRules.filter((rule) => {
|
||||
if (rule.useFolderRegex) {
|
||||
const regex = this.validateRegexPattern(rule.folderPath);
|
||||
if (!regex) {
|
||||
return false;
|
||||
}
|
||||
return regex.test(folderPath);
|
||||
}
|
||||
return rule.folderPath === folderPath;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates human-readable text for a condition (shared utility).
|
||||
*/
|
||||
getConditionText(condition: AutoArchiveCondition): string {
|
||||
if (condition.type === "fileAge") {
|
||||
return `File age ≥ ${condition.value} days`;
|
||||
} else if (condition.type === "regexPattern") {
|
||||
return `File name matches: ${condition.value}`;
|
||||
}
|
||||
return "Unknown condition";
|
||||
}
|
||||
|
||||
getRuleDisplayText(rule: AutoArchiveRule): string {
|
||||
const icon = rule.enabled ? "✓" : "○";
|
||||
|
||||
if (rule.conditions.length === 0) {
|
||||
return `${icon} (no conditions)`;
|
||||
}
|
||||
|
||||
if (rule.conditions.length === 1) {
|
||||
const conditionText = this.getConditionText(rule.conditions[0]);
|
||||
return `${icon} ${conditionText}`;
|
||||
}
|
||||
|
||||
// Multiple conditions: join with logic operator
|
||||
const operator = rule.logicOperator === "OR" ? " OR " : " AND ";
|
||||
const conditionsText = rule.conditions
|
||||
.map((c) => this.getConditionText(c))
|
||||
.join(operator);
|
||||
|
||||
// Truncate if too long
|
||||
const fullText = `${icon} ${conditionsText}`;
|
||||
const MAX_LENGTH = 60;
|
||||
if (fullText.length > MAX_LENGTH) {
|
||||
const firstCondition = this.getConditionText(rule.conditions[0]);
|
||||
return `${icon} ${firstCondition} ${operator}... (${rule.conditions.length} total)`;
|
||||
}
|
||||
|
||||
return fullText;
|
||||
}
|
||||
|
||||
setupContextMenu(workspace: Workspace, plugin: SimpleArchiver): void {
|
||||
plugin.registerEvent(
|
||||
workspace.on("file-menu", (menu, file) => {
|
||||
// Only show for folders
|
||||
if (!(file instanceof TFolder)) {
|
||||
return;
|
||||
}
|
||||
|
||||
menu.addItem((item) => {
|
||||
item.setTitle("Auto-archive").setIcon("clock");
|
||||
|
||||
// Add submenu with proper type safety
|
||||
const submenu = this.getSubmenu(item);
|
||||
if (!submenu) {
|
||||
console.error("Failed to create submenu");
|
||||
return;
|
||||
}
|
||||
|
||||
// Add "Add rule" item
|
||||
submenu.addItem((subitem: MenuEntry) => {
|
||||
subitem
|
||||
.setTitle("Add rule")
|
||||
.setIcon("plus")
|
||||
.onClick(() => {
|
||||
this.openAutoArchiveRuleForFolder(
|
||||
plugin,
|
||||
file.path,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// Check if there are existing rules for this folder
|
||||
const existingRules = this.getRulesForFolder(file.path);
|
||||
|
||||
if (existingRules.length > 0) {
|
||||
// Add "Edit Rule" submenu item
|
||||
submenu.addItem((subitem: MenuEntry) => {
|
||||
subitem.setTitle("Edit Rule").setIcon("pencil");
|
||||
|
||||
const editSubmenu = this.getSubmenu(subitem);
|
||||
if (!editSubmenu) return;
|
||||
|
||||
// Add each rule as a submenu item
|
||||
for (const rule of existingRules) {
|
||||
editSubmenu.addItem((ruleItem: MenuEntry) => {
|
||||
const displayText =
|
||||
this.getRuleDisplayText(rule);
|
||||
const icon = rule.enabled
|
||||
? "check"
|
||||
: "circle";
|
||||
|
||||
ruleItem
|
||||
.setTitle(displayText)
|
||||
.setIcon(icon)
|
||||
.onClick(() => {
|
||||
new AutoArchiveRuleModal(
|
||||
this.app,
|
||||
plugin,
|
||||
rule,
|
||||
async () => {
|
||||
await plugin.saveSettings();
|
||||
},
|
||||
).open();
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely retrieves submenu from menu item with type safety.
|
||||
*/
|
||||
private getSubmenu(item: MenuEntry): MenuGroup | null {
|
||||
try {
|
||||
return typeof item.setSubmenu === "function"
|
||||
? item.setSubmenu()
|
||||
: null;
|
||||
} catch (error) {
|
||||
console.error("Failed to create submenu:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private openAutoArchiveRuleForFolder(
|
||||
plugin: SimpleArchiver,
|
||||
folderPath: string,
|
||||
): void {
|
||||
try {
|
||||
const obsidianApis = this.app as unknown as ObsidianInternalApis;
|
||||
if (!obsidianApis.setting) {
|
||||
console.error("Unable to access settings API");
|
||||
return;
|
||||
}
|
||||
|
||||
obsidianApis.setting.open();
|
||||
|
||||
const pluginId = plugin.manifest.id;
|
||||
obsidianApis.setting.openTabById(pluginId);
|
||||
|
||||
window.setTimeout(() => {
|
||||
const tabs = obsidianApis.setting
|
||||
.pluginTabs as PluginSettingTab[];
|
||||
for (const tab of tabs) {
|
||||
if (tab instanceof SimpleArchiverSettingsTab) {
|
||||
tab.activeTab = "autoArchive";
|
||||
tab.display();
|
||||
|
||||
const newRule: AutoArchiveRule = {
|
||||
id: crypto.randomUUID(),
|
||||
enabled: true,
|
||||
folderPath,
|
||||
useFolderRegex: false,
|
||||
applyRecursively: false,
|
||||
conditions: [],
|
||||
logicOperator: "AND",
|
||||
};
|
||||
|
||||
plugin.settings.autoArchiveRules.push(newRule);
|
||||
|
||||
try {
|
||||
new AutoArchiveRuleModal(
|
||||
this.app,
|
||||
plugin,
|
||||
newRule,
|
||||
async () => {
|
||||
await plugin.saveSettings();
|
||||
tab.display();
|
||||
},
|
||||
async () => {
|
||||
plugin.settings.autoArchiveRules =
|
||||
plugin.settings.autoArchiveRules.filter(
|
||||
(r) => r.id !== newRule.id,
|
||||
);
|
||||
await plugin.saveSettings();
|
||||
tab.display();
|
||||
},
|
||||
).open();
|
||||
} catch (error) {
|
||||
plugin.settings.autoArchiveRules =
|
||||
plugin.settings.autoArchiveRules.filter(
|
||||
(r) => r.id !== newRule.id,
|
||||
);
|
||||
console.error(
|
||||
"Failed to open auto-archive rule modal:",
|
||||
error,
|
||||
);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}, SETTINGS_TAB_RENDER_DELAY_MS);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"Failed to open auto-archive rule for folder:",
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,96 +0,0 @@
|
|||
import type {
|
||||
AutoArchiveRuntimeData,
|
||||
AutoArchiveRule,
|
||||
SimpleArchiverSettings,
|
||||
} from "./AutoArchiveTypes";
|
||||
|
||||
/**
|
||||
* Default settings for auto-archive functionality.
|
||||
* These are composed into the plugin's overall DEFAULT_SETTINGS.
|
||||
*/
|
||||
export const AUTO_ARCHIVE_DEFAULT_SETTINGS: Partial<SimpleArchiverSettings> = {
|
||||
autoArchiveRules: [],
|
||||
autoArchiveFrequency: 60,
|
||||
autoArchiveStartupDelaySeconds: 30,
|
||||
};
|
||||
|
||||
export const AUTO_ARCHIVE_DEFAULT_RUNTIME_DATA: AutoArchiveRuntimeData = {
|
||||
lastAutoArchiveRunAt: 0,
|
||||
};
|
||||
|
||||
/**
|
||||
* Migrates auto-archive settings from loaded data, ensuring backward compatibility
|
||||
* and normalizing all required fields to safe defaults.
|
||||
*
|
||||
* @param settings The loaded settings object (may be partial from old configs)
|
||||
* @returns Object with normalized settings and a flag indicating if changes were made
|
||||
*/
|
||||
export function migrateAutoArchiveSettings(settings: SimpleArchiverSettings): {
|
||||
settings: SimpleArchiverSettings;
|
||||
changed: boolean;
|
||||
} {
|
||||
let changed = false;
|
||||
|
||||
// Migrate per-rule defaults
|
||||
if (settings.autoArchiveRules) {
|
||||
settings.autoArchiveRules = settings.autoArchiveRules.map((rule) => {
|
||||
const updates: Partial<AutoArchiveRule> = {};
|
||||
|
||||
if (!rule.logicOperator) {
|
||||
changed = true;
|
||||
updates.logicOperator = "AND" as "AND" | "OR";
|
||||
}
|
||||
|
||||
if (rule.useFolderRegex === undefined) {
|
||||
changed = true;
|
||||
updates.useFolderRegex = false;
|
||||
}
|
||||
|
||||
if (rule.applyRecursively === undefined) {
|
||||
changed = true;
|
||||
updates.applyRecursively = false;
|
||||
}
|
||||
|
||||
return { ...rule, ...updates };
|
||||
});
|
||||
}
|
||||
|
||||
// Normalize startup delay seconds
|
||||
if (
|
||||
!Number.isFinite(settings.autoArchiveStartupDelaySeconds) ||
|
||||
settings.autoArchiveStartupDelaySeconds < 0
|
||||
) {
|
||||
changed = true;
|
||||
settings.autoArchiveStartupDelaySeconds =
|
||||
AUTO_ARCHIVE_DEFAULT_SETTINGS.autoArchiveStartupDelaySeconds ?? 30;
|
||||
}
|
||||
|
||||
// Normalize auto-archive frequency (minutes)
|
||||
if (
|
||||
!Number.isFinite(settings.autoArchiveFrequency) ||
|
||||
settings.autoArchiveFrequency <= 0
|
||||
) {
|
||||
changed = true;
|
||||
settings.autoArchiveFrequency =
|
||||
AUTO_ARCHIVE_DEFAULT_SETTINGS.autoArchiveFrequency ?? 60;
|
||||
}
|
||||
|
||||
return { settings, changed };
|
||||
}
|
||||
|
||||
export function normalizeAutoArchiveRuntimeData(
|
||||
runtimeData: Partial<AutoArchiveRuntimeData> | null | undefined,
|
||||
): AutoArchiveRuntimeData {
|
||||
const lastAutoArchiveRunAt = runtimeData?.lastAutoArchiveRunAt;
|
||||
|
||||
if (
|
||||
!Number.isFinite(lastAutoArchiveRunAt) ||
|
||||
(lastAutoArchiveRunAt ?? 0) < 0
|
||||
) {
|
||||
return { ...AUTO_ARCHIVE_DEFAULT_RUNTIME_DATA };
|
||||
}
|
||||
|
||||
return {
|
||||
lastAutoArchiveRunAt: lastAutoArchiveRunAt ?? 0,
|
||||
};
|
||||
}
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
export interface AutoArchiveCondition {
|
||||
type: "fileAge" | "regexPattern";
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface AutoArchiveRule {
|
||||
id: string;
|
||||
enabled: boolean;
|
||||
folderPath: string;
|
||||
useFolderRegex: boolean;
|
||||
applyRecursively: boolean;
|
||||
conditions: AutoArchiveCondition[];
|
||||
logicOperator: "AND" | "OR";
|
||||
}
|
||||
|
||||
export interface SimpleArchiverSettings {
|
||||
archiveFolder: string;
|
||||
autoArchiveRules: AutoArchiveRule[];
|
||||
autoArchiveFrequency: number;
|
||||
autoArchiveStartupDelaySeconds: number;
|
||||
}
|
||||
|
||||
export interface AutoArchiveRuntimeData {
|
||||
lastAutoArchiveRunAt: number;
|
||||
}
|
||||
|
||||
export interface ArchiveResult {
|
||||
success: boolean;
|
||||
message: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for Obsidian's internal settings API.
|
||||
* Used for type-safe access to settings functionality.
|
||||
*/
|
||||
export interface ObsidianInternalApis {
|
||||
setting: {
|
||||
open(): void;
|
||||
openTabById(pluginId: string): void;
|
||||
pluginTabs: unknown[];
|
||||
};
|
||||
}
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
/**
|
||||
* Auto-Archive Module Barrel Export
|
||||
*
|
||||
* This file consolidates all public APIs from the auto-archive subsystem,
|
||||
* allowing main.ts to import everything via a single import statement.
|
||||
*/
|
||||
|
||||
export {
|
||||
AUTO_ARCHIVE_DEFAULT_SETTINGS,
|
||||
AUTO_ARCHIVE_DEFAULT_RUNTIME_DATA,
|
||||
migrateAutoArchiveSettings,
|
||||
normalizeAutoArchiveRuntimeData,
|
||||
} from "./AutoArchiveSettings";
|
||||
|
||||
export {
|
||||
createAutoArchiveService,
|
||||
startAutoArchive,
|
||||
stopAutoArchiveScheduler,
|
||||
} from "./AutoArchiveLifecycle";
|
||||
|
||||
export { setupAutoArchiveContextMenu } from "./AutoArchiveContextMenu";
|
||||
|
||||
export { AutoArchiveService } from "./AutoArchiveService";
|
||||
|
||||
export type {
|
||||
ArchiveResult,
|
||||
AutoArchiveRuntimeData,
|
||||
AutoArchiveCondition,
|
||||
AutoArchiveRule,
|
||||
SimpleArchiverSettings,
|
||||
} from "./AutoArchiveTypes";
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import esbuild from "esbuild";
|
||||
import process from "process";
|
||||
import { builtinModules } from "module";
|
||||
import builtins from "builtin-modules";
|
||||
|
||||
const banner = `/*
|
||||
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
|
||||
|
|
@ -30,7 +30,7 @@ const context = await esbuild.context({
|
|||
"@lezer/common",
|
||||
"@lezer/highlight",
|
||||
"@lezer/lr",
|
||||
...builtinModules,
|
||||
...builtins,
|
||||
],
|
||||
format: "cjs",
|
||||
target: "es2018",
|
||||
|
|
|
|||
377
main.ts
377
main.ts
|
|
@ -1,40 +1,31 @@
|
|||
import {
|
||||
App,
|
||||
Editor,
|
||||
MarkdownView,
|
||||
Modal,
|
||||
normalizePath,
|
||||
Notice,
|
||||
Plugin,
|
||||
PluginSettingTab,
|
||||
Setting,
|
||||
TAbstractFile,
|
||||
} from "obsidian";
|
||||
|
||||
import {
|
||||
AUTO_ARCHIVE_DEFAULT_SETTINGS,
|
||||
AUTO_ARCHIVE_DEFAULT_RUNTIME_DATA,
|
||||
migrateAutoArchiveSettings,
|
||||
normalizeAutoArchiveRuntimeData,
|
||||
createAutoArchiveService,
|
||||
startAutoArchive,
|
||||
stopAutoArchiveScheduler,
|
||||
setupAutoArchiveContextMenu,
|
||||
AutoArchiveService,
|
||||
type AutoArchiveRuntimeData,
|
||||
type ArchiveResult,
|
||||
type SimpleArchiverSettings,
|
||||
} from "./autoarchive";
|
||||
import { SimpleArchiverPromptModal } from "./modals";
|
||||
import { SimpleArchiverSettingsTab } from "./SettingsTab";
|
||||
interface SimpleArchiverSettings {
|
||||
archiveFolder: string;
|
||||
}
|
||||
|
||||
const AUTO_ARCHIVE_DATA_FILE = "autoarchive_data.json";
|
||||
interface ArchiveResult {
|
||||
success: boolean;
|
||||
message: string;
|
||||
}
|
||||
|
||||
const DEFAULT_SETTINGS: SimpleArchiverSettings = {
|
||||
archiveFolder: "Archive",
|
||||
...AUTO_ARCHIVE_DEFAULT_SETTINGS,
|
||||
} as SimpleArchiverSettings;
|
||||
};
|
||||
|
||||
export default class SimpleArchiver extends Plugin {
|
||||
settings: SimpleArchiverSettings;
|
||||
autoArchiveRuntimeData: AutoArchiveRuntimeData;
|
||||
private autoArchiveService: AutoArchiveService;
|
||||
|
||||
async onload() {
|
||||
await this.loadSettings();
|
||||
|
|
@ -44,24 +35,17 @@ export default class SimpleArchiver extends Plugin {
|
|||
name: "Move to archive",
|
||||
editorCheckCallback: (
|
||||
checking: boolean,
|
||||
_editor: Editor,
|
||||
view: MarkdownView,
|
||||
editor: Editor,
|
||||
view: MarkdownView
|
||||
) => {
|
||||
const canBeArchived =
|
||||
view.file && !this.isFileArchived(view.file);
|
||||
|
||||
if (canBeArchived && view.file != null) {
|
||||
if (!checking) {
|
||||
void this.archiveFile(view.file)
|
||||
.then((result) => {
|
||||
new Notice(result.message);
|
||||
})
|
||||
.catch((e: unknown) => {
|
||||
const msg =
|
||||
e instanceof Error ? e.message : String(e);
|
||||
new Notice(`Error: ${msg}`);
|
||||
console.error(msg);
|
||||
});
|
||||
this.archiveFile(view.file).then((result) => {
|
||||
new Notice(result.message);
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
|
|
@ -76,24 +60,17 @@ export default class SimpleArchiver extends Plugin {
|
|||
name: "Move out of archive",
|
||||
editorCheckCallback: (
|
||||
checking: boolean,
|
||||
_editor: Editor,
|
||||
view: MarkdownView,
|
||||
editor: Editor,
|
||||
view: MarkdownView
|
||||
) => {
|
||||
const canBeUnarchived =
|
||||
view.file && this.isFileArchived(view.file);
|
||||
|
||||
if (canBeUnarchived && view.file != null) {
|
||||
if (!checking) {
|
||||
void this.unarchiveFile(view.file)
|
||||
.then((result) => {
|
||||
new Notice(result.message);
|
||||
})
|
||||
.catch((e: unknown) => {
|
||||
const msg =
|
||||
e instanceof Error ? e.message : String(e);
|
||||
new Notice(`Error: ${msg}`);
|
||||
console.error(msg);
|
||||
});
|
||||
this.unarchiveFile(view.file).then((result) => {
|
||||
new Notice(result.message);
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
|
|
@ -103,37 +80,7 @@ export default class SimpleArchiver extends Plugin {
|
|||
},
|
||||
});
|
||||
|
||||
this.autoArchiveService = createAutoArchiveService(
|
||||
this.app,
|
||||
() => ({
|
||||
...this.settings,
|
||||
lastAutoArchiveRunAt:
|
||||
this.autoArchiveRuntimeData.lastAutoArchiveRunAt,
|
||||
}),
|
||||
(file) => this.archiveFile(file),
|
||||
(file) => this.isFileArchived(file),
|
||||
async (lastRunAt) => {
|
||||
await this.saveAutoArchiveRuntimeData({
|
||||
lastAutoArchiveRunAt: lastRunAt,
|
||||
});
|
||||
},
|
||||
);
|
||||
startAutoArchive(this.autoArchiveService);
|
||||
|
||||
this.addSettingTab(
|
||||
new SimpleArchiverSettingsTab(
|
||||
this.app,
|
||||
this,
|
||||
this.autoArchiveService,
|
||||
),
|
||||
);
|
||||
|
||||
// Setup auto-archive context menu
|
||||
setupAutoArchiveContextMenu(
|
||||
this.autoArchiveService,
|
||||
this.app.workspace,
|
||||
this,
|
||||
);
|
||||
this.addSettingTab(new SimpleArchiverSettingsTab(this.app, this));
|
||||
|
||||
// Archive file context menu
|
||||
this.registerEvent(
|
||||
|
|
@ -151,12 +98,11 @@ export default class SimpleArchiver extends Plugin {
|
|||
if (result.success) {
|
||||
new Notice(result.message);
|
||||
} else {
|
||||
new Notice(`Error: ${result.message}`);
|
||||
console.error(result.message);
|
||||
new Error(result.message);
|
||||
}
|
||||
});
|
||||
});
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
this.registerEvent(
|
||||
|
|
@ -172,7 +118,7 @@ export default class SimpleArchiver extends Plugin {
|
|||
await this.archiveAllFiles(files);
|
||||
});
|
||||
});
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
// Unarchive file context menu
|
||||
|
|
@ -191,12 +137,11 @@ export default class SimpleArchiver extends Plugin {
|
|||
if (result.success) {
|
||||
new Notice(result.message);
|
||||
} else {
|
||||
new Notice(`Error: ${result.message}`);
|
||||
console.error(result.message);
|
||||
new Error(result.message);
|
||||
}
|
||||
});
|
||||
});
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
this.registerEvent(
|
||||
|
|
@ -212,7 +157,7 @@ export default class SimpleArchiver extends Plugin {
|
|||
await this.unarchiveAllFiles(files);
|
||||
});
|
||||
});
|
||||
}),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -220,30 +165,13 @@ export default class SimpleArchiver extends Plugin {
|
|||
return file.path.startsWith(this.settings.archiveFolder);
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely resolves the original file path from an archived path.
|
||||
* Handles edge cases like files at vault root or missing parent folders.
|
||||
*/
|
||||
private resolveOriginalPath(archivedPath: string): string | null {
|
||||
const archiveFolderPrefix = this.settings.archiveFolder + "/";
|
||||
|
||||
// Check that the file is actually in the archive folder
|
||||
if (!archivedPath.startsWith(archiveFolderPrefix)) {
|
||||
console.error(`File is not in archive folder: ${archivedPath}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Remove the archive folder prefix to get the original path
|
||||
return archivedPath.substring(archiveFolderPrefix.length);
|
||||
}
|
||||
|
||||
private async archiveFile(file: TAbstractFile): Promise<ArchiveResult> {
|
||||
if (this.isFileArchived(file)) {
|
||||
return { success: false, message: "Item is already archived" };
|
||||
}
|
||||
|
||||
const destinationFilePath = normalizePath(
|
||||
`${this.settings.archiveFolder}/${file.path}`,
|
||||
`${this.settings.archiveFolder}/${file.path}`
|
||||
);
|
||||
|
||||
const existingItem =
|
||||
|
|
@ -269,7 +197,7 @@ export default class SimpleArchiver extends Plugin {
|
|||
success: false,
|
||||
message: "Archive operation cancelled",
|
||||
});
|
||||
},
|
||||
}
|
||||
);
|
||||
prompt.open();
|
||||
});
|
||||
|
|
@ -293,10 +221,10 @@ export default class SimpleArchiver extends Plugin {
|
|||
}
|
||||
|
||||
private async moveFileToArchive(
|
||||
file: TAbstractFile,
|
||||
file: TAbstractFile
|
||||
): Promise<ArchiveResult> {
|
||||
const destinationPath = normalizePath(
|
||||
`${this.settings.archiveFolder}/${file.parent?.path}`,
|
||||
`${this.settings.archiveFolder}/${file.parent?.path}`
|
||||
);
|
||||
|
||||
const destinationFolder =
|
||||
|
|
@ -307,7 +235,7 @@ export default class SimpleArchiver extends Plugin {
|
|||
}
|
||||
|
||||
const destinationFilePath = normalizePath(
|
||||
`${destinationPath}/${file.name}`,
|
||||
`${destinationPath}/${file.name}`
|
||||
);
|
||||
|
||||
try {
|
||||
|
|
@ -317,69 +245,43 @@ export default class SimpleArchiver extends Plugin {
|
|||
message: `${file.name} archived successfully`,
|
||||
};
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
return {
|
||||
success: false,
|
||||
message: `Unable to archive ${file.name}: ${errorMessage}`,
|
||||
message: `Unable to archive ${file.name}: ${error}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private async moveFileOutOfArchive(
|
||||
file: TAbstractFile,
|
||||
file: TAbstractFile
|
||||
): Promise<ArchiveResult> {
|
||||
const originalPath = this.resolveOriginalPath(file.path);
|
||||
|
||||
if (!originalPath) {
|
||||
return {
|
||||
success: false,
|
||||
message: `Unable to unarchive: Invalid archive path ${file.path}`,
|
||||
};
|
||||
}
|
||||
|
||||
// Extract parent folder path by finding the last slash
|
||||
const lastSlashIndex = originalPath.lastIndexOf("/");
|
||||
const originalParentPath =
|
||||
lastSlashIndex > 0
|
||||
? originalPath.substring(0, lastSlashIndex)
|
||||
: null;
|
||||
const originalPath = file.path.substring(
|
||||
this.settings.archiveFolder.length + 1
|
||||
);
|
||||
const originalParentPath = originalPath.substring(
|
||||
0,
|
||||
originalPath.lastIndexOf("/")
|
||||
);
|
||||
|
||||
if (originalParentPath) {
|
||||
const originalFolder =
|
||||
this.app.vault.getFolderByPath(originalParentPath);
|
||||
|
||||
if (!originalFolder) {
|
||||
try {
|
||||
await this.app.vault.createFolder(
|
||||
normalizePath(originalParentPath),
|
||||
);
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
return {
|
||||
success: false,
|
||||
message: `Unable to create folder: ${errorMessage}`,
|
||||
};
|
||||
}
|
||||
if (originalFolder == null) {
|
||||
await this.app.vault.createFolder(normalizePath(originalParentPath));
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await this.app.fileManager.renameFile(
|
||||
file,
|
||||
normalizePath(originalPath),
|
||||
);
|
||||
await this.app.fileManager.renameFile(file, normalizePath(originalPath));
|
||||
return {
|
||||
success: true,
|
||||
message: `${file.name} unarchived successfully`,
|
||||
};
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
return {
|
||||
success: false,
|
||||
message: `Unable to unarchive ${file.name}: ${errorMessage}`,
|
||||
message: `Unable to unarchive ${file.name}: ${error}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -389,14 +291,9 @@ export default class SimpleArchiver extends Plugin {
|
|||
return { success: false, message: "Item is not archived" };
|
||||
}
|
||||
|
||||
const originalPath = this.resolveOriginalPath(file.path);
|
||||
|
||||
if (!originalPath) {
|
||||
return {
|
||||
success: false,
|
||||
message: `Unable to unarchive: Invalid archive path`,
|
||||
};
|
||||
}
|
||||
const originalPath = file.path.substring(
|
||||
this.settings.archiveFolder.length + 1
|
||||
);
|
||||
|
||||
const existingItem = this.app.vault.getAbstractFileByPath(originalPath);
|
||||
|
||||
|
|
@ -419,7 +316,7 @@ export default class SimpleArchiver extends Plugin {
|
|||
success: false,
|
||||
message: "Unarchive operation cancelled",
|
||||
});
|
||||
},
|
||||
}
|
||||
);
|
||||
prompt.open();
|
||||
});
|
||||
|
|
@ -441,104 +338,98 @@ export default class SimpleArchiver extends Plugin {
|
|||
new Notice(`${unarchived} files unarchived`);
|
||||
}
|
||||
|
||||
onunload() {
|
||||
stopAutoArchiveScheduler(this.autoArchiveService);
|
||||
}
|
||||
|
||||
private async loadSettings() {
|
||||
const loadedData = await this.loadData();
|
||||
const lastAutoArchiveRunAt =
|
||||
this.getLegacyLastAutoArchiveRunAt(loadedData);
|
||||
|
||||
this.settings = Object.assign({}, DEFAULT_SETTINGS, loadedData);
|
||||
|
||||
delete (
|
||||
this.settings as Partial<SimpleArchiverSettings> & {
|
||||
lastAutoArchiveRunAt?: number;
|
||||
}
|
||||
).lastAutoArchiveRunAt;
|
||||
|
||||
this.autoArchiveRuntimeData =
|
||||
await this.loadAutoArchiveRuntimeData(lastAutoArchiveRunAt);
|
||||
|
||||
// Migrate auto-archive settings for backward compatibility
|
||||
const { changed } = migrateAutoArchiveSettings(this.settings);
|
||||
|
||||
// Persist the migration if any changes were made
|
||||
if (changed || lastAutoArchiveRunAt !== null) {
|
||||
await this.saveSettings();
|
||||
}
|
||||
this.settings = Object.assign(
|
||||
{},
|
||||
DEFAULT_SETTINGS,
|
||||
await this.loadData()
|
||||
);
|
||||
}
|
||||
|
||||
async saveSettings() {
|
||||
await this.saveData(this.settings);
|
||||
}
|
||||
}
|
||||
|
||||
async saveAutoArchiveRuntimeData(runtimeData: AutoArchiveRuntimeData) {
|
||||
this.autoArchiveRuntimeData =
|
||||
normalizeAutoArchiveRuntimeData(runtimeData);
|
||||
await this.app.vault.adapter.write(
|
||||
`${this.manifest.dir}/${AUTO_ARCHIVE_DATA_FILE}`,
|
||||
JSON.stringify(this.autoArchiveRuntimeData, null, 2),
|
||||
);
|
||||
}
|
||||
class SimpleArchiverPromptModal extends Modal {
|
||||
constructor(
|
||||
app: App,
|
||||
title: string,
|
||||
message: string,
|
||||
yesButtonText: string,
|
||||
noButtonText: string,
|
||||
callback: () => Promise<void>,
|
||||
cancelCallback: () => Promise<void>
|
||||
) {
|
||||
super(app);
|
||||
|
||||
private async loadAutoArchiveRuntimeData(
|
||||
legacyLastAutoArchiveRunAt: number | null,
|
||||
): Promise<AutoArchiveRuntimeData> {
|
||||
const existingRuntimeData = await this.readAutoArchiveRuntimeDataFile();
|
||||
this.setTitle(title);
|
||||
|
||||
if (existingRuntimeData !== null) {
|
||||
return existingRuntimeData;
|
||||
}
|
||||
this.setContent(message);
|
||||
|
||||
const migratedRuntimeData = normalizeAutoArchiveRuntimeData({
|
||||
lastAutoArchiveRunAt:
|
||||
legacyLastAutoArchiveRunAt ??
|
||||
AUTO_ARCHIVE_DEFAULT_RUNTIME_DATA.lastAutoArchiveRunAt,
|
||||
});
|
||||
|
||||
if (legacyLastAutoArchiveRunAt !== null) {
|
||||
await this.saveAutoArchiveRuntimeData(migratedRuntimeData);
|
||||
}
|
||||
|
||||
return migratedRuntimeData;
|
||||
}
|
||||
|
||||
private async readAutoArchiveRuntimeDataFile(): Promise<AutoArchiveRuntimeData | null> {
|
||||
const runtimeDataPath = `${this.manifest.dir}/${AUTO_ARCHIVE_DATA_FILE}`;
|
||||
|
||||
if (!(await this.app.vault.adapter.exists(runtimeDataPath))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const content = await this.app.vault.adapter.read(runtimeDataPath);
|
||||
const parsedData = JSON.parse(
|
||||
content,
|
||||
) as Partial<AutoArchiveRuntimeData>;
|
||||
return normalizeAutoArchiveRuntimeData(parsedData);
|
||||
} catch (error) {
|
||||
console.error("Failed to load auto-archive runtime data:", error);
|
||||
return { ...AUTO_ARCHIVE_DEFAULT_RUNTIME_DATA };
|
||||
}
|
||||
}
|
||||
|
||||
private getLegacyLastAutoArchiveRunAt(loadedData: unknown): number | null {
|
||||
if (
|
||||
loadedData === null ||
|
||||
typeof loadedData !== "object" ||
|
||||
!("lastAutoArchiveRunAt" in loadedData)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { lastAutoArchiveRunAt } = loadedData as {
|
||||
lastAutoArchiveRunAt?: unknown;
|
||||
};
|
||||
|
||||
return typeof lastAutoArchiveRunAt === "number"
|
||||
? lastAutoArchiveRunAt
|
||||
: null;
|
||||
new Setting(this.contentEl)
|
||||
.addButton((btn) =>
|
||||
btn
|
||||
.setButtonText(yesButtonText)
|
||||
.setWarning()
|
||||
.onClick(() => {
|
||||
callback();
|
||||
this.close();
|
||||
})
|
||||
)
|
||||
.addButton((btn) =>
|
||||
btn.setButtonText(noButtonText).onClick(() => {
|
||||
cancelCallback();
|
||||
this.close();
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SimpleArchiverSettingsTab extends PluginSettingTab {
|
||||
plugin: SimpleArchiver;
|
||||
|
||||
constructor(app: App, plugin: SimpleArchiver) {
|
||||
super(app, plugin);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
display(): void {
|
||||
const { containerEl } = this;
|
||||
|
||||
containerEl.empty();
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Archive folder")
|
||||
.setDesc(
|
||||
"The folder to use as the Archive. If the folder doesn't exist, it will be created when archiving a " +
|
||||
'note. Folder names must not contain "\\", "/" or ":" and must not start with ".".'
|
||||
)
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder("Archive folder")
|
||||
.setValue(normalizePath(this.plugin.settings.archiveFolder))
|
||||
.onChange(async (value) => {
|
||||
if (this.setArchiveFolder(value)) {
|
||||
await this.plugin.saveSettings();
|
||||
} else {
|
||||
text.setValue(this.plugin.settings.archiveFolder);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private validateArchiveFolderName(value: string): boolean {
|
||||
// Validate folder does not start with '.', contain ':' or contain a relative path
|
||||
return !/^\.|[:/\\]\.|:/.test(value);
|
||||
}
|
||||
|
||||
private setArchiveFolder(value: string): boolean {
|
||||
if (!this.validateArchiveFolderName(value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.plugin.settings.archiveFolder = value;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "simple-archiver",
|
||||
"name": "Simple Archiver",
|
||||
"version": "0.6.2",
|
||||
"version": "0.5.2",
|
||||
"minAppVersion": "1.8.0",
|
||||
"description": "Move old, stinky notes and folders to an archive, where they belong.",
|
||||
"author": "Mike Farr",
|
||||
|
|
|
|||
401
modals.ts
401
modals.ts
|
|
@ -1,401 +0,0 @@
|
|||
import { App, Modal, normalizePath, Setting } from "obsidian";
|
||||
|
||||
import type {
|
||||
AutoArchiveCondition,
|
||||
AutoArchiveRule,
|
||||
} from "./autoarchive/AutoArchiveTypes";
|
||||
import type SimpleArchiver from "./main";
|
||||
|
||||
export class SimpleArchiverPromptModal extends Modal {
|
||||
constructor(
|
||||
app: App,
|
||||
title: string,
|
||||
message: string,
|
||||
yesButtonText: string,
|
||||
noButtonText: string,
|
||||
callback: () => Promise<void>,
|
||||
cancelCallback: () => Promise<void>,
|
||||
) {
|
||||
super(app);
|
||||
|
||||
this.setTitle(title);
|
||||
|
||||
this.setContent(message);
|
||||
|
||||
new Setting(this.contentEl)
|
||||
.addButton((btn) =>
|
||||
btn
|
||||
.setButtonText(yesButtonText)
|
||||
.setWarning()
|
||||
.onClick(() => {
|
||||
void callback();
|
||||
this.close();
|
||||
}),
|
||||
)
|
||||
.addButton((btn) =>
|
||||
btn.setButtonText(noButtonText).onClick(() => {
|
||||
void cancelCallback();
|
||||
this.close();
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class AutoArchiveRuleModal extends Modal {
|
||||
plugin: SimpleArchiver;
|
||||
rule: AutoArchiveRule;
|
||||
draftRule: AutoArchiveRule;
|
||||
onSave: () => Promise<void>;
|
||||
onCancel?: () => Promise<void>;
|
||||
folderPathInput: HTMLInputElement;
|
||||
validationErrorEl: HTMLDivElement;
|
||||
closeReason: "none" | "saved" | "cancelled" = "none";
|
||||
|
||||
constructor(
|
||||
app: App,
|
||||
plugin: SimpleArchiver,
|
||||
rule: AutoArchiveRule,
|
||||
onSave: () => Promise<void>,
|
||||
onCancel?: () => Promise<void>,
|
||||
) {
|
||||
super(app);
|
||||
this.plugin = plugin;
|
||||
this.rule = rule;
|
||||
this.draftRule = this.cloneRule(rule);
|
||||
this.onSave = onSave;
|
||||
this.onCancel = onCancel;
|
||||
}
|
||||
|
||||
onOpen() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
|
||||
this.setTitle("Edit Auto-Archive Rule");
|
||||
|
||||
this.validationErrorEl = contentEl.createDiv({
|
||||
cls: "auto-archive-rule-validation-error",
|
||||
});
|
||||
|
||||
new Setting(contentEl)
|
||||
.setName("Folder path")
|
||||
.setDesc(
|
||||
"The folder to apply this rule to (e.g., 'Projects' or 'Notes/Daily')",
|
||||
)
|
||||
.addText((text) => {
|
||||
this.folderPathInput = text.inputEl;
|
||||
text.setPlaceholder("folder/path")
|
||||
.setValue(this.draftRule.folderPath)
|
||||
.onChange((value) => {
|
||||
this.draftRule.folderPath = value;
|
||||
});
|
||||
});
|
||||
|
||||
new Setting(contentEl)
|
||||
.setName("Use folder path as regular expression")
|
||||
.setDesc(
|
||||
"When enabled, the folder path will be treated as a regular expression pattern to match multiple folders",
|
||||
)
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(this.draftRule.useFolderRegex || false)
|
||||
.onChange((value) => {
|
||||
this.draftRule.useFolderRegex = value;
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(contentEl)
|
||||
.setName("Apply recursively to subfolders")
|
||||
.setDesc(
|
||||
"When enabled, the rule will be applied to all files in subfolders as well",
|
||||
)
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(this.draftRule.applyRecursively || false)
|
||||
.onChange((value) => {
|
||||
this.draftRule.applyRecursively = value;
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(contentEl)
|
||||
.setName("Logic operator")
|
||||
.setDesc("How to combine multiple conditions")
|
||||
.addDropdown((dropdown) =>
|
||||
dropdown
|
||||
.addOption("AND", "AND (all conditions must match)")
|
||||
.addOption("OR", "OR (any condition must match)")
|
||||
.setValue(this.draftRule.logicOperator || "AND")
|
||||
.onChange((value) => {
|
||||
this.draftRule.logicOperator = value as "AND" | "OR";
|
||||
}),
|
||||
);
|
||||
|
||||
contentEl.createEl("h3", { text: "Conditions" });
|
||||
|
||||
const conditionsContainer = contentEl.createDiv({
|
||||
cls: "auto-archive-conditions-container",
|
||||
});
|
||||
this.displayConditions(conditionsContainer);
|
||||
|
||||
new Setting(contentEl).addButton((button) =>
|
||||
button.setButtonText("Add Condition").onClick(() => {
|
||||
this.addCondition();
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(contentEl)
|
||||
.addButton((button) =>
|
||||
button
|
||||
.setButtonText("Save")
|
||||
.setCta()
|
||||
.onClick(async () => {
|
||||
this.clearValidationError();
|
||||
const validationMessage = this.validateRule();
|
||||
if (validationMessage) {
|
||||
this.showValidationError(validationMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
this.applyDraftToRule();
|
||||
await this.onSave();
|
||||
this.closeReason = "saved";
|
||||
this.close();
|
||||
}),
|
||||
)
|
||||
.addButton((button) =>
|
||||
button.setButtonText("Cancel").onClick(async () => {
|
||||
await this.handleCancel();
|
||||
this.closeReason = "cancelled";
|
||||
this.close();
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private displayConditions(containerEl: HTMLElement): void {
|
||||
containerEl.empty();
|
||||
|
||||
if (this.draftRule.conditions.length === 0) {
|
||||
containerEl.createEl("p", {
|
||||
text: "No conditions added yet. Add at least one condition.",
|
||||
cls: "setting-item-description",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = 0; i < this.draftRule.conditions.length; i++) {
|
||||
const condition = this.draftRule.conditions[i];
|
||||
this.displayCondition(containerEl, condition, i);
|
||||
}
|
||||
}
|
||||
|
||||
private displayCondition(
|
||||
containerEl: HTMLElement,
|
||||
condition: AutoArchiveCondition,
|
||||
index: number,
|
||||
): void {
|
||||
const conditionEl = containerEl.createDiv({
|
||||
cls: "auto-archive-condition",
|
||||
});
|
||||
|
||||
new Setting(conditionEl)
|
||||
.setName(`Condition ${index + 1}`)
|
||||
.addDropdown((dropdown) =>
|
||||
dropdown
|
||||
.addOption("fileAge", "File age (days)")
|
||||
.addOption("regexPattern", "File name regex")
|
||||
.setValue(condition.type)
|
||||
.onChange((value) => {
|
||||
const conditionType = value as
|
||||
| "fileAge"
|
||||
| "regexPattern";
|
||||
if (
|
||||
conditionType === "fileAge" ||
|
||||
conditionType === "regexPattern"
|
||||
) {
|
||||
condition.type = conditionType;
|
||||
condition.value = "";
|
||||
this.displayConditions(containerEl);
|
||||
}
|
||||
}),
|
||||
)
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder(
|
||||
condition.type === "fileAge"
|
||||
? "Number of days"
|
||||
: "Regular expression",
|
||||
)
|
||||
.setValue(condition.value)
|
||||
.onChange((value) => {
|
||||
condition.value = value;
|
||||
}),
|
||||
)
|
||||
.addButton((button) =>
|
||||
button
|
||||
.setButtonText("Remove")
|
||||
.setWarning()
|
||||
.onClick(() => {
|
||||
this.draftRule.conditions.splice(index, 1);
|
||||
this.displayConditions(containerEl);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private addCondition(): void {
|
||||
this.draftRule.conditions.push({
|
||||
type: "fileAge",
|
||||
value: "",
|
||||
});
|
||||
|
||||
const conditionsContainer = this.contentEl.querySelector(
|
||||
".auto-archive-conditions-container",
|
||||
) as HTMLElement;
|
||||
if (conditionsContainer) {
|
||||
this.displayConditions(conditionsContainer);
|
||||
}
|
||||
}
|
||||
|
||||
private validateRule(): string | null {
|
||||
const folderPath = (this.draftRule.folderPath ?? "").trim();
|
||||
if (!folderPath) {
|
||||
return "Folder path is required.";
|
||||
}
|
||||
|
||||
if (this.draftRule.conditions.length === 0) {
|
||||
return "Cannot save rule with no conditions.";
|
||||
}
|
||||
|
||||
for (let index = 0; index < this.draftRule.conditions.length; index++) {
|
||||
const condition = this.draftRule.conditions[index];
|
||||
const conditionValue = (condition.value ?? "").trim();
|
||||
if (!conditionValue) {
|
||||
return `Condition ${index + 1} requires a value.`;
|
||||
}
|
||||
|
||||
if (condition.type === "fileAge") {
|
||||
const dayCount = Number(conditionValue);
|
||||
if (!Number.isInteger(dayCount) || dayCount <= 0) {
|
||||
return `Condition ${index + 1} must be a positive whole number of days.`;
|
||||
}
|
||||
}
|
||||
|
||||
if (condition.type === "regexPattern") {
|
||||
try {
|
||||
new RegExp(conditionValue);
|
||||
} catch {
|
||||
return `Condition ${index + 1} has an invalid regular expression.`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const archiveFolder = this.normalizeRulePath(
|
||||
this.plugin.settings.archiveFolder,
|
||||
);
|
||||
|
||||
if (!archiveFolder) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (this.draftRule.useFolderRegex) {
|
||||
try {
|
||||
const folderRegex = new RegExp(this.draftRule.folderPath);
|
||||
if (folderRegex.test(archiveFolder)) {
|
||||
return "Rule folder path cannot match the archive folder path.";
|
||||
}
|
||||
} catch {
|
||||
return "Folder path regex is invalid.";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const ruleFolderPath = this.normalizeRulePath(
|
||||
this.draftRule.folderPath,
|
||||
);
|
||||
if (this.isArchiveFolderOverlap(ruleFolderPath, archiveFolder)) {
|
||||
return "Rule folder path cannot match or overlap with the archive folder path.";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private showValidationError(message: string): void {
|
||||
this.validationErrorEl.setText(message);
|
||||
this.validationErrorEl.addClass("is-visible");
|
||||
}
|
||||
|
||||
private clearValidationError(): void {
|
||||
this.validationErrorEl.empty();
|
||||
this.validationErrorEl.removeClass("is-visible");
|
||||
}
|
||||
|
||||
private normalizeRulePath(path: string): string {
|
||||
const trimmedPath = (path ?? "").trim();
|
||||
if (!trimmedPath) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return normalizePath(trimmedPath).replace(/^\/+|\/+$/g, "");
|
||||
}
|
||||
|
||||
private isArchiveFolderOverlap(
|
||||
ruleFolderPath: string,
|
||||
archiveFolderPath: string,
|
||||
): boolean {
|
||||
if (!ruleFolderPath) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (ruleFolderPath === archiveFolderPath) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (ruleFolderPath.startsWith(`${archiveFolderPath}/`)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return archiveFolderPath.startsWith(`${ruleFolderPath}/`);
|
||||
}
|
||||
|
||||
private cloneRule(rule: AutoArchiveRule): AutoArchiveRule {
|
||||
return {
|
||||
...rule,
|
||||
conditions: rule.conditions.map((condition) => ({
|
||||
...condition,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
private applyDraftToRule(): void {
|
||||
this.rule.enabled = this.draftRule.enabled;
|
||||
this.rule.folderPath = this.draftRule.folderPath;
|
||||
this.rule.useFolderRegex = this.draftRule.useFolderRegex;
|
||||
this.rule.applyRecursively = this.draftRule.applyRecursively;
|
||||
this.rule.logicOperator = this.draftRule.logicOperator;
|
||||
this.rule.conditions = this.draftRule.conditions.map((condition) => ({
|
||||
...condition,
|
||||
}));
|
||||
}
|
||||
|
||||
private async handleCancel(): Promise<void> {
|
||||
if (this.onCancel) {
|
||||
await this.onCancel();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.rule.folderPath) {
|
||||
this.plugin.settings.autoArchiveRules =
|
||||
this.plugin.settings.autoArchiveRules.filter(
|
||||
(r) => r.id !== this.rule.id,
|
||||
);
|
||||
await this.plugin.saveSettings();
|
||||
}
|
||||
}
|
||||
|
||||
onClose() {
|
||||
const { contentEl } = this;
|
||||
if (this.closeReason === "none") {
|
||||
void this.handleCancel();
|
||||
}
|
||||
contentEl.empty();
|
||||
}
|
||||
}
|
||||
365
package-lock.json
generated
365
package-lock.json
generated
|
|
@ -1,17 +1,18 @@
|
|||
{
|
||||
"name": "simple-archiver",
|
||||
"version": "0.6.2",
|
||||
"version": "0.5.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "simple-archiver",
|
||||
"version": "0.6.2",
|
||||
"version": "0.5.2",
|
||||
"license": "GPL-3.0-only",
|
||||
"devDependencies": {
|
||||
"@types/node": "^16.11.6",
|
||||
"@typescript-eslint/eslint-plugin": "5.29.0",
|
||||
"@typescript-eslint/parser": "5.29.0",
|
||||
"builtin-modules": "3.3.0",
|
||||
"esbuild": "^0.25.0",
|
||||
"obsidian": "latest",
|
||||
"tslib": "2.4.0",
|
||||
|
|
@ -19,9 +20,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@codemirror/state": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.0.tgz",
|
||||
"integrity": "sha512-MwBHVK60IiIHDcoMet78lxt6iw5gJOGSbNbOIVBHWVXIH4/Nq1+GQgLLGgI1KlnN86WDXsPudVaqYHKBIx7Eyw==",
|
||||
"version": "6.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz",
|
||||
"integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
|
|
@ -30,23 +31,22 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@codemirror/view": {
|
||||
"version": "6.38.6",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.6.tgz",
|
||||
"integrity": "sha512-qiS0z1bKs5WOvHIAC0Cybmv4AJSkAXgX5aD6Mqd2epSLlVJsQl8NG23jCVouIgkh4All/mrbdsf2UOLFnJw0tw==",
|
||||
"version": "6.36.4",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.36.4.tgz",
|
||||
"integrity": "sha512-ZQ0V5ovw/miKEXTvjgzRyjnrk9TwriUB1k4R5p7uNnHR9Hus+D1SXHGdJshijEzPFjU25xea/7nhIeSqYFKdbA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.5.0",
|
||||
"crelt": "^1.0.6",
|
||||
"style-mod": "^4.1.0",
|
||||
"w3c-keyname": "^2.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
|
||||
"integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz",
|
||||
"integrity": "sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
|
|
@ -61,9 +61,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
|
||||
"integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.0.tgz",
|
||||
"integrity": "sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
|
|
@ -78,9 +78,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
|
||||
"integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.0.tgz",
|
||||
"integrity": "sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
|
|
@ -95,9 +95,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
|
||||
"integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.0.tgz",
|
||||
"integrity": "sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
|
@ -112,9 +112,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
|
||||
"integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.0.tgz",
|
||||
"integrity": "sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
|
|
@ -129,9 +129,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
|
||||
"integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.0.tgz",
|
||||
"integrity": "sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
|
@ -146,9 +146,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
|
||||
"integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.0.tgz",
|
||||
"integrity": "sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
|
|
@ -163,9 +163,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
|
||||
"integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.0.tgz",
|
||||
"integrity": "sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
|
@ -180,9 +180,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
|
||||
"integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.0.tgz",
|
||||
"integrity": "sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
|
|
@ -197,9 +197,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
|
||||
"integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.0.tgz",
|
||||
"integrity": "sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
|
|
@ -214,9 +214,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
|
||||
"integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.0.tgz",
|
||||
"integrity": "sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
|
|
@ -231,9 +231,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
|
||||
"integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.0.tgz",
|
||||
"integrity": "sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
|
|
@ -248,9 +248,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
|
||||
"integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.0.tgz",
|
||||
"integrity": "sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
|
|
@ -265,9 +265,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
|
||||
"integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.0.tgz",
|
||||
"integrity": "sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
|
|
@ -282,9 +282,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
|
||||
"integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.0.tgz",
|
||||
"integrity": "sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
|
|
@ -299,9 +299,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
|
||||
"integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.0.tgz",
|
||||
"integrity": "sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
|
|
@ -316,9 +316,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
|
||||
"integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.0.tgz",
|
||||
"integrity": "sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
|
@ -333,9 +333,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-arm64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
|
||||
"integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.0.tgz",
|
||||
"integrity": "sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
|
|
@ -350,9 +350,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
|
||||
"integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.0.tgz",
|
||||
"integrity": "sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
|
@ -367,9 +367,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
|
||||
"integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.0.tgz",
|
||||
"integrity": "sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
|
|
@ -384,9 +384,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
|
||||
"integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.0.tgz",
|
||||
"integrity": "sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
|
@ -400,27 +400,10 @@
|
|||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openharmony-arm64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
|
||||
"integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openharmony"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
|
||||
"integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.0.tgz",
|
||||
"integrity": "sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
|
@ -435,9 +418,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
|
||||
"integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.0.tgz",
|
||||
"integrity": "sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
|
|
@ -452,9 +435,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
|
||||
"integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.0.tgz",
|
||||
"integrity": "sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
|
|
@ -469,9 +452,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
|
||||
"integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.0.tgz",
|
||||
"integrity": "sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
|
@ -486,9 +469,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@eslint-community/eslint-utils": {
|
||||
"version": "4.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
|
||||
"integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==",
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz",
|
||||
"integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
|
|
@ -506,9 +489,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@eslint-community/regexpp": {
|
||||
"version": "4.12.2",
|
||||
"resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
|
||||
"integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
|
||||
"version": "4.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz",
|
||||
"integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
|
|
@ -650,9 +633,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
|
||||
"integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
|
|
@ -881,9 +864,9 @@
|
|||
"peer": true
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.16.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
|
||||
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
|
||||
"version": "8.14.1",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
|
||||
"integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
|
|
@ -906,9 +889,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/ajv": {
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
|
||||
"integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
|
||||
"version": "6.12.6",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
|
|
@ -978,9 +961,9 @@
|
|||
"peer": true
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "1.1.13",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
|
||||
"integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
|
|
@ -1002,6 +985,19 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/builtin-modules": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz",
|
||||
"integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/callsites": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
||||
|
|
@ -1061,14 +1057,6 @@
|
|||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/crelt": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
|
||||
"integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
|
|
@ -1086,9 +1074,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
|
||||
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
|
@ -1139,9 +1127,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
|
||||
"integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz",
|
||||
"integrity": "sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
|
|
@ -1152,32 +1140,31 @@
|
|||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.25.12",
|
||||
"@esbuild/android-arm": "0.25.12",
|
||||
"@esbuild/android-arm64": "0.25.12",
|
||||
"@esbuild/android-x64": "0.25.12",
|
||||
"@esbuild/darwin-arm64": "0.25.12",
|
||||
"@esbuild/darwin-x64": "0.25.12",
|
||||
"@esbuild/freebsd-arm64": "0.25.12",
|
||||
"@esbuild/freebsd-x64": "0.25.12",
|
||||
"@esbuild/linux-arm": "0.25.12",
|
||||
"@esbuild/linux-arm64": "0.25.12",
|
||||
"@esbuild/linux-ia32": "0.25.12",
|
||||
"@esbuild/linux-loong64": "0.25.12",
|
||||
"@esbuild/linux-mips64el": "0.25.12",
|
||||
"@esbuild/linux-ppc64": "0.25.12",
|
||||
"@esbuild/linux-riscv64": "0.25.12",
|
||||
"@esbuild/linux-s390x": "0.25.12",
|
||||
"@esbuild/linux-x64": "0.25.12",
|
||||
"@esbuild/netbsd-arm64": "0.25.12",
|
||||
"@esbuild/netbsd-x64": "0.25.12",
|
||||
"@esbuild/openbsd-arm64": "0.25.12",
|
||||
"@esbuild/openbsd-x64": "0.25.12",
|
||||
"@esbuild/openharmony-arm64": "0.25.12",
|
||||
"@esbuild/sunos-x64": "0.25.12",
|
||||
"@esbuild/win32-arm64": "0.25.12",
|
||||
"@esbuild/win32-ia32": "0.25.12",
|
||||
"@esbuild/win32-x64": "0.25.12"
|
||||
"@esbuild/aix-ppc64": "0.25.0",
|
||||
"@esbuild/android-arm": "0.25.0",
|
||||
"@esbuild/android-arm64": "0.25.0",
|
||||
"@esbuild/android-x64": "0.25.0",
|
||||
"@esbuild/darwin-arm64": "0.25.0",
|
||||
"@esbuild/darwin-x64": "0.25.0",
|
||||
"@esbuild/freebsd-arm64": "0.25.0",
|
||||
"@esbuild/freebsd-x64": "0.25.0",
|
||||
"@esbuild/linux-arm": "0.25.0",
|
||||
"@esbuild/linux-arm64": "0.25.0",
|
||||
"@esbuild/linux-ia32": "0.25.0",
|
||||
"@esbuild/linux-loong64": "0.25.0",
|
||||
"@esbuild/linux-mips64el": "0.25.0",
|
||||
"@esbuild/linux-ppc64": "0.25.0",
|
||||
"@esbuild/linux-riscv64": "0.25.0",
|
||||
"@esbuild/linux-s390x": "0.25.0",
|
||||
"@esbuild/linux-x64": "0.25.0",
|
||||
"@esbuild/netbsd-arm64": "0.25.0",
|
||||
"@esbuild/netbsd-x64": "0.25.0",
|
||||
"@esbuild/openbsd-arm64": "0.25.0",
|
||||
"@esbuild/openbsd-x64": "0.25.0",
|
||||
"@esbuild/sunos-x64": "0.25.0",
|
||||
"@esbuild/win32-arm64": "0.25.0",
|
||||
"@esbuild/win32-ia32": "0.25.0",
|
||||
"@esbuild/win32-x64": "0.25.0"
|
||||
}
|
||||
},
|
||||
"node_modules/escape-string-regexp": {
|
||||
|
|
@ -1357,9 +1344,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/esquery": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
|
||||
"integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==",
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz",
|
||||
"integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"peer": true,
|
||||
|
|
@ -1480,9 +1467,9 @@
|
|||
"peer": true
|
||||
},
|
||||
"node_modules/fastq": {
|
||||
"version": "1.20.1",
|
||||
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz",
|
||||
"integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==",
|
||||
"version": "1.19.1",
|
||||
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",
|
||||
"integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
|
|
@ -1551,9 +1538,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/flatted": {
|
||||
"version": "3.4.2",
|
||||
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
|
||||
"integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==",
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
|
||||
"integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"peer": true
|
||||
|
|
@ -1577,7 +1564,7 @@
|
|||
"version": "7.2.3",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
||||
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
|
||||
"deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
|
||||
"deprecated": "Glob versions prior to v9 are no longer supported",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"peer": true,
|
||||
|
|
@ -1780,9 +1767,9 @@
|
|||
"peer": true
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
|
||||
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
||||
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
|
|
@ -1893,9 +1880,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
|
||||
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"peer": true,
|
||||
|
|
@ -1932,9 +1919,9 @@
|
|||
"peer": true
|
||||
},
|
||||
"node_modules/obsidian": {
|
||||
"version": "1.12.3",
|
||||
"resolved": "https://registry.npmjs.org/obsidian/-/obsidian-1.12.3.tgz",
|
||||
"integrity": "sha512-HxWqe763dOqzXjnNiHmAJTRERN8KILBSqxDSEqbeSr7W8R8Jxezzbca+nz1LiiqXnMpM8lV2jzAezw3CZ4xNUw==",
|
||||
"version": "1.8.7",
|
||||
"resolved": "https://registry.npmjs.org/obsidian/-/obsidian-1.8.7.tgz",
|
||||
"integrity": "sha512-h4bWwNFAGRXlMlMAzdEiIM2ppTGlrh7uGOJS6w4gClrsjc+ei/3YAtU2VdFUlCiPuTHpY4aBpFJJW75S1Tl/JA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
|
@ -1942,8 +1929,8 @@
|
|||
"moment": "2.29.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@codemirror/state": "6.5.0",
|
||||
"@codemirror/view": "6.38.6"
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/once": {
|
||||
|
|
@ -2068,9 +2055,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
|
||||
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
|
|
@ -2201,9 +2188,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "7.7.4",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
|
||||
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
|
||||
"version": "7.7.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
|
||||
"integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
|
|
@ -2277,9 +2264,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/style-mod": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz",
|
||||
"integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==",
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz",
|
||||
"integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "simple-archiver",
|
||||
"version": "0.6.2",
|
||||
"version": "0.5.2",
|
||||
"description": "Send old, stinky notes to the archive, where they belong.",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
|
|
@ -17,6 +17,7 @@
|
|||
"@types/node": "^16.11.6",
|
||||
"@typescript-eslint/eslint-plugin": "5.29.0",
|
||||
"@typescript-eslint/parser": "5.29.0",
|
||||
"builtin-modules": "3.3.0",
|
||||
"esbuild": "^0.25.0",
|
||||
"obsidian": "latest",
|
||||
"tslib": "2.4.0",
|
||||
|
|
|
|||
93
styles.css
93
styles.css
|
|
@ -6,96 +6,3 @@ available in the app when your plugin is enabled.
|
|||
If your plugin does not need CSS, delete this file.
|
||||
|
||||
*/
|
||||
|
||||
/* Auto-archive settings styles */
|
||||
.setting-tab-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.setting-tab-content {
|
||||
padding-top: 12px;
|
||||
}
|
||||
|
||||
.setting-tab-buttons {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 20px;
|
||||
border-bottom: 1px solid var(--background-modifier-border);
|
||||
}
|
||||
|
||||
.setting-tab-button {
|
||||
padding: 8px 16px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-bottom: 2px solid transparent;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.setting-tab-button.active {
|
||||
color: var(--text-normal);
|
||||
border-bottom-color: var(--interactive-accent);
|
||||
}
|
||||
|
||||
.setting-tab-button:hover {
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
.auto-archive-rules-container {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.auto-archive-rule {
|
||||
padding: 16px;
|
||||
margin-bottom: 16px;
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: 4px;
|
||||
background-color: var(--background-secondary);
|
||||
}
|
||||
|
||||
.auto-archive-rule-header {
|
||||
border-bottom: 1px solid var(--background-modifier-border);
|
||||
padding-bottom: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.auto-archive-rule-conditions {
|
||||
padding-left: 16px;
|
||||
font-size: 0.9em;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.auto-archive-rule-logic {
|
||||
margin-top: 4px;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 600;
|
||||
color: var(--text-accent);
|
||||
}
|
||||
|
||||
.auto-archive-rule-condition {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.auto-archive-conditions-container {
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.auto-archive-condition {
|
||||
margin-bottom: 12px;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: 4px;
|
||||
background-color: var(--background-primary);
|
||||
}
|
||||
|
||||
.auto-archive-rule-validation-error {
|
||||
display: none;
|
||||
color: var(--text-error);
|
||||
font-weight: 600;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.auto-archive-rule-validation-error.is-visible {
|
||||
display: block;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue