feat: 루틴 뷰 만들기

기본적인 view를 만들 수 있는 ReactView 클래스를 만들고, 이를 상속하여 기본적인 형태의 RoutineView를 만들었다.
This commit is contained in:
sechan100 2024-09-24 11:51:32 +09:00
parent ebc27bcc79
commit 75ac8602ba
24 changed files with 2916 additions and 0 deletions

10
.editorconfig Normal file
View file

@ -0,0 +1,10 @@
# top-most EditorConfig file
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = tab
indent_size = 4
tab_width = 4

3
.eslintignore Normal file
View file

@ -0,0 +1,3 @@
node_modules/
main.js

23
.eslintrc Normal file
View file

@ -0,0 +1,23 @@
{
"root": true,
"parser": "@typescript-eslint/parser",
"env": { "node": true },
"plugins": [
"@typescript-eslint"
],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended"
],
"parserOptions": {
"sourceType": "module"
},
"rules": {
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": ["off", {}],
"@typescript-eslint/ban-ts-comment": "off",
"no-prototype-builtins": "off",
"@typescript-eslint/no-empty-function": "off"
}
}

34
.github/workflows/release.yml vendored Normal file
View file

@ -0,0 +1,34 @@
name: Release Obsidian plugin
on:
push:
tags:
- "*"
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version: "18.x"
- name: Build plugin
run: |
npm install
npm run build
- 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

25
.gitignore vendored Normal file
View file

@ -0,0 +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
# dist
dist/

1
.npmrc Normal file
View file

@ -0,0 +1 @@
tag-version-prefix=""

View file

@ -1,2 +1,39 @@
<<<<<<< HEAD
# daily-routine-2
new version of daily-routine obsidian plugin
=======
### Daily Routine
<img src="https://github.com/user-attachments/assets/4437d94c-bf5c-4269-83ee-b77ee3f4838a">
**Daily Routine** overcomes the limitations of checkboxes and todos that provide only one-time checks by introducing a new unit called **routine** for todos that need to be achieved daily. With routines, you can help complete todos that need to be repeated at specific intervals.
### Features
1. Create routines.
2. Provides the progress of all routines as a percentage.
3. Offers commands to check or uncheck all routines.
4. (Coming soon) Track records of desired routines and provide statistics.
## Creating a Routine
A routine is essentially no different from the existing checkboxes offered by Markdown. Unlike checkboxes that required clicking the exact box icon in preview mode, you can mark routines complete by clicking anywhere on the routine line.
Routines can be written with the following syntax: `> - [ ]`
This is identical to writing a 'quoted checkbox.'
> Since using an entirely different syntax would prevent using the default checkbox features provided by Obsidian, a syntax identical to the quoted checkbox has been temporarily chosen. However, if problems arise in the long term, there is a willingness to change the syntax for writing routines.
## Displaying Routine Progress
You can see the progress of routines with an icon. This can be turned on/off in the settings.
## Check-All, Uncheck-All
Provides commands to check or uncheck all routines. This is useful for simple routine files not used in conjunction with the daily note.
## (Coming Soon) Routine Record Tracking and Statistics
This will be added later.
>>>>>>> 65e807f (feat: 루틴 뷰 만들기)

48
esbuild.config.mjs Normal file
View file

@ -0,0 +1,48 @@
import esbuild from "esbuild";
import process from "process";
import builtins from "builtin-modules";
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 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",
});
if (prod) {
await context.rebuild();
process.exit(0);
} else {
await context.watch();
}

39
main.ts Normal file
View file

