This commit is contained in:
wudanyang 2025-07-24 02:51:01 +08:00
parent efb942c967
commit 2b8cc4b1be
5 changed files with 3419 additions and 86 deletions

4
.gitignore vendored
View file

@ -18,5 +18,9 @@ main.js
# obsidian
data.json
# JXA scripts (generated at runtime)
ticktick-fetch.js
ticktick-toggle.js
# Exclude macOS Finder (System Explorer) View States
.DS_Store

863
main.ts
View file

@ -1,85 +1,284 @@
import { App, Editor, MarkdownView, Modal, Notice, Plugin, PluginSettingTab, Setting } from 'obsidian';
import {
App,
Plugin,
PluginSettingTab,
Setting,
ItemView,
WorkspaceLeaf,
Notice,
TFile,
moment
} from 'obsidian';
// Remember to rename these classes and interfaces!
interface MyPluginSettings {
mySetting: string;
// macOS JXA interface for TickTick
declare global {
interface Window {
require?: any;
}
}
const DEFAULT_SETTINGS: MyPluginSettings = {
mySetting: 'default'
const VIEW_TYPE_TODAY_TASKS = "today-tasks-view";
interface TickTickTodaySettings {
refreshInterval: number; // in minutes
showCompleted: boolean;
autoRefresh: boolean;
tickTickAppName: string;
}
export default class MyPlugin extends Plugin {
settings: MyPluginSettings;
const DEFAULT_SETTINGS: TickTickTodaySettings = {
refreshInterval: 5,
showCompleted: true,
autoRefresh: true,
tickTickAppName: 'TickTick'
}
interface Task {
text: string;
completed: boolean;
file?: string;
line?: number;
priority: string;
dueDate?: string;
id?: string;
project?: string;
projectId?: string; // 原始项目ID用于构建链接
tags?: string[];
pinned?: boolean;
}
interface TickTickTask {
title: string;
isCompleted: boolean;
priority: number;
dueDate?: string;
project?: string;
tags?: string[];
id: string;
}
export default class TickTickTodayPlugin extends Plugin {
settings: TickTickTodaySettings;
private refreshInterval: NodeJS.Timeout | null = null;
private projectsCache: Map<string, string> = new Map(); // projectId -> projectName
private jxaScriptPaths: {
fetch: string;
toggle: string;
} | null = null;
getJXAScriptPaths() {
return this.jxaScriptPaths;
}
async onload() {
await this.loadSettings();
// This creates an icon in the left ribbon.
const ribbonIconEl = this.addRibbonIcon('dice', 'Sample Plugin', (evt: MouseEvent) => {
// Called when the user clicks the icon.
new Notice('This is a notice!');
// Create JXA script files on startup
await this.createJXAScripts();
// Register the view
this.registerView(
VIEW_TYPE_TODAY_TASKS,
(leaf) => new TodayTasksView(leaf, this)
);
// Add ribbon icon
const ribbonIconEl = this.addRibbonIcon('calendar-check', 'Today\'s Tasks', (evt: MouseEvent) => {
this.activateView();
});
// Perform additional things with the ribbon
ribbonIconEl.addClass('my-plugin-ribbon-class');
ribbonIconEl.addClass('ticktick-today-ribbon-class');
// This adds a status bar item to the bottom of the app. Does not work on mobile apps.
const statusBarItemEl = this.addStatusBarItem();
statusBarItemEl.setText('Status Bar Text');
// This adds a simple command that can be triggered anywhere
// Add command to open today's tasks
this.addCommand({
id: 'open-sample-modal-simple',
name: 'Open sample modal (simple)',
id: 'open-today-tasks',
name: 'Open Today\'s Tasks',
callback: () => {
new SampleModal(this.app).open();
this.activateView();
}
});
// This adds an editor command that can perform some operation on the current editor instance
// Add command to refresh tasks
this.addCommand({
id: 'sample-editor-command',
name: 'Sample editor command',
editorCallback: (editor: Editor, view: MarkdownView) => {
console.log(editor.getSelection());
editor.replaceSelection('Sample Editor Command');
}
});
// This adds a complex command that can check whether the current state of the app allows execution of the command
this.addCommand({
id: 'open-sample-modal-complex',
name: 'Open sample modal (complex)',
checkCallback: (checking: boolean) => {
// Conditions to check
const markdownView = this.app.workspace.getActiveViewOfType(MarkdownView);
if (markdownView) {
// If checking is true, we're simply "checking" if the command can be run.
// If checking is false, then we want to actually perform the operation.
if (!checking) {
new SampleModal(this.app).open();
}
// This command will only show up in Command Palette when the check function returns true
return true;
}
id: 'refresh-today-tasks',
name: 'Refresh Today\'s Tasks',
callback: () => {
this.refreshTasks();
}
});
// This adds a settings tab so the user can configure various aspects of the plugin
this.addSettingTab(new SampleSettingTab(this.app, this));
// Add settings tab
this.addSettingTab(new TickTickTodaySettingTab(this.app, this));
// If the plugin hooks up any global DOM events (on parts of the app that doesn't belong to this plugin)
// Using this function will automatically remove the event listener when this plugin is disabled.
this.registerDomEvent(document, 'click', (evt: MouseEvent) => {
console.log('click', evt);
// Auto-open the view on startup
this.app.workspace.onLayoutReady(() => {
this.activateView();
});
// When registering intervals, this function will automatically clear the interval when the plugin is disabled.
this.registerInterval(window.setInterval(() => console.log('setInterval'), 5 * 60 * 1000));
// Setup auto-refresh
if (this.settings.autoRefresh) {
this.setupAutoRefresh();
}
}
onunload() {
if (this.refreshInterval) {
clearInterval(this.refreshInterval);
}
// Clean up JXA script files
this.cleanupJXAScripts();
}
private async createJXAScripts() {
try {
const fs = require('fs');
const path = require('path');
// Get plugin directory path
// @ts-ignore
const pluginDir = this.app.vault.adapter.basePath + '/.obsidian/plugins/ticktick-today';
// Create fetch script
const fetchScript = `
function run() {
try {
const app = Application('${this.settings.tickTickAppName}');
// 获取今日任务
const todayTasks = app.todayTasks();
// 获取所有项目信息
const projects = app.projects();
// 返回包含任务和项目信息的对象
return JSON.stringify({
tasks: todayTasks,
projects: projects,
timestamp: new Date().toISOString()
});
} catch (e) {
return JSON.stringify({
error: e.toString(),
timestamp: new Date().toISOString()
});
}
}
`;
// Create toggle script
const toggleScript = `
function run(argv) {
try {
if (argv.length === 0) {
return JSON.stringify({
success: false,
error: 'Task ID is required'
});
}
const taskId = argv[0];
const app = Application('${this.settings.tickTickAppName}');
// Use the working toggleTask method
const result = app.toggleTask(taskId);
return JSON.stringify({
success: true,
taskId: taskId,
result: result,
message: 'Task toggled successfully'
});
} catch (error) {
return JSON.stringify({
success: false,
error: error.toString(),
taskId: argv[0] || 'unknown'
});
}
}
`;
// Write script files
const fetchPath = path.join(pluginDir, 'ticktick-fetch.js');
const togglePath = path.join(pluginDir, 'ticktick-toggle.js');
fs.writeFileSync(fetchPath, fetchScript);
fs.writeFileSync(togglePath, toggleScript);
this.jxaScriptPaths = {
fetch: fetchPath,
toggle: togglePath
};
console.log('JXA scripts created:', this.jxaScriptPaths);
} catch (error) {
console.error('Failed to create JXA scripts:', error);
new Notice('创建JXA脚本失败: ' + error);
}
}
private cleanupJXAScripts() {
if (!this.jxaScriptPaths) return;
try {
const fs = require('fs');
if (fs.existsSync(this.jxaScriptPaths.fetch)) {
fs.unlinkSync(this.jxaScriptPaths.fetch);
}
if (fs.existsSync(this.jxaScriptPaths.toggle)) {
fs.unlinkSync(this.jxaScriptPaths.toggle);
}
console.log('JXA scripts cleaned up');
} catch (error) {
console.error('Failed to cleanup JXA scripts:', error);
}
}
async activateView() {
const { workspace } = this.app;
let leaf: WorkspaceLeaf | null = null;
const leaves = workspace.getLeavesOfType(VIEW_TYPE_TODAY_TASKS);
if (leaves.length > 0) {
// A leaf with our view already exists, use that
leaf = leaves[0];
} else {
// Our view could not be found in the workspace, create a new leaf
// in the right sidebar for it
leaf = workspace.getRightLeaf(false);
await leaf?.setViewState({ type: VIEW_TYPE_TODAY_TASKS, active: true });
}
// "Reveal" the leaf in case it is in a collapsed sidebar
if (leaf) {
workspace.revealLeaf(leaf);
}
}
async refreshTasks() {
const leaves = this.app.workspace.getLeavesOfType(VIEW_TYPE_TODAY_TASKS);
leaves.forEach(leaf => {
if (leaf.view instanceof TodayTasksView) {
leaf.view.refreshTasks();
}
});
}
setupAutoRefresh() {
if (this.refreshInterval) {
clearInterval(this.refreshInterval);
}
if (this.settings.autoRefresh && this.settings.refreshInterval > 0) {
this.refreshInterval = setInterval(() => {
this.refreshTasks();
}, this.settings.refreshInterval * 60 * 1000);
}
}
async loadSettings() {
@ -88,29 +287,512 @@ export default class MyPlugin extends Plugin {
async saveSettings() {
await this.saveData(this.settings);
this.setupAutoRefresh();
// Recreate JXA scripts with updated settings
await this.createJXAScripts();
}
async getTodaysTasks(): Promise<Task[]> {
return await this.getTickTickTasks();
}
private async getTickTickTasks(): Promise<Task[]> {
try {
// Check if we're on macOS and can use JXA
if (process.platform !== 'darwin') {
new Notice('TickTick integration requires macOS');
return [];
}
if (!this.jxaScriptPaths) {
new Notice('JXA脚本未初始化');
return [];
}
const { exec } = require('child_process');
return new Promise((resolve) => {
exec(`osascript -l JavaScript "${this.jxaScriptPaths!.fetch}"`, (error: any, stdout: any, stderr: any) => {
if (error) {
console.error('TickTick JXA Error:', error);
new Notice('无法连接到TickTick应用: ' + error.message);
resolve([]);
return;
}
try {
const result = JSON.parse(stdout.trim());
if (result.error) {
console.error('TickTick API Error:', result.error);
new Notice('TickTick API错误: ' + result.error);
resolve([]);
return;
}
// 更新项目缓存
if (result.projects) {
this.updateProjectsCache(result.projects);
}
// Parse the TickTick response format
const tickTickTasks: Task[] = this.parseTickTickResponse(result.tasks || result);
new Notice(`获取到 ${tickTickTasks.length} 个今日任务`);
resolve(this.sortTasks(tickTickTasks));
} catch (parseError) {
console.error('Parse Error:', parseError);
new Notice('解析TickTick数据失败: ' + parseError);
resolve([]);
}
});
});
} catch (error) {
console.error('TickTick Integration Error:', error);
new Notice('TickTick集成失败: ' + error);
return [];
}
}
private parseTickTickResponse(response: any): Task[] {
const tasks: Task[] = [];
try {
// Parse the JSON string response from TickTick
const data = typeof response === 'string' ? JSON.parse(response) : response;
// The response is an array of date groups
if (Array.isArray(data)) {
data.forEach((dateGroup: any) => {
// Each dateGroup has a 'tasks' array
if (dateGroup.tasks && Array.isArray(dateGroup.tasks)) {
dateGroup.tasks.forEach((task: any) => {
const taskObj: Task = {
text: task.title || 'Untitled Task',
completed: task.status === 2, // TickTick uses status 2 for completed tasks
priority: this.convertTickTickPriority(task.priority || 0),
dueDate: task.dueDate || task.startDate || '',
id: task.id || '',
project: this.getProjectName(task.projectId),
projectId: task.projectId || '', // 保存原始项目ID
tags: task.tags || [],
pinned: task.pinnedTime && task.pinnedTime !== "-1" // TickTick uses pinnedTime field
};
tasks.push(taskObj);
});
}
});
}
} catch (error) {
console.error('Error parsing TickTick response:', error);
new Notice('解析任务数据时出错: ' + error);
}
return tasks;
}
private updateProjectsCache(projects: any): void {
try {
// 解析项目数据(可能是字符串格式)
const projectsData = typeof projects === 'string'
? JSON.parse(projects)
: projects;
if (Array.isArray(projectsData)) {
projectsData.forEach((project: any) => {
if (project.id && project.name) {
this.projectsCache.set(project.id, project.name);
}
});
console.log('Projects cache updated:', this.projectsCache);
}
} catch (error) {
console.error('Error updating projects cache:', error);
}
}
private getProjectName(projectId: string): string {
if (!projectId) return '';
// 首先尝试从缓存中获取真实项目名称
if (this.projectsCache.has(projectId)) {
const projectName = this.projectsCache.get(projectId) || '';
// 如果是Inbox显示为"收集箱"
if (projectName === 'Inbox') {
return '收集箱';
}
return projectName;
}
// 如果是inbox显示为"收集箱"
if (projectId.includes('inbox')) {
return '收集箱';
}
// 如果缓存中没有找到显示项目ID的最后8位作为备用
return `项目 ${projectId.slice(-8)}`;
}
private convertTickTickPriority(priority: number): string {
switch (priority) {
case 5: return '!!!'; // High priority
case 3: return '!!'; // Medium priority
case 1: return '!'; // Low priority
default: return ''; // No priority
}
}
private sortTasks(tasks: Task[]): Task[] {
return tasks.sort((a, b) => {
// Completed tasks go to bottom
if (a.completed !== b.completed) {
return a.completed ? 1 : -1;
}
// Pinned tasks go to top (among non-completed tasks)
if (a.pinned !== b.pinned) {
return a.pinned ? -1 : 1;
}
// Sort by due date (earliest first)
if (a.dueDate && b.dueDate) {
const dateA = new Date(a.dueDate).getTime();
const dateB = new Date(b.dueDate).getTime();
if (dateA !== dateB) {
return dateA - dateB;
}
} else if (a.dueDate && !b.dueDate) {
return -1; // Tasks with due date come first
} else if (!a.dueDate && b.dueDate) {
return 1; // Tasks with due date come first
}
// Sort by priority as secondary criteria
const priorityOrder = { '!!!': 0, '!!': 1, '!': 2, 'A': 3, 'B': 4, 'C': 5, '': 6 };
const aPriority = priorityOrder[a.priority as keyof typeof priorityOrder] ?? 6;
const bPriority = priorityOrder[b.priority as keyof typeof priorityOrder] ?? 6;
if (aPriority !== bPriority) {
return aPriority - bPriority;
}
// Sort alphabetically as final criteria
return a.text.localeCompare(b.text);
});
}
}
class SampleModal extends Modal {
constructor(app: App) {
super(app);
class TodayTasksView extends ItemView {
plugin: TickTickTodayPlugin;
private tasks: Task[] = [];
constructor(leaf: WorkspaceLeaf, plugin: TickTickTodayPlugin) {
super(leaf);
this.plugin = plugin;
}
onOpen() {
const {contentEl} = this;
contentEl.setText('Woah!');
getViewType() {
return VIEW_TYPE_TODAY_TASKS;
}
onClose() {
const {contentEl} = this;
contentEl.empty();
getDisplayText() {
return "Today's Tasks";
}
getIcon() {
return "calendar-check";
}
async onOpen() {
const container = this.containerEl.children[1];
container.empty();
// Header with link to TickTick "Today" smart list
const header = container.createEl("h4");
const link = header.createEl("a", {
text: "Today's Tasks",
href: "ticktick://v1/show?smartlist=today",
cls: "ticktick-link"
});
link.target = "_blank";
await this.refreshTasks();
}
async onClose() {
// Nothing to clean up
}
async refreshTasks() {
this.tasks = await this.plugin.getTodaysTasks();
this.renderTasks();
}
private renderTasks() {
const container = this.containerEl.children[1];
// Clear previous content except the header
const header = container.querySelector('h4');
container.empty();
if (header) {
container.appendChild(header);
} else {
container.createEl("h4", { text: "Today's Tasks" });
}
// Add refresh button
const refreshBtn = container.createEl("button", {
text: "🔄 Refresh",
cls: "mod-cta"
});
refreshBtn.addEventListener('click', () => this.refreshTasks());
// Add task count
const completedCount = this.tasks.filter(t => t.completed).length;
const totalCount = this.tasks.length;
if (this.tasks.length === 0) {
container.createEl("p", { text: "No tasks for today! 🎉" });
return;
}
// Group tasks by completion status
const incompleteTasks = this.tasks.filter(t => !t.completed);
const completedTasks = this.tasks.filter(t => t.completed);
// Render incomplete tasks
if (incompleteTasks.length > 0) {
const incompleteSection = container.createEl("div", { cls: "task-section" });
incompleteSection.createEl("h5", { text: `📋 Todo (${incompleteTasks.length})` });
this.renderTaskList(incompleteSection, incompleteTasks);
}
// Render completed tasks (if enabled in settings)
if (completedTasks.length > 0 && this.plugin.settings.showCompleted) {
const completedSection = container.createEl("div", { cls: "task-section" });
completedSection.createEl("h5", { text: `✅ Completed (${completedTasks.length})` });
this.renderTaskList(completedSection, completedTasks);
}
}
private renderTaskList(container: HTMLElement, tasks: Task[]) {
const taskList = container.createEl("ul", { cls: "task-list" });
tasks.forEach(task => {
const taskItem = taskList.createEl("li", { cls: "task-item" });
if (task.completed) {
taskItem.addClass("task-completed");
}
// Checkbox
const checkbox = taskItem.createEl("input", {
type: "checkbox",
cls: "task-checkbox"
});
checkbox.checked = task.completed;
checkbox.addEventListener('change', async (event) => {
event.stopPropagation();
task.completed = checkbox.checked;
if (task.completed) {
taskItem.addClass("task-completed");
} else {
taskItem.removeClass("task-completed");
}
try {
await this.toggleTask(task);
} catch (error) {
task.completed = !task.completed;
checkbox.checked = task.completed;
if (task.completed) {
taskItem.addClass("task-completed");
} else {
taskItem.removeClass("task-completed");
}
console.error('Toggle failed:', error);
}
});
// Task content container
const taskContent = taskItem.createEl("div", { cls: "task-content" });
// Task text (clickable)
const taskText = taskContent.createEl("div", {
cls: "task-text"
});
if (task.id) {
// 创建可点击的链接
const taskLink = taskText.createEl("a", {
text: task.text,
cls: "task-link"
});
const projectId = task.projectId || 'inbox'; // 如果没有项目ID使用inbox作为默认值
taskLink.href = `https://dida365.com/webapp/#p/${projectId}/tasks/${task.id}`;
taskLink.target = "_blank";
taskLink.addEventListener('click', (event) => {
event.stopPropagation();
// 使用 Obsidian 的方式打开外部链接
window.open(`https://dida365.com/webapp/#p/${projectId}/tasks/${task.id}`, '_blank');
});
} else {
// 如果没有ID只显示文本
taskText.textContent = task.text;
}
// Meta information (project + datetime)
const metaInfo = [];
if (task.project) {
metaInfo.push(task.project);
}
if (task.dueDate) {
const dateStr = this.formatDateTime(task.dueDate);
if (dateStr) {
metaInfo.push(dateStr);
}
}
if (metaInfo.length > 0) {
const taskMeta = taskContent.createEl("div", { cls: "task-meta" });
if (task.project) {
taskMeta.createEl("span", {
text: task.project,
cls: "task-project"
});
}
if (task.dueDate) {
const dateStr = this.formatDateTime(task.dueDate);
if (dateStr) {
taskMeta.createEl("span", {
text: dateStr,
cls: "task-datetime"
});
}
}
}
});
}
private async toggleTask(task: Task) {
if (task.file) {
// Handle Obsidian tasks
const file = this.app.vault.getAbstractFileByPath(task.file);
if (file instanceof TFile && task.line) {
const content = await this.app.vault.read(file);
const lines = content.split('\n');
if (lines[task.line - 1]) {
const currentLine = lines[task.line - 1];
const newLine = task.completed
? currentLine.replace(/\[ \]/, '[x]')
: currentLine.replace(/\[x\]/, '[ ]');
lines[task.line - 1] = newLine;
await this.app.vault.modify(file, lines.join('\n'));
new Notice(`Task ${task.completed ? 'completed' : 'uncompleted'}!`);
}
}
} else if (task.id) {
// Handle TickTick tasks
await this.toggleTickTickTask(task);
}
}
private async toggleTickTickTask(task: Task) {
try {
if (!task.id) {
throw new Error('任务ID缺失');
}
if (!this.plugin.getJXAScriptPaths()) {
throw new Error('JXA脚本未初始化');
}
const { exec } = require('child_process');
const scriptPaths = this.plugin.getJXAScriptPaths()!;
return new Promise<void>((resolve) => {
exec(`osascript -l JavaScript "${scriptPaths.toggle}" "${task.id}"`, (error: any, stdout: any, stderr: any) => {
if (error) {
console.error('TickTick Toggle Error:', error);
new Notice('切换TickTick任务失败: ' + error.message);
resolve();
return;
}
try {
const result = JSON.parse(stdout.trim());
if (result.success) {
new Notice(`任务 "${task.text}" 状态已切换`);
console.log('TickTick toggle successful:', result);
} else {
console.error('TickTick Toggle Failed:', result.error);
new Notice('切换任务状态失败: ' + result.error);
}
} catch (parseError) {
console.error('Parse Toggle Result Error:', parseError);
new Notice('解析切换结果失败');
}
resolve();
});
});
} catch (error) {
console.error('TickTick Toggle Integration Error:', error);
new Notice('TickTick任务切换失败: ' + error);
throw error;
}
}
private formatDateTime(dateString: string): string {
if (!dateString) return '';
try {
const date = new Date(dateString);
if (isNaN(date.getTime())) return '';
const now = new Date();
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
const taskDate = new Date(date.getFullYear(), date.getMonth(), date.getDate());
// 计算天数差异
const diffTime = taskDate.getTime() - today.getTime();
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
// 格式化时间(如果有具体时间的话)
const timeStr = date.getHours() !== 0 || date.getMinutes() !== 0
? ` ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`
: '';
if (diffDays === 0) {
return `今天${timeStr}`;
} else if (diffDays === 1) {
return `明天${timeStr}`;
} else if (diffDays === -1) {
return `昨天${timeStr}`;
} else if (diffDays > 0) {
return `${diffDays}天后${timeStr}`;
} else {
return `${Math.abs(diffDays)}天前${timeStr}`;
}
} catch (error) {
return '';
}
}
}
class SampleSettingTab extends PluginSettingTab {
plugin: MyPlugin;
class TickTickTodaySettingTab extends PluginSettingTab {
plugin: TickTickTodayPlugin;
constructor(app: App, plugin: MyPlugin) {
constructor(app: App, plugin: TickTickTodayPlugin) {
super(app, plugin);
this.plugin = plugin;
}
@ -121,13 +803,46 @@ class SampleSettingTab extends PluginSettingTab {
containerEl.empty();
new Setting(containerEl)
.setName('Setting #1')
.setDesc('It\'s a secret')
.addText(text => text
.setPlaceholder('Enter your secret')
.setValue(this.plugin.settings.mySetting)
.setName('Auto Refresh')
.setDesc('Automatically refresh tasks at regular intervals')
.addToggle(toggle => toggle
.setValue(this.plugin.settings.autoRefresh)
.onChange(async (value) => {
this.plugin.settings.mySetting = value;
this.plugin.settings.autoRefresh = value;
await this.plugin.saveSettings();
}));
new Setting(containerEl)
.setName('Refresh Interval')
.setDesc('How often to refresh tasks (in minutes)')
.addSlider(slider => slider
.setLimits(1, 60, 1)
.setValue(this.plugin.settings.refreshInterval)
.setDynamicTooltip()
.onChange(async (value) => {
this.plugin.settings.refreshInterval = value;
await this.plugin.saveSettings();
}));
new Setting(containerEl)
.setName('Show Completed Tasks')
.setDesc('Show completed tasks in the sidebar')
.addToggle(toggle => toggle
.setValue(this.plugin.settings.showCompleted)
.onChange(async (value) => {
this.plugin.settings.showCompleted = value;
await this.plugin.saveSettings();
this.plugin.refreshTasks();
}));
new Setting(containerEl)
.setName('TickTick App Name')
.setDesc('Name of the TickTick application (default: TickTick)')
.addText(text => text
.setPlaceholder('TickTick')
.setValue(this.plugin.settings.tickTickAppName)
.onChange(async (value) => {
this.plugin.settings.tickTickAppName = value || 'TickTick';
await this.plugin.saveSettings();
}));
}

View file

@ -1,11 +1,10 @@
{
"id": "sample-plugin",
"name": "Sample Plugin",
"id": "ticktick-today",
"name": "TickTick Today",
"version": "1.0.0",
"minAppVersion": "0.15.0",
"description": "Demonstrates some of the capabilities of the Obsidian API.",
"author": "Obsidian",
"authorUrl": "https://obsidian.md",
"fundingUrl": "https://obsidian.md/pricing",
"isDesktopOnly": false
"description": "Display TickTick today's tasks in a sidebar panel",
"author": "wudanyang27",
"authorUrl": "https://github.com/wudanyang27",
"isDesktopOnly": true
}

2406
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,8 +1,217 @@
/*
/* TickTick Today Plugin - Simplified Styles */
This CSS file will be included with your plugin, and
available in the app when your plugin is enabled.
.ticktick-today-ribbon-class {
color: var(--text-accent);
}
If your plugin does not need CSS, delete this file.
/* Task sections */
.task-section {
margin-bottom: 1rem;
}
*/
.task-section h5 {
margin: 0.5rem 0;
color: var(--text-accent);
font-weight: 600;
font-size: 0.9em;
}
/* Task statistics */
.task-stats {
font-size: 0.85em;
color: var(--text-muted);
margin-bottom: 1rem;
padding: 0.3rem;
text-align: center;
}
/* Task list */
.task-list {
list-style: none;
padding: 0;
margin: 0;
}
.task-item {
display: flex;
align-items: flex-start;
padding: 0.4rem;
margin-bottom: 0.3rem;
border-radius: 3px;
line-height: 1.3;
}
.task-item:hover {
background: var(--background-secondary);
}
.task-item.task-completed {
opacity: 0.6;
}
.task-item.task-completed .task-text {
text-decoration: line-through;
color: var(--text-muted);
}
/* Task checkbox */
.task-checkbox {
appearance: none;
-webkit-appearance: none;
width: 16px;
height: 16px;
border: 1px solid var(--text-muted);
border-radius: 2px;
margin-right: 0.5rem;
margin-top: 0.1rem;
cursor: pointer;
background: var(--background-primary);
flex-shrink: 0;
}
.task-checkbox:checked {
background: var(--interactive-accent);
border-color: var(--interactive-accent);
}
.task-checkbox:checked::after {
content: '✓';
position: absolute;
margin-top: -1px;
margin-left: 1px;
color: white;
font-size: 12px;
font-weight: bold;
}
/* Task content */
.task-content {
flex: 1;
min-width: 0;
}
.task-text {
display: block;
font-size: 0.9em;
margin-bottom: 0.2rem;
word-wrap: break-word;
}
.task-link {
color: var(--text-normal);
text-decoration: none;
cursor: pointer;
display: inline-block;
width: 100%;
}
.task-link:hover {
color: var(--text-accent);
text-decoration: underline;
}
.task-meta {
display: flex;
gap: 0.5rem;
font-size: 0.75em;
color: var(--text-muted);
}
.task-project {
background: var(--background-modifier-border);
padding: 0.1rem 0.3rem;
border-radius: 2px;
}
.task-datetime {
font-style: italic;
}
/* Refresh button */
button.mod-cta {
margin-bottom: 1rem;
width: 100%;
font-size: 0.85em;
padding: 0.4rem;
}
/* View container */
.view-content {
padding: 0.8rem;
}
/* Empty state */
.view-content p {
text-align: center;
color: var(--text-muted);
margin: 2rem 0;
}
/* Refresh button */
button.mod-cta {
margin-bottom: 1rem;
width: 100%;
}
/* View container adjustments */
.view-content {
padding: 1rem;
}
/* Responsive design for narrow sidebars */
@media (max-width: 400px) {
.task-item {
flex-direction: column;
align-items: flex-start;
}
.task-checkbox {
align-self: flex-end;
margin-top: 0.5rem;
margin-right: 0;
}
.task-file-link {
margin-top: 0.25rem;
margin-right: 0;
}
}
/* Dark theme specific adjustments */
.theme-dark .task-item {
border-left-color: var(--interactive-accent);
}
.theme-dark .task-priority {
background: var(--background-modifier-border);
}
/* Animation for task completion */
.task-item {
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Scrollable content for long task lists */
.view-content {
overflow-y: auto;
max-height: 100%;
}
/* Empty state styling */
.view-content p {
text-align: center;
color: var(--text-muted);
font-style: italic;
margin: 2rem 0;
}