mirror of
https://github.com/quorafind/Obsidian-Daily-Notes-Editor.git
synced 2026-07-22 07:00:27 +00:00
feat: support tag or folder flow editor
This commit is contained in:
parent
89073a5306
commit
2eb78f227c
6 changed files with 784 additions and 187 deletions
|
|
@ -5,149 +5,68 @@
|
|||
import { TFile, moment } from "obsidian";
|
||||
import DailyNote from "./DailyNote.svelte";
|
||||
import { inview } from "svelte-inview";
|
||||
import {
|
||||
getAllDailyNotes,
|
||||
getDailyNote,
|
||||
createDailyNote,
|
||||
getDateFromFile,
|
||||
getDailyNoteSettings,
|
||||
DEFAULT_DAILY_NOTE_FORMAT
|
||||
} from 'obsidian-daily-notes-interface';
|
||||
import { TimeRange, SelectionMode, TimeField } from "../types/time";
|
||||
import { onMount } from "svelte";
|
||||
import { TimeRange } from "../types/time";
|
||||
import { FileManager, FileManagerOptions } from "../utils/fileManager";
|
||||
|
||||
|
||||
export let plugin: DailyNoteViewPlugin;
|
||||
export let leaf: WorkspaceLeaf;
|
||||
export let selectedRange: TimeRange = "all";
|
||||
export let customRange: { start: Date; end: Date } | null = null;
|
||||
export let selectionMode: SelectionMode = "daily";
|
||||
export let target: string = "";
|
||||
export let timeField: TimeField = "mtime"; // 默认使用修改时间
|
||||
|
||||
const size = 1;
|
||||
let intervalId;
|
||||
|
||||
let cacheDailyNotes: Record<string, any>;
|
||||
let allDailyNotes: TFile[] = [];
|
||||
let renderedDailyNotes: TFile[] = [];
|
||||
let filteredDailyNotes: TFile[] = [];
|
||||
let renderedFiles: TFile[] = [];
|
||||
let filteredFiles: TFile[] = [];
|
||||
|
||||
let hasMore = true;
|
||||
let hasFetch = false;
|
||||
let hasCurrentDay: boolean = true;
|
||||
|
||||
let firstLoaded = true;
|
||||
let loaderRef: HTMLDivElement;
|
||||
|
||||
$: if (hasMore && !hasFetch) {
|
||||
cacheDailyNotes = getAllDailyNotes();
|
||||
// Build notes list by date in descending order.
|
||||
for (const string of Object.keys(cacheDailyNotes).sort().reverse()) {
|
||||
allDailyNotes.push(<TFile>cacheDailyNotes[string]);
|
||||
}
|
||||
hasFetch = true;
|
||||
checkDailyNote();
|
||||
filterNotesByRange();
|
||||
}
|
||||
// Create the file manager
|
||||
let fileManager: FileManager;
|
||||
|
||||
$: fileManagerOptions = {
|
||||
mode: selectionMode,
|
||||
target: target,
|
||||
timeRange: selectedRange,
|
||||
customRange: customRange,
|
||||
app: plugin.app,
|
||||
timeField: timeField
|
||||
} as FileManagerOptions;
|
||||
|
||||
$: if (selectedRange && hasFetch) {
|
||||
filterNotesByRange();
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
checkDailyNote();
|
||||
startFillViewport();
|
||||
});
|
||||
|
||||
function filterNotesByRange() {
|
||||
const now = moment();
|
||||
const fileFormat = getDailyNoteSettings().format || DEFAULT_DAILY_NOTE_FORMAT;
|
||||
$: if (fileManager && (selectedRange !== fileManager.options.timeRange ||
|
||||
customRange !== fileManager.options.customRange ||
|
||||
selectionMode !== fileManager.options.mode ||
|
||||
target !== fileManager.options.target ||
|
||||
timeField !== fileManager.options.timeField)) {
|
||||
fileManager.updateOptions({
|
||||
timeRange: selectedRange,
|
||||
customRange: customRange,
|
||||
mode: selectionMode,
|
||||
target: target,
|
||||
timeField: timeField
|
||||
});
|
||||
|
||||
// Reset filtered notes
|
||||
filteredDailyNotes = [];
|
||||
|
||||
// Filter notes based on selected range
|
||||
if (selectedRange === 'all') {
|
||||
filteredDailyNotes = [...allDailyNotes];
|
||||
} else {
|
||||
filteredDailyNotes = allDailyNotes.filter(file => {
|
||||
const fileDate = moment(file.basename, fileFormat);
|
||||
|
||||
switch (selectedRange) {
|
||||
case 'week':
|
||||
return fileDate.isSame(now, 'week');
|
||||
case 'month':
|
||||
return fileDate.isSame(now, 'month');
|
||||
case 'year':
|
||||
return fileDate.isSame(now, 'year');
|
||||
case 'last-week':
|
||||
return fileDate.isBetween(
|
||||
moment().subtract(1, 'week').startOf('week'),
|
||||
moment().subtract(1, 'week').endOf('week'),
|
||||
null,
|
||||
'[]'
|
||||
);
|
||||
case 'last-month':
|
||||
return fileDate.isBetween(
|
||||
moment().subtract(1, 'month').startOf('month'),
|
||||
moment().subtract(1, 'month').endOf('month'),
|
||||
null,
|
||||
'[]'
|
||||
);
|
||||
case 'last-year':
|
||||
return fileDate.isBetween(
|
||||
moment().subtract(1, 'year').startOf('year'),
|
||||
moment().subtract(1, 'year').endOf('year'),
|
||||
null,
|
||||
'[]'
|
||||
);
|
||||
case 'quarter':
|
||||
return fileDate.isSame(now, 'quarter');
|
||||
case 'last-quarter':
|
||||
return fileDate.isBetween(
|
||||
moment().subtract(1, 'quarter').startOf('quarter'),
|
||||
moment().subtract(1, 'quarter').endOf('quarter'),
|
||||
null,
|
||||
'[]'
|
||||
);
|
||||
case 'custom':
|
||||
if (customRange) {
|
||||
const startDate = moment(customRange.start);
|
||||
const endDate = moment(customRange.end);
|
||||
return fileDate.isBetween(startDate, endDate, null, '[]');
|
||||
}
|
||||
return false;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Reset rendered notes and start filling viewport again
|
||||
renderedDailyNotes = [];
|
||||
hasMore = filteredDailyNotes.length > 0;
|
||||
// Reset rendered files and start filling viewport again
|
||||
renderedFiles = [];
|
||||
filteredFiles = fileManager.getFilteredFiles();
|
||||
hasMore = filteredFiles.length > 0;
|
||||
firstLoaded = true;
|
||||
startFillViewport();
|
||||
}
|
||||
|
||||
function checkDailyNote() {
|
||||
// @ts-ignore
|
||||
const currentDate = moment();
|
||||
const currentDailyNote = getDailyNote(currentDate, cacheDailyNotes);
|
||||
|
||||
// console.log(currentDate, cacheDailyNotes);
|
||||
if (!currentDailyNote) {
|
||||
hasCurrentDay = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function createNewDailyNote() {
|
||||
const currentDate = moment();
|
||||
if (!hasCurrentDay) {
|
||||
const currentDailyNote: any = await createDailyNote(currentDate);
|
||||
renderedDailyNotes.push(currentDailyNote);
|
||||
|
||||
renderedDailyNotes = sortDailyNotes(renderedDailyNotes);
|
||||
hasCurrentDay = true;
|
||||
}
|
||||
}
|
||||
onMount(() => {
|
||||
fileManager = new FileManager(fileManagerOptions);
|
||||
filteredFiles = fileManager.getFilteredFiles();
|
||||
hasMore = filteredFiles.length > 0;
|
||||
startFillViewport();
|
||||
});
|
||||
|
||||
function startFillViewport() {
|
||||
if (!intervalId) {
|
||||
|
|
@ -161,13 +80,13 @@
|
|||
}
|
||||
|
||||
function infiniteHandler() {
|
||||
if (!hasFetch || !hasMore) return;
|
||||
if (filteredDailyNotes.length === 0 && hasFetch) {
|
||||
if (!fileManager || !hasMore) return;
|
||||
if (filteredFiles.length === 0) {
|
||||
hasMore = false;
|
||||
} else {
|
||||
renderedDailyNotes = [
|
||||
...renderedDailyNotes,
|
||||
...filteredDailyNotes.splice(0, size)
|
||||
renderedFiles = [
|
||||
...renderedFiles,
|
||||
...filteredFiles.splice(0, size)
|
||||
];
|
||||
if (firstLoaded) {
|
||||
window.setTimeout(() => {
|
||||
|
|
@ -184,71 +103,51 @@
|
|||
}
|
||||
}
|
||||
|
||||
async function createNewDailyNote() {
|
||||
const newNote = await fileManager.createNewDailyNote();
|
||||
if (newNote) {
|
||||
renderedFiles = [newNote, ...renderedFiles];
|
||||
}
|
||||
}
|
||||
|
||||
export function tick() {
|
||||
renderedDailyNotes = renderedDailyNotes;
|
||||
renderedFiles = renderedFiles;
|
||||
}
|
||||
|
||||
export function check() {
|
||||
checkDailyNote();
|
||||
}
|
||||
|
||||
function sortDailyNotes(notes: TFile[]): TFile[] {
|
||||
const fileFormat = getDailyNoteSettings().format || DEFAULT_DAILY_NOTE_FORMAT;
|
||||
|
||||
return notes.sort((a, b) => {
|
||||
return moment(b.basename, fileFormat).valueOf() - moment(a.basename, fileFormat).valueOf();
|
||||
});
|
||||
fileManager.checkDailyNote();
|
||||
}
|
||||
|
||||
export function fileCreate(file: TFile) {
|
||||
const fileDate = getDateFromFile(file as any, 'day');
|
||||
const fileFormat = getDailyNoteSettings().format || DEFAULT_DAILY_NOTE_FORMAT;
|
||||
if (!fileDate) return;
|
||||
|
||||
if (renderedDailyNotes.length === 0) {
|
||||
allDailyNotes.push(file);
|
||||
allDailyNotes = sortDailyNotes(allDailyNotes);
|
||||
filterNotesByRange();
|
||||
return;
|
||||
fileManager.fileCreate(file);
|
||||
|
||||
// Update the rendered files if needed
|
||||
if (selectionMode === "daily") {
|
||||
// For daily notes, we need to check if the file should be added to the rendered files
|
||||
const filteredFiles = fileManager.getFilteredFiles();
|
||||
if (filteredFiles.some(f => f.basename === file.basename) &&
|
||||
!renderedFiles.some(f => f.basename === file.basename)) {
|
||||
renderedFiles = [file, ...renderedFiles];
|
||||
}
|
||||
} else {
|
||||
// For folder and tag modes, we can simply update the rendered files
|
||||
renderedFiles = fileManager.getFilteredFiles().slice(0, renderedFiles.length);
|
||||
}
|
||||
|
||||
const lastRenderedDailyNote = renderedDailyNotes[renderedDailyNotes.length - 1];
|
||||
const firstRenderedDailyNote = renderedDailyNotes[0];
|
||||
const lastRenderedDailyNoteDate = moment(lastRenderedDailyNote.basename, fileFormat);
|
||||
const firstRenderedDailyNoteDate = moment(firstRenderedDailyNote.basename, fileFormat);
|
||||
|
||||
if (fileDate.isBetween(lastRenderedDailyNoteDate, firstRenderedDailyNoteDate)) {
|
||||
renderedDailyNotes.push(file);
|
||||
renderedDailyNotes = sortDailyNotes(renderedDailyNotes);
|
||||
} else if (fileDate.isBefore(lastRenderedDailyNoteDate)) {
|
||||
allDailyNotes.push(file);
|
||||
allDailyNotes = sortDailyNotes(allDailyNotes);
|
||||
filterNotesByRange();
|
||||
} else if (fileDate.isAfter(firstRenderedDailyNoteDate)) {
|
||||
renderedDailyNotes.push(file);
|
||||
renderedDailyNotes = sortDailyNotes(renderedDailyNotes);
|
||||
}
|
||||
|
||||
if (fileDate.isSame(moment(), 'day')) hasCurrentDay = true;
|
||||
}
|
||||
|
||||
|
||||
export function fileDelete(file: TFile) {
|
||||
if (!getDateFromFile(file as any, 'day')) return;
|
||||
renderedDailyNotes = renderedDailyNotes.filter((dailyNote) => {
|
||||
fileManager.fileDelete(file);
|
||||
|
||||
// Remove the file from rendered files if it exists
|
||||
renderedFiles = renderedFiles.filter((dailyNote) => {
|
||||
return dailyNote.basename !== file.basename;
|
||||
});
|
||||
allDailyNotes = allDailyNotes.filter((dailyNote) => {
|
||||
return dailyNote.basename !== file.basename;
|
||||
});
|
||||
filterNotesByRange();
|
||||
checkDailyNote();
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="daily-note-view">
|
||||
<div class="dn-range-indicator">
|
||||
{#if selectedRange !== 'all'}
|
||||
{#if selectionMode === "daily" && selectedRange !== 'all'}
|
||||
<div class="dn-range-text">
|
||||
{#if selectedRange === 'custom' && customRange}
|
||||
Showing notes from: {moment(customRange.start).format('YYYY-MM-DD')} to {moment(customRange.end).format('YYYY-MM-DD')}
|
||||
|
|
@ -256,19 +155,37 @@
|
|||
Showing notes for: {selectedRange}
|
||||
{/if}
|
||||
</div>
|
||||
{:else if selectionMode === "folder"}
|
||||
<div class="dn-range-text">
|
||||
Showing files from folder: {target}
|
||||
{#if selectedRange !== 'all'}
|
||||
<span class="dn-time-field">
|
||||
({timeField === 'ctime' ? 'created' : 'modified'} {selectedRange})
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if selectionMode === "tag"}
|
||||
<div class="dn-range-text">
|
||||
Showing files with tag: {target}
|
||||
{#if selectedRange !== 'all'}
|
||||
<span class="dn-time-field">
|
||||
({timeField === 'ctime' ? 'created' : 'modified'} {selectedRange})
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if renderedDailyNotes.length === 0}
|
||||
{#if renderedFiles.length === 0}
|
||||
<div class="dn-stock"></div>
|
||||
{/if}
|
||||
{#if !hasCurrentDay && (selectedRange === 'all' || selectedRange === 'week' || selectedRange === 'month' || selectedRange === 'year' || selectedRange === 'quarter')}
|
||||
{#if selectionMode === "daily" && !fileManager?.hasCurrentDayNote() && (selectedRange === 'all' || selectedRange === 'week' || selectedRange === 'month' || selectedRange === 'year' || selectedRange === 'quarter')}
|
||||
<div class="dn-blank-day" on:click={createNewDailyNote} aria-hidden="true">
|
||||
<div class="dn-blank-day-text">
|
||||
Create a daily note for today ✍
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{#each renderedDailyNotes as file (file)}
|
||||
{#each renderedFiles as file (file)}
|
||||
<DailyNote file={file} plugin={plugin} leaf={leaf}/>
|
||||
{/each}
|
||||
<div bind:this={loaderRef} class="dn-view-loader" use:inview={{
|
||||
|
|
@ -329,4 +246,9 @@
|
|||
text-align: center;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.dn-time-field {
|
||||
font-size: 0.9em;
|
||||
color: var(--color-base-40);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ import {
|
|||
DailyNoteSettingTab,
|
||||
DEFAULT_SETTINGS,
|
||||
} from "./dailyNoteSettings";
|
||||
import { TimeRange } from "./types/time";
|
||||
import { TimeRange, TimeField } from "./types/time";
|
||||
import {
|
||||
getAllDailyNotes,
|
||||
getDailyNote,
|
||||
|
|
@ -45,6 +45,9 @@ class DailyNoteView extends ItemView {
|
|||
scope: Scope;
|
||||
|
||||
selectedDaysRange: TimeRange = "all";
|
||||
selectionMode: "daily" | "folder" | "tag" = "daily";
|
||||
target: string = "";
|
||||
timeField: TimeField = "mtime"; // 默认使用修改时间
|
||||
|
||||
customRange: {
|
||||
start: Date;
|
||||
|
|
@ -63,11 +66,25 @@ class DailyNoteView extends ItemView {
|
|||
}
|
||||
|
||||
getDisplayText(): string {
|
||||
return "Daily Note";
|
||||
if (this.selectionMode === "daily") {
|
||||
return "Daily Notes";
|
||||
} else if (this.selectionMode === "folder") {
|
||||
return `Folder: ${this.target}`;
|
||||
} else if (this.selectionMode === "tag") {
|
||||
return `Tag: ${this.target}`;
|
||||
}
|
||||
return "Notes";
|
||||
}
|
||||
|
||||
getIcon(): string {
|
||||
return "daily-note";
|
||||
if (this.selectionMode === "daily") {
|
||||
return "daily-note";
|
||||
} else if (this.selectionMode === "folder") {
|
||||
return "folder";
|
||||
} else if (this.selectionMode === "tag") {
|
||||
return "tag";
|
||||
}
|
||||
return "document";
|
||||
}
|
||||
|
||||
onFileCreate = (file: TAbstractFile) => {
|
||||
|
|
@ -92,6 +109,25 @@ class DailyNoteView extends ItemView {
|
|||
}
|
||||
}
|
||||
|
||||
setSelectionMode(mode: "daily" | "folder" | "tag", target: string = "") {
|
||||
this.selectionMode = mode;
|
||||
this.target = target;
|
||||
|
||||
if (this.view) {
|
||||
this.view.$set({
|
||||
selectionMode: mode,
|
||||
target: target,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
setTimeField(field: TimeField) {
|
||||
this.timeField = field;
|
||||
if (this.view) {
|
||||
this.view.$set({ timeField: field });
|
||||
}
|
||||
}
|
||||
|
||||
openDailyNoteEditor() {
|
||||
this.plugin.openDailyNoteEditor();
|
||||
}
|
||||
|
|
@ -100,6 +136,65 @@ class DailyNoteView extends ItemView {
|
|||
this.scope.register(["Mod"], "f", (e) => {
|
||||
// do-nothing
|
||||
});
|
||||
|
||||
// Add action for selecting view mode
|
||||
this.addAction("layers-2", "Select view mode", (e) => {
|
||||
const menu = new Menu();
|
||||
|
||||
// Add mode selection options
|
||||
const addModeOption = (
|
||||
title: string,
|
||||
mode: "daily" | "folder" | "tag"
|
||||
) => {
|
||||
menu.addItem((item) => {
|
||||
item.setTitle(title);
|
||||
item.setChecked(this.selectionMode === mode);
|
||||
item.onClick(() => {
|
||||
if (mode === "daily") {
|
||||
this.setSelectionMode(mode);
|
||||
} else {
|
||||
// For folder and tag modes, we need to prompt for the target
|
||||
const modal = new SelectTargetModal(
|
||||
this.plugin.app,
|
||||
mode,
|
||||
(target: string) => {
|
||||
this.setSelectionMode(mode, target);
|
||||
}
|
||||
);
|
||||
modal.open();
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
addModeOption("Daily Notes", "daily");
|
||||
addModeOption("Folder", "folder");
|
||||
addModeOption("Tag", "tag");
|
||||
|
||||
menu.showAtMouseEvent(e);
|
||||
});
|
||||
|
||||
// Add action for selecting time field (for folder and tag modes)
|
||||
this.addAction("clock", "Select time field", (e) => {
|
||||
const menu = new Menu();
|
||||
|
||||
// Add time field selection options
|
||||
const addTimeFieldOption = (title: string, field: TimeField) => {
|
||||
menu.addItem((item) => {
|
||||
item.setTitle(title);
|
||||
item.setChecked(this.timeField === field);
|
||||
item.onClick(() => {
|
||||
this.setTimeField(field);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
addTimeFieldOption("Creation Time", "ctime");
|
||||
addTimeFieldOption("Modification Time", "mtime");
|
||||
|
||||
menu.showAtMouseEvent(e);
|
||||
});
|
||||
|
||||
this.addAction("calendar-range", "Select date range", (e) => {
|
||||
const menu = new Menu();
|
||||
// Add range selection options
|
||||
|
|
@ -146,6 +241,9 @@ class DailyNoteView extends ItemView {
|
|||
leaf: this.leaf,
|
||||
selectedRange: this.selectedDaysRange,
|
||||
customRange: this.customRange,
|
||||
selectionMode: this.selectionMode,
|
||||
target: this.target,
|
||||
timeField: this.timeField,
|
||||
},
|
||||
});
|
||||
this.app.vault.on("create", this.onFileCreate);
|
||||
|
|
@ -239,6 +337,106 @@ class CustomRangeModal extends Modal {
|
|||
}
|
||||
}
|
||||
|
||||
class SelectTargetModal extends Modal {
|
||||
saveCallback: (target: string) => void;
|
||||
mode: "folder" | "tag";
|
||||
targetInput: HTMLInputElement;
|
||||
|
||||
constructor(
|
||||
app: App,
|
||||
mode: "folder" | "tag",
|
||||
saveCallback: (target: string) => void
|
||||
) {
|
||||
super(app);
|
||||
this.mode = mode;
|
||||
this.saveCallback = saveCallback;
|
||||
}
|
||||
|
||||
onOpen() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
|
||||
contentEl.createEl("h2", {
|
||||
text: this.mode === "folder" ? "Select Folder" : "Select Tag",
|
||||
});
|
||||
|
||||
const form = contentEl.createEl("form");
|
||||
form.addEventListener("submit", (e) => {
|
||||
e.preventDefault();
|
||||
this.save();
|
||||
});
|
||||
|
||||
const targetSetting = form.createDiv();
|
||||
targetSetting.addClass("setting-item");
|
||||
|
||||
const targetSettingInfo = targetSetting.createDiv();
|
||||
targetSettingInfo.addClass("setting-item-info");
|
||||
|
||||
targetSettingInfo.createEl("div", {
|
||||
text: this.mode === "folder" ? "Folder Path" : "Tag Name",
|
||||
cls: "setting-item-name",
|
||||
});
|
||||
|
||||
targetSettingInfo.createEl("div", {
|
||||
text:
|
||||
this.mode === "folder"
|
||||
? "Enter the path to the folder (e.g., 'folder/subfolder')"
|
||||
: "Enter the tag name without the '#' (e.g., 'tag')",
|
||||
cls: "setting-item-description",
|
||||
});
|
||||
|
||||
const targetSettingControl = targetSetting.createDiv();
|
||||
targetSettingControl.addClass("setting-item-control");
|
||||
|
||||
this.targetInput = targetSettingControl.createEl("input", {
|
||||
type: "text",
|
||||
value: "",
|
||||
});
|
||||
this.targetInput.addClass("target-input");
|
||||
|
||||
const footerEl = contentEl.createDiv();
|
||||
footerEl.addClass("modal-button-container");
|
||||
|
||||
footerEl
|
||||
.createEl("button", {
|
||||
text: "Cancel",
|
||||
cls: "mod-warning",
|
||||
attr: {
|
||||
type: "button",
|
||||
},
|
||||
})
|
||||
.addEventListener("click", () => {
|
||||
this.close();
|
||||
});
|
||||
|
||||
footerEl
|
||||
.createEl("button", {
|
||||
text: "Save",
|
||||
cls: "mod-cta",
|
||||
attr: {
|
||||
type: "submit",
|
||||
},
|
||||
})
|
||||
.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
this.save();
|
||||
});
|
||||
}
|
||||
|
||||
save() {
|
||||
const target = this.targetInput.value.trim();
|
||||
if (target) {
|
||||
this.saveCallback(target);
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
|
||||
onClose() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
}
|
||||
}
|
||||
|
||||
export default class DailyNoteViewPlugin extends Plugin {
|
||||
private view: DailyNoteView;
|
||||
lastActiveFile: TFile;
|
||||
|
|
@ -295,6 +493,32 @@ export default class DailyNoteViewPlugin extends Plugin {
|
|||
workspace.revealLeaf(leaf);
|
||||
}
|
||||
|
||||
async openFolderView(folderPath: string, timeField: TimeField = "mtime") {
|
||||
const workspace = this.app.workspace;
|
||||
const leaf = workspace.getLeaf(true);
|
||||
await leaf.setViewState({ type: DAILY_NOTE_VIEW_TYPE });
|
||||
|
||||
// Get the view and set the selection mode to folder
|
||||
const view = leaf.view as DailyNoteView;
|
||||
view.setSelectionMode("folder", folderPath);
|
||||
view.setTimeField(timeField);
|
||||
|
||||
workspace.revealLeaf(leaf);
|
||||
}
|
||||
|
||||
async openTagView(tagName: string, timeField: TimeField = "mtime") {
|
||||
const workspace = this.app.workspace;
|
||||
const leaf = workspace.getLeaf(true);
|
||||
await leaf.setViewState({ type: DAILY_NOTE_VIEW_TYPE });
|
||||
|
||||
// Get the view and set the selection mode to tag
|
||||
const view = leaf.view as DailyNoteView;
|
||||
view.setSelectionMode("tag", tagName);
|
||||
view.setTimeField(timeField);
|
||||
|
||||
workspace.revealLeaf(leaf);
|
||||
}
|
||||
|
||||
async ensureTodaysDailyNoteExists() {
|
||||
try {
|
||||
const currentDate = moment();
|
||||
|
|
@ -353,7 +577,7 @@ export default class DailyNoteViewPlugin extends Plugin {
|
|||
|
||||
// 0.14.x doesn't have WorkspaceContainer; this can just be an instanceof check once 15.x is mandatory:
|
||||
if (
|
||||
parent === app.workspace.rootSplit ||
|
||||
parent === this.app.workspace.rootSplit ||
|
||||
(WorkspaceContainer &&
|
||||
parent instanceof WorkspaceContainer)
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -42,7 +42,6 @@
|
|||
padding: 0 !important;
|
||||
overflow-y: clip;
|
||||
}
|
||||
|
||||
.daily-note-view .daily-note-container::before {
|
||||
content: "";
|
||||
display: block;
|
||||
|
|
@ -54,7 +53,11 @@
|
|||
background-color: var(--color-base-25);
|
||||
}
|
||||
|
||||
.daily-note-view .daily-note-container:first-child::before {
|
||||
.daily-note-view .daily-note-container:first-of-type::before {
|
||||
height: 0px;
|
||||
}
|
||||
|
||||
.daily-note-view .dn-range-indicator + .daily-note-container::before {
|
||||
height: 0px;
|
||||
}
|
||||
|
||||
|
|
|
|||
16
src/types/time.d.ts
vendored
16
src/types/time.d.ts
vendored
|
|
@ -1 +1,15 @@
|
|||
export type TimeRange = "week" | "month" | "year" | "all" | "last-week" | "last-month" | "last-year" | "quarter" | "last-quarter" | "custom";
|
||||
export type TimeRange =
|
||||
| "week"
|
||||
| "month"
|
||||
| "year"
|
||||
| "all"
|
||||
| "last-week"
|
||||
| "last-month"
|
||||
| "last-year"
|
||||
| "quarter"
|
||||
| "last-quarter"
|
||||
| "custom";
|
||||
|
||||
export type SelectionMode = "daily" | "folder" | "tag";
|
||||
|
||||
export type TimeField = "ctime" | "mtime";
|
||||
|
|
|
|||
431
src/utils/fileManager.ts
Normal file
431
src/utils/fileManager.ts
Normal file
|
|
@ -0,0 +1,431 @@
|
|||
import { TFile, moment, App } from "obsidian";
|
||||
import {
|
||||
getAllDailyNotes,
|
||||
getDailyNote,
|
||||
createDailyNote,
|
||||
getDateFromFile,
|
||||
getDailyNoteSettings,
|
||||
DEFAULT_DAILY_NOTE_FORMAT,
|
||||
} from "obsidian-daily-notes-interface";
|
||||
import { TimeRange, TimeField } from "../types/time";
|
||||
|
||||
export interface FileManagerOptions {
|
||||
mode: "daily" | "folder" | "tag";
|
||||
target?: string;
|
||||
timeRange?: TimeRange;
|
||||
customRange?: { start: Date; end: Date } | null;
|
||||
app?: App;
|
||||
timeField?: TimeField;
|
||||
}
|
||||
|
||||
export class FileManager {
|
||||
private allFiles: TFile[] = [];
|
||||
private filteredFiles: TFile[] = [];
|
||||
private hasFetched: boolean = false;
|
||||
private hasCurrentDay: boolean = true;
|
||||
private cacheDailyNotes: Record<string, any> = {};
|
||||
|
||||
// Make options public so it can be accessed from outside
|
||||
public options: FileManagerOptions;
|
||||
|
||||
constructor(options: FileManagerOptions) {
|
||||
this.options = options;
|
||||
this.fetchFiles();
|
||||
}
|
||||
|
||||
public fetchFiles(): void {
|
||||
if (this.hasFetched) return;
|
||||
|
||||
switch (this.options.mode) {
|
||||
case "daily":
|
||||
this.fetchDailyNotes();
|
||||
break;
|
||||
case "folder":
|
||||
this.fetchFolderFiles();
|
||||
break;
|
||||
case "tag":
|
||||
this.fetchTaggedFiles();
|
||||
break;
|
||||
}
|
||||
|
||||
this.hasFetched = true;
|
||||
this.checkDailyNote();
|
||||
this.filterFilesByRange();
|
||||
}
|
||||
|
||||
private fetchDailyNotes(): void {
|
||||
this.cacheDailyNotes = getAllDailyNotes();
|
||||
// Build notes list by date in descending order.
|
||||
for (const string of Object.keys(this.cacheDailyNotes)
|
||||
.sort()
|
||||
.reverse()) {
|
||||
this.allFiles.push(<TFile>this.cacheDailyNotes[string]);
|
||||
}
|
||||
}
|
||||
|
||||
private fetchFolderFiles(): void {
|
||||
if (!this.options.target || !this.options.app) return;
|
||||
|
||||
// Get all files in the vault
|
||||
const allFiles = this.options.app.vault.getFiles();
|
||||
|
||||
// Filter files by folder path
|
||||
this.allFiles = allFiles.filter((file) => {
|
||||
const folderPath = file.parent?.path || "";
|
||||
return (
|
||||
folderPath === this.options.target ||
|
||||
folderPath.startsWith(this.options.target + "/")
|
||||
);
|
||||
});
|
||||
|
||||
// Sort files by modification time (newest first)
|
||||
this.allFiles = this.allFiles.sort((a, b) => {
|
||||
return b.stat.mtime - a.stat.mtime;
|
||||
});
|
||||
}
|
||||
|
||||
private fetchTaggedFiles(): void {
|
||||
if (!this.options.target || !this.options.app) return;
|
||||
|
||||
// Get all files with the specified tag
|
||||
const allFiles = this.options.app.vault.getFiles();
|
||||
const targetTag = this.options.target.startsWith("#")
|
||||
? this.options.target
|
||||
: "#" + this.options.target;
|
||||
|
||||
this.allFiles = allFiles.filter((file) => {
|
||||
// Check if the file has the target tag in its cache
|
||||
const fileCache =
|
||||
this.options.app?.metadataCache.getFileCache(file);
|
||||
if (!fileCache || !fileCache.tags) return false;
|
||||
|
||||
return fileCache.tags.some((tag) => tag.tag === targetTag);
|
||||
});
|
||||
|
||||
// Sort files by modification time (newest first)
|
||||
this.allFiles = this.allFiles.sort((a, b) => {
|
||||
return b.stat.mtime - a.stat.mtime;
|
||||
});
|
||||
}
|
||||
|
||||
public filterFilesByRange(): TFile[] {
|
||||
// If no time range is specified, return all files
|
||||
if (!this.options.timeRange) {
|
||||
this.filteredFiles = [...this.allFiles];
|
||||
return this.filteredFiles;
|
||||
}
|
||||
|
||||
// Reset the filtered files list
|
||||
this.filteredFiles = [];
|
||||
|
||||
// If the time range is "all", return all files
|
||||
if (this.options.timeRange === "all") {
|
||||
this.filteredFiles = [...this.allFiles];
|
||||
return this.filteredFiles;
|
||||
}
|
||||
|
||||
// Use different filtering methods based on different modes
|
||||
if (this.options.mode === "daily") {
|
||||
// Daily mode: filter daily notes by date
|
||||
this.filterDailyNotesByRange();
|
||||
} else {
|
||||
// Folder and tag modes: filter files by creation or modification time
|
||||
this.filterFilesByTimeRange();
|
||||
}
|
||||
|
||||
return this.filteredFiles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter files by time range
|
||||
* Applicable to folder and tag modes
|
||||
*/
|
||||
private filterFilesByTimeRange(): void {
|
||||
const now = moment();
|
||||
const timeField = this.options.timeField || "mtime"; // 默认使用修改时间
|
||||
|
||||
// Filter files by creation or modification time
|
||||
this.filteredFiles = this.allFiles.filter((file) => {
|
||||
// Get the time of the file based on the timeField option
|
||||
const fileDate = moment(file.stat[timeField]);
|
||||
|
||||
return this.isDateInRange(fileDate, now);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter daily notes by date
|
||||
* Applicable to daily mode
|
||||
*/
|
||||
private filterDailyNotesByRange(): void {
|
||||
const now = moment();
|
||||
const fileFormat =
|
||||
getDailyNoteSettings().format || DEFAULT_DAILY_NOTE_FORMAT;
|
||||
|
||||
this.filteredFiles = this.allFiles.filter((file) => {
|
||||
const fileDate = moment(file.basename, fileFormat);
|
||||
|
||||
return this.isDateInRange(fileDate, now);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the file date is in the range
|
||||
* @param fileDate file date
|
||||
* @param now current date
|
||||
* @returns whether in the range
|
||||
*/
|
||||
private isDateInRange(
|
||||
fileDate: moment.Moment,
|
||||
now: moment.Moment
|
||||
): boolean {
|
||||
switch (this.options.timeRange) {
|
||||
case "week":
|
||||
return fileDate.isSame(now, "week");
|
||||
case "month":
|
||||
return fileDate.isSame(now, "month");
|
||||
case "year":
|
||||
return fileDate.isSame(now, "year");
|
||||
case "last-week":
|
||||
return fileDate.isBetween(
|
||||
moment().subtract(1, "week").startOf("week"),
|
||||
moment().subtract(1, "week").endOf("week"),
|
||||
null,
|
||||
"[]"
|
||||
);
|
||||
case "last-month":
|
||||
return fileDate.isBetween(
|
||||
moment().subtract(1, "month").startOf("month"),
|
||||
moment().subtract(1, "month").endOf("month"),
|
||||
null,
|
||||
"[]"
|
||||
);
|
||||
case "last-year":
|
||||
return fileDate.isBetween(
|
||||
moment().subtract(1, "year").startOf("year"),
|
||||
moment().subtract(1, "year").endOf("year"),
|
||||
null,
|
||||
"[]"
|
||||
);
|
||||
case "quarter":
|
||||
return fileDate.isSame(now, "quarter");
|
||||
case "last-quarter":
|
||||
return fileDate.isBetween(
|
||||
moment().subtract(1, "quarter").startOf("quarter"),
|
||||
moment().subtract(1, "quarter").endOf("quarter"),
|
||||
null,
|
||||
"[]"
|
||||
);
|
||||
case "custom":
|
||||
if (this.options.customRange) {
|
||||
const startDate = moment(this.options.customRange.start);
|
||||
const endDate = moment(this.options.customRange.end);
|
||||
return fileDate.isBetween(startDate, endDate, null, "[]");
|
||||
}
|
||||
return false;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public checkDailyNote(): boolean {
|
||||
if (this.options.mode !== "daily") {
|
||||
this.hasCurrentDay = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
const currentDate = moment();
|
||||
const currentDailyNote = getDailyNote(
|
||||
currentDate,
|
||||
this.cacheDailyNotes
|
||||
);
|
||||
|
||||
if (!currentDailyNote) {
|
||||
this.hasCurrentDay = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
this.hasCurrentDay = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
public async createNewDailyNote(): Promise<TFile | null> {
|
||||
if (this.options.mode !== "daily" || this.hasCurrentDay) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const currentDate = moment();
|
||||
const currentDailyNote: any = await createDailyNote(currentDate);
|
||||
|
||||
if (currentDailyNote) {
|
||||
this.allFiles.push(currentDailyNote);
|
||||
this.allFiles = this.sortDailyNotes(this.allFiles);
|
||||
this.hasCurrentDay = true;
|
||||
this.filterFilesByRange();
|
||||
return currentDailyNote;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public fileCreate(file: TFile): void {
|
||||
if (this.options.mode === "daily") {
|
||||
this.handleDailyNoteCreate(file);
|
||||
} else if (this.options.mode === "folder") {
|
||||
this.handleFolderFileCreate(file);
|
||||
} else if (this.options.mode === "tag") {
|
||||
this.handleTaggedFileCreate(file);
|
||||
}
|
||||
}
|
||||
|
||||
private handleDailyNoteCreate(file: TFile): void {
|
||||
const fileDate = getDateFromFile(file as any, "day");
|
||||
const fileFormat =
|
||||
getDailyNoteSettings().format || DEFAULT_DAILY_NOTE_FORMAT;
|
||||
if (!fileDate) return;
|
||||
|
||||
if (this.filteredFiles.length === 0) {
|
||||
this.allFiles.push(file);
|
||||
this.allFiles = this.sortDailyNotes(this.allFiles);
|
||||
this.filterFilesByRange();
|
||||
return;
|
||||
}
|
||||
|
||||
const lastFilteredFile =
|
||||
this.filteredFiles[this.filteredFiles.length - 1];
|
||||
const firstFilteredFile = this.filteredFiles[0];
|
||||
const lastFilteredFileDate = moment(
|
||||
lastFilteredFile.basename,
|
||||
fileFormat
|
||||
);
|
||||
const firstFilteredFileDate = moment(
|
||||
firstFilteredFile.basename,
|
||||
fileFormat
|
||||
);
|
||||
|
||||
if (fileDate.isBetween(lastFilteredFileDate, firstFilteredFileDate)) {
|
||||
this.filteredFiles.push(file);
|
||||
this.filteredFiles = this.sortDailyNotes(this.filteredFiles);
|
||||
} else if (fileDate.isBefore(lastFilteredFileDate)) {
|
||||
this.allFiles.push(file);
|
||||
this.allFiles = this.sortDailyNotes(this.allFiles);
|
||||
this.filterFilesByRange();
|
||||
} else if (fileDate.isAfter(firstFilteredFileDate)) {
|
||||
this.filteredFiles.push(file);
|
||||
this.filteredFiles = this.sortDailyNotes(this.filteredFiles);
|
||||
}
|
||||
|
||||
if (fileDate.isSame(moment(), "day")) this.hasCurrentDay = true;
|
||||
}
|
||||
|
||||
private handleFolderFileCreate(file: TFile): void {
|
||||
if (!this.options.target) return;
|
||||
|
||||
// Check if the file belongs to the target folder
|
||||
const folderPath = file.parent?.path || "";
|
||||
if (
|
||||
folderPath === this.options.target ||
|
||||
folderPath.startsWith(this.options.target + "/")
|
||||
) {
|
||||
// Add the file to the collections
|
||||
this.allFiles.push(file);
|
||||
|
||||
// Sort files by modification time (newest first)
|
||||
this.allFiles = this.allFiles.sort((a, b) => {
|
||||
return b.stat.mtime - a.stat.mtime;
|
||||
});
|
||||
|
||||
// Update filtered files
|
||||
this.filterFilesByRange();
|
||||
}
|
||||
}
|
||||
|
||||
private handleTaggedFileCreate(file: TFile): void {
|
||||
if (!this.options.target || !this.options.app) return;
|
||||
|
||||
// Check if the file has the target tag
|
||||
const targetTag = this.options.target.startsWith("#")
|
||||
? this.options.target
|
||||
: "#" + this.options.target;
|
||||
|
||||
const fileCache = this.options.app.metadataCache.getFileCache(file);
|
||||
if (!fileCache || !fileCache.tags) return;
|
||||
|
||||
if (fileCache.tags.some((tag) => tag.tag === targetTag)) {
|
||||
// Add the file to the collections
|
||||
this.allFiles.push(file);
|
||||
|
||||
// Sort files by modification time (newest first)
|
||||
this.allFiles = this.allFiles.sort((a, b) => {
|
||||
return b.stat.mtime - a.stat.mtime;
|
||||
});
|
||||
|
||||
// Update filtered files
|
||||
this.filterFilesByRange();
|
||||
}
|
||||
}
|
||||
|
||||
public fileDelete(file: TFile): void {
|
||||
if (
|
||||
this.options.mode === "daily" &&
|
||||
getDateFromFile(file as any, "day")
|
||||
) {
|
||||
this.filteredFiles = this.filteredFiles.filter((f) => {
|
||||
return f.basename !== file.basename;
|
||||
});
|
||||
this.allFiles = this.allFiles.filter((f) => {
|
||||
return f.basename !== file.basename;
|
||||
});
|
||||
this.filterFilesByRange();
|
||||
this.checkDailyNote();
|
||||
} else {
|
||||
// Handle deletion for folder and tag modes
|
||||
this.filteredFiles = this.filteredFiles.filter((f) => {
|
||||
return f.basename !== file.basename;
|
||||
});
|
||||
this.allFiles = this.allFiles.filter((f) => {
|
||||
return f.basename !== file.basename;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private sortDailyNotes(notes: TFile[]): TFile[] {
|
||||
const fileFormat =
|
||||
getDailyNoteSettings().format || DEFAULT_DAILY_NOTE_FORMAT;
|
||||
|
||||
return notes.sort((a, b) => {
|
||||
return (
|
||||
moment(b.basename, fileFormat).valueOf() -
|
||||
moment(a.basename, fileFormat).valueOf()
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public getAllFiles(): TFile[] {
|
||||
return this.allFiles;
|
||||
}
|
||||
|
||||
public getFilteredFiles(): TFile[] {
|
||||
return this.filteredFiles;
|
||||
}
|
||||
|
||||
public hasCurrentDayNote(): boolean {
|
||||
return this.hasCurrentDay;
|
||||
}
|
||||
|
||||
public updateOptions(options: Partial<FileManagerOptions>): void {
|
||||
this.options = { ...this.options, ...options };
|
||||
|
||||
if (options.timeRange || options.customRange) {
|
||||
this.filterFilesByRange();
|
||||
}
|
||||
|
||||
if (options.mode || options.target) {
|
||||
this.allFiles = [];
|
||||
this.filteredFiles = [];
|
||||
this.hasFetched = false;
|
||||
this.fetchFiles();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
.daily-note.svelte-18gimyz.svelte-18gimyz{margin-bottom:var(--size-4-5);padding-bottom:var(--size-4-5)}.daily-note.svelte-18gimyz:has(.is-readable-line-width) .daily-note-title.svelte-18gimyz{max-width:var(--file-line-width);margin-left:auto;margin-right:auto}.daily-note-title.svelte-18gimyz.svelte-18gimyz{display:flex;justify-content:space-between;margin-top:var(--size-4-12)}.daily-node-icon.svelte-18gimyz.svelte-18gimyz{width:var(--size-4-8);height:var(--size-4-8);cursor:pointer}.daily-node-icon.svelte-18gimyz.svelte-18gimyz:hover{background-color:var(--background-modifier-hover)}.dn-stock.svelte-i23r02{height:1000px;width:100%}.no-more-text.svelte-i23r02{margin-left:auto;margin-right:auto;text-align:center}.dn-blank-day.svelte-i23r02{display:flex;margin-left:auto;margin-right:auto;max-width:var(--file-line-width);color:var(--color-base-40);padding-top:20px;padding-bottom:20px;transition:all 300ms}.dn-blank-day.svelte-i23r02:hover{padding-top:40px;padding-bottom:40px;transition:padding 300ms}.dn-blank-day-text.svelte-i23r02{margin-left:auto;margin-right:auto;text-align:center}.dn-range-indicator.svelte-i23r02{margin-left:auto;margin-right:auto;max-width:var(--file-line-width);padding:5px 0}.dn-range-text.svelte-i23r02{font-size:0.8em;color:var(--color-base-50);text-align:center;font-style:italic}.dn-editor {
|
||||
.daily-note.svelte-18gimyz.svelte-18gimyz{margin-bottom:var(--size-4-5);padding-bottom:var(--size-4-5)}.daily-note.svelte-18gimyz:has(.is-readable-line-width) .daily-note-title.svelte-18gimyz{max-width:var(--file-line-width);margin-left:auto;margin-right:auto}.daily-note-title.svelte-18gimyz.svelte-18gimyz{display:flex;justify-content:space-between;margin-top:var(--size-4-12)}.daily-node-icon.svelte-18gimyz.svelte-18gimyz{width:var(--size-4-8);height:var(--size-4-8);cursor:pointer}.daily-node-icon.svelte-18gimyz.svelte-18gimyz:hover{background-color:var(--background-modifier-hover)}.dn-stock.svelte-gpxxkc{height:1000px;width:100%}.no-more-text.svelte-gpxxkc{margin-left:auto;margin-right:auto;text-align:center}.dn-blank-day.svelte-gpxxkc{display:flex;margin-left:auto;margin-right:auto;max-width:var(--file-line-width);color:var(--color-base-40);padding-top:20px;padding-bottom:20px;transition:all 300ms}.dn-blank-day.svelte-gpxxkc:hover{padding-top:40px;padding-bottom:40px;transition:padding 300ms}.dn-blank-day-text.svelte-gpxxkc{margin-left:auto;margin-right:auto;text-align:center}.dn-range-indicator.svelte-gpxxkc{margin-left:auto;margin-right:auto;max-width:var(--file-line-width);padding:5px 0}.dn-range-text.svelte-gpxxkc{font-size:0.8em;color:var(--color-base-50);text-align:center;font-style:italic}.dn-time-field.svelte-gpxxkc{font-size:0.9em;color:var(--color-base-40)}.dn-editor {
|
||||
/*overflow-y: hidden;*/
|
||||
}
|
||||
|
||||
|
|
@ -42,7 +42,6 @@
|
|||
padding: 0 !important;
|
||||
overflow-y: clip;
|
||||
}
|
||||
|
||||
.daily-note-view .daily-note-container::before {
|
||||
content: "";
|
||||
display: block;
|
||||
|
|
@ -54,7 +53,11 @@
|
|||
background-color: var(--color-base-25);
|
||||
}
|
||||
|
||||
.daily-note-view .daily-note-container:first-child::before {
|
||||
.daily-note-view .daily-note-container:first-of-type::before {
|
||||
height: 0px;
|
||||
}
|
||||
|
||||
.daily-note-view .dn-range-indicator + .daily-note-container::before {
|
||||
height: 0px;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue