Compare commits

..

No commits in common. "main" and "1.0.2" have entirely different histories.
main ... 1.0.2

18 changed files with 1601 additions and 924 deletions

1
.env Normal file
View file

@ -0,0 +1 @@
ICLOUD_DIRECTORY = /Users/sechan/Library/Mobile Documents/iCloud~md~obsidian/Documents/main/.obsidian/plugins/daily-routine-2/

View file

@ -1,12 +1,12 @@
{
"id": "daily-routine",
"name": "Daily Routine",
"version": "1.0.8",
"minAppVersion": "1.8.0",
"description": "Manage your daily tasks as 'Routine' and organize your everyday life more effectively.",
"author": "sechan100",
"id": "daily-routine",
"name": "Daily Routine",
"version": "1.0.2",
"minAppVersion": "1.8.0",
"description": "Manage your daily tasks as 'ROUTINES' and organize your everyday life more effectively.",
"author": "sechan100",
"repo": "sechan100/daily-routine-2",
"authorUrl": "https://www.linkedin.com/in/sechan-baek-80b3462b0",
"authorUrl": "https://www.linkedin.com/in/sechan-beak-80b3462b0",
"fundingUrl": "https://buymeacoffee.com/sechan100",
"isDesktopOnly": false
"isDesktopOnly": false
}