@ -0,0 +1,39 @@
import { Plugin } from 'obsidian';
import { setPlugin } from 'src/utils/plugin-service-locator';
import { DailyRoutinePluginSettings, DailyRoutineSettingTab, DEFAULT_SETTINGS } from 'src/settings/DailyRoutineSettingTab';
import { RoutineView } from 'src/routine/routine-view';
import { activateView } from 'src/view/activate-view';
export default class DailyRoutinePlugin extends Plugin {
settings: DailyRoutinePluginSettings;
async onload() {
await this.loadSettings();
// plugin locator 설정
setPlugin(this);
// setting tab
this.addSettingTab(new DailyRoutineSettingTab(this.app, this));
this.registerView(
RoutineView.VIEW_TYPE,
(leaf) => new RoutineView(leaf)
);
this.addRibbonIcon("dice", "Routine View", () => {
activateView(RoutineView.VIEW_TYPE);
});
}
onunload() {
}
async loadSettings() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
}
async saveSettings() {
await this.saveData(this.settings);
}
}

11
manifest.json Normal file
View file

@ -0,0 +1,11 @@
{
"id": "daily-routine",
"name": "Daily Routine",
"version": "1.0.0",
"minAppVersion": "0.15.0",
"description": "Transforms daily todos into 'routines' to support your daily productivity.",
"author": "sechan100",
"authorEmail": "100sechan.dev@gmail.com",
"repo": "sechan100/daily-routine-2",
"isDesktopOnly": false
}

2304
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

31
package.json Normal file
View file

@ -0,0 +1,31 @@
{
"name": "daily-routine-obsidian-plugin",
"version": "1.0.1",
"description": "This is a sample plugin for Obsidian (https://obsidian.md)",
"main": "main.js",
"scripts": {
"dev": "node esbuild.config.mjs",
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
"version": "node version-bump.mjs && git add manifest.json versions.json"
},
"keywords": [],
"author": "",
"license": "MIT",
"devDependencies": {
"@types/lodash": "^4.17.7",
"@types/node": "^16.11.6",
"@types/react": "^18.3.8",
"@types/react-dom": "^18.3.0",
"@typescript-eslint/eslint-plugin": "5.29.0",
"@typescript-eslint/parser": "5.29.0",
"builtin-modules": "3.3.0",
"esbuild": "0.17.3",
"obsidian": "latest",
"tslib": "2.4.0",
"typescript": "4.7.4"
},
"dependencies": {
"react": "^18.3.1",
"react-dom": "^18.3.1"
}
}

View file

@ -0,0 +1,24 @@
import { WorkspaceLeaf } from "obsidian";
import { ReactView } from "../view/react-view";
export class RoutineView extends ReactView {
static VIEW_TYPE = "daily-routine-view";
constructor(leaf: WorkspaceLeaf) {
super(leaf, {
viewTypeName: RoutineView.VIEW_TYPE,
displayText: "Routine"
});
}
render() {
return (
<div>
<h1>Routine</h1>
</div>
);
}
}

View file

@ -0,0 +1,29 @@
import DailyRoutinePlugin from "main";
import { App, PluginSettingTab, Setting } from "obsidian";
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface DailyRoutinePluginSettings {
}
export const DEFAULT_SETTINGS: DailyRoutinePluginSettings = {
}
export class DailyRoutineSettingTab extends PluginSettingTab {
plugin: DailyRoutinePlugin;
constructor(app: App, plugin: DailyRoutinePlugin) {
super(app, plugin);
this.plugin = plugin;
}
display(): void {
const { containerEl } = this;
containerEl.empty();
}
}

34
src/utils/mode-switch.ts Normal file
View file

@ -0,0 +1,34 @@
import { getMarkdownView } from "src/utils/utils";
const findModeSwitchBtn = () => {
const query = 'button.clickable-icon.view-action';
return Array.from(getMarkdownView().containerEl.querySelectorAll(query
) as NodeListOf<HTMLButtonElement>)
.find(btn => {
return (
btn.children[0].classList.contains('lucide-book-open')
||
btn.children[0].classList.contains('lucide-edit-3')
)
}) as HTMLButtonElement;
}
export const modeSwitcher = {
click: () => {
const btn = findModeSwitchBtn();
btn.click();
},
toSource: () => {
if(getMarkdownView().getMode() === 'preview'){
modeSwitcher.click();
}
},
toPreview: () => {
if(getMarkdownView().getMode() === 'source'){
modeSwitcher.click();
}
},
getMode: () => {
return getMarkdownView().getMode();
}
}

