mirror of
https://github.com/chen2226/obsidian-typecho.git
synced 2026-07-22 05:47:58 +00:00
分类面板-分类下属文章、(未完)
This commit is contained in:
parent
a34aec74bf
commit
d1fc492918
9 changed files with 359 additions and 11 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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": "标题",
|
||||
|
|
|
|||
2
main.ts
2
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,
|
||||
|
|
|
|||
129
styles.css
129
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);
|
||||
}
|
||||
|
|
|
|||
38
view/article_preview.ts
Normal file
38
view/article_preview.ts
Normal file
|
|
@ -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 时间匹配
|
||||
}
|
||||
}
|
||||
168
view/browse_articles.ts
Normal file
168
view/browse_articles.ts
Normal file
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
// 初始展开
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
Loading…
Reference in a new issue