Merge pull request #19 from liufree/dev

feat: add dark theme
This commit is contained in:
liufree 2025-05-10 01:27:37 +08:00 committed by GitHub
commit b335970c0b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 459 additions and 53 deletions

View file

@ -1,7 +1,7 @@
{
"id": "querydash",
"name": "QueryDash",
"version": "1.0.5",
"version": "1.0.6",
"minAppVersion": "1.8.0",
"description": "Refer to Dataview and add search, sorting, and pagination functions, just like Notion.",
"author": "lwx",

View file

@ -0,0 +1,242 @@
import {PlusOutlined} from '@ant-design/icons';
import type {ActionType, ProColumns} from '@ant-design/pro-components';
import {
EditableProTable,
ProCard,
ProFormField,
} from '@ant-design/pro-components';
import type {InputRef} from 'antd';
import {Button, Form, Input, Space, Tag} from 'antd';
import React, {useRef, useState} from 'react';
const waitTime = (time: number = 100) => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(true);
}, time);
});
};
const TagList: React.FC<{
value?: {
key: string;
label: string;
}[];
onChange?: (
value: {
key: string;
label: string;
}[],
) => void;
}> = ({value, onChange}) => {
const ref = useRef<InputRef | null>(null);
const [newTags, setNewTags] = useState<
{
key: string;
label: string;
}[]
>([]);
const [inputValue, setInputValue] = useState<string>('');
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setInputValue(e.target.value);
};
const handleInputConfirm = () => {
let tempsTags = [...(value || [])];
if (
inputValue &&
tempsTags.filter((tag) => tag.label === inputValue).length === 0
) {
tempsTags = [
...tempsTags,
{key: `new-${tempsTags.length}`, label: inputValue},
];
}
onChange?.(tempsTags);
setNewTags([]);
setInputValue('');
};
return (
<Space>
{(value || []).concat(newTags).map((item) => (
<Tag key={item.key}>{item.label}</Tag>
))}
<Input
ref={ref}
type="text"
size="small"
style={{width: 78}}
value={inputValue}
onChange={handleInputChange}
onBlur={handleInputConfirm}
onPressEnter={handleInputConfirm}
/>
</Space>
);
};
type DataSourceType = {
id: React.Key;
title?: string;
labels?: {
key: string;
label: string;
}[];
state?: string;
created_at?: number;
children?: DataSourceType[];
};
const defaultData: DataSourceType[] = [
{
id: 624748504,
title: '活动名称一',
labels: [{key: 'woman', label: '川妹子'}],
state: 'open',
created_at: 1590486176000,
},
{
id: 624691229,
title: '活动名称二',
labels: [{key: 'man', label: '西北汉子'}],
state: 'closed',
created_at: 1590481162000,
},
];
const columns: ProColumns<DataSourceType>[] = [
{
title: '活动名称',
dataIndex: 'title',
formItemProps: {
rules: [
{
required: true,
message: '此项为必填项',
},
],
},
width: '30%',
},
{
title: '状态',
key: 'state',
dataIndex: 'state',
valueType: 'select',
valueEnum: {
all: {text: '全部', status: 'Default'},
open: {
text: '未解决',
status: 'Error',
},
closed: {
text: '已解决',
status: 'Success',
},
},
},
{
title: '标签',
dataIndex: 'labels',
width: '20%',
formItemProps: {
rules: [
{
required: true,
message: '此项为必填项',
},
],
},
renderFormItem: (_, {isEditable}) => {
return isEditable ? <TagList/> : <Input/>;
},
render: (_, row) =>
row?.labels?.map((item) => <Tag key={item.key}>{item.label}</Tag>),
},
{
title: '操作',
valueType: 'option',
width: 250,
render: (text, record, _, action) => [
<a
key="editable"
onClick={() => {
action?.startEditable?.(record.id);
}}
>
</a>,
<EditableProTable.RecordCreator
key="copy"
record={{
...record,
id: (Math.random() * 1000000).toFixed(0),
}}
>
<a></a>
</EditableProTable.RecordCreator>,
],
},
];
export default () => {
const [editableKeys, setEditableRowKeys] = useState<React.Key[]>([]);
const [dataSource, setDataSource] = useState<readonly DataSourceType[]>([]);
const [form] = Form.useForm();
return (
<>
<EditableProTable<DataSourceType>
rowKey="id"
scroll={{
x: 960,
}}
headerTitle="可编辑表格"
maxLength={5}
// 关闭默认的新建按钮
recordCreatorProps={{
position: 'top',
record: () => ({id: (Math.random() * 1000000).toFixed(0)}),
}}
columns={columns}
request={async () => ({
data: defaultData,
total: 3,
success: true,
})}
value={dataSource}
onChange={setDataSource}
search={{
labelWidth: 'auto',
}}
pagination={{
pageSize: 5,
onChange: (page) => console.log(page),
}}
editable={{
form,
editableKeys,
onSave: async () => {
await waitTime(2000);
},
onChange: setEditableRowKeys,
actionRender: (row, config, dom) => [dom.save, dom.cancel],
}}
/>
<ProCard title="表格数据" headerBordered collapsible defaultCollapsed>
<ProFormField
ignoreFormItem
fieldProps={{
style: {
width: '100%',
},
}}
mode="read"
valueType="jsonCode"
text={JSON.stringify(dataSource)}
/>
</ProCard>
</>
);
};

