mirror of
https://github.com/mfarr/obsidian-archive.git
synced 2026-07-22 12:10:27 +00:00
Compare commits
19 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a7428dcbea | ||
|
|
bd5494721e | ||
|
|
36b85192b5 | ||
|
|
f0a66bf194 | ||
|
|
22e54c1381 | ||
|
|
1791d59567 | ||
|
|
92085da3a7 | ||
|
|
7fd371c2e3 | ||
|
|
aac53c74a3 | ||
|
|
e0af8a11a9 | ||
|
|
2d0e15754d | ||
|
|
1f3fa3a026 | ||
|
|
90dd0c9ce8 | ||
|
|
a51cca64ec | ||
|
|
293f83b41a | ||
|
|
76d3d74748 | ||
|
|
beeeaaa7d7 | ||
|
|
6d0061ad49 | ||
|
|
d16c56bd1a |
21 changed files with 2630 additions and 337 deletions
|
|
@ -1,10 +1,15 @@
|
|||
# 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
|
||||
# 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
|
||||
|
|
|
|||
43
.github/workflows/claude-code-review.yml
vendored
Normal file
43
.github/workflows/claude-code-review.yml
vendored
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
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
Normal file
50
.github/workflows/claude.yml
vendored
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
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,35 +1,48 @@
|
|||
name: Release Obsidian plugin
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "*"
|
||||
push:
|
||||
tags:
|
||||
- "*"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
attestations: write
|
||||
artifact-metadata: write
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "18.x"
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "24"
|
||||
cache: "npm"
|
||||
|
||||
- name: Build plugin
|
||||
run: |
|
||||
npm install
|
||||
npm run build
|
||||
- name: Build plugin
|
||||
run: |
|
||||
npm install
|
||||
npm run build
|
||||
|
||||
- name: Create release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
tag="${GITHUB_REF#refs/tags/}"
|
||||
- name: Attest
|
||||
uses: actions/attest@v4
|
||||
with:
|
||||
subject-path: |
|
||||
main.js
|
||||
manifest.json
|
||||
styles.css
|
||||
|
||||
gh release create "$tag" \
|
||||
--title="$tag" \
|
||||
--draft \
|
||||
main.js manifest.json styles.css
|
||||
- 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
|
||||
|
|
|
|||
47
.gitignore
vendored
47
.gitignore
vendored
|
|
@ -1,22 +1,25 @@
|
|||
# vscode
|
||||
.vscode
|
||||
|
||||
# Intellij
|
||||
*.iml
|
||||
.idea
|
||||
|
||||
# npm
|
||||
node_modules
|
||||
|
||||
# Don't include the compiled main.js file in the repo.
|
||||
# They should be uploaded to GitHub releases instead.
|
||||
main.js
|
||||
|
||||
# Exclude sourcemaps
|
||||
*.map
|
||||
|
||||
# obsidian
|
||||
data.json
|
||||
|
||||
# Exclude macOS Finder (System Explorer) View States
|
||||
.DS_Store
|
||||
# vscode
|
||||
.vscode
|
||||
|
||||
# Intellij
|
||||
*.iml
|
||||
.idea
|
||||
|
||||
# npm
|
||||
node_modules
|
||||
|
||||
# Don't include the compiled main.js file in the repo.
|
||||
# They should be uploaded to GitHub releases instead.
|
||||
main.js
|
||||
|
||||
# Exclude sourcemaps
|
||||
*.map
|
||||
|
||||
# obsidian
|
||||
data.json
|
||||
|
||||
# Exclude macOS Finder (System Explorer) View States
|
||||
.DS_Store
|
||||
|
||||
# Exclude pnpm lock file, using package-lock.json as ground-truth
|
||||
pnpm-lock.yaml
|
||||
|
|
|
|||
57
CLAUDE.md
Normal file
57
CLAUDE.md
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
# 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
|
||||
100
README.md
100
README.md
|
|
@ -2,20 +2,102 @@
|
|||
|
||||
> _Move old, stinky notes and folders to an archive, where they belong!_
|
||||
|
||||
Simple Archiver moves files or an entire folder to an archive folder that you specify in the plugin's settings. The items are moved to the same relative path in the archive. If the base archive folder doesn't exist when you send something to the archive, it will be created automatically.
|
||||
Simple Archiver moves files or an entire folder to an archive folder that you configure. The items are moved to the same relative path in the archive folder. Conversely, items that have been archived can be moved out of the archive to their original location.
|
||||
|
||||
Archiving can be done via:
|
||||
|
||||
- `Simple Archive: Move to archive` command
|
||||
- `Move to archive` file menu item
|
||||
- `Move all to archive` multi-file menu item
|
||||
- `Simple Archive: Move to archive` command
|
||||
- `Move to archive` file menu item
|
||||
- `Move all to archive` multi-file menu item
|
||||
|
||||
## Known Issues & Limitations
|
||||
Unarchiving can be done via:
|
||||
|
||||
- Archiving a file or folder that already exists at the same path in the archive will fail
|
||||
- Archive folder setting value is not checked for validity
|
||||
- `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 archive merges the contents
|
||||
- Unarchive
|
||||
- Archiving a folder that already exists in the archive merges the contents
|
||||
- Archiving a file that already exists in the archive gives the option to rename
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions to this repo are welcome, **but**, please reach out by submitting an issue before submitting a massive PR for a feature that I might not be interested in maintaining.
|
||||
|
||||
### AI Contributions
|
||||
|
||||
AI-assisted contributions will be considered with discretion. Obviously vibe-coded contributions will receive little of my time and less of my patience.
|
||||
|
||||
### Contributors
|
||||
|
||||
- [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)!
|
||||
|
||||
### v0.5.1
|
||||
|
||||
- **New**: Add `Move out of archive` functionality to files/folders that exist in the archive. Items will be moved out of the archive into their original location (issue #5). Thank you to [nicholaslck](https://github.com/nicholaslck)!
|
||||
|
||||
### v0.4.0
|
||||
|
||||
- **New**: Add "replace" option when attempting to archive a file or folder when an item with the same name and path already exists in the archive.
|
||||
|
||||
### v0.3.1
|
||||
|
||||
- **Fix**: Unable to archive files/folders in the vault root
|
||||
|
||||
### v0.3.1
|
||||
|
||||
- **New**: Validate archive folder name setting before saving
|
||||
|
||||
### v0.2.0
|
||||
|
||||
- **New**: Allow multiple files to be archived
|
||||
|
||||
### v0.1.0
|
||||
|
||||
- **New**: Basic archive functionality
|
||||
|
|
|
|||
316
SettingsTab.ts
Normal file
316
SettingsTab.ts
Normal file
|
|
@ -0,0 +1,316 @@
|
|||
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();
|
||||
}
|
||||
}
|
||||
22
autoarchive/AutoArchiveContextMenu.ts
Normal file
22
autoarchive/AutoArchiveContextMenu.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
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);
|
||||
}
|
||||
61
autoarchive/AutoArchiveLifecycle.ts
Normal file
61
autoarchive/AutoArchiveLifecycle.ts
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
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();
|
||||
}
|
||||
611
autoarchive/AutoArchiveService.ts
Normal file
611
autoarchive/AutoArchiveService.ts
Normal file
|
|
@ -0,0 +1,611 @@
|
|||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
96
autoarchive/AutoArchiveSettings.ts
Normal file
96
autoarchive/AutoArchiveSettings.ts
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
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,
|
||||
};
|
||||
}
|
||||
42
autoarchive/AutoArchiveTypes.ts
Normal file
42
autoarchive/AutoArchiveTypes.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
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[];
|
||||
};
|
||||
}
|
||||
31
autoarchive/index.ts
Normal file
31
autoarchive/index.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
/**
|
||||
* 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 builtins from "builtin-modules";
|
||||
import { builtinModules } from "module";
|
||||
|
||||
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",
|
||||
...builtins,
|
||||
...builtinModules,
|
||||
],
|
||||
format: "cjs",
|
||||
target: "es2018",
|
||||
|
|
|
|||
529
main.ts
529
main.ts
|
|
@ -1,25 +1,40 @@
|
|||
import {
|
||||
App,
|
||||
Editor,
|
||||
MarkdownView,
|
||||
normalizePath,
|
||||
Notice,
|
||||
Plugin,
|
||||
PluginSettingTab,
|
||||
Setting,
|
||||
TAbstractFile,
|
||||
} from "obsidian";
|
||||
|
||||
interface SimpleArchiverSettings {
|
||||
archiveFolder: string;
|
||||
}
|
||||
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";
|
||||
|
||||
const AUTO_ARCHIVE_DATA_FILE = "autoarchive_data.json";
|
||||
|
||||
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();
|
||||
|
|
@ -29,16 +44,24 @@ 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?.path.startsWith(
|
||||
this.settings.archiveFolder
|
||||
);
|
||||
const canBeArchived =
|
||||
view.file && !this.isFileArchived(view.file);
|
||||
|
||||
if (canBeArchived && view.file != null) {
|
||||
if (!checking) {
|
||||
this.moveToArchive(view.file);
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
|
|
@ -48,11 +71,74 @@ export default class SimpleArchiver extends Plugin {
|
|||
},
|
||||
});
|
||||
|
||||
this.addSettingTab(new SimpleArchiverSettingsTab(this.app, this));
|
||||
this.addCommand({
|
||||
id: "move-out-of-archive",
|
||||
name: "Move out of archive",
|
||||
editorCheckCallback: (
|
||||
checking: boolean,
|
||||
_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);
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
});
|
||||
|
||||
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,
|
||||
);
|
||||
|
||||
// Archive file context menu
|
||||
this.registerEvent(
|
||||
this.app.workspace.on("file-menu", (menu, file) => {
|
||||
if (file.path.startsWith(this.settings.archiveFolder)) {
|
||||
if (this.isFileArchived(file)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -60,45 +146,158 @@ export default class SimpleArchiver extends Plugin {
|
|||
item.setTitle("Move to archive")
|
||||
.setIcon("archive")
|
||||
.onClick(async () => {
|
||||
if (await this.moveToArchive(file)) {
|
||||
new Notice(`${file.name} archived`);
|
||||
const result = await this.archiveFile(file);
|
||||
|
||||
if (result.success) {
|
||||
new Notice(result.message);
|
||||
} else {
|
||||
new Notice(`Error: ${result.message}`);
|
||||
console.error(result.message);
|
||||
}
|
||||
});
|
||||
});
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
this.registerEvent(
|
||||
this.app.workspace.on("files-menu", (menu, files) => {
|
||||
if (files.some((file) => this.isFileArchived(file))) {
|
||||
return;
|
||||
}
|
||||
|
||||
menu.addItem((item) => {
|
||||
item.setTitle("Move all to archive")
|
||||
.setIcon("archive")
|
||||
.onClick(async () => {
|
||||
await this.moveAllToArchive(files);
|
||||
await this.archiveAllFiles(files);
|
||||
});
|
||||
});
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
// Unarchive file context menu
|
||||
this.registerEvent(
|
||||
this.app.workspace.on("file-menu", (menu, file) => {
|
||||
if (!this.isFileArchived(file)) {
|
||||
return;
|
||||
}
|
||||
|
||||
menu.addItem((item) => {
|
||||
item.setTitle("Move out of archive")
|
||||
.setIcon("archive-restore")
|
||||
.onClick(async () => {
|
||||
const result = await this.unarchiveFile(file);
|
||||
|
||||
if (result.success) {
|
||||
new Notice(result.message);
|
||||
} else {
|
||||
new Notice(`Error: ${result.message}`);
|
||||
console.error(result.message);
|
||||
}
|
||||
});
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
this.registerEvent(
|
||||
this.app.workspace.on("files-menu", (menu, files) => {
|
||||
if (files.some((file) => !this.isFileArchived(file))) {
|
||||
return;
|
||||
}
|
||||
|
||||
menu.addItem((item) => {
|
||||
item.setTitle("Move all out of archive")
|
||||
.setIcon("archive-restore")
|
||||
.onClick(async () => {
|
||||
await this.unarchiveAllFiles(files);
|
||||
});
|
||||
});
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private async moveToArchive(file: TAbstractFile): Promise<boolean> {
|
||||
if (file.path.startsWith(this.settings.archiveFolder)) {
|
||||
return false;
|
||||
private isFileArchived(file: TAbstractFile): boolean {
|
||||
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;
|
||||
}
|
||||
|
||||
const existingItem = this.app.vault.getAbstractFileByPath(
|
||||
`${this.settings.archiveFolder}/${file.path}`
|
||||
// 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}`,
|
||||
);
|
||||
|
||||
if (existingItem != null) {
|
||||
new Notice(
|
||||
`Unable to archive ${file.name}, item already exists in archive`
|
||||
);
|
||||
const existingItem =
|
||||
this.app.vault.getAbstractFileByPath(destinationFilePath);
|
||||
|
||||
return false;
|
||||
if (existingItem != null) {
|
||||
// Same item exists in archive, prompt to replace
|
||||
return new Promise<ArchiveResult>((resolve) => {
|
||||
const prompt = new SimpleArchiverPromptModal(
|
||||
this.app,
|
||||
"Replace archived item?",
|
||||
`An item called "${file.name}" already exists in the destination folder in the archive. Would you like to replace it?`,
|
||||
"Replace",
|
||||
"Cancel",
|
||||
async () => {
|
||||
await this.app.fileManager.trashFile(existingItem);
|
||||
const response = await this.moveFileToArchive(file);
|
||||
|
||||
resolve(response);
|
||||
},
|
||||
async () => {
|
||||
resolve({
|
||||
success: false,
|
||||
message: "Archive operation cancelled",
|
||||
});
|
||||
},
|
||||
);
|
||||
prompt.open();
|
||||
});
|
||||
}
|
||||
|
||||
const destinationPath = `${this.settings.archiveFolder}/${file.parent?.path}`;
|
||||
// If no existing item, proceed with archiving
|
||||
const response = await this.moveFileToArchive(file);
|
||||
return response;
|
||||
}
|
||||
|
||||
private async archiveAllFiles(files: TAbstractFile[]) {
|
||||
let archived = 0;
|
||||
|
||||
for (const file of files) {
|
||||
if ((await this.archiveFile(file)).success) {
|
||||
archived++;
|
||||
}
|
||||
}
|
||||
|
||||
new Notice(`${archived} files archived`);
|
||||
}
|
||||
|
||||
private async moveFileToArchive(
|
||||
file: TAbstractFile,
|
||||
): Promise<ArchiveResult> {
|
||||
const destinationPath = normalizePath(
|
||||
`${this.settings.archiveFolder}/${file.parent?.path}`,
|
||||
);
|
||||
|
||||
const destinationFolder =
|
||||
this.app.vault.getFolderByPath(destinationPath);
|
||||
|
|
@ -107,83 +306,239 @@ export default class SimpleArchiver extends Plugin {
|
|||
await this.app.vault.createFolder(destinationPath);
|
||||
}
|
||||
|
||||
await this.app.fileManager.renameFile(
|
||||
file,
|
||||
`${this.settings.archiveFolder}/${file.path}`
|
||||
const destinationFilePath = normalizePath(
|
||||
`${destinationPath}/${file.name}`,
|
||||
);
|
||||
|
||||
return true;
|
||||
try {
|
||||
await this.app.fileManager.renameFile(file, destinationFilePath);
|
||||
return {
|
||||
success: true,
|
||||
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}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private async moveAllToArchive(files: TAbstractFile[]) {
|
||||
let archived = 0;
|
||||
private async moveFileOutOfArchive(
|
||||
file: TAbstractFile,
|
||||
): Promise<ArchiveResult> {
|
||||
const originalPath = this.resolveOriginalPath(file.path);
|
||||
|
||||
for (const file of files) {
|
||||
if (await this.moveToArchive(file)) {
|
||||
archived++;
|
||||
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;
|
||||
|
||||
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}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
new Notice(`${archived} files archived`);
|
||||
try {
|
||||
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}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private async unarchiveFile(file: TAbstractFile): Promise<ArchiveResult> {
|
||||
if (!this.isFileArchived(file)) {
|
||||
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 existingItem = this.app.vault.getAbstractFileByPath(originalPath);
|
||||
|
||||
if (existingItem != null) {
|
||||
return new Promise<ArchiveResult>((resolve) => {
|
||||
const prompt = new SimpleArchiverPromptModal(
|
||||
this.app,
|
||||
"Replace existing item?",
|
||||
`An item called "${file.name}" already exists in the original location. Would you like to replace it?`,
|
||||
"Replace",
|
||||
"Cancel",
|
||||
async () => {
|
||||
await this.app.fileManager.trashFile(existingItem);
|
||||
const response = await this.moveFileOutOfArchive(file);
|
||||
|
||||
resolve(response);
|
||||
},
|
||||
async () => {
|
||||
resolve({
|
||||
success: false,
|
||||
message: "Unarchive operation cancelled",
|
||||
});
|
||||
},
|
||||
);
|
||||
prompt.open();
|
||||
});
|
||||
}
|
||||
|
||||
const response = await this.moveFileOutOfArchive(file);
|
||||
return response;
|
||||
}
|
||||
|
||||
private async unarchiveAllFiles(files: TAbstractFile[]) {
|
||||
let unarchived = 0;
|
||||
|
||||
for (const file of files) {
|
||||
if ((await this.unarchiveFile(file)).success) {
|
||||
unarchived++;
|
||||
}
|
||||
}
|
||||
|
||||
new Notice(`${unarchived} files unarchived`);
|
||||
}
|
||||
|
||||
onunload() {
|
||||
stopAutoArchiveScheduler(this.autoArchiveService);
|
||||
}
|
||||
|
||||
private async loadSettings() {
|
||||
this.settings = Object.assign(
|
||||
{},
|
||||
DEFAULT_SETTINGS,
|
||||
await this.loadData()
|
||||
);
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
async saveSettings() {
|
||||
await this.saveData(this.settings);
|
||||
}
|
||||
}
|
||||
|
||||
class SimpleArchiverSettingsTab extends PluginSettingTab {
|
||||
plugin: SimpleArchiver;
|
||||
|
||||
constructor(app: App, plugin: SimpleArchiver) {
|
||||
super(app, plugin);
|
||||
this.plugin = plugin;
|
||||
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),
|
||||
);
|
||||
}
|
||||
|
||||
display(): void {
|
||||
const { containerEl } = this;
|
||||
private async loadAutoArchiveRuntimeData(
|
||||
legacyLastAutoArchiveRunAt: number | null,
|
||||
): Promise<AutoArchiveRuntimeData> {
|
||||
const existingRuntimeData = await this.readAutoArchiveRuntimeDataFile();
|
||||
|
||||
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;
|
||||
if (existingRuntimeData !== null) {
|
||||
return existingRuntimeData;
|
||||
}
|
||||
|
||||
this.plugin.settings.archiveFolder = value;
|
||||
return true;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "simple-archiver",
|
||||
"name": "Simple Archiver",
|
||||
"version": "0.3.0",
|
||||
"version": "0.6.2",
|
||||
"minAppVersion": "1.8.0",
|
||||
"description": "Move old, stinky notes and folders to an archive, where they belong.",
|
||||
"author": "Mike Farr",
|
||||
|
|
|
|||
401
modals.ts
Normal file
401
modals.ts
Normal file
|
|
@ -0,0 +1,401 @@
|
|||
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();
|
||||
}
|
||||
}
|
||||
369
package-lock.json
generated
369
package-lock.json
generated
|
|
@ -1,18 +1,17 @@
|
|||
{
|
||||
"name": "obsidian-simple-archiver",
|
||||
"version": "0.0.1",
|
||||
"name": "simple-archiver",
|
||||
"version": "0.6.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "obsidian-simple-archiver",
|
||||
"version": "0.0.1",
|
||||
"name": "simple-archiver",
|
||||
"version": "0.6.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",
|
||||
|
|
@ -20,9 +19,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@codemirror/state": {
|
||||
"version": "6.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz",
|
||||
"integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==",
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.0.tgz",
|
||||
"integrity": "sha512-MwBHVK60IiIHDcoMet78lxt6iw5gJOGSbNbOIVBHWVXIH4/Nq1+GQgLLGgI1KlnN86WDXsPudVaqYHKBIx7Eyw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
|
|
@ -31,22 +30,23 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@codemirror/view": {
|
||||
"version": "6.36.4",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.36.4.tgz",
|
||||
"integrity": "sha512-ZQ0V5ovw/miKEXTvjgzRyjnrk9TwriUB1k4R5p7uNnHR9Hus+D1SXHGdJshijEzPFjU25xea/7nhIeSqYFKdbA==",
|
||||
"version": "6.38.6",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.6.tgz",
|
||||
"integrity": "sha512-qiS0z1bKs5WOvHIAC0Cybmv4AJSkAXgX5aD6Mqd2epSLlVJsQl8NG23jCVouIgkh4All/mrbdsf2UOLFnJw0tw==",
|
||||
"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.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz",
|
||||
"integrity": "sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ==",
|
||||
"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==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
|
|
@ -61,9 +61,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"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==",
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
|
||||
"integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
|
|
@ -78,9 +78,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.0.tgz",
|
||||
"integrity": "sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g==",
|
||||
"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==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
|
|
@ -95,9 +95,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"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==",
|
||||
"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==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
|
@ -112,9 +112,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.0.tgz",
|
||||
"integrity": "sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw==",
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
|
||||
"integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
|
|
@ -129,9 +129,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.0.tgz",
|
||||
"integrity": "sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg==",
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
|
||||
"integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
|
@ -146,9 +146,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.0.tgz",
|
||||
"integrity": "sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w==",
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
|
||||
"integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
|
|
@ -163,9 +163,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.0.tgz",
|
||||
"integrity": "sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A==",
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
|
||||
"integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
|
@ -180,9 +180,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.0.tgz",
|
||||
"integrity": "sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg==",
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
|
||||
"integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
|
|
@ -197,9 +197,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.0.tgz",
|
||||
"integrity": "sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg==",
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
|
||||
"integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
|
|
@ -214,9 +214,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.0.tgz",
|
||||
"integrity": "sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg==",
|
||||
"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==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
|
|
@ -231,9 +231,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.0.tgz",
|
||||
"integrity": "sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw==",
|
||||
"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==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
|
|
@ -248,9 +248,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.0.tgz",
|
||||
"integrity": "sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ==",
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
|
||||
"integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
|
|
@ -265,9 +265,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"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==",
|
||||
"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==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
|
|
@ -282,9 +282,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"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==",
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
|
||||
"integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
|
|
@ -299,9 +299,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.0.tgz",
|
||||
"integrity": "sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA==",
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
|
||||
"integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
|
|
@ -316,9 +316,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"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==",
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
|
||||
"integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
|
@ -333,9 +333,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-arm64": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.0.tgz",
|
||||
"integrity": "sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw==",
|
||||
"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==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
|
|
@ -350,9 +350,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.0.tgz",
|
||||
"integrity": "sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA==",
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
|
||||
"integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
|
@ -367,9 +367,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.0.tgz",
|
||||
"integrity": "sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw==",
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
|
||||
"integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
|
|
@ -384,9 +384,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.0.tgz",
|
||||
"integrity": "sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg==",
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
|
||||
"integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
|
@ -400,10 +400,27 @@
|
|||
"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.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.0.tgz",
|
||||
"integrity": "sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg==",
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
|
||||
"integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
|
@ -418,9 +435,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.0.tgz",
|
||||
"integrity": "sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw==",
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
|
||||
"integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
|
|
@ -435,9 +452,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"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==",
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
|
||||
"integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
|
|
@ -452,9 +469,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"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==",
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
|
||||
"integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
|
@ -469,9 +486,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@eslint-community/eslint-utils": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz",
|
||||
"integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==",
|
||||
"version": "4.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
|
||||
"integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
|
|
@ -489,9 +506,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@eslint-community/regexpp": {
|
||||
"version": "4.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz",
|
||||
"integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==",
|
||||
"version": "4.12.2",
|
||||
"resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
|
||||
"integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
|
|
@ -633,9 +650,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
|
||||
"integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
|
|
@ -864,9 +881,9 @@
|
|||
"peer": true
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.14.1",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
|
||||
"integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==",
|
||||
"version": "8.16.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
|
||||
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
|
|
@ -889,9 +906,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/ajv": {
|
||||
"version": "6.12.6",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
|
||||
"integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
|
|
@ -961,9 +978,9 @@
|
|||
"peer": true
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"version": "1.1.13",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
|
||||
"integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
|
|
@ -985,19 +1002,6 @@
|
|||
"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",
|
||||
|
|
@ -1057,6 +1061,14 @@
|
|||
"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",
|
||||
|
|
@ -1074,9 +1086,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
|
||||
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
|
@ -1127,9 +1139,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz",
|
||||
"integrity": "sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==",
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
|
||||
"integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
|
|
@ -1140,31 +1152,32 @@
|
|||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@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"
|
||||
"@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"
|
||||
}
|
||||
},
|
||||
"node_modules/escape-string-regexp": {
|
||||
|
|
@ -1344,9 +1357,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/esquery": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz",
|
||||
"integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==",
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
|
||||
"integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"peer": true,
|
||||
|
|
@ -1467,9 +1480,9 @@
|
|||
"peer": true
|
||||
},
|
||||
"node_modules/fastq": {
|
||||
"version": "1.19.1",
|
||||
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",
|
||||
"integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==",
|
||||
"version": "1.20.1",
|
||||
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz",
|
||||
"integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
|
|
@ -1538,9 +1551,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/flatted": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
|
||||
"integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
|
||||
"version": "3.4.2",
|
||||
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
|
||||
"integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"peer": true
|
||||
|
|
@ -1564,7 +1577,7 @@
|
|||
"version": "7.2.3",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
||||
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
|
||||
"deprecated": "Glob versions prior to v9 are no longer supported",
|
||||
"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",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"peer": true,
|
||||
|
|
@ -1767,9 +1780,9 @@
|
|||
"peer": true
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
||||
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
|
||||
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
|
|
@ -1880,9 +1893,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
|
||||
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"peer": true,
|
||||
|
|
@ -1919,9 +1932,9 @@
|
|||
"peer": true
|
||||
},
|
||||
"node_modules/obsidian": {
|
||||
"version": "1.8.7",
|
||||
"resolved": "https://registry.npmjs.org/obsidian/-/obsidian-1.8.7.tgz",
|
||||
"integrity": "sha512-h4bWwNFAGRXlMlMAzdEiIM2ppTGlrh7uGOJS6w4gClrsjc+ei/3YAtU2VdFUlCiPuTHpY4aBpFJJW75S1Tl/JA==",
|
||||
"version": "1.12.3",
|
||||
"resolved": "https://registry.npmjs.org/obsidian/-/obsidian-1.12.3.tgz",
|
||||
"integrity": "sha512-HxWqe763dOqzXjnNiHmAJTRERN8KILBSqxDSEqbeSr7W8R8Jxezzbca+nz1LiiqXnMpM8lV2jzAezw3CZ4xNUw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
|
@ -1929,8 +1942,8 @@
|
|||
"moment": "2.29.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.0.0"
|
||||
"@codemirror/state": "6.5.0",
|
||||
"@codemirror/view": "6.38.6"
|
||||
}
|
||||
},
|
||||
"node_modules/once": {
|
||||
|
|
@ -2055,9 +2068,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
|
||||
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
|
|
@ -2188,9 +2201,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "7.7.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
|
||||
"integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
|
||||
"version": "7.7.4",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
|
||||
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
|
|
@ -2264,9 +2277,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/style-mod": {
|
||||
"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==",
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz",
|
||||
"integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "simple-archiver",
|
||||
"version": "0.3.0",
|
||||
"version": "0.6.2",
|
||||
"description": "Send old, stinky notes to the archive, where they belong.",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
|
|
@ -17,7 +17,6 @@
|
|||
"@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,3 +6,96 @@ 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