Initial commit
This commit is contained in:
37
shared/constants/api.ts
Normal file
37
shared/constants/api.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
export const API_ENDPOINTS = {
|
||||
FILES: '/api/files',
|
||||
FILES_CONTENT: '/api/files/content',
|
||||
FILES_MOVE: '/api/files/move',
|
||||
FILES_RECYCLE: '/api/files/recycle',
|
||||
FILES_EXISTS: '/api/files/exists',
|
||||
FILES_RENAME: '/api/files/rename',
|
||||
FILES_CREATE: '/api/files/create',
|
||||
FILES_MKDIR: '/api/files/mkdir',
|
||||
FILES_RESTORE: '/api/files/restore',
|
||||
FILES_PERMANENT_DELETE: '/api/files/permanent-delete',
|
||||
FILES_EMPTY_RECYCLE_BIN: '/api/files/empty-recycle-bin',
|
||||
TIME: '/api/time',
|
||||
TIME_EVENT: '/api/time/event',
|
||||
TIME_STATS: '/api/time/stats',
|
||||
TIME_DAY: '/api/time/day',
|
||||
TIME_MONTH: '/api/time/month',
|
||||
TIME_YEAR: '/api/time/year',
|
||||
TODO: '/api/todo',
|
||||
TODO_ADD: '/api/todo/add',
|
||||
TODO_TOGGLE: '/api/todo/toggle',
|
||||
TODO_UPDATE: '/api/todo/update',
|
||||
TODO_DELETE: '/api/todo/delete',
|
||||
TODO_MIGRATE: '/api/todo/migrate',
|
||||
SEARCH: '/api/search',
|
||||
EVENTS: '/api/events',
|
||||
SYNC: '/api/sync',
|
||||
SYNC_UPLOAD: '/api/sync/upload',
|
||||
SYNC_DOWNLOAD: '/api/sync/download',
|
||||
SYNC_STATUS: '/api/sync/status',
|
||||
PYDEMOS: '/api/pydemos',
|
||||
PYDEMOS_CREATE: '/api/pydemos/create',
|
||||
PYDEMOS_RENAME: '/api/pydemos/rename',
|
||||
PYDEMOS_DELETE: '/api/pydemos/delete',
|
||||
} as const
|
||||
|
||||
export type ApiEndpoint = typeof API_ENDPOINTS[keyof typeof API_ENDPOINTS]
|
||||
14
shared/constants/asyncImport.ts
Normal file
14
shared/constants/asyncImport.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
export const ASYNC_IMPORT_STATUS = {
|
||||
PDF_PARSING_TITLE: '# 正在解析 PDF 内容...',
|
||||
HTML_PARSING_TITLE: '# 正在解析 HTML 内容...',
|
||||
PDF_PARSING_CONTENT: '# 正在解析 PDF 内容...\n\n> 请稍候,系统正在后台解析您的 PDF 文件。解析完成后,本文档将自动更新。\n> \n> 您可以继续进行其他操作。',
|
||||
HTML_PARSING_CONTENT: '# 正在解析 HTML 内容...\n\n> 请稍候,系统正在解析您的 HTML 文件。解析完成后,本文档将自动更新。',
|
||||
} as const
|
||||
|
||||
export const isAsyncImportProcessingContent = (text: string): boolean => {
|
||||
const trimmed = text.trimStart()
|
||||
return (
|
||||
trimmed.startsWith(ASYNC_IMPORT_STATUS.PDF_PARSING_TITLE) ||
|
||||
trimmed.startsWith(ASYNC_IMPORT_STATUS.HTML_PARSING_TITLE)
|
||||
)
|
||||
}
|
||||
1
shared/constants/dates.ts
Normal file
1
shared/constants/dates.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const WEEK_DAYS = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'] as const
|
||||
22
shared/constants/errors.ts
Normal file
22
shared/constants/errors.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
export const ERROR_CODES = {
|
||||
PATH_NOT_FOUND: 'PATH_NOT_FOUND',
|
||||
NOT_A_DIRECTORY: 'NOT_A_DIRECTORY',
|
||||
ACCESS_DENIED: 'ACCESS_DENIED',
|
||||
FILE_EXISTS: 'FILE_EXISTS',
|
||||
INVALID_PATH: 'INVALID_PATH',
|
||||
VALIDATION_ERROR: 'VALIDATION_ERROR',
|
||||
INTERNAL_ERROR: 'INTERNAL_ERROR',
|
||||
NOT_FOUND: 'NOT_FOUND',
|
||||
BAD_REQUEST: 'BAD_REQUEST',
|
||||
NAME_GENERATION_FAILED: 'NAME_GENERATION_FAILED',
|
||||
SSE_UNSUPPORTED: 'SSE_UNSUPPORTED',
|
||||
ALREADY_EXISTS: 'ALREADY_EXISTS',
|
||||
NOT_A_FILE: 'NOT_A_FILE',
|
||||
FORBIDDEN: 'FORBIDDEN',
|
||||
UNSUPPORTED_MEDIA_TYPE: 'UNSUPPORTED_MEDIA_TYPE',
|
||||
PAYLOAD_TOO_LARGE: 'PAYLOAD_TOO_LARGE',
|
||||
RESOURCE_LOCKED: 'RESOURCE_LOCKED',
|
||||
INVALID_NAME: 'INVALID_NAME',
|
||||
} as const
|
||||
|
||||
export type ErrorCode = typeof ERROR_CODES[keyof typeof ERROR_CODES]
|
||||
5
shared/constants/index.ts
Normal file
5
shared/constants/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export { API_ENDPOINTS, type ApiEndpoint } from './api.js'
|
||||
export { ERROR_CODES, type ErrorCode } from './errors.js'
|
||||
export { ERROR_MESSAGES, getErrorMessage } from './messages.js'
|
||||
export { ASYNC_IMPORT_STATUS, isAsyncImportProcessingContent } from './asyncImport.js'
|
||||
export { WEEK_DAYS } from './dates.js'
|
||||
26
shared/constants/messages.ts
Normal file
26
shared/constants/messages.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { ERROR_CODES } from './errors'
|
||||
|
||||
export const ERROR_MESSAGES: Record<string, string> = {
|
||||
[ERROR_CODES.PATH_NOT_FOUND]: '路径不存在',
|
||||
[ERROR_CODES.NOT_A_DIRECTORY]: '不是目录',
|
||||
[ERROR_CODES.ACCESS_DENIED]: '访问被拒绝',
|
||||
[ERROR_CODES.FILE_EXISTS]: '文件已存在',
|
||||
[ERROR_CODES.INVALID_PATH]: '无效路径',
|
||||
[ERROR_CODES.VALIDATION_ERROR]: '验证失败',
|
||||
[ERROR_CODES.INTERNAL_ERROR]: '内部错误',
|
||||
[ERROR_CODES.NOT_FOUND]: '资源不存在',
|
||||
[ERROR_CODES.BAD_REQUEST]: '请求错误',
|
||||
[ERROR_CODES.NAME_GENERATION_FAILED]: '名称生成失败',
|
||||
[ERROR_CODES.SSE_UNSUPPORTED]: 'SSE 不支持',
|
||||
[ERROR_CODES.ALREADY_EXISTS]: '已存在',
|
||||
[ERROR_CODES.NOT_A_FILE]: '不是文件',
|
||||
[ERROR_CODES.FORBIDDEN]: '禁止访问',
|
||||
[ERROR_CODES.UNSUPPORTED_MEDIA_TYPE]: '不支持的媒体类型',
|
||||
[ERROR_CODES.PAYLOAD_TOO_LARGE]: '请求体过大',
|
||||
[ERROR_CODES.RESOURCE_LOCKED]: '资源已锁定',
|
||||
[ERROR_CODES.INVALID_NAME]: '无效名称',
|
||||
}
|
||||
|
||||
export const getErrorMessage = (code: string): string => {
|
||||
return ERROR_MESSAGES[code] || '未知错误'
|
||||
}
|
||||
171
shared/errors/index.ts
Normal file
171
shared/errors/index.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
import type { ErrorCode } from '../constants/errors.js'
|
||||
|
||||
export type { ErrorCode } from '../constants/errors.js'
|
||||
|
||||
export interface ErrorDetails {
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
export class AppError extends Error {
|
||||
public readonly statusCode: number
|
||||
public readonly details?: ErrorDetails
|
||||
|
||||
constructor(
|
||||
public readonly code: ErrorCode,
|
||||
message: string,
|
||||
statusCode: number = 500,
|
||||
details?: ErrorDetails
|
||||
) {
|
||||
super(message)
|
||||
this.name = 'AppError'
|
||||
this.statusCode = statusCode
|
||||
this.details = details
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
name: this.name,
|
||||
code: this.code,
|
||||
message: this.message,
|
||||
statusCode: this.statusCode,
|
||||
details: this.details
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class ValidationError extends AppError {
|
||||
constructor(message: string, details?: ErrorDetails) {
|
||||
super('VALIDATION_ERROR', message, 400, details)
|
||||
this.name = 'ValidationError'
|
||||
}
|
||||
}
|
||||
|
||||
export class NotFoundError extends AppError {
|
||||
constructor(message: string = 'Resource not found', details?: ErrorDetails) {
|
||||
super('NOT_FOUND', message, 404, details)
|
||||
this.name = 'NotFoundError'
|
||||
}
|
||||
}
|
||||
|
||||
export class AccessDeniedError extends AppError {
|
||||
constructor(message: string = 'Access denied', details?: ErrorDetails) {
|
||||
super('ACCESS_DENIED', message, 403, details)
|
||||
this.name = 'AccessDeniedError'
|
||||
}
|
||||
}
|
||||
|
||||
export class BadRequestError extends AppError {
|
||||
constructor(message: string, details?: ErrorDetails) {
|
||||
super('BAD_REQUEST', message, 400, details)
|
||||
this.name = 'BadRequestError'
|
||||
}
|
||||
}
|
||||
|
||||
export class PathNotFoundError extends AppError {
|
||||
constructor(message: string = 'Path not found', details?: ErrorDetails) {
|
||||
super('PATH_NOT_FOUND', message, 404, details)
|
||||
this.name = 'PathNotFoundError'
|
||||
}
|
||||
}
|
||||
|
||||
export class NotADirectoryError extends AppError {
|
||||
constructor(message: string = '不是目录', details?: ErrorDetails) {
|
||||
super('NOT_A_DIRECTORY', message, 400, details)
|
||||
this.name = 'NotADirectoryError'
|
||||
}
|
||||
}
|
||||
|
||||
export class FileExistsError extends AppError {
|
||||
constructor(message: string = 'File already exists', details?: ErrorDetails) {
|
||||
super('FILE_EXISTS', message, 409, details)
|
||||
this.name = 'FileExistsError'
|
||||
}
|
||||
}
|
||||
|
||||
export class InvalidPathError extends AppError {
|
||||
constructor(message: string = '无效路径', details?: ErrorDetails) {
|
||||
super('INVALID_PATH', message, 400, details)
|
||||
this.name = 'InvalidPathError'
|
||||
}
|
||||
}
|
||||
|
||||
export class AlreadyExistsError extends AppError {
|
||||
constructor(message: string = 'Resource already exists', details?: ErrorDetails) {
|
||||
super('ALREADY_EXISTS', message, 409, details)
|
||||
this.name = 'AlreadyExistsError'
|
||||
}
|
||||
}
|
||||
|
||||
export class ForbiddenError extends AppError {
|
||||
constructor(message: string = '禁止访问', details?: ErrorDetails) {
|
||||
super('FORBIDDEN', message, 403, details)
|
||||
this.name = 'ForbiddenError'
|
||||
}
|
||||
}
|
||||
|
||||
export class UnsupportedMediaTypeError extends AppError {
|
||||
constructor(message: string = '不支持的媒体类型', details?: ErrorDetails) {
|
||||
super('UNSUPPORTED_MEDIA_TYPE', message, 415, details)
|
||||
this.name = 'UnsupportedMediaTypeError'
|
||||
}
|
||||
}
|
||||
|
||||
export class PayloadTooLargeError extends AppError {
|
||||
constructor(message: string = 'Payload too large', details?: ErrorDetails) {
|
||||
super('PAYLOAD_TOO_LARGE', message, 413, details)
|
||||
this.name = 'PayloadTooLargeError'
|
||||
}
|
||||
}
|
||||
|
||||
export class ResourceLockedError extends AppError {
|
||||
constructor(message: string = '资源已锁定', details?: ErrorDetails) {
|
||||
super('RESOURCE_LOCKED', message, 423, details)
|
||||
this.name = 'ResourceLockedError'
|
||||
}
|
||||
}
|
||||
|
||||
export class InternalError extends AppError {
|
||||
constructor(message: string = '服务器内部错误', details?: ErrorDetails) {
|
||||
super('INTERNAL_ERROR', message, 500, details)
|
||||
this.name = 'InternalError'
|
||||
}
|
||||
}
|
||||
|
||||
export function isAppError(error: unknown): error is AppError {
|
||||
return error instanceof AppError
|
||||
}
|
||||
|
||||
export function isNodeError(error: unknown): error is NodeJS.ErrnoException {
|
||||
return error instanceof Error && 'code' in error
|
||||
}
|
||||
|
||||
export function createErrorFromCode(code: ErrorCode, message: string, details?: ErrorDetails): AppError {
|
||||
switch (code) {
|
||||
case 'VALIDATION_ERROR':
|
||||
return new ValidationError(message, details)
|
||||
case 'NOT_FOUND':
|
||||
case 'PATH_NOT_FOUND':
|
||||
return new NotFoundError(message, details)
|
||||
case 'ACCESS_DENIED':
|
||||
case 'FORBIDDEN':
|
||||
return new AccessDeniedError(message, details)
|
||||
case 'BAD_REQUEST':
|
||||
case 'NOT_A_DIRECTORY':
|
||||
case 'INVALID_PATH':
|
||||
case 'INVALID_NAME':
|
||||
return new BadRequestError(message, details)
|
||||
case 'FILE_EXISTS':
|
||||
case 'ALREADY_EXISTS':
|
||||
return new FileExistsError(message, details)
|
||||
case 'UNSUPPORTED_MEDIA_TYPE':
|
||||
return new UnsupportedMediaTypeError(message, details)
|
||||
case 'PAYLOAD_TOO_LARGE':
|
||||
return new PayloadTooLargeError(message, details)
|
||||
case 'RESOURCE_LOCKED':
|
||||
return new ResourceLockedError(message, details)
|
||||
case 'INTERNAL_ERROR':
|
||||
return new InternalError(message, details)
|
||||
default:
|
||||
return new AppError(code, message, 500, details)
|
||||
}
|
||||
}
|
||||
4
shared/index.ts
Normal file
4
shared/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from './types/index.js'
|
||||
export * from './constants'
|
||||
export * from './errors'
|
||||
export * from './utils'
|
||||
15
shared/modules/ai/index.ts
Normal file
15
shared/modules/ai/index.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { defineApiModule } from '../types.js'
|
||||
|
||||
export const AI_MODULE = defineApiModule({
|
||||
id: 'ai',
|
||||
name: 'AI',
|
||||
basePath: '/ai',
|
||||
order: 70,
|
||||
version: '1.0.0',
|
||||
frontend: {
|
||||
enabled: false,
|
||||
},
|
||||
backend: {
|
||||
enabled: true,
|
||||
},
|
||||
})
|
||||
15
shared/modules/document-parser/index.ts
Normal file
15
shared/modules/document-parser/index.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { defineApiModule } from '../types.js'
|
||||
|
||||
export const DOCUMENT_PARSER_MODULE = defineApiModule({
|
||||
id: 'document-parser',
|
||||
name: 'Document Parser',
|
||||
basePath: '/document-parser',
|
||||
order: 60,
|
||||
version: '1.0.0',
|
||||
frontend: {
|
||||
enabled: false,
|
||||
},
|
||||
backend: {
|
||||
enabled: true,
|
||||
},
|
||||
})
|
||||
12
shared/modules/home/index.ts
Normal file
12
shared/modules/home/index.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { defineModule } from '../types.js'
|
||||
|
||||
export const HOME_MODULE = defineModule({
|
||||
id: 'home',
|
||||
name: '首页',
|
||||
basePath: '/home',
|
||||
order: 0,
|
||||
version: '1.0.0',
|
||||
backend: {
|
||||
enabled: false,
|
||||
},
|
||||
})
|
||||
1
shared/modules/index.ts
Normal file
1
shared/modules/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './types.js'
|
||||
10
shared/modules/pydemos/api.ts
Normal file
10
shared/modules/pydemos/api.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { defineEndpoints } from '../types.js'
|
||||
|
||||
export const PYDEMOS_ENDPOINTS = defineEndpoints({
|
||||
list: { path: '/', method: 'GET' },
|
||||
create: { path: '/create', method: 'POST' },
|
||||
delete: { path: '/delete', method: 'DELETE' },
|
||||
rename: { path: '/rename', method: 'POST' },
|
||||
})
|
||||
|
||||
export type PyDemosEndpoints = typeof PYDEMOS_ENDPOINTS
|
||||
15
shared/modules/pydemos/index.ts
Normal file
15
shared/modules/pydemos/index.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { defineApiModule } from '../types.js'
|
||||
import { PYDEMOS_ENDPOINTS } from './api.js'
|
||||
|
||||
export * from './api.js'
|
||||
|
||||
export const PYDEMOS_MODULE = defineApiModule({
|
||||
id: 'pydemos',
|
||||
name: 'Python Demos',
|
||||
basePath: '/pydemos',
|
||||
order: 50,
|
||||
version: '1.0.0',
|
||||
endpoints: PYDEMOS_ENDPOINTS,
|
||||
})
|
||||
|
||||
export type { PyDemosEndpoints } from './api.js'
|
||||
10
shared/modules/recycle-bin/api.ts
Normal file
10
shared/modules/recycle-bin/api.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { defineEndpoints } from '../types.js'
|
||||
|
||||
export const RECYCLE_BIN_ENDPOINTS = defineEndpoints({
|
||||
list: { path: '/', method: 'GET' },
|
||||
restore: { path: '/restore', method: 'POST' },
|
||||
permanent: { path: '/permanent', method: 'DELETE' },
|
||||
empty: { path: '/empty', method: 'DELETE' },
|
||||
})
|
||||
|
||||
export type RecycleBinEndpoints = typeof RECYCLE_BIN_ENDPOINTS
|
||||
15
shared/modules/recycle-bin/index.ts
Normal file
15
shared/modules/recycle-bin/index.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { defineApiModule } from '../types.js'
|
||||
import { RECYCLE_BIN_ENDPOINTS } from './api.js'
|
||||
|
||||
export * from './api.js'
|
||||
|
||||
export const RECYCLE_BIN_MODULE = defineApiModule({
|
||||
id: 'recycle-bin',
|
||||
name: '回收站',
|
||||
basePath: '/recycle-bin',
|
||||
order: 40,
|
||||
version: '1.0.0',
|
||||
endpoints: RECYCLE_BIN_ENDPOINTS,
|
||||
})
|
||||
|
||||
export type { RecycleBinEndpoints } from './api.js'
|
||||
12
shared/modules/remote/api.ts
Normal file
12
shared/modules/remote/api.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { defineEndpoints } from '../types.js'
|
||||
|
||||
export const REMOTE_ENDPOINTS = defineEndpoints({
|
||||
getConfig: { path: '/config', method: 'GET' },
|
||||
saveConfig: { path: '/config', method: 'POST' },
|
||||
getScreenshot: { path: '/screenshot', method: 'GET' },
|
||||
saveScreenshot: { path: '/screenshot', method: 'POST' },
|
||||
getData: { path: '/data', method: 'GET' },
|
||||
saveData: { path: '/data', method: 'POST' },
|
||||
})
|
||||
|
||||
export type RemoteEndpoints = typeof REMOTE_ENDPOINTS
|
||||
16
shared/modules/remote/index.ts
Normal file
16
shared/modules/remote/index.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { defineApiModule } from '../types.js'
|
||||
import { REMOTE_ENDPOINTS } from './api.js'
|
||||
|
||||
export * from './types.js'
|
||||
export * from './api.js'
|
||||
|
||||
export const REMOTE_MODULE = defineApiModule({
|
||||
id: 'remote',
|
||||
name: '远程',
|
||||
basePath: '/remote',
|
||||
order: 25,
|
||||
version: '1.0.0',
|
||||
endpoints: REMOTE_ENDPOINTS,
|
||||
})
|
||||
|
||||
export type { RemoteEndpoints } from './api.js'
|
||||
11
shared/modules/remote/types.ts
Normal file
11
shared/modules/remote/types.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export interface RemoteDevice {
|
||||
id: string
|
||||
deviceName: string
|
||||
serverHost: string
|
||||
desktopPort: number
|
||||
gitPort: number
|
||||
}
|
||||
|
||||
export interface RemoteConfig {
|
||||
devices: RemoteDevice[]
|
||||
}
|
||||
12
shared/modules/search/index.ts
Normal file
12
shared/modules/search/index.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { defineModule } from '../types.js'
|
||||
|
||||
export const SEARCH_MODULE = defineModule({
|
||||
id: 'search',
|
||||
name: '搜索',
|
||||
basePath: '/search',
|
||||
order: 10,
|
||||
version: '1.0.0',
|
||||
backend: {
|
||||
enabled: false,
|
||||
},
|
||||
})
|
||||
12
shared/modules/settings/index.ts
Normal file
12
shared/modules/settings/index.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { defineModule } from '../types.js'
|
||||
|
||||
export const SETTINGS_MODULE = defineModule({
|
||||
id: 'settings',
|
||||
name: '设置',
|
||||
basePath: '/settings',
|
||||
order: 100,
|
||||
version: '1.0.0',
|
||||
backend: {
|
||||
enabled: false,
|
||||
},
|
||||
})
|
||||
13
shared/modules/time-tracking/api.ts
Normal file
13
shared/modules/time-tracking/api.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { defineEndpoints } from '../types.js'
|
||||
|
||||
export const TIME_TRACKING_ENDPOINTS = defineEndpoints({
|
||||
current: { path: '/current', method: 'GET' },
|
||||
event: { path: '/event', method: 'POST' },
|
||||
day: { path: '/day/:date', method: 'GET' },
|
||||
week: { path: '/week/:startDate', method: 'GET' },
|
||||
month: { path: '/month/:yearMonth', method: 'GET' },
|
||||
year: { path: '/year/:year', method: 'GET' },
|
||||
stats: { path: '/stats', method: 'GET' },
|
||||
})
|
||||
|
||||
export type TimeTrackingEndpoints = typeof TIME_TRACKING_ENDPOINTS
|
||||
15
shared/modules/time-tracking/index.ts
Normal file
15
shared/modules/time-tracking/index.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { defineApiModule } from '../types.js'
|
||||
import { TIME_TRACKING_ENDPOINTS } from './api.js'
|
||||
|
||||
export * from './api.js'
|
||||
|
||||
export const TIME_TRACKING_MODULE = defineApiModule({
|
||||
id: 'time-tracking',
|
||||
name: '时间统计',
|
||||
basePath: '/time',
|
||||
order: 20,
|
||||
version: '1.0.0',
|
||||
endpoints: TIME_TRACKING_ENDPOINTS,
|
||||
})
|
||||
|
||||
export type { TimeTrackingEndpoints } from './api.js'
|
||||
12
shared/modules/todo/api.ts
Normal file
12
shared/modules/todo/api.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { defineEndpoints } from '../types.js'
|
||||
|
||||
export const TODO_ENDPOINTS = defineEndpoints({
|
||||
list: { path: '/', method: 'GET' },
|
||||
save: { path: '/save', method: 'POST' },
|
||||
add: { path: '/add', method: 'POST' },
|
||||
toggle: { path: '/toggle', method: 'POST' },
|
||||
update: { path: '/update', method: 'POST' },
|
||||
delete: { path: '/delete', method: 'DELETE' },
|
||||
})
|
||||
|
||||
export type TodoEndpoints = typeof TODO_ENDPOINTS
|
||||
16
shared/modules/todo/index.ts
Normal file
16
shared/modules/todo/index.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { defineApiModule } from '../types.js'
|
||||
import { TODO_ENDPOINTS } from './api.js'
|
||||
|
||||
export * from './types.js'
|
||||
export * from './api.js'
|
||||
|
||||
export const TODO_MODULE = defineApiModule({
|
||||
id: 'todo',
|
||||
name: 'TODO',
|
||||
basePath: '/todo',
|
||||
order: 30,
|
||||
version: '1.0.0',
|
||||
endpoints: TODO_ENDPOINTS,
|
||||
})
|
||||
|
||||
export type { TodoEndpoints } from './api.js'
|
||||
22
shared/modules/todo/types.ts
Normal file
22
shared/modules/todo/types.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import type { DayTodo } from '../../types/todo.js'
|
||||
|
||||
export interface TodoFilePath {
|
||||
relPath: string
|
||||
fullPath: string
|
||||
}
|
||||
|
||||
export interface ParsedTodoFile {
|
||||
fullPath: string
|
||||
dayTodos: DayTodo[]
|
||||
}
|
||||
|
||||
export interface GetTodoResult {
|
||||
dayTodos: DayTodo[]
|
||||
year: number
|
||||
month: number
|
||||
}
|
||||
|
||||
export interface MigrationContext {
|
||||
todayStr: string
|
||||
yesterdayStr: string
|
||||
}
|
||||
83
shared/modules/types.ts
Normal file
83
shared/modules/types.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'
|
||||
|
||||
export interface EndpointDefinition {
|
||||
path: string
|
||||
method: HttpMethod
|
||||
description?: string
|
||||
}
|
||||
|
||||
export interface ModuleEndpoints {
|
||||
[key: string]: EndpointDefinition
|
||||
}
|
||||
|
||||
export interface ModuleFrontendConfig {
|
||||
enabled?: boolean
|
||||
icon?: string
|
||||
component?: string
|
||||
}
|
||||
|
||||
export interface ModuleBackendConfig {
|
||||
enabled?: boolean
|
||||
createModule?: string
|
||||
}
|
||||
|
||||
export interface ModuleDefinition<
|
||||
TId extends string = string,
|
||||
TEndpoints extends ModuleEndpoints = ModuleEndpoints
|
||||
> {
|
||||
id: TId
|
||||
name: string
|
||||
basePath: string
|
||||
order: number
|
||||
version?: string
|
||||
endpoints?: TEndpoints
|
||||
dependencies?: string[]
|
||||
icon?: string
|
||||
frontend?: ModuleFrontendConfig
|
||||
backend?: ModuleBackendConfig
|
||||
}
|
||||
|
||||
export interface ApiModuleConfig<
|
||||
TId extends string = string,
|
||||
TEndpoints extends ModuleEndpoints = ModuleEndpoints
|
||||
> extends ModuleDefinition<TId, TEndpoints> {
|
||||
version: string
|
||||
}
|
||||
|
||||
export function defineModule<
|
||||
TId extends string,
|
||||
TEndpoints extends ModuleEndpoints
|
||||
>(
|
||||
config: ModuleDefinition<TId, TEndpoints>
|
||||
): ModuleDefinition<TId, TEndpoints> {
|
||||
return config
|
||||
}
|
||||
|
||||
export function defineApiModule<
|
||||
TId extends string,
|
||||
TEndpoints extends ModuleEndpoints
|
||||
>(
|
||||
config: ApiModuleConfig<TId, TEndpoints>
|
||||
): ApiModuleConfig<TId, TEndpoints> {
|
||||
return config
|
||||
}
|
||||
|
||||
export function defineEndpoints<TEndpoints extends ModuleEndpoints>(
|
||||
endpoints: TEndpoints
|
||||
): TEndpoints {
|
||||
return endpoints
|
||||
}
|
||||
|
||||
export type ExtractEndpointPaths<TEndpoints extends ModuleEndpoints> = {
|
||||
[K in keyof TEndpoints]: TEndpoints[K]['path']
|
||||
}
|
||||
|
||||
export type ExtractEndpointMethods<TEndpoints extends ModuleEndpoints> = {
|
||||
[K in keyof TEndpoints]: TEndpoints[K]['method']
|
||||
}
|
||||
|
||||
export type EndpointConfig = EndpointDefinition
|
||||
|
||||
export interface ModuleApiConfig {
|
||||
endpoints: ModuleEndpoints
|
||||
}
|
||||
12
shared/modules/weread/index.ts
Normal file
12
shared/modules/weread/index.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { defineModule } from '../types.js'
|
||||
|
||||
export const WEREAD_MODULE = defineModule({
|
||||
id: 'weread',
|
||||
name: '微信读书',
|
||||
basePath: '/weread',
|
||||
order: 20,
|
||||
version: '1.0.0',
|
||||
backend: {
|
||||
enabled: false,
|
||||
},
|
||||
})
|
||||
1
shared/types.ts
Normal file
1
shared/types.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './types/index.js'
|
||||
9
shared/types/api.ts
Normal file
9
shared/types/api.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export type ApiErrorDTO = {
|
||||
code: string
|
||||
message: string
|
||||
details?: unknown
|
||||
}
|
||||
|
||||
export type ApiResponse<T> =
|
||||
| { success: true; data: T }
|
||||
| { success: false; error: ApiErrorDTO }
|
||||
22
shared/types/file.ts
Normal file
22
shared/types/file.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
export type FileKind = 'file' | 'dir'
|
||||
|
||||
export interface FileItemDTO {
|
||||
name: string
|
||||
type: FileKind
|
||||
size: number
|
||||
modified: string
|
||||
path: string
|
||||
}
|
||||
|
||||
export interface FileContentDTO {
|
||||
content: string
|
||||
metadata: {
|
||||
size: number
|
||||
modified: string
|
||||
}
|
||||
}
|
||||
|
||||
export type PathExistsDTO = {
|
||||
exists: boolean
|
||||
type: FileKind | null
|
||||
}
|
||||
9
shared/types/index.ts
Normal file
9
shared/types/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export * from './file.js'
|
||||
export * from './module.js'
|
||||
export * from './tab.js'
|
||||
export * from './time.js'
|
||||
export * from './todo.js'
|
||||
export * from './pydemos.js'
|
||||
export * from './recycle-bin.js'
|
||||
export * from './settings.js'
|
||||
export * from './api.js'
|
||||
34
shared/types/module.ts
Normal file
34
shared/types/module.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import type { LucideIcon } from 'lucide-react'
|
||||
import type { FileItemDTO as FileItem } from './file.js'
|
||||
|
||||
export type {
|
||||
HttpMethod,
|
||||
EndpointConfig,
|
||||
EndpointDefinition,
|
||||
ModuleEndpoints,
|
||||
ModuleApiConfig,
|
||||
ModuleDefinition,
|
||||
ApiModuleConfig,
|
||||
} from '../modules/types.js'
|
||||
|
||||
export type Brand<T, TBrand extends string> = T & { __brand: TBrand }
|
||||
export type ModuleId = Brand<string, 'ModuleId'>
|
||||
|
||||
import type { ModuleDefinition, ModuleEndpoints } from '../modules/types.js'
|
||||
|
||||
export interface FrontendModuleConfig<
|
||||
TEndpoints extends ModuleEndpoints = ModuleEndpoints
|
||||
> extends Omit<ModuleDefinition<string, TEndpoints>, 'icon' | 'basePath'> {
|
||||
basePath?: string
|
||||
icon: LucideIcon
|
||||
component: React.ComponentType
|
||||
}
|
||||
|
||||
export interface InternalModuleConfig extends FrontendModuleConfig {
|
||||
tabId: string
|
||||
fileItem: FileItem
|
||||
}
|
||||
|
||||
export const createModuleId = (id: string): ModuleId => {
|
||||
return id as ModuleId
|
||||
}
|
||||
11
shared/types/pydemos.ts
Normal file
11
shared/types/pydemos.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export interface PyDemoItem {
|
||||
name: string
|
||||
path: string
|
||||
created: string
|
||||
fileCount: number
|
||||
}
|
||||
|
||||
export interface PyDemoMonth {
|
||||
month: number
|
||||
demos: PyDemoItem[]
|
||||
}
|
||||
14
shared/types/recycle-bin.ts
Normal file
14
shared/types/recycle-bin.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import type { FileKind } from './file.js'
|
||||
|
||||
export interface RecycleBinItemDTO {
|
||||
name: string
|
||||
originalName: string
|
||||
type: FileKind
|
||||
deletedDate: string
|
||||
path: string
|
||||
}
|
||||
|
||||
export interface RecycleBinGroupDTO {
|
||||
date: string
|
||||
items: RecycleBinItemDTO[]
|
||||
}
|
||||
7
shared/types/settings.ts
Normal file
7
shared/types/settings.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export type ThemeMode = 'light' | 'dark'
|
||||
|
||||
export interface SettingsDTO {
|
||||
theme?: 'light' | 'dark'
|
||||
wallpaperOpacity?: number
|
||||
markdownFontSize?: number
|
||||
}
|
||||
1
shared/types/tab.ts
Normal file
1
shared/types/tab.ts
Normal file
@@ -0,0 +1 @@
|
||||
export type TabType = 'markdown' | 'todo' | 'settings' | 'search' | 'recycle-bin' | 'weread' | 'time-tracking' | 'pydemos' | 'remote' | 'remote-desktop' | 'remote-git' | 'other'
|
||||
101
shared/types/time.ts
Normal file
101
shared/types/time.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
export type { TabType } from './tab.js'
|
||||
|
||||
export interface TimePeriod {
|
||||
start: string
|
||||
end: string
|
||||
}
|
||||
|
||||
export interface TabRecord {
|
||||
tabId: string
|
||||
filePath: string | null
|
||||
fileName: string
|
||||
tabType: import('./tab.js').TabType
|
||||
duration: number
|
||||
focusedPeriods: TimePeriod[]
|
||||
}
|
||||
|
||||
export interface TimingSession {
|
||||
id: string
|
||||
startTime: string
|
||||
endTime?: string
|
||||
duration: number
|
||||
status: 'active' | 'paused' | 'ended'
|
||||
tabRecords: TabRecord[]
|
||||
}
|
||||
|
||||
export interface TabSummary {
|
||||
fileName: string
|
||||
tabType: import('./tab.js').TabType
|
||||
totalDuration: number
|
||||
focusCount: number
|
||||
}
|
||||
|
||||
export interface DayTimeData {
|
||||
date: string
|
||||
totalDuration: number
|
||||
sessions: TimingSession[]
|
||||
tabSummary: Record<string, TabSummary>
|
||||
lastUpdated: string
|
||||
}
|
||||
|
||||
export interface DaySummary {
|
||||
totalDuration: number
|
||||
sessions: number
|
||||
topTabs: Array<{ fileName: string; duration: number }>
|
||||
}
|
||||
|
||||
export interface MonthTimeData {
|
||||
year: number
|
||||
month: number
|
||||
days: Record<string, DaySummary>
|
||||
monthlyTotal: number
|
||||
averageDaily: number
|
||||
activeDays: number
|
||||
lastUpdated: string
|
||||
}
|
||||
|
||||
export interface YearTimeData {
|
||||
year: number
|
||||
months: Record<string, { totalDuration: number; activeDays: number }>
|
||||
yearlyTotal: number
|
||||
averageMonthly: number
|
||||
averageDaily: number
|
||||
totalActiveDays: number
|
||||
}
|
||||
|
||||
export interface CurrentTimerState {
|
||||
isRunning: boolean
|
||||
isPaused: boolean
|
||||
currentSession: {
|
||||
id: string
|
||||
startTime: string
|
||||
duration: number
|
||||
currentTab: {
|
||||
tabId: string
|
||||
fileName: string
|
||||
tabType: import('./tab.js').TabType
|
||||
} | null
|
||||
} | null
|
||||
todayDuration: number
|
||||
}
|
||||
|
||||
export interface TimeStats {
|
||||
totalDuration: number
|
||||
activeDays: number
|
||||
averageDaily: number
|
||||
longestDay: { date: string; duration: number } | null
|
||||
longestSession: { date: string; duration: number } | null
|
||||
topTabs: Array<{ fileName: string; duration: number; percentage: number }>
|
||||
tabTypeDistribution: Array<{ tabType: import('./tab.js').TabType; duration: number; percentage: number }>
|
||||
}
|
||||
|
||||
export interface TimeTrackingEvent {
|
||||
type: 'tab-switch' | 'tab-open' | 'tab-close' | 'window-focus' | 'window-blur' | 'app-quit' | 'heartbeat'
|
||||
timestamp: string
|
||||
tabInfo?: {
|
||||
tabId: string
|
||||
filePath: string | null
|
||||
fileName: string
|
||||
tabType: import('./tab.js').TabType
|
||||
}
|
||||
}
|
||||
10
shared/types/todo.ts
Normal file
10
shared/types/todo.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export interface TodoItem {
|
||||
id: string
|
||||
content: string
|
||||
completed: boolean
|
||||
}
|
||||
|
||||
export interface DayTodo {
|
||||
date: string
|
||||
items: TodoItem[]
|
||||
}
|
||||
159
shared/utils/date.ts
Normal file
159
shared/utils/date.ts
Normal file
@@ -0,0 +1,159 @@
|
||||
export const pad2 = (n: number) => String(n).padStart(2, '0')
|
||||
|
||||
export const pad3 = (n: number) => String(n).padStart(3, '0')
|
||||
|
||||
export const formatTimestamp = (d: Date) => {
|
||||
const yyyy = d.getFullYear()
|
||||
const mm = pad2(d.getMonth() + 1)
|
||||
const dd = pad2(d.getDate())
|
||||
const hh = pad2(d.getHours())
|
||||
const mi = pad2(d.getMinutes())
|
||||
const ss = pad2(d.getSeconds())
|
||||
const ms = pad3(d.getMilliseconds())
|
||||
return `${yyyy}${mm}${dd}_${hh}${mi}${ss}_${ms}`
|
||||
}
|
||||
|
||||
export const formatDate = (date: Date) => {
|
||||
const yyyy = date.getFullYear()
|
||||
const mm = pad2(date.getMonth() + 1)
|
||||
const dd = pad2(date.getDate())
|
||||
return `${yyyy}-${mm}-${dd}`
|
||||
}
|
||||
|
||||
export const formatTime = (date: Date) => {
|
||||
const hh = pad2(date.getHours())
|
||||
const mm = pad2(date.getMinutes())
|
||||
const ss = pad2(date.getSeconds())
|
||||
return `${hh}:${mm}:${ss}`
|
||||
}
|
||||
|
||||
export const formatDateTime = (date: Date) => {
|
||||
return `${formatDate(date)} ${formatTime(date)}`
|
||||
}
|
||||
|
||||
export const getTodayDate = (): string => {
|
||||
return formatDate(new Date())
|
||||
}
|
||||
|
||||
export const getTomorrowDate = (): string => {
|
||||
const tomorrow = new Date()
|
||||
tomorrow.setDate(tomorrow.getDate() + 1)
|
||||
return formatDate(tomorrow)
|
||||
}
|
||||
|
||||
export const formatDateDisplay = (dateStr: string): string => {
|
||||
const today = getTodayDate()
|
||||
const tomorrow = getTomorrowDate()
|
||||
if (dateStr === today) return '今天'
|
||||
if (dateStr === tomorrow) return '明天'
|
||||
const [year, month, day] = dateStr.split('-')
|
||||
const currentYear = new Date().getFullYear()
|
||||
if (parseInt(year) === currentYear) {
|
||||
return `${parseInt(month)}月${parseInt(day)}日`
|
||||
}
|
||||
return `${year}年${parseInt(month)}月${parseInt(day)}日`
|
||||
}
|
||||
|
||||
export const formatDuration = (ms: number): string => {
|
||||
const seconds = Math.floor(ms / 1000)
|
||||
const minutes = Math.floor(seconds / 60)
|
||||
const hours = Math.floor(minutes / 60)
|
||||
|
||||
if (hours > 0) {
|
||||
const remainingMinutes = minutes % 60
|
||||
return `${hours}小时${remainingMinutes > 0 ? `${remainingMinutes}分钟` : ''}`
|
||||
}
|
||||
if (minutes > 0) {
|
||||
return `${minutes}分钟`
|
||||
}
|
||||
return `${seconds}秒`
|
||||
}
|
||||
|
||||
export const formatDurationShort = (ms: number): string => {
|
||||
const seconds = Math.floor(ms / 1000)
|
||||
const minutes = Math.floor(seconds / 60)
|
||||
const hours = Math.floor(minutes / 60)
|
||||
|
||||
if (hours > 0) {
|
||||
const remainingMinutes = minutes % 60
|
||||
return remainingMinutes > 0 ? `${hours}h${remainingMinutes}m` : `${hours}h`
|
||||
}
|
||||
if (minutes > 0) {
|
||||
return `${minutes}m`
|
||||
}
|
||||
return `${seconds}s`
|
||||
}
|
||||
|
||||
export const getWeekStart = (date: Date): Date => {
|
||||
const d = new Date(date)
|
||||
const day = d.getDay()
|
||||
const diff = d.getDate() - day + (day === 0 ? -6 : 1)
|
||||
d.setDate(diff)
|
||||
d.setHours(0, 0, 0, 0)
|
||||
return d
|
||||
}
|
||||
|
||||
export const getWeekEnd = (date: Date): Date => {
|
||||
const weekStart = getWeekStart(date)
|
||||
const weekEnd = new Date(weekStart)
|
||||
weekEnd.setDate(weekStart.getDate() + 6)
|
||||
return weekEnd
|
||||
}
|
||||
|
||||
export const getMonthStart = (date: Date): Date => {
|
||||
return new Date(date.getFullYear(), date.getMonth(), 1)
|
||||
}
|
||||
|
||||
export const getMonthEnd = (date: Date): Date => {
|
||||
return new Date(date.getFullYear(), date.getMonth() + 1, 0)
|
||||
}
|
||||
|
||||
export const getYearStart = (date: Date): Date => {
|
||||
return new Date(date.getFullYear(), 0, 1)
|
||||
}
|
||||
|
||||
export const getYearEnd = (date: Date): Date => {
|
||||
return new Date(date.getFullYear(), 11, 31)
|
||||
}
|
||||
|
||||
export const isSameDay = (date1: Date, date2: Date): boolean => {
|
||||
return formatDate(date1) === formatDate(date2)
|
||||
}
|
||||
|
||||
export const isToday = (date: Date): boolean => {
|
||||
return isSameDay(date, new Date())
|
||||
}
|
||||
|
||||
export const addDays = (date: Date, days: number): Date => {
|
||||
const result = new Date(date)
|
||||
result.setDate(result.getDate() + days)
|
||||
return result
|
||||
}
|
||||
|
||||
export const addMonths = (date: Date, months: number): Date => {
|
||||
const result = new Date(date)
|
||||
result.setMonth(result.getMonth() + months)
|
||||
return result
|
||||
}
|
||||
|
||||
export const addYears = (date: Date, years: number): Date => {
|
||||
const result = new Date(date)
|
||||
result.setFullYear(result.getFullYear() + years)
|
||||
return result
|
||||
}
|
||||
|
||||
export const parseDate = (dateStr: string): Date | null => {
|
||||
const match = dateStr.match(/^(\d{4})-(\d{2})-(\d{2})$/)
|
||||
if (!match) return null
|
||||
const [, year, month, day] = match
|
||||
return new Date(parseInt(year), parseInt(month) - 1, parseInt(day))
|
||||
}
|
||||
|
||||
export const getDaysInMonth = (year: number, month: number): number => {
|
||||
return new Date(year, month, 0).getDate()
|
||||
}
|
||||
|
||||
export const getDayOfWeek = (date: Date): string => {
|
||||
const days = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
|
||||
return days[date.getDay()]
|
||||
}
|
||||
3
shared/utils/index.ts
Normal file
3
shared/utils/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './path'
|
||||
export * from './tabType'
|
||||
export * from './date'
|
||||
54
shared/utils/path.ts
Normal file
54
shared/utils/path.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
export const toPosixPath = (p: string) => p.replace(/\\/g, '/')
|
||||
|
||||
export const toWindowsPath = (p: string) => p.replace(/\//g, '\\')
|
||||
|
||||
export const normalizePath = (p: string) => {
|
||||
const parts = p.replace(/\\/g, '/').split('/')
|
||||
const stack: string[] = []
|
||||
for (const part of parts) {
|
||||
if (!part || part === '.') continue
|
||||
if (part === '..') {
|
||||
stack.pop()
|
||||
continue
|
||||
}
|
||||
stack.push(part)
|
||||
}
|
||||
return stack.join('/')
|
||||
}
|
||||
|
||||
export const joinPaths = (...paths: string[]) => {
|
||||
return paths.map(normalizePath).join('/').replace(/\/+/g, '/')
|
||||
}
|
||||
|
||||
export const getDirectoryName = (filePath: string) => {
|
||||
const normalized = normalizePath(filePath)
|
||||
const lastSlash = normalized.lastIndexOf('/')
|
||||
return lastSlash === -1 ? '' : normalized.slice(0, lastSlash)
|
||||
}
|
||||
|
||||
export const getFileName = (filePath: string) => {
|
||||
const normalized = normalizePath(filePath)
|
||||
const lastSlash = normalized.lastIndexOf('/')
|
||||
return lastSlash === -1 ? normalized : normalized.slice(lastSlash + 1)
|
||||
}
|
||||
|
||||
export const getFileExtension = (filePath: string) => {
|
||||
const fileName = getFileName(filePath)
|
||||
const lastDot = fileName.lastIndexOf('.')
|
||||
return lastDot === -1 ? '' : fileName.slice(lastDot + 1)
|
||||
}
|
||||
|
||||
export const removeExtension = (filePath: string) => {
|
||||
const ext = getFileExtension(filePath)
|
||||
if (!ext) return filePath
|
||||
return filePath.slice(0, -(ext.length + 1))
|
||||
}
|
||||
|
||||
export const isAbsolutePath = (p: string) => {
|
||||
return p.startsWith('/') || /^[A-Za-z]:/.test(p)
|
||||
}
|
||||
|
||||
export const isHiddenPath = (p: string) => {
|
||||
const normalized = normalizePath(p)
|
||||
return normalized.split('/').some(part => part.startsWith('.'))
|
||||
}
|
||||
65
shared/utils/tabType.ts
Normal file
65
shared/utils/tabType.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import type { TabType } from '../types/tab.js'
|
||||
|
||||
const KNOWN_MODULE_IDS = [
|
||||
'home', 'settings', 'search', 'weread',
|
||||
'recycle-bin', 'todo', 'time-tracking', 'pydemos'
|
||||
] as const
|
||||
|
||||
export function getTabTypeFromPath(filePath: string | null): TabType {
|
||||
if (!filePath) return 'other'
|
||||
|
||||
if (filePath.startsWith('remote-git://')) {
|
||||
return 'remote-git'
|
||||
}
|
||||
|
||||
if (filePath.startsWith('remote-desktop://')) {
|
||||
return 'remote-desktop'
|
||||
}
|
||||
|
||||
if (filePath.startsWith('remote-') && filePath !== 'remote-tab') {
|
||||
return 'remote-desktop'
|
||||
}
|
||||
|
||||
if (filePath === 'remote-tab' || filePath === 'remote') {
|
||||
return 'remote'
|
||||
}
|
||||
|
||||
for (const moduleId of KNOWN_MODULE_IDS) {
|
||||
if (filePath === `${moduleId}-tab` || filePath === moduleId) {
|
||||
if (moduleId === 'home' || moduleId === 'settings' || moduleId === 'search' || moduleId === 'weread') {
|
||||
return 'other'
|
||||
}
|
||||
return moduleId as TabType
|
||||
}
|
||||
}
|
||||
|
||||
if (filePath.endsWith('.md')) {
|
||||
return 'markdown'
|
||||
}
|
||||
|
||||
return 'other'
|
||||
}
|
||||
|
||||
export function getFileNameFromPath(filePath: string | null): string {
|
||||
if (!filePath) return '未知'
|
||||
|
||||
for (const moduleId of KNOWN_MODULE_IDS) {
|
||||
if (filePath === `${moduleId}-tab` || filePath === moduleId) {
|
||||
const names: Record<string, string> = {
|
||||
'home': '首页',
|
||||
'settings': '设置',
|
||||
'search': '搜索',
|
||||
'weread': '微信读书',
|
||||
'recycle-bin': '回收站',
|
||||
'todo': 'TODO',
|
||||
'time-tracking': '时间统计',
|
||||
'pydemos': 'Python Demo',
|
||||
'remote': '远程桌面',
|
||||
}
|
||||
return names[moduleId] ?? moduleId
|
||||
}
|
||||
}
|
||||
|
||||
const parts = filePath.split('/')
|
||||
return parts[parts.length - 1] || filePath
|
||||
}
|
||||
Reference in New Issue
Block a user