View file

@ -1,29 +1,45 @@
import React, {useEffect} from 'react';
import React, {useEffect, useState} from 'react';
import {App} from "obsidian";
import TableView from "./tableview/TableView";
import ListView from "./listview/ListView";
import {ConfigProvider, ConfigProviderProps, Timeline} from "antd";
import {ConfigProvider, theme} from "antd";
import enUS from 'antd/locale/en_US';
import TimeLineView from "./timelineview/TimeLineView";
import KanbanView from "./kanbanview/KanbanView";
interface QueryDashViewDashs {
app: App; // Obsidian app
source: string;
}
const QueryDashView: React.FC<QueryDashViewDashs> = ({app, source}) => {
const [sourceType, setSourceType] = React.useState<string>("table");
const [sourceType, setSourceType] = useState<string>("table");
const [isDarkTheme, setIsDarkTheme] = useState<boolean>(
document.body.classList.contains("theme-dark")
);
useEffect(() => {
// 获取第一个单词
// Detect Obsidian's theme
const updateTheme = () => {
const isDark = document.body.classList.contains("theme-dark");
setIsDarkTheme(isDark);
};
// Initial theme detection
updateTheme();
// Listen for theme changes
const observer = new MutationObserver(updateTheme);
observer.observe(document.body, {attributes: true, attributeFilter: ["class"]});
return () => observer.disconnect();
}, []);
useEffect(() => {
// Get the first word of the source
const sourceType = source.split(" ")[0].toLowerCase();
setSourceType(sourceType);
}, []);
}, [source]);
const getView = (app: App, source: string) => {
if (sourceType === "table") {
@ -35,13 +51,23 @@ const QueryDashView: React.FC<QueryDashViewDashs> = ({app, source}) => {
} else if (sourceType === "task") {
return <KanbanView app={app} source={source}/>;
}
}
};
return (
<ConfigProvider locale={enUS}>
<ConfigProvider
locale={enUS}
theme={{
algorithm: isDarkTheme ? theme.darkAlgorithm : theme.defaultAlgorithm,
// token: {
// colorPrimary: isDarkTheme ? "#1E90FF" : "#1890FF", // 自定义主色
// colorBgBase: isDarkTheme ? "#1F1F1F" : "#FFFFFF", // 自定义背景色
// colorText: isDarkTheme ? "#E0E0E0" : "#000000", // 自定义文本颜色
// },
}}
>
{getView(app, source)}
</ConfigProvider>
)
);
};
export default QueryDashView;

View file

@ -0,0 +1,77 @@
import React, {useState} from "react";
import {Input, Typography} from "antd";
import {EditOutlined} from "@ant-design/icons";
import {TFile} from "obsidian";
const {Text, Link} = Typography;
const EditableText: React.FC<{ app: any, item: any; onSave: (newText: string) => void; }> = ({app, item, onSave}) => {
const [isEditing, setIsEditing] = useState(false);
const [editableText, setEditableText] = useState(item.text);
const handleSave = () => {
setIsEditing(false);
onSave(editableText); // 保存编辑后的值
};
const getSelectionState = (item: any) => {
const selectionState = {
eState: {
cursor: {
from: {line: item.line, ch: item.position.start.col},
to: {line: item.line + item.lineCount - 1, ch: item.position.end.col},
},
line: item.line,
},
};
return selectionState;
};
const ellipsisLink = (item: any, display: string, path: string) => (
<Link
onClick={() => {
const selectionState = getSelectionState(item);
// MacOS interprets the Command key as Meta.
app.workspace.openLinkText(
item.link.toFile().obsidianLink(),
item.path,
false,
selectionState as any
);
/*const file = app.vault.getAbstractFileByPath(path);
if (file && file instanceof TFile) {
const leaf = app.workspace.getLeaf();
leaf.openFile(file);
}*/
}}
>
<Text
ellipsis={{expanded: true}}
>
{display}
</Text>
</Link>
);
return isEditing ? (
<Input
value={editableText}
onChange={(e) => setEditableText(e.target.value)}
onBlur={handleSave} // 失去焦点时保存
onPressEnter={handleSave} // 按下回车时保存
autoFocus
/>
) : (
<div style={{display: "flex"}}>
{ellipsisLink(item, editableText, item.path)}
<EditOutlined
style={{marginLeft: 8, cursor: "pointer"}}
onClick={() => setIsEditing(true)} // 点击图标进入编辑模式
/>
</div>
);
};
export default EditableText;

View file