View file

@ -0,0 +1,16 @@
import DailyRoutinePlugin from "main";
let pluginThisRef: DailyRoutinePlugin | null = null;
export const plugin = () => {
if(pluginThisRef === null) {
throw new Error('Plugin not initialized');
}
return pluginThisRef;
}
export const setPlugin = (plugin: DailyRoutinePlugin) => {
pluginThisRef = plugin;
}

View file

@ -0,0 +1,91 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable @typescript-eslint/no-explicit-any */
import DailyRoutinePlugin from "main";
import { debounce, MarkdownPostProcessorContext } from "obsidian";
// Intercepting 프로세서가 실행되는 컨텍스트동안만 살아있는 로컬 스코프를 제공
export class SurroundProcessorContext {
#map: Map<string, any>;
#markdownPostProcessorContext: MarkdownPostProcessorContext;
constructor(markdownPostProcessorContext: MarkdownPostProcessorContext){
this.#map = new Map();
this.#markdownPostProcessorContext = markdownPostProcessorContext;
}
get markdownPostProcessorContext(){
return this.#markdownPostProcessorContext;
}
set(key: string, value: any){
this.#map.set(key, value);
}
get(key: string){
return this.#map.get(key);
}
remove(key: string){
const value = this.#map.get(key);
this.#map.delete(key);
return value;
}
}
let context: SurroundProcessorContext | null = null;
type PreProcessor = (ctx: SurroundProcessorContext) => void;
type Processor = (element: HTMLElement, ctx: SurroundProcessorContext) => void;
type PostProcessor = (ctx: SurroundProcessorContext) => void;
const processors = {
pre: (ctx: SurroundProcessorContext) => {},
processor: (element: HTMLElement, ctx: SurroundProcessorContext) => {},
post: (ctx: SurroundProcessorContext) => {},
}
export const surroundProcessor = {
setPre: (pre: PreProcessor) => {
processors.pre = pre;
},
setProcessor: (processor: Processor) => {
processors.processor = processor;
},
setPost: (post: PostProcessor) => {
processors.post = post;
},
}
let firstProcessorCalled = false;
const _d = debounce((ctx: SurroundProcessorContext) => {
// 반드시 필요한 후처리 작업
console.groupEnd();
context = null; // context clear
firstProcessorCalled = false;
console.groupCollapsed("[SurroundProcessor - POST]");
processors.post(ctx);
console.groupEnd();
}, 100, true);
export function surroundProcessorEntryPoint(
this: DailyRoutinePlugin,
element: HTMLElement,
markdownPostProcessorContext: MarkdownPostProcessorContext
) {
if(!firstProcessorCalled){
context = new SurroundProcessorContext(markdownPostProcessorContext);
// 기존 프로세서에서 제공하는 context를 내부변수로서 제공
context.set("markdownPostProcessorContext", markdownPostProcessorContext);
console.groupCollapsed("[SurroundProcessor - PRE]");
processors.pre(context);
console.groupEnd();
firstProcessorCalled = true;
console.groupCollapsed("[MarkdownPostProcessor]");
}
const ctx = context as SurroundProcessorContext;
console.debug("[PROCESSOR]", element);
processors.processor(element, ctx);
// processor가 여러번 실행되어도, 모든 프로세서가 호출되고 마지막에 한번만 호출됨
_d(ctx);
}

37
src/utils/utils.ts Normal file
View file