2081
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
{
"name": "daily-routine-obsidian-plugin",
"version": "1.0.8",
"version": "1.0.2",
"description": "This is a sample plugin for Obsidian (https://obsidian.md)",
"main": "main.js",
"scripts": {
@ -22,7 +22,7 @@
"@typescript-eslint/eslint-plugin": "5.29.0",
"@typescript-eslint/parser": "5.29.0",
"builtin-modules": "3.3.0",
"esbuild": "^0.27.2",
"esbuild": "^0.24.0",
"eslint-plugin-fsd-import": "^0.0.13",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
@ -44,7 +44,7 @@
"lodash": "^4.17.21",
"mitt": "^3.0.1",
"neverthrow": "^8.1.1",
"npm": "^11.7.0",
"npm": "^10.8.3",
"react": "^18.3.1",
"react-bem-helper": "^1.4.1",
"react-calendar": "^5.0.0",

View file

@ -11,7 +11,7 @@ export interface DailyRoutinePluginSettings {
}
export const DEFAULT_SETTINGS: DailyRoutinePluginSettings = {
dailyRoutineFolderPath: "daily_routine",
dailyRoutineFolderPath: "DAILY_ROUTINE",
isMondayStartOfWeek: true,
confirmUncheckTask: true
}
@ -33,13 +33,14 @@ export class DailyRoutineSettingTab extends PluginSettingTab {
new Setting(containerEl)
.setName("Daily routine folder path")
.setDesc("This is the path to the folder where the Daily Routine Plugin saves notes, routines, and other data.")
.addText(textComponent => {
new FileSuggest(textComponent, "folder")
.setPlaceholder("daily_routine")
.setDefaultValue(this.plugin.settings.dailyRoutineFolderPath??"")
.onChange((value) => {
this.updateSettings({ dailyRoutineFolderPath: normalizePath(value)});
})
.addText(text => {
new FileSuggest(text.inputEl, "folder");
text
.setPlaceholder("DAILY_ROUTINE")
.setValue(this.plugin.settings.dailyRoutineFolderPath??"")
// .onChange(async (value) => {
// this.save({ dailyRoutineFolderPath: normalizePath(value)});
// })
})
// Start of Week
@ -53,9 +54,9 @@ export class DailyRoutineSettingTab extends PluginSettingTab {
"sunday": "Sunday"
})
.setValue(this.plugin.settings.isMondayStartOfWeek ? "monday" : "sunday")
.onChange((value) => {
.onChange(async (value) => {
const isMondayStartOfWeek = value === "monday";
this.updateSettings({ isMondayStartOfWeek });
this.save({ isMondayStartOfWeek });
})
})
@ -66,23 +67,16 @@ export class DailyRoutineSettingTab extends PluginSettingTab {
.addToggle(toggle => {
toggle
.setValue(this.plugin.settings.confirmUncheckTask)
.onChange((value) => {
this.updateSettings({ confirmUncheckTask: value });
.onChange(async (value) => {
this.save({ confirmUncheckTask: value });
})
})
}
// 닫을 때 저장하기 위해서 override
override hide(): void {
super.hide();
// 저장하고 view를 새로고침
this.plugin.saveSettings()
.then(() => useLeaf.getState().refresh());
}
updateSettings(partial: Partial<DailyRoutinePluginSettings>) {
async save(partial: Partial<DailyRoutinePluginSettings>) {
const settings = {...this.plugin.settings, ...partial};
this.plugin.settings = {...this.plugin.settings, ...partial};
await this.plugin.saveSettings();
useLeaf.getState().refresh();
}
}

View file

@ -8,9 +8,8 @@ import { RoutineGroupEntity } from "../domain/routine-group";
const parse = async (file: TFile): Promise<RoutineGroup> => {
const fm = await fileAccessor.loadFrontMatter(file);
const name = file.basename.startsWith(GROUP_PREFIX) ? file.basename.slice(GROUP_PREFIX.length) : file.basename;
const properties = RoutineGroupEntity.validateGroupProperties(fm);
const properties = RoutineGroupEntity.validateGroupProperties(fileAccessor.loadFrontMatter(file));
if(properties.isErr()) throw new Error(`[RoutineGroup '${name}' Parse Error] ${properties.error}`);
return {

View file

@ -8,16 +8,8 @@ import dedent from "dedent";
import { Routine } from "../domain/routine-type";
/**
* File을 Routine .
*
* .
* @param file
* @returns
*/
const parse = async (file: TFile): Promise<Routine> => {
const fm = await fileAccessor.loadFrontMatter(file);
const result = RoutineEntity.validateRoutineProperties(fm);
const result = RoutineEntity.validateRoutineProperties(fileAccessor.loadFrontMatter(file));
if(result.isErr()){
new Notice(`Routine '${file.basename}' frontmatter error: ${result.error}`);
throw new Error(`[Routine '${file.basename}' Parse Error] ${result.error}`);
@ -40,7 +32,7 @@ export interface RoutineQuery {
load(routineName: string): Promise<Routine>;
}
export interface RoutineRepository extends RoutineQuery {
create(entity: Routine): Promise<boolean>;
persist(entity: Routine): Promise<boolean>;
delete(routineName: string): Promise<void>;
changeName(originalName: string, newName: string): Promise<void>;
update(routine: Routine): Promise<Routine>;
@ -70,7 +62,7 @@ export const routineRepository: RoutineRepository = {
return await parse(file);
},
async create(routine: Routine){
async persist(routine: Routine){
const path = ROUTINE_PATH(routine.name);
const file = fileAccessor.loadFile(path);
if(!file){

View file

@ -8,7 +8,6 @@ import { useCallback, useEffect, useMemo } from 'react';
import { ModalApi } from './create-modal';
import { TextEditComponent } from "@shared/components/TextEditComponent"
import { Button } from "@shared/components/Button"
import { plugin } from "@shared/utils/plugin-service-locator";
type ModalProps = {
@ -174,22 +173,14 @@ export const SaveBtn = ({
name,
}: SaveBtnProps) => {
/**
* Enter Key로 Save Button을
*/
const handleEnterKeyDown = useCallback((e: KeyboardEvent) => {
if(e.key === "Enter" && !disabled) onSaveBtnClick?.();
}, [disabled, onSaveBtnClick]);
useEffect(() => {
const handleEnterKeyDown = ({ key, isComposing}: KeyboardEvent) => {
if(isComposing) return;
if(key === "Enter" && !disabled){
onSaveBtnClick?.();
}
}
document.addEventListener('keydown', handleEnterKeyDown);
return () => document.removeEventListener('keydown', handleEnterKeyDown);
}, [disabled, onSaveBtnClick]);
}, [handleEnterKeyDown]);
return (
<div css={{

View file

@ -1,6 +1,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { plugin } from "@shared/utils/plugin-service-locator";
import { TFile, TFolder, getFrontMatterInfo, parseYaml } from "obsidian";
import { TFile, TFolder } from "obsidian";
export interface FileAccessor {
@ -57,7 +58,7 @@ export interface FileAccessor {
*/
writeFrontMatter: (file: TFile, frontMatterModifier: (frontmatter: any) => any) => Promise<void>;
loadFrontMatter: (file: TFile) => Promise<object>;
loadFrontMatter: (file: TFile) => object | null;
}
export const fileAccessor: FileAccessor = {
@ -115,30 +116,8 @@ export const fileAccessor: FileAccessor = {
});
},
/**
* 1 metadataCache에서 frontmatter를 ,
* frontmatter를 .
* .
* @param file
* @returns
*/
loadFrontMatter: async (file: TFile): Promise<object> => {
loadFrontMatter: (file: TFile) => {
const metadataCache = plugin().app.metadataCache.getFileCache(file);
if(metadataCache && metadataCache.frontmatter){
return metadataCache.frontmatter;
} else {
const content = await fileAccessor.readFileAsReadonly(file);
return parseFrontmatterFromContent(content);
}
return metadataCache?.frontmatter ?? null;
}
}
export const parseFrontmatterFromContent = (fileContent: string): object => {
const fmInfo = getFrontMatterInfo(fileContent);
if(!fmInfo.exists){
return {};
}
const yaml = fmInfo.frontmatter.replace('---', '').trim()
return parseYaml(yaml);
}

View file

@ -112,19 +112,15 @@ export class Day {
}
get dow(): DayOfWeek {
let dayOfWeekNum = parseInt(this.#moment.format('d'), 10);
// ISO 8601 (Monday = 1) 체계를 사용하는 경우 조정
if(this.#moment.localeData().firstDayOfWeek() === 1) {
dayOfWeekNum = dayOfWeekNum === 0 ? 6 : dayOfWeekNum - 1; // Sunday(0)으로 맞춤
}
const dayOfWeekNum = this.#moment.format('ddd');
switch(dayOfWeekNum) {
case 0: return DayOfWeek.SUN;
case 1: return DayOfWeek.MON;
case 2: return DayOfWeek.TUE;
case 3: return DayOfWeek.WED;
case 4: return DayOfWeek.THU;
case 5: return DayOfWeek.FRI;
case 6: return DayOfWeek.SAT;
case "Sun": return DayOfWeek.SUN;
case "Mon": return DayOfWeek.MON;
case "Tue": return DayOfWeek.TUE;
case "Wed": return DayOfWeek.WED;
case "Thu": return DayOfWeek.THU;
case "Fri": return DayOfWeek.FRI;
case "Sat": return DayOfWeek.SAT;
default: throw new Error('Invalid day of week number.');
}
}

View file

@ -1,5 +1,5 @@
import { DR_SETTING } from "@app/settings/setting-provider";
import { Day, DayOfWeek } from "./day";
import { Day } from "./day";
export class Week {
@ -11,30 +11,25 @@ export class Week {
* DR_SETTING.isMondayStartOfWeek() ,
* startDay와 endDay를 .
*/
private constructor(startOfDay: Day) {
this.#startDay = startOfDay;
this.#endDay = startOfDay.clone(m => m.add(6, "day"));
}
static of(day: Day): Week {
constructor(day: Day) {
const isMondayStart = DR_SETTING.isMondayStartOfWeek();
const s = day.clone(m => m.startOf("week"));
let startDay: Day;
// 월요일 시작인데 일요일인 경우
if(isMondayStart && s.dow === DayOfWeek.SUN) {
startDay = s.clone(m => m.subtract(6, "day"));
// 월요일 시작인데 일요일인 경우(하루 뒤로)
if(isMondayStart && s.format("ddd") === "Sun") {
this.#startDay = s.clone(m => m.add(1, "day"));
}
// 일요일 시작인데 월요일인 경우(하루 앞으로)
else if(!isMondayStart && s.dow === DayOfWeek.MON) {
startDay = s.clone(m => m.subtract(1, "day"));
else if(!isMondayStart && s.format("ddd") === "Mon") {
this.#startDay = s.clone(m => m.subtract(1, "day"));
}
// 잘 부합하는 경우
else {
startDay = s;
this.#startDay = s;
}
return new Week(startDay);
// endDay는 startDay로부터 6일 뒤로 설정한다.
this.#endDay = this.#startDay.clone(m => m.add(6, "day"));
}
get startDay() {
@ -45,17 +40,19 @@ export class Week {
return this.#endDay;
}
get weekNum() {
get weekNum(){
return this.#startDay.week;
}
add_cpy(amount: number) {
const newStartOfweek = this.#startDay.clone(m => m.add(amount * 7, "day"));
return new Week(newStartOfweek);
return new Week(
this.#startDay.clone(m => m.add(amount * 7, "day"))
);
}
subtract_cpy(amount: number) {
const newStartOfweek = this.#startDay.clone(m => m.subtract(amount * 7, "day"))
return new Week(newStartOfweek);
return new Week(
this.#startDay.clone(m => m.subtract(amount * 7, "day"))
);
}
}

View file

@ -1,21 +1,14 @@
import { AbstractInputSuggest, TAbstractFile, TextComponent, TFile, TFolder } from "obsidian";
// Credits go to Liam's Periodic Notes Plugin: https://github.com/liamcain/obsidian-periodic-notes
import { AbstractInputSuggest, TAbstractFile, TFile, TFolder } from "obsidian";
import { plugin } from "@shared/utils/plugin-service-locator";
export class FileSuggest extends AbstractInputSuggest<TAbstractFile> {
#isTypeRight: (file: TAbstractFile) => boolean;
/**
* TextComponent.setValue() onChange .
* FilSuggest.selectSuggestion() onChange를
*/
#onSelectCbs: ((path: string) => void)[];
constructor(private textComponent: TextComponent, type: "file" | "folder") {
super(plugin().app, textComponent.inputEl);
// File인지 Folder인지 검사하는 함수 초기화
constructor(private inputEl: HTMLInputElement, type: "file" | "folder") {
super(plugin().app, inputEl);
this.#isTypeRight =
type === "file"
?
@ -26,10 +19,6 @@ export class FileSuggest extends AbstractInputSuggest<TAbstractFile> {
function (file: TAbstractFile): file is TFolder {
return file instanceof TFolder;
};
this.#onSelectCbs = [];
textComponent.inputEl.blur();
}
getSuggestions(inputStr: string): TAbstractFile[] {
@ -52,27 +41,9 @@ export class FileSuggest extends AbstractInputSuggest<TAbstractFile> {
override selectSuggestion(value: TAbstractFile, evt: MouseEvent | KeyboardEvent): void {
this.setValue(value.path);
this.#onSelectCbs.forEach(cb => cb(value.path));
this.close();
}
onChange(callback: (value: string) => void): FileSuggest {
this.textComponent.onChange(callback);
this.#onSelectCbs.push(callback);
return this;
}
override renderSuggestion(file: TAbstractFile, el: HTMLElement): void {
el.setText(file.path);
}
setPlaceholder(placeholder: string): FileSuggest {
this.textComponent.setPlaceholder(placeholder);
return this;
}
setDefaultValue(value: string): FileSuggest {
this.textComponent.setValue(value);
return this;
}
}

View file

@ -0,0 +1,201 @@
// Credits go to Liam's Periodic Notes Plugin: https://github.com/liamcain/obsidian-periodic-notes
import { ISuggestOwner, Scope, AbstractInputSuggest } from "obsidian";
import { createPopper, Instance as PopperInstance } from "@popperjs/core";
import { plugin } from "@shared/utils/plugin-service-locator";
const wrapAround = (value: number, size: number): number => {
return ((value % size) + size) % size;
};
class Suggest<T> {
private owner: ISuggestOwner<T>;
private values: T[];
private suggestions: HTMLDivElement[];
private selectedItem: number;
private containerEl: HTMLElement;
constructor(
owner: ISuggestOwner<T>,
containerEl: HTMLElement,
scope: Scope
) {
this.owner = owner;
this.containerEl = containerEl;
containerEl.on(
"click",
".suggestion-item",
this.onSuggestionClick.bind(this)
);
containerEl.on(
"mousemove",
".suggestion-item",
this.onSuggestionMouseover.bind(this)
);
scope.register([], "ArrowUp", (event) => {
if (!event.isComposing) {
this.setSelectedItem(this.selectedItem - 1, true);
return false;
}
});
scope.register([], "ArrowDown", (event) => {
if (!event.isComposing) {
this.setSelectedItem(this.selectedItem + 1, true);
return false;
}
});
scope.register([], "Enter", (event) => {
if (!event.isComposing) {
this.useSelectedItem(event);
return false;
}
});
}
onSuggestionClick(event: MouseEvent, el: HTMLDivElement): void {
event.preventDefault();
const item = this.suggestions.indexOf(el);
this.setSelectedItem(item, false);
this.useSelectedItem(event);
}
onSuggestionMouseover(_event: MouseEvent, el: HTMLDivElement): void {
const item = this.suggestions.indexOf(el);
this.setSelectedItem(item, false);
}
setSuggestions(values: T[]) {
this.containerEl.empty();
const suggestionEls: HTMLDivElement[] = [];
values.forEach((value) => {
const suggestionEl = this.containerEl.createDiv("suggestion-item");
this.owner.renderSuggestion(value, suggestionEl);
suggestionEls.push(suggestionEl);
});
this.values = values;
this.suggestions = suggestionEls;
this.setSelectedItem(0, false);
}
useSelectedItem(event: MouseEvent | KeyboardEvent) {
const currentValue = this.values[this.selectedItem];
if (currentValue) {
this.owner.selectSuggestion(currentValue, event);
}
}
setSelectedItem(selectedIndex: number, scrollIntoView: boolean) {
const normalizedIndex = wrapAround(
selectedIndex,
this.suggestions.length
);
const prevSelectedSuggestion = this.suggestions[this.selectedItem];
const selectedSuggestion = this.suggestions[normalizedIndex];
prevSelectedSuggestion?.removeClass("is-selected");
selectedSuggestion?.addClass("is-selected");
this.selectedItem = normalizedIndex;
if (scrollIntoView) {
selectedSuggestion.scrollIntoView(false);
}
}
}
export abstract class TextInputSuggest<T> implements ISuggestOwner<T> {
protected inputEl: HTMLInputElement | HTMLTextAreaElement;
private popper: PopperInstance;
private scope: Scope;
private suggestEl: HTMLElement;
private suggest: Suggest<T>;
constructor(inputEl: HTMLInputElement | HTMLTextAreaElement) {
this.inputEl = inputEl;
this.scope = new Scope();
this.suggestEl = createDiv("suggestion-container");
const suggestion = this.suggestEl.createDiv("suggestion");
this.suggest = new Suggest(this, suggestion, this.scope);
this.scope.register([], "Escape", this.close.bind(this));
plugin().registerDomEvent(this.inputEl, "input", this.onInputChanged.bind(this));
plugin().registerDomEvent(this.inputEl, "focus", this.onInputChanged.bind(this));
plugin().registerDomEvent(this.inputEl, "blur", this.close.bind(this));
this.suggestEl.on(
"mousedown",
".suggestion-container",
(event: MouseEvent) => {
event.preventDefault();
}
);
}
onInputChanged(): void {
const inputStr = this.inputEl.value;
const suggestions = this.getSuggestions(inputStr);
if(!suggestions) {
this.close();
return;
}
if(suggestions.length > 0) {
this.suggest.setSuggestions(suggestions);
this.open(plugin().app.workspace.containerEl, this.inputEl);
} else {
this.close();
}
}
open(container: HTMLElement, inputEl: HTMLElement): void {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
plugin().app.keymap.pushScope(this.scope);
container.appendChild(this.suggestEl);
this.popper = createPopper(inputEl, this.suggestEl, {
placement: "bottom-start",
modifiers: [
{
name: "sameWidth",
enabled: true,
fn: ({ state, instance }) => {
// Note: positioning needs to be calculated twice -
// first pass - positioning it according to the width of the popper
// second pass - position it with the width bound to the reference element
// we need to early exit to avoid an infinite loop
const targetWidth = `${state.rects.reference.width}px`;
if (state.styles.popper.width === targetWidth) {
return;
}
state.styles.popper.width = targetWidth;
instance.update();
},
phase: "beforeWrite",
requires: ["computeStyles"],
},
],
});
}
close(): void {
plugin().app.keymap.popScope(this.scope);
this.suggest.setSuggestions([]);
if (this.popper) this.popper.destroy();
this.suggestEl.detach();
}
abstract getSuggestions(inputStr: string): T[];
abstract renderSuggestion(item: T, el: HTMLElement): void;
abstract selectSuggestion(item: T): void;
}

View file

@ -1,4 +1,4 @@
import BEMHelper, { WordSet } from "react-bem-helper";
import BEMHelper, { WordSet } from "react-bem-helper"
export type DailyRoutineBEM = (element?: string, modifiers?: WordSet, extra?: WordSet) => string;

View file

@ -21,7 +21,7 @@ export const useStartRoutineModal = createModal(({ modal }: StartRoutineModalPro
const [routine, dispatch] = useReducer<RoutineReducer>(routineReducer, createNewRoutine());
const onSaveBtnClick = useCallback(async () => {
await routineRepository.create(routine);
await routineRepository.persist(routine);
mergeNotes();
modal.close();
new Notice(`Routine '${routine.name}' started! 🎉`);

View file

@ -20,7 +20,7 @@ interface WeeksProps {
export const WeeksWidget = ({ className }: WeeksProps) => {
const { note, setNote } = useRoutineNote();
const activeDay = useMemo(() => note.day, [note]);
const activeWeek = useMemo(() => Week.of(activeDay), [activeDay]);
const activeWeek = useMemo(() => new Week(activeDay), [activeDay]);
const currentNotePerformance = useMemo(() => NoteEntity.getPerformance(note), [note]);
const [ weeks, setWeeks ] = useState<WeekNode[]>([]);
const { leafBgColor } = useLeaf();
@ -40,12 +40,12 @@ export const WeeksWidget = ({ className }: WeeksProps) => {
const loadWeekNum = 1;
let loadTarget: Week;
if(edge === "start"){
const currentEdgeWeek = Week.of(week.days[0].day);
const currentEdgeWeek = new Week(week.days[0].day);
loadTarget = currentEdgeWeek.subtract_cpy(loadWeekNum);
return await loadWeekNodes(loadTarget);
}
else { // edge === "end"
const currentEdgeWeek = Week.of(week.days[week.days.length-1].day);
const currentEdgeWeek = new Week(week.days[week.days.length-1].day);
loadTarget = currentEdgeWeek.add_cpy(loadWeekNum);
return await loadWeekNodes(loadTarget);
}

View file

@ -12,7 +12,6 @@ export const loadWeekNodes: LoadWeekNodes = async (week, { prev, next } = { prev
const startDay = startWeek.startDay;
const endWeek = week.add_cpy(next);
const endDay = endWeek.endDay;
const realNotes = await noteRepository.loadBetween(startDay, endDay);
// 일단 실제로 가져온 노트들을 기반으로 dayNodes의 기본 틀을 만든다.
const dayNodes: DayNode[] = realNotes.map(note => ({
@ -55,7 +54,7 @@ export const loadWeekNodes: LoadWeekNodes = async (week, { prev, next } = { prev
for(const node of dayNodes) {
const week = weeks[weekIdx];
if(week.week === null) week.week = Week.of(node.day);
if(week.week === null) week.week = new Week(node.day);
week.days.push(node);
if(week.days.length === 7){

View file

@ -1,3 +1,3 @@
{
"1.0.8": "1.8.0"
}
"1.0.2": "1.8.0"
}