루틴 체크시 루틴파일에 achievement 기록하는 기능 개발

This commit is contained in:
sechan100 2024-09-29 18:03:02 +09:00
parent 3c1f60c8a1
commit a0944057b1
26 changed files with 494 additions and 171 deletions

View file

@ -1,48 +1,79 @@
import esbuild from "esbuild";
import process from "process";
import builtins from "builtin-modules";
import fs from "fs";
const banner =
`/*
const banner = `/*
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
if you want to view the source, please visit the github repository of this plugin
*/
`;
const prod = (process.argv[2] === "production");
const prod = process.argv[2] === "production";
// CSS를 하나로 합치기 위한 rename 플러그인
const renamePlugin = {
name: 'rename-styles',
setup(build) {
build.onEnd(() => {
const { outfile } = build.initialOptions;
const outcss = outfile.replace(/\.js$/, '.css'); // main.js -> main.css
const fixcss = outfile.replace(/main\.js$/, 'styles.css'); // main.css -> styles.css
if(fs.existsSync(outcss)) {
console.log('Start renaming', outcss, 'to', fixcss);
fs.renameSync(outcss, fixcss); // main.css -> styles.css
console.log('End renaming', outcss, 'to', fixcss);
} else {
console.log('CSS file not found:', outcss); // 문제 발생 시 경로 확인용
}
});
},
};
const context = await esbuild.context({
banner: {
js: banner,
},
entryPoints: ["main.ts"],
bundle: true,
external: [
"obsidian",
"electron",
"@codemirror/autocomplete",
"@codemirror/collab",
"@codemirror/commands",
"@codemirror/language",
"@codemirror/lint",
"@codemirror/search",
"@codemirror/state",
"@codemirror/view",
"@lezer/common",
"@lezer/highlight",
"@lezer/lr",
...builtins],
format: "cjs",
target: "es2018",
logLevel: "info",
sourcemap: prod ? false : "inline",
treeShaking: true,
outfile: "main.js",
banner: {
js: banner,
},
entryPoints: ["src/main.ts"], // 엔트리 포인트
bundle: true,
external: [
"obsidian",
"electron",
"@codemirror/autocomplete",
"@codemirror/collab",
"@codemirror/commands",
"@codemirror/language",
"@codemirror/lint",
"@codemirror/search",
"@codemirror/state",
"@codemirror/view",
"@lezer/common",
"@lezer/highlight",
"@lezer/lr",
...builtins,
],
format: "cjs", // CommonJS 포맷
target: "es2018",
logLevel: "info",
sourcemap: prod ? false : "inline",
treeShaking: true,
outfile: "main.js", // JavaScript 번들 파일
plugins: [
renamePlugin, // CSS 파일 이름 변경 플러그인
],
loader: {
".tsx": "tsx", // TypeScript와 TSX 파일을 처리
".ts": "ts",
".css": "css", // CSS 파일을 로드
},
// CSS 파일을 추출하여 하나로 합침
write: true,
});
if (prod) {
await context.rebuild();
process.exit(0);
if(prod) {
await context.rebuild();
process.exit(0);
} else {
await context.watch();
}
await context.watch();
}

View file

@ -10,7 +10,7 @@ export const devOnlyTest = () => {
const test = () => {
routineManager.editRoutine('🖊️ 문서 작성하기', {
routineManager.edit('🖊️ 문서 작성하기', {
name: '🖊️ 문서 작성하기ㅋ',
newProperties: {
dayOfWeeks: [1, 2, 3, 4, 5, 6, 0]

1
src/index.d.ts vendored Normal file
View file

@ -0,0 +1 @@
declare module '*.css';

84
src/lib/day.ts Normal file
View file

@ -0,0 +1,84 @@
import { moment } from "obsidian";
import { plugin } from "./plugin-service-locator";
import { DEFAULT_SETTINGS } from "settings/DailyRoutineSettingTab";
export enum DayOfWeek {
SUN,
MON,
TUE,
WEN,
THU,
FRI,
SAT
}
export class Day {
moment: moment.Moment;
static defaultFormat = 'YYYY-MM-DD';
/**
*
* @param day "YYYY-MM-DD"
*/
constructor(day: string) {
const m = moment(day);
if(m.isValid()) {
this.moment = m;
} else {
throw new Error(`Invalid date format. ${day}`);
}
}
static fromNow(): Day{
return new Day(moment().format(Day.defaultFormat));
}
getAsUserCustomFormat(){
return this.moment.format(gerFormat());
}
getAsDefaultFormat(){
return this.moment.format(Day.defaultFormat);
}
getDayOfWeek(): DayOfWeek {
const dayOfWeekNum = Number(moment().format('d'));
switch(dayOfWeekNum) {
case 0: return DayOfWeek.SUN;
case 1: return DayOfWeek.MON;
case 2: return DayOfWeek.TUE;
case 3: return DayOfWeek.WEN;
case 4: return DayOfWeek.THU;
case 5: return DayOfWeek.FRI;
case 6: return DayOfWeek.SAT;
default: throw new Error('Invalid day of week number.');
}
}
isSameDay(day: Day){
return this.moment.isSame(day.moment);
}
isSameDayOfWeek(day: Day){
return this.getDayOfWeek() === day.getDayOfWeek();
}
isSameDayOfWeeks(dayOfWeek: DayOfWeek){
return this.getDayOfWeek() === dayOfWeek;
}
}
/**
* .
*/
const gerFormat = (): string => {
const format = plugin().settings.dateFormat;
if(format && format !== '') {
return format;
} else {
return DEFAULT_SETTINGS.dateFormat as string;
}
}

View file

@ -1,5 +1,5 @@
import { TFile, TFolder } from "obsidian";
import { plugin } from "src/shared/utils/plugin-service-locator";
import { plugin } from "lib/plugin-service-locator";

View file

@ -1,5 +1,5 @@
import { Editor, MarkdownView } from "obsidian";
import { plugin } from "./plugin-service-locator"
import { plugin } from "../plugin-service-locator"

View file

@ -1,5 +1,5 @@
import { WorkspaceLeaf } from "obsidian";
import { plugin } from "src/shared/utils/plugin-service-locator";
import { plugin } from "lib/plugin-service-locator";
/**
* viewType에 .

View file

@ -1,9 +1,9 @@
import { Plugin } from 'obsidian';
import { setPlugin } from 'src/shared/utils/plugin-service-locator';
import { DailyRoutinePluginSettings, DailyRoutineSettingTab, DEFAULT_SETTINGS } from 'src/settings/DailyRoutineSettingTab';
import { DailyRoutineView } from 'src/view/daily-routine-view';
import { activateView } from 'src/shared/view/activate-view';
import { devOnlyTest } from 'src/dev-only-test-btn';
import { setPlugin } from './lib/plugin-service-locator';
import { DailyRoutinePluginSettings, DailyRoutineSettingTab, DEFAULT_SETTINGS } from './settings/DailyRoutineSettingTab';
import { DailyRoutineView } from './view/daily-routine-view';
import { activateView } from './lib/view/activate-view';
import { devOnlyTest } from './dev-only-test-btn';
export default class DailyRoutinePlugin extends Plugin {
settings: DailyRoutinePluginSettings;

View file

@ -1,21 +1,25 @@
import { routineManager, Routine } from "./routine"
import { momentProvider } from "src/shared/utils/moment-provider";
import { Day } from "lib/day";
export interface RoutineNote {
title: string;
day: Day;
routines: Routine[];
}
export const createNewRoutineNote = async (): Promise<RoutineNote> => {
const now = momentProvider.getNow();
const day = Day.fromNow();
const routines = await routineManager.getAllRoutines();
const todayRoutines = routines.filter(r => r.properties.dayOfWeeks.contains(momentProvider.getDayOfWeekNum()));
const todayRoutines = routines.filter(r => {
return r.properties.dayOfWeeks.contains(day.getDayOfWeek());
});
return {
title: now,
title: day.getAsUserCustomFormat(),
day: day,
routines: todayRoutines
}
}

View file

@ -1,7 +1,8 @@
import { TFile } from "obsidian";
import { fileAccessor } from "src/shared/file/file-accessor"
import { plugin } from "src/shared/utils/plugin-service-locator";
import { DayOfWeek } from "src/shared/utils/moment-provider";
import { fileAccessor } from "lib/file-accessor"
import { plugin } from "lib/plugin-service-locator";
import { Day, DayOfWeek } from "lib/day";
import { update } from "lodash";
/**
@ -41,7 +42,7 @@ export const routineManager = {
* @param routineName 루틴파일명: identifier
* @param cmd
*/
editRoutine: async (routineName: string, cmd: {
edit: async (routineName: string, cmd: {
name?: string,
newProperties?: Partial<RoutineProperties>
}) => {
@ -68,14 +69,84 @@ export const routineManager = {
*
* @param routineName 루틴파일명: identifier
*/
getRoutine: async (routineName: string): Promise<Routine> => {
get: async (routineName: string): Promise<Routine> => {
const path = getRoutinePath(routineName);
const file = fileAccessor.getFile(path);
return await readRoutineFile(file);
},
/**
*
*/
updateAchievement: async ({routineName, day, checked}: {
routineName: string,
day: Day,
checked: boolean
}) => {
const file = fileAccessor.getFile(getRoutinePath(routineName));
const content = await fileAccessor.readFileAsWritable(file);
if(!content){
throw new Error('Routine file is empty.');
}
updateAchievement(file, {date: day.getAsDefaultFormat(), checked});
}
}
interface Achievement {
date: string; // YYYY-MM-DD
checked: boolean;
}
/**
* day에 routine의 .
*/
const getAchievements = (content: string): Achievement[] => {
// # Achievement 섹션을 추출
const achievementsRegex = /# Achievement\n((?:- \[(?: |x)\] \d{4}-\d{1,2}-\d{1,2}\n?)+)/;
const achievementsString = content.match(achievementsRegex)?.[0]??'';
if(!achievementsString || achievementsString.length === 0) {
throw new Error("Routine file does not have 'Achievement' section.");
}
// - [ |x] YYYY-MM-DD 형식의 체크박스를 추출
const checkboxRegex = /- \[( |x)\] \d{4}-\d{1,2}-\d{1,2}/i;
const achievements = achievementsString.match(checkboxRegex)??[];
return achievements.map(a => {
const date = a.match(/\d{4}-\d{1,2}-\d{1,2}/)?.[0];
if(!date) {
throw new Error('Date format is invalid.');
}
const checked = a.includes('[X]') || a.includes('[x]');
return {date, checked};
});
}
/**
* .
* @param content
* @param cmd
* achievement가 , .
*/
const updateAchievement = (file: TFile, cmd: Achievement) => {
const newAchievements = `- [${cmd.checked?'x':' '}] ${cmd.date}`;
const targetRegex = new RegExp(`- \\[(?: |x)\\] ${cmd.date}`);
fileAccessor.writeFile(file, (content => {
// 이미 해당 날짜의 성취로그가 있으면
if(content.match(targetRegex)){
return content.replace(targetRegex, newAchievements);
} else {
return content.replace(/# Achievement/i, `# Achievement\n${newAchievements}`);
}
}));
}
/**
* .
*/