@ -0,0 +1,37 @@
import { Editor, MarkdownView } from "obsidian";
import { plugin } from "./plugin-service-locator"
export const getFilePath = () => {
const file = plugin().app.workspace.getActiveFile();
if(!file) throw new Error('No active file');
return file.path;
}
export const getMarkdownView: () => MarkdownView = () => {
const view = plugin().app.workspace.getActiveViewOfType(MarkdownView);
if(!view) throw new Error('No active markdown view');
return view;
}
export interface Line {
lineNum: number; // 0 ~ n;
text: string;
}
export const searchText = (e: Editor, regex: RegExp): Line[] => {
const lineCount = e.lineCount();
const results: Line[] = [];
for (let i = 0; i < lineCount; i++) {
const l = e.getLine(i);
const match = l.match(regex);
if(match) {
results.push({lineNum: i, text: l});
}
}
return results;
}

28
src/view/activate-view.ts Normal file
View file

@ -0,0 +1,28 @@
import { WorkspaceLeaf } from "obsidian";
import { plugin } from "src/utils/plugin-service-locator";
/**
* viewType에 .
* @param viewTypeName
* @param pos -1: left, 0: center(default), 1: right
*/
export const activateView = async (viewTypeName: string, pos = 0) => {
const app = plugin().app;
let leaf = app.workspace.getLeavesOfType(viewTypeName)[0];
let getLeaf;
if(pos === -1) {
getLeaf = () => app.workspace.getLeftLeaf(false)
} else if(pos === 0) {
getLeaf = () => app.workspace.getLeaf(false)
} else {
getLeaf = () => app.workspace.getRightLeaf(false)
}
if(!leaf) {
leaf = getLeaf() as WorkspaceLeaf;
await leaf.setViewState({ type: viewTypeName });
}
app.workspace.revealLeaf(leaf);
}

49
src/view/react-view.tsx Normal file
View file

@ -0,0 +1,49 @@
import { StrictMode } from "react";
import { App, ItemView, WorkspaceLeaf } from "obsidian";
import { Root, createRoot } from "react-dom/client";
/**
* React를
*/
export abstract class ReactView extends ItemView {
root: Root | null = null;
viewTypeName: string;
displayText: string;
constructor(leaf: WorkspaceLeaf, info: {
viewTypeName: string,
displayText: string
}) {
super(leaf);
this.viewTypeName = info.viewTypeName;
this.displayText = info.displayText;
}
getViewType() {
return this.viewTypeName;
}
getDisplayText(){
return this.displayText;
}
/**
* .
*/
abstract render(): JSX.Element;
async onOpen() {
this.root = createRoot(this.containerEl.children[1]);
this.root.render(
<StrictMode>
{this.render()}
</StrictMode>,
);
}
async onClose() {
this.root?.unmount();
}
}

0
styles.css Normal file
View file

25
tsconfig.json Normal file
View file

@ -0,0 +1,25 @@
{
"compilerOptions": {
"baseUrl": ".",
"jsx": "react-jsx",
"inlineSourceMap": true,
"inlineSources": true,
"module": "ESNext",
"target": "ES6",
"allowJs": true,
"noImplicitAny": true,
"moduleResolution": "node",
"importHelpers": true,
"isolatedModules": true,
"strictNullChecks": true,
"lib": [
"DOM",
"ES5",
"ES6",
"ES7"
]
},
"include": [
"**/*.ts"
, "src/routine/react-view.tsx" ]
}

14
version-bump.mjs Normal file
View file

@ -0,0 +1,14 @@
import { readFileSync, writeFileSync } from "fs";
const targetVersion = process.env.npm_package_version;
// read minAppVersion from manifest.json and bump version to target version
let manifest = JSON.parse(readFileSync("manifest.json", "utf8"));
const { minAppVersion } = manifest;
manifest.version = targetVersion;
writeFileSync("manifest.json", JSON.stringify(manifest, null, "\t"));
// update versions.json with target version and minAppVersion from manifest.json
let versions = JSON.parse(readFileSync("versions.json", "utf8"));
versions[targetVersion] = minAppVersion;
writeFileSync("versions.json", JSON.stringify(versions, null, "\t"));

3
versions.json Normal file
View file

@ -0,0 +1,3 @@
{
"1.0.0": "0.15.0"
}