diff --git a/locales/en.json b/locales/en.json index 2233f66..bf781d5 100644 --- a/locales/en.json +++ b/locales/en.json @@ -6,7 +6,12 @@ "get": "get", "empty": "Cannot be empty", "input": "Please input", - "enter": "(Press Enter to add)" + "enter": "(Press Enter to add)", + "loading": "Loading...", + "previousPage": "Previous Page", + "nextPage": "Next Page", + "noDataFound": "No data found", + "errorOccurred": "An error occurred" }, "sync": { "title": "Sync to Typecho", @@ -19,7 +24,9 @@ "noFileSelected": "No file selected", "titleOrContentCannotBeEmpty": "Title or content cannot be empty", "readFileError": "Read file error, please check the file content", - "noTypechoUser": "No Typecho user, please input the user who uses the operation" + "noTypechoUser": "No Typecho user, please input the user who uses the operation", + "failedToFetchArticles": "Failed to fetch articles", + "failedToFetchPostDetails": "Failed to fetch post details" }, "field": { "title": "title", diff --git a/locales/zh.json b/locales/zh.json index ebad838..52a1d80 100644 --- a/locales/zh.json +++ b/locales/zh.json @@ -6,7 +6,12 @@ "get": "获取", "empty": "不能为空", "input": "请输入", - "enter": "(按回车添加)" + "enter": "(按回车添加)", + "loading": "加载中...", + "previousPage": "上一页", + "nextPage": "下一页", + "noDataFound": "没有找到数据", + "errorOccurred": "发生错误" }, "sync": { "title": "同步到Typecho", @@ -19,7 +24,9 @@ "noFileSelected": "未选择文件", "titleOrContentCannotBeEmpty": "标题或内容不能为空", "readFileError": "读取文件错误,请检查文件内容", - "noTypechoUser": "未设置 Typecho 用户" + "noTypechoUser": "未设置 Typecho 用户", + "failedToFetchArticles": "获取文章列表失败", + "failedToFetchPostDetails": "获取文章详情失败" }, "field": { "title": "标题", diff --git a/main.ts b/main.ts index b5b2c8e..1d680a2 100644 --- a/main.ts +++ b/main.ts @@ -1,6 +1,6 @@ import { Plugin, addIcon } from "obsidian"; import { settingTab } from "./setting/setting_tab"; -import { PushModal } from "./view/push_modal"; +import { PushModal } from "./view/push"; import { CategoryView, VIEW_TYPE as ArticleViewType } from "./view/categories"; import { TypechoPluginSettings, diff --git a/styles.css b/styles.css index 9d2a15a..767452e 100644 --- a/styles.css +++ b/styles.css @@ -109,3 +109,132 @@ background: #333; border-left: 1px dashed #444; } + +.browse-articles-modal { + max-width: 800px; + margin: 0 auto; +} + +.browse-articles-modal .browse-article-item { + padding: 1rem; + border-bottom: 1px solid var(--background-modifier-border); + transition: background-color 0.2s; +} + +.browse-articles-modal .browse-article-item:hover { + background-color: var(--background-secondary); +} + +.browse-articles-modal .article-title { + font-size: 1.1rem; + margin-bottom: 0.5rem; +} + +.browse-articles-modal .article-title .download-icon { + margin-left: 1rem; + font-size: 0.9rem; + color: var(--text-muted); + cursor: pointer; +} + +.browse-articles-modal .article-title a { + color: var(--text-link); + text-decoration: none; +} + +.browse-articles-modal .article-date { + color: var(--text-muted); + font-size: 0.9em; +} + +.browse-articles-modal .pagination { + display: flex; + justify-content: center; + align-items: center; + margin-top: 1.5rem; + gap: 1rem; +} + +.browse-articles-modal .pagination button { + padding: 0.4rem 0.8rem; + border-radius: var(--radius); + border: 1px solid var(--background-modifier-border); + background-color: var(--background-primary); + color: var(--text-normal); + cursor: pointer; + transition: all 0.2s; +} + +.browse-articles-modal .pagination button:hover:not(:disabled) { + background-color: var(--interactive-accent); + color: var(--background-primary); + border-color: var(--interactive-accent); +} + +.browse-articles-modal .pagination button:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.browse-articles-modal .no-articles { + padding: 2rem; + text-align: center; + color: var(--text-muted); +} + +.preview-popup { + position: absolute; + background-color: var(--background-secondary); + color: var(--text-normal); + border: 1px solid var(--background-modifier-border); + border-radius: var(--radius); + padding: 0.75rem 1rem; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + z-index: 9999; + font-size: 0.9em; + max-width: 300px; + word-break: break-word; + transition: opacity 0.2s ease-in-out; +} + +.preview-popup::after { + content: ""; + position: absolute; + top: -6px; + left: 16px; + border-width: 0 6px 6px 6px; + border-style: solid; + border-color: transparent transparent var(--background-secondary) transparent; +} + +.article-preview-modal { + max-width: 80vw; + max-height: 80vh; + overflow: auto; + opacity: 1; + transform: scale(1); + transition: opacity 0.3s ease, transform 0.3s ease; +} + +/* 初始状态:透明 + 缩小 */ +.article-preview-modal.fade-enter { + opacity: 0; + transform: scale(0.95); +} + +/* 进入动画激活态 */ +.article-preview-modal.fade-enter-active { + opacity: 1; + transform: scale(1); +} + +/* 退出动画激活态 */ +.article-preview-modal.fade-exit { + opacity: 1; + transform: scale(1); +} + +.article-preview-modal.fade-exit-active { + opacity: 0; + transform: scale(0.95); +} diff --git a/view/add_metas.ts b/view/add_metasl.ts similarity index 100% rename from view/add_metas.ts rename to view/add_metasl.ts diff --git a/view/article_preview.ts b/view/article_preview.ts new file mode 100644 index 0000000..2db17e5 --- /dev/null +++ b/view/article_preview.ts @@ -0,0 +1,38 @@ +import { App, Modal } from "obsidian"; + +export class ArticlePreview extends Modal { + private animationTimeout: number | null = null; + private htmlContent: string; + + constructor(app: App, htmlContent: string) { + super(app); + this.htmlContent = htmlContent; + } + + onOpen() { + const { contentEl } = this; + contentEl.empty(); + contentEl.addClass("article-preview-modal"); + + // 初始状态:透明 + 缩小 + contentEl.addClass("fade-enter"); + contentEl.innerHTML = this.htmlContent; + + // 延迟触发进入动画,确保浏览器能识别初始状态 + requestAnimationFrame(() => { + contentEl.removeClass("fade-enter"); + contentEl.addClass("fade-enter-active"); + }); + } + + onClose() { + const { contentEl } = this; + contentEl.addClass("fade-exit"); + contentEl.removeClass("fade-enter-active"); + + // 设置动画持续时间,确保动画完成后再清空内容 + this.animationTimeout = window.setTimeout(() => { + contentEl.empty(); + }, 300); // 与 CSS 中的 transition 时间匹配 + } +} diff --git a/view/browse_articles.ts b/view/browse_articles.ts new file mode 100644 index 0000000..d49e131 --- /dev/null +++ b/view/browse_articles.ts @@ -0,0 +1,168 @@ +import { App, Modal, htmlToMarkdown, setIcon } from "obsidian"; +import { HttpUtils } from "../utils/request"; +import i18n from "../utils/i18n"; +import { ArticlePreview } from "./article_preview"; + +export class BrowseArticles extends Modal { + data: any; + currentPage = 1; + totalPages = 1; + contentContainer: HTMLElement; + + constructor(app: App, data: any) { + super(app); + this.data = data; + this.modalEl.addClass("browse-articles-modal"); + } + + async onOpen() { + const { contentEl } = this; + contentEl.empty(); + + const header = contentEl.createEl("div", { cls: "modal-header" }); + header.createEl("h2", { + text: `${this.data.name}-${i18n.t("field.article")}`, + }); + + // 创建内容容器 + this.contentContainer = contentEl.createEl("div", { + cls: "modal-content", + }); + + await this.loadPage(this.currentPage); + } + + async loadPage(page: number) { + this.contentContainer.empty(); + const loadingEl = this.contentContainer.createEl("div", { + text: i18n.t("common.loading"), + }); + try { + const params = { + page: page.toString(), + pageSize: "10", + filterType: "category", + filterSlug: this.data.slug, + }; + + let url = "/posts?"; + for (const key in params) { + url += `${key}=${params[key as keyof typeof params]}&`; + } + + const response = await HttpUtils.get(url); + + if ( + response.status === "success" && + response.data?.dataSet?.length > 0 + ) { + loadingEl.setText(""); + this.totalPages = response.data.pages; + this.currentPage = page; + response.data.dataSet.forEach((post: any) => { + const postEl = this.contentContainer.createEl("div", { + cls: "browse-article-item", + }); + // 标题部分 + const titleEl = postEl.createEl("div", { + cls: "article-title", + }); + titleEl.createEl("a", { + text: post.title, + href: post.permalink, + attr: { target: "_blank" }, + }); + + // 日期部分 + postEl.createEl("div", { + cls: "article-date", + text: `${post.year}-${post.month}-${post.day}`, + }); + + // 保存本地 + const downloadIcon = titleEl.createEl("span", { + cls: "download-icon", + }); + setIcon(downloadIcon, "hard-drive-download"); + downloadIcon.addEventListener("click", async (e) => { + const content = htmlToMarkdown(post.digest).toString(); + // 保存本地根目录 + const rootPath = this.app.vault.getRoot().path; + const fileName = `${post.title}.md`; + const filePath = `${rootPath}/${fileName}`; + this.app.vault.create(filePath, content) + .then(() => { + console.log('ok') + // this.app.workspace.openLinkText(filePath, ""); + }); + }); + + // 点击预览内容 + postEl.addEventListener("click", (e) => { + const target = e.target as HTMLElement; + if (target.closest("a") || target.closest("svg")) { + return; + } + e.stopPropagation(); + e.preventDefault(); + new ArticlePreview(this.app, post.digest).open(); + }); + }); + + // 清除旧的分页控件再重新渲染 + this.paginationEl?.remove(); + this.renderPagination(); + } else { + this.contentContainer.createEl("p", { + cls: "no-articles", + text: i18n.t("field.no_articles_found"), + }); + } + } catch (error) { + loadingEl.setText(""); + this.contentContainer.createEl("p", { + text: i18n.t("common.error_occurred"), + }); + console.error("Failed to fetch articles:", error); + } + } + + private paginationEl: HTMLElement | null = null; + renderPagination() { + // 先清除旧的分页控件 + this.paginationEl?.remove(); + + // 创建新的分页控件并保存引用 + this.paginationEl = this.contentContainer.createEl("div", { + cls: "pagination", + }); + + const prevBtn = this.paginationEl.createEl("button", { + text: "上一页", + }); + prevBtn.disabled = this.currentPage <= 1; + prevBtn.addEventListener("click", () => { + if (this.currentPage > 1) { + this.loadPage(this.currentPage - 1); + } + }); + + this.paginationEl.createEl("span", { + text: `第 ${this.currentPage} 页 / 共 ${this.totalPages} 页`, + }); + + const nextBtn = this.paginationEl.createEl("button", { + text: "下一页", + }); + nextBtn.disabled = this.currentPage >= this.totalPages; + nextBtn.addEventListener("click", () => { + if (this.currentPage < this.totalPages) { + this.loadPage(this.currentPage + 1); + } + }); + } + onClose() { + const { contentEl } = this; + contentEl.empty(); + } +} diff --git a/view/categories.ts b/view/categories.ts index dd7990c..6492777 100644 --- a/view/categories.ts +++ b/view/categories.ts @@ -4,9 +4,9 @@ import i18n from "../utils/i18n"; import { getSettings } from "../main"; import { HttpUtils } from "../utils/request"; import { Util } from "../utils/util"; +import { BrowseArticles } from "./browse_articles"; const VIEW_TYPE = "category-view"; - class CategoryView extends ItemView { getViewType(): string { return VIEW_TYPE; @@ -112,7 +112,7 @@ class CategoryView extends ItemView { linkIcon.addEventListener("click", (e) => { e.stopPropagation(); window.open(node.url); - }) + }); // 显示节点标签 wrapper.createEl("span", { text: node.name, @@ -127,9 +127,8 @@ class CategoryView extends ItemView { setIcon(articleIcon, "newspaper"); articleIcon.addEventListener("click", (e) => { - console.log(e) - console.log(node) - }) + new BrowseArticles(this.app, node).open(); + }); } // 初始展开 diff --git a/view/push_modal.ts b/view/push.ts similarity index 99% rename from view/push_modal.ts rename to view/push.ts index c743696..16c2ef5 100644 --- a/view/push_modal.ts +++ b/view/push.ts @@ -1,6 +1,6 @@ import { App, Modal, Notice } from "obsidian"; import { getSettings } from "../main"; -import { addMetas } from "./add_metas"; +import { addMetas } from "./add_metasl"; import { HttpUtils } from "../utils/request"; import i18n from "../utils/i18n"; export class PushModal extends Modal {