View file

@ -1,7 +1,7 @@
import DailyRoutinePlugin from "main";
import { App, normalizePath, PluginSettingTab, Setting } from "obsidian";
import { FileSuggest } from "./suggesters/FileSuggester";
import { momentProvider } from "src/shared/utils/moment-provider";
import { Day } from "lib/day";
export interface DailyRoutinePluginSettings {
@ -50,7 +50,7 @@ export class DailyRoutineSettingTab extends PluginSettingTab {
fragment.append("The date format of the routine note.");
fragment.append(sampleEl);
const updateSample = () => {
sampleEl.textContent = `Sample: ${momentProvider.getNow()}`;
sampleEl.textContent = `Sample: ${Day.fromNow().getAsUserCustomFormat()}`;
};
updateSample(); // 초기화
return {

View file

@ -2,7 +2,7 @@
import { TAbstractFile, TFile, TFolder } from "obsidian";
import { TextInputSuggest } from "./suggest";
import { plugin } from "src/shared/utils/plugin-service-locator";
import { plugin } from "lib/plugin-service-locator";
export class FileSuggest extends TextInputSuggest<TAbstractFile> {

View file

@ -2,7 +2,7 @@
import { ISuggestOwner, Scope } from "obsidian";
import { createPopper, Instance as PopperInstance } from "@popperjs/core";
import { plugin } from "src/shared/utils/plugin-service-locator";
import { plugin } from "lib/plugin-service-locator";
const wrapAround = (value: number, size: number): number => {
return ((value % size) + size) % size;

View file

@ -1,43 +0,0 @@
import { moment } from "obsidian";
import { plugin } from "./plugin-service-locator";
import { DEFAULT_SETTINGS } from "src/settings/DailyRoutineSettingTab";
const gerFormat = (): string => {
const format = plugin().settings.dateFormat;
if(format && format !== '') {
return format;
} else {
return DEFAULT_SETTINGS.dateFormat as string;
}
}
export const momentProvider = {
getNow: () => {
return moment().format(gerFormat());
},
// 0, 1, 2, 3, 4, 5, 6
getDayOfWeekNum: () => {
return Number(moment().format('d'));
},
// "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"
getDayOfWeek: () => {
return moment().format('ddd');
},
isToday: (dataTime: string) => {
return moment(momentProvider.getNow()).isSame(dataTime);
}
}
export enum DayOfWeek {
SUN,
MON,
TUE,
WEN,
THU,
FRI,
SAT
}

View file

@ -1,8 +1,9 @@
import { WorkspaceLeaf } from "obsidian";
import { ReactView } from "../shared/view/react-view";
import { RoutineNoteView } from "./routine-note-view";
import { ReactView } from "../lib/view/react-view";
import { RoutineNoteView } from "./routine-note";
import { createNewRoutineNote, RoutineNote } from "../model/routine-note";
import { useEffect, useState } from "react";
import { Day } from "lib/day";
export class DailyRoutineView extends ReactView {
@ -27,6 +28,7 @@ export class DailyRoutineView extends ReactView {
const DailyRoutineViewComponent = () => {
const [routineNote, setRoutineNote] = useState<RoutineNote>({
routines: [],
day: Day.fromNow(),
title: "Fallback"
});

View file

@ -1,21 +0,0 @@
import { RoutineNote } from "../model/routine-note";
import { RoutineComponent } from "./routine";
interface Props {
routineNote: RoutineNote
}
export const RoutineNoteView = ({ routineNote }: Props) => {
return (
<div>
{routineNote.routines.map((routine, idx) => {
return (
<div key={idx}>
<RoutineComponent routine={routine} />
</div>
)
})}
</div>
);
}

View file

@ -0,0 +1,14 @@
.dr-note__header {
display: flex;
justify-content: start;
align-items: end;
margin: 1em;
gap: 10px;
}
.dr-note__header h1 {
display: inline-block;
margin: 0;
}

View file

@ -0,0 +1,38 @@
import { RoutineNote } from "../../model/routine-note";
import { RoutineComponent } from "../routine";
import React, { useCallback } from "react";
import "./index.css";
import { Routine, routineManager } from "model/routine";
interface Props {
routineNote: RoutineNote
}
export const RoutineNoteView = ({ routineNote }: Props) => {
const onCheckChange = useCallback((routine: Routine, checked: boolean) => {
routineManager.updateAchievement({
routineName: routine.name,
day: routineNote.day,
checked
});
}, [routineNote]);
return (
<div className="dr-note">
<header className="dr-note__header">
<h1>Daily Routine</h1>
<span>{routineNote.title}</span>
</header>
{routineNote.routines.map((routine, idx) => {
return (
<div key={idx}>
<RoutineComponent onCheckChange={onCheckChange} routine={routine} />
</div>
)
})}
</div>
);
}

141
src/view/routine/index.css Normal file
View file

@ -0,0 +1,141 @@
/* #####################################################################
* ####################### GLOBAL STYLES ###############################
* ##################################################################### */
.app-container {
--color-accent-1-hsl: calc(var(--accent-h) - 3), calc(var(--accent-s) * 1.02), calc(var(--accent-l) * 1.15);
--color-accent-2-hsl: calc(var(--accent-h) - 5), calc(var(--accent-s) * 1.05), calc(var(--accent-l) * 1.29);
}
/* #####################################################################
* ####################### ROUTINE COMPONENT ###########################
* ##################################################################### */
/* 최상위 루틴 */
.dr-routine {
display: block;
font-size: 16px; /* 전체적 루틴의 사이즈를 결정 */
width: 100%;
padding: calc(0.5em) 0;
/* border: 1px solid #c8ccd4; */
cursor: pointer;
}
/* 전체 감싸주는 라벨 */
.dr-routine label {
margin: 0;
padding: 0;
border: 0;
font: inherit;
vertical-align: baseline;
line-height: 1;
}
/* 레이아웃 */
.dr-routine *,
.dr-routine ::after,
.dr-routine ::before {
box-sizing: border-box;
}
/* 보이지 않는 데이터용 input checkbox */
.dr-routine input[type="checkbox"] {
visibility: hidden;
display: none;
}
/* 실제로 보이는 디자인용 체크박스 */
.dr-routine .dr-routine__item .dr-routine__cbx {
position: relative;
top: calc(1/8 * 1em);
display: inline-block;
width: calc(14/16 * 1em);
height: calc(14/16 * 1em);
margin-right: calc(0.5em);
border: calc(1/16 * 1em) solid #c8ccd4;
border-radius: 3px; /* 체크박스 곡률 */
cursor: pointer;
}
.dr-routine .dr-routine__item .dr-routine__cbx svg {
position: relative;
width: calc(7/8 * 1em);
height: calc(3/4 * 1em);
top: calc(-1/16 * 1em);
transform: scale(0);
fill: none;
stroke-linecap: round;
stroke-linejoin: round;
}
.dr-routine .dr-routine__item .dr-routine__cbx svg polyline {
stroke-width: 2.2;
stroke: hsla(var(--color-accent-1-hsl), 1.0);
}
/* 체크 표시 아이콘 */
.dr-routine .dr-routine__item .dr-routine__cbx:before {
content: '';
position: absolute;
top: 50%;
left: 50%;
margin: calc(-5/8 * 1em) 0 0 calc(-5/8 * 1em);
width: calc(5/4 * 1em);
height: calc(5/4 * 1em);
border-radius: 100%;
background: hsla(var(--color-accent-1-hsl), 1.0);
transform: scale(0);
}
/* 체크 이펙트 */
.dr-routine .dr-routine__item .dr-routine__cbx:after {
content: '';
position: absolute;
top: calc(5/16 * 1em);
left: calc(5/16 * 1em);
width: calc(1/8 * 1em);
height: calc(1/8 * 1em);
border-radius: 2px;
box-shadow: 0 calc(9/8 * -1em) 0 hsla(var(--color-accent-1-hsl), 1.0), calc(3/4 * 1em) calc(3/4 * -1em) 0 hsla(var(--color-accent-1-hsl), 1.0), calc(9/8 * 1em) 0 0 hsla(var(--color-accent-1-hsl), 1.0), calc(3/4 * 1em) calc(3/4 * 1em) 0 hsla(var(--color-accent-1-hsl), 1.0), 0 calc(9/8 * 1em) 0 hsla(var(--color-accent-1-hsl), 1.0), calc(3/4 * -1em) calc(3/4 * 1em) 0 hsla(var(--color-accent-1-hsl), 1.0), calc(9/8 * -1em) 0 0 hsla(var(--color-accent-1-hsl), 1.0), calc(3/4 * -1em) calc(3/4 * -1em) 0 hsla(var(--color-accent-1-hsl), 1.0);
transform: scale(0);
}
.dr-routine .dr-routine__item input:checked + .dr-routine__cbx {
border-color: transparent;
}
.dr-routine .dr-routine__item input:checked + .dr-routine__cbx svg {
transform: scale(1);
transition: all 0.4s ease;
transition-delay: 0.1s;
}
.dr-routine .dr-routine__item input:checked + .dr-routine__cbx:before {
transform: scale(1);
opacity: 0;
transition: all 0.3s ease;
}
.dr-routine .dr-routine__item input:checked + .dr-routine__cbx:after {
transform: scale(1);
opacity: 0;
transition: all 0.6s ease;
}
/* 루틴 이름 */
.dr-routine .dr-routine__item .dr-routine__name {
position: relative;
cursor: pointer;
transition: color 0.3s ease;
}
.dr-routine .dr-routine__item .dr-routine__name:after {
content: '';
position: absolute;
top: 50%;
left: 0;
width: 0;
height: 1px;
background: #9098a9;
}
.dr-routine .dr-routine__item input:checked ~ .dr-routine__name {
color: #9098a9;
}
.dr-routine .dr-routine__item input:checked ~ .dr-routine__name:after {
width: 100%;
transition: all 0.4s ease;
}

View file

@ -1,11 +1,15 @@
import { Routine } from "../model/routine"
import { Routine } from "../../model/routine"
import React from "react"
import "./index.css"
interface Props {
routine: Routine
routine: Routine;
onCheckChange: (routine: Routine, checked: boolean) => void;
}
export const RoutineComponent = ({ routine }: Props) => {
export const RoutineComponent = ({ routine, onCheckChange }: Props) => {
const id = `routine-${routine.name}`
return (
@ -13,7 +17,7 @@ export const RoutineComponent = ({ routine }: Props) => {
{/* 전체적으로 감싸주는 label */}
<label htmlFor={id} className="dr-routine__item">
{/* 체크박스(hidden) */}
<input type="checkbox" id={id} className="hidden"/>
<input onChange={(e) => onCheckChange(routine, e.target.checked)} type="checkbox" id={id} className="hidden"/>
{/* 체크박스(display) */}
<label htmlFor={id} className="dr-routine__cbx">
<svg viewBox="0 0 14 12">

File diff suppressed because one or more lines are too long

View file

@ -1,6 +1,6 @@
{
"compilerOptions": {
"baseUrl": ".",
"baseUrl": "src/",
"jsx": "react-jsx",
"inlineSourceMap": true,
"inlineSources": true,
@ -8,6 +8,8 @@
"target": "ES6",
"allowJs": true,
"noImplicitAny": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"moduleResolution": "node",
"importHelpers": true,
"isolatedModules": true,
@ -19,7 +21,5 @@
"ES7"
]
},
"include": [
"**/*.ts"
, "src/model/react-view.tsx" ]
"include": ["**/*.ts", "**/*.tsx", "src/index.d.ts"],
}