mirror of
https://github.com/pahkey/obsidian-wikidocs-plugin.git
synced 2026-07-22 11:30:32 +00:00
337 lines
9.5 KiB
TypeScript
337 lines
9.5 KiB
TypeScript
import { App, Modal, Notice, Plugin, PluginSettingTab, Setting, TFile, TFolder } from "obsidian";
|
|
|
|
import {
|
|
deleteFolderContents,
|
|
ensureLineBreaks,
|
|
extractEmbeddedImages,
|
|
extractTitleFromFilePath,
|
|
getFileModifiedTime,
|
|
removeFrontMatter,
|
|
sanitizeFileName,
|
|
showConfirmationDialog
|
|
} from "./lib/utils";
|
|
|
|
import {
|
|
DEFAULT_SETTINGS,
|
|
WikiDocsPluginSettings,
|
|
} from "./lib/config";
|
|
|
|
import {
|
|
ApiClient,
|
|
} from "./lib/api";
|
|
|
|
import {
|
|
addFrontMatterToFile,
|
|
extractMetadataFromFrontMatter,
|
|
getBookIdFromMetadata,
|
|
savePagesToMarkdown
|
|
} from "./lib/md";
|
|
|
|
export default class WikiDocsPlugin extends Plugin {
|
|
settings: WikiDocsPluginSettings;
|
|
apiClient: ApiClient;
|
|
|
|
|
|
async onload() {
|
|
await this.loadSettings();
|
|
this.apiClient = new ApiClient(this.settings);
|
|
|
|
// 툴바에 아이콘 추가
|
|
this.addRibbonIcon("book", "위키독스 책 목록 가져오기", async (evt: MouseEvent) => {
|
|
// 책 목록 가져오기 명령 실행
|
|
const bookId = await this.promptForBookSelection();
|
|
if (bookId) {
|
|
this.apiClient.downloadBook(this.app, bookId);
|
|
}
|
|
});
|
|
|
|
// 책 리스트에서 선택하여 가져오기
|
|
this.addCommand({
|
|
id: "fetch-selected-book",
|
|
name: "위키독스 책 목록 가져오기",
|
|
callback: async () => {
|
|
const bookId = await this.promptForBookSelection();
|
|
if (bookId) {
|
|
this.apiClient.downloadBook(this.app, bookId);
|
|
}
|
|
},
|
|
});
|
|
|
|
// 컨텍스트 메뉴에 항목 추가
|
|
this.registerEvent(
|
|
this.app.workspace.on("file-menu", (menu, file) => {
|
|
if (file instanceof TFolder) {
|
|
const metadataFilePath = `${file.path}/metadata.md`;
|
|
const metadataFile = this.app.vault.getAbstractFileByPath(metadataFilePath);
|
|
if (metadataFile instanceof TFile) {
|
|
// 위키독스로부터 내려받기
|
|
menu.addItem((item) => {
|
|
item.setTitle("위키독스 내려받기")
|
|
.setIcon("cloud-download")
|
|
.onClick(async () => {
|
|
const confirmed = await showConfirmationDialog(
|
|
"[주의] 이 책이 위키독스 기준으로 업데이트됩니다.\n" +
|
|
"'위키독스 보내기'로 수정한 데이터를 전송했는지 확인해 주세요.\n" +
|
|
"정말로 내려받으시겠습니까?"
|
|
);
|
|
if (confirmed) {
|
|
await this.syncFromServer(file);
|
|
} else {
|
|
// new Notice("동작이 취소되었습니다.");
|
|
}
|
|
});
|
|
});
|
|
|
|
// 위키독스로 보내기
|
|
menu.addItem((item) => {
|
|
item.setTitle("위키독스 보내기")
|
|
.setIcon("cloud-upload")
|
|
.onClick(async () => {
|
|
await this.syncToServer(file);
|
|
});
|
|
});
|
|
}
|
|
}
|
|
})
|
|
);
|
|
|
|
this.registerEvent(
|
|
this.app.vault.on("create", async (file) => {
|
|
if (file instanceof TFile && file.extension === "md") {
|
|
const content = await this.app.vault.read(file);
|
|
if (content.trim() === "") {
|
|
await addFrontMatterToFile(file); // Front Matter 추가
|
|
} else {
|
|
}
|
|
}
|
|
})
|
|
);
|
|
|
|
this.registerEvent(
|
|
this.app.vault.on("rename", async (file) => {
|
|
if (file instanceof TFile && file.extension === "md") {
|
|
addFrontMatterToFile(file);
|
|
}
|
|
})
|
|
);
|
|
|
|
// 설정 탭 추가
|
|
this.addSettingTab(new WikiDocsPluginSettingTab(this.app, this));
|
|
}
|
|
|
|
async onunload() {
|
|
}
|
|
|
|
async syncFromServer(folder: TFolder) {
|
|
const folderName = folder.name;
|
|
|
|
try {
|
|
// Step 1: 책 폴더와 페이지 삭제 (동기적 흐름)
|
|
await deleteFolderContents(folder);
|
|
|
|
// 최상위 폴더 삭제 상태 확인 (필요 시)
|
|
// await this.waitForFolderDeletion(folder.path);
|
|
|
|
// Step 2: 서버에서 책 데이터 가져오기
|
|
const bookId = await getBookIdFromMetadata(folder.path);
|
|
if (!bookId) {
|
|
new Notice(`책의 메타데이터가 존재하지 않습니다.`);
|
|
return;
|
|
}
|
|
|
|
const bookResponse = await this.apiClient.fetchWithAuth(`/books/${bookId}/`);
|
|
if (!bookResponse.ok) {
|
|
new Notice("책 내려받기가 실패했습니다.");
|
|
return;
|
|
}
|
|
|
|
const bookData = await bookResponse.json();
|
|
|
|
// Step 3: 책 데이터를 새로 저장
|
|
await savePagesToMarkdown(this.app, bookData.pages, folder.path);
|
|
this.app.workspace.trigger("iconize:refresh");
|
|
|
|
new Notice(`"${bookData.subject}" 책을 성공적으로 내려받았습니다.`);
|
|
} catch (error) {
|
|
console.error(`Failed to sync folder "${folderName}"`, error);
|
|
new Notice(`책 내려받기가 실패했습니다.`);
|
|
}
|
|
}
|
|
|
|
async syncToServer(folder: TFolder) {
|
|
const folderName = folder.name;
|
|
const files = this.app.vault.getFiles().filter((file) => file.path.startsWith(folder.path));
|
|
|
|
let changedCount = 0;
|
|
|
|
for (const file of files) {
|
|
try {
|
|
if (file.name === "metadata.md") {
|
|
// metadata.md 파일은 업로드하지 않음
|
|
continue;
|
|
}
|
|
|
|
const fileContent = await this.app.vault.read(file);
|
|
const metadata = await extractMetadataFromFrontMatter(file);
|
|
|
|
if (!metadata.id) {
|
|
console.error(`No ID found in Front Matter for file: ${file.path}`);
|
|
continue;
|
|
}
|
|
|
|
// parent_id가 null이거나 "null"인 경우 -1로 설정
|
|
if (
|
|
metadata.parent_id === null ||
|
|
(typeof metadata.parent_id === "string" && metadata.parent_id === "null")
|
|
) {
|
|
metadata.parent_id = -1;
|
|
}
|
|
|
|
// 동기화 시점 확인
|
|
const lastSynced = metadata.last_synced ? new Date(metadata.last_synced) : null;
|
|
const fileModifiedAt = getFileModifiedTime(file);
|
|
|
|
const needsSync =
|
|
!lastSynced ||
|
|
fileModifiedAt.getTime() - lastSynced.getTime() > 1000 || // 수정 시간 비교
|
|
sanitizeFileName(metadata.subject) !== sanitizeFileName(extractTitleFromFilePath(file.path)); // 제목 변경 감지
|
|
|
|
if (needsSync) {
|
|
console.log(`File "${file.path}" needs to be synced.`);
|
|
changedCount++;
|
|
|
|
const contentWithoutFrontMatter = ensureLineBreaks(removeFrontMatter(fileContent));
|
|
|
|
// 이미지 파일 처리
|
|
const embeddedImages = extractEmbeddedImages(contentWithoutFrontMatter);
|
|
if (metadata.id != -1) { // 신규 파일이 아닌 경우에만 이미지 업로드
|
|
await this.apiClient.uploadImagesForPage(this.app, metadata.id, embeddedImages);
|
|
}
|
|
|
|
// 서버에 업데이트
|
|
metadata.subject = extractTitleFromFilePath(file.path);
|
|
const page_id = await this.apiClient.updatePageOnServer(metadata, contentWithoutFrontMatter);
|
|
|
|
if (metadata.id == -1) { // 신규 파일인 경우에 이미지 업로드후 저장 한번 더!!
|
|
await this.apiClient.uploadImagesForPage(this.app, page_id, embeddedImages);
|
|
await this.apiClient.updatePageOnServer(metadata, contentWithoutFrontMatter);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error(`Failed to sync file to server: ${file.path}`, error);
|
|
}
|
|
}
|
|
|
|
if (changedCount > 0) {
|
|
await this.syncFromServer(folder);
|
|
}
|
|
|
|
new Notice(`책을 성공적으로 내보냈습니다!`);
|
|
}
|
|
|
|
|
|
async promptForBookSelection(): Promise<number | null> {
|
|
const response = await this.apiClient.fetchWithAuth(`/books/`);
|
|
if (!response.ok) {
|
|
new Notice("책 목록을 가져오지 못했습니다.");
|
|
return null;
|
|
}
|
|
const books = await response.json();
|
|
|
|
return new Promise((resolve) => {
|
|
const modal = new Modal(this.app);
|
|
|
|
modal.onClose = () => resolve(null);
|
|
|
|
// 헤더
|
|
const header = modal.contentEl.createEl("h2", {
|
|
text: "위키독스 책을 선택해 주세요.",
|
|
});
|
|
header.classList.add("book-selection-header");
|
|
|
|
// 리스트
|
|
const list = modal.contentEl.createEl("ul");
|
|
list.classList.add("book-selection-list");
|
|
|
|
books.forEach((book: { id: number; subject: string }) => {
|
|
const listItem = list.createEl("li", {
|
|
text: book.subject,
|
|
});
|
|
listItem.classList.add("book-selection-list-item");
|
|
|
|
listItem.addEventListener("click", () => {
|
|
resolve(book.id);
|
|
modal.close();
|
|
});
|
|
});
|
|
|
|
// 빈 목록 처리
|
|
if (books.length === 0) {
|
|
const emptyMessage = list.createEl("li", {
|
|
text: "책 목록이 비어 있습니다.",
|
|
});
|
|
emptyMessage.classList.add("book-selection-empty-message");
|
|
}
|
|
|
|
modal.open();
|
|
});
|
|
}
|
|
|
|
|
|
// 설정 저장
|
|
async loadSettings() {
|
|
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
|
|
}
|
|
|
|
async saveSettings() {
|
|
await this.saveData(this.settings);
|
|
}
|
|
}
|
|
|
|
class WikiDocsPluginSettingTab extends PluginSettingTab {
|
|
plugin: WikiDocsPlugin;
|
|
|
|
constructor(app: App, plugin: WikiDocsPlugin) {
|
|
super(app, plugin);
|
|
this.plugin = plugin;
|
|
}
|
|
|
|
display(): void {
|
|
const { containerEl } = this;
|
|
containerEl.empty();
|
|
|
|
containerEl.createEl("h2", { text: "플러그인 설정" });
|
|
|
|
new Setting(containerEl)
|
|
.setName("API Base URL")
|
|
.setDesc("API의 기본 URL을 설정합니다.")
|
|
.addText((text) => {
|
|
text
|
|
.setPlaceholder("https://wikidocs.net/napi")
|
|
.setValue(this.plugin.settings.apiBaseUrl)
|
|
.onChange(async (value) => {
|
|
this.plugin.settings.apiBaseUrl = value;
|
|
await this.plugin.saveSettings();
|
|
});
|
|
|
|
// 클래스 추가
|
|
text.inputEl.classList.add("plugin-setting-input");
|
|
});
|
|
|
|
new Setting(containerEl)
|
|
.setName("API Token")
|
|
.setDesc("API 인증 토큰을 입력합니다.")
|
|
.addText((text) => {
|
|
text
|
|
.setPlaceholder("위키독스에서 발급한 토큰을 입력해 주세요.")
|
|
.setValue(this.plugin.settings.apiToken)
|
|
.onChange(async (value) => {
|
|
this.plugin.settings.apiToken = value;
|
|
await this.plugin.saveSettings();
|
|
});
|
|
|
|
// 클래스 추가
|
|
text.inputEl.classList.add("plugin-setting-input");
|
|
});
|
|
}
|
|
}
|