@ -1,9 +1,11 @@
import React, {useEffect, useState} from "react";
import {ProCard} from "@ant-design/pro-components";
import {Checkbox, CheckboxChangeEvent, CheckboxProps, Typography} from "antd";
import {Checkbox, CheckboxChangeEvent, CheckboxProps, Col, Input, Row, Typography} from "antd";
import {getAPI, STask} from "obsidian-dataview";
import {ViewProps} from "../../models/ViewProps";
import {TFile, Vault} from "obsidian";
import EditableText from "./EditableText";
import EditableTable from "../../components/EditableTable";
const {Text, Link} = Typography;
@ -21,21 +23,29 @@ const KanbanView: React.FC<ViewProps> = ({app, source}) => {
leaf.openFile(file);
}
}}
>
<Text ellipsis>{display}</Text>
>
<Text
style={display ? {maxWidth: 200, color: '#1890ff', cursor: 'pointer'} : {
color: '#1890ff',
cursor: 'pointer'
}}
ellipsis={{expanded: true}}
>
</Text>
</Link>
);
const parseTaskData = (value: any) => {
const tasks = value.values || [];
const groupedTasks: Record<string, any[]> = {
done: [],
doing: [],
todo: [],
done: [],
};
tasks.forEach((task: any) => {
const status = task.status === "x" ? "done" : task.status === "?" ? "doing" : "todo";
const status = task.status === "x" ? "done" : "todo";
groupedTasks[status].push({
...task,
type: "link",
@ -48,24 +58,25 @@ const KanbanView: React.FC<ViewProps> = ({app, source}) => {
}));
};
const executeTaskQuery = async (dvApi: any, source: any) => {
const queryResult = await dvApi.query(source);
console.log("queryResult", queryResult);
if (queryResult.successful) {
return parseTaskData(queryResult.value);
}
return [];
const executeTaskQuery = (dvApi: any, source: any) => {
dvApi.query(source).then((result: any) => {
console.log("queryResult", result);
if (result.successful) {
const data = parseTaskData(result.value);
console.log("Data:", data); // 输出 data
setColumns([...data]);
}
});
};
const onChange: (e: CheckboxChangeEvent, item: STask) => void = async (e, item) => {
console.log("completed", e.target.checked);
const onChange: (checked: boolean, updateText: string, item: STask) => void = async (checked, updateText, item) => {
console.log("completed", checked);
console.log("item", item);
const completed = e.target.checked;
const completed = checked;
const status = completed ? "x" : " ";
console.log("status", status);
let updatedText: string = item.text;
await rewriteTask(app.vault, item, status, updatedText)
setRefresh(true)
let updatedText: string = updateText;
await rewriteTask(app.vault, item, status, updatedText);
};
@ -118,34 +129,84 @@ const KanbanView: React.FC<ViewProps> = ({app, source}) => {
let newText = filetext.join(hasRN ? "\r\n" : "\n");
console.log("newText", newText);
console.log("taskPath", task.path);
await vault.adapter.write(task.path, newText);
vault.adapter.write(task.path, newText).then((res) => {
});
}
useEffect(() => {
executeTaskQuery(dv, source)
.then((data) => setColumns(JSON.parse(JSON.stringify(data))))
.catch((error) => console.error("Error executing query:", error));
}, [app, source, refresh]);
useEffect(() => {
const onModify = (file: TFile) => {
setTimeout(() => {
setRefresh((prev) => !prev); // 触发刷新
}, 300); // 延迟 500 毫秒,可根据实际情况调整
console.log("文件已修改:", file.path);
};
const onCreate = (file: TFile) => {
console.log("文件已创建:", file.path);
setRefresh(!refresh); // 触发刷新
};
const onDelete = (file: TFile) => {
console.log("文件已删除:", file.path);
setRefresh(!refresh); // 触发刷新
};
// 监听文件事件
app.vault.on("modify", onModify);
app.vault.on("create", onCreate);
app.vault.on("delete", onDelete);
// 清理事件监听器
return () => {
app.vault.off("modify", onModify);
app.vault.off("create", onCreate);
app.vault.off("delete", onDelete);
};
}, [app]);
return (
<ProCard ghost gutter={8}>
{columns.map((column) => (
<ProCard key={column.title} title={column.title} bordered>
{column.items.map((item: any, index: number) => (
<>
<ProCard key={index} bordered>
<Checkbox onChange={(e) => onChange(e, item)} checked={item.checked}>
{item.type === "link"
? ellipsisLink(item.text, item.path)
: <Text>{item.text}</Text>}
</Checkbox>
</ProCard>
</>
))}
</ProCard>
))}
</ProCard>
<>
{/*<EditableTable></EditableTable>*/}
<ProCard ghost gutter={8}>
{columns.map((column) => (
<ProCard key={column.title} title={column.title} bordered>
{column.items.map((item: any, index: number) => (
<>
<ProCard key={index} bordered>
<Row>
<Col span={1}>
<Checkbox
onChange={(e) => onChange(e.target.checked, item.text, item)}
checked={item.checked}
/>
</Col>
<Col span={18}>
<EditableText
app={app}
item={item}
onSave={(newText) => {
console.log("保存的新文本:", newText);
onChange(item.checked, newText, item);
}}
/>
</Col>
</Row>
</ProCard>
</>
))}
</ProCard>
))}
</ProCard>
</>
);
};