Add HelpPanel and TodoItemTouch components, extend task commands, and refactor task and view logic

- Introduced `HelpPanel.vue` for displaying keyboard shortcuts and command descriptions.
- Added `TodoItemTouch.vue`, a mobile-friendly task item component with updated bindings and improved actions.
- Extended task commands with support for tagging, due date parsing, and dynamic text formatting.
- Implemented `useActions` utility for parsing and executing command-based task modifications.
- Streamlined task editing and creation in `useTasks` for consistency and API integration.
- Updated `ListScreen` to support collapsible, categorized task lists with visual enhancements.
- Refactored `App.vue` for adaptive input handling on mobile versus desktop views.
- Enhanced API communication in `useApi` with cleaner header generation and error handling.
This commit is contained in:
2026-02-23 16:34:52 +01:00
parent ec76a52fdd
commit 56f89b6669
21 changed files with 1347 additions and 214 deletions

View File

@@ -0,0 +1,109 @@
import type { Task } from '../types.ts'
import {
archiveCommand,
beginCommand,
checkCommand,
deleteCommand,
dueCommand,
editTaskCommand,
flagCommand,
insertTaskCommand,
moveCommand,
restoreCommand,
stopCommand,
switchCommand,
tagRenameCommand,
} from '../utils/actions.ts'
import { parseCommand } from '../utils/parser.ts'
import { useTasks } from './useTasks.ts'
export default function useActions() {
const { tasks: tasksOriginal, createTask, updateTask } = useTasks()
const run = (value: string) => {
const cmd = parseCommand(value)
let tasksToUpdate: Task[] = []
let taskToCreate: Pick<Task, 'tag' | 'title' | 'dueDate'> | null = null
const tasks = JSON.parse(JSON.stringify(tasksOriginal.value)) as Task[]
if (cmd) {
const ids = cmd.id
? (cmd.id.match(/\d+/g) || []).map(s => Number.parseInt(s))
: []
switch (cmd.command.toLowerCase()) {
case 'mv':
case 'move':
tasksToUpdate = moveCommand(tasks, ids, cmd)
break
case 'b':
case 'begin':
tasksToUpdate = beginCommand(tasks, ids)
break
case 'c':
case 'check':
tasksToUpdate = checkCommand(tasks, ids)
break
case 'd':
case 'delete':
tasksToUpdate = deleteCommand(ids, cmd, tasks)
break
case 'fl':
case 'flag':
tasksToUpdate = flagCommand(tasks, ids)
break
case 'st':
case 'stop':
tasksToUpdate = stopCommand(tasks, ids)
break
case 'sw':
case 'switch':
tasksToUpdate = switchCommand(tasks, ids)
break
case 'a':
case 'archive':
tasksToUpdate = archiveCommand(ids, cmd, tasks)
break
case 're':
case 'restore':
tasksToUpdate = restoreCommand(ids, cmd, tasks)
break
case 't':
case 'task':
taskToCreate = insertTaskCommand(cmd)
break
case 'e':
case 'edit':
tasksToUpdate = editTaskCommand(ids, cmd, tasks)
break
case 'due':
tasksToUpdate = dueCommand(ids, cmd, tasks)
break
case 'tr':
case 'tagre':
case 'tagrename':
tasksToUpdate = tagRenameCommand(cmd, tasks)
break
/* Visibility */
// case 'hide':
// updateCandidate = hideCommand(updateCandidate, cmd)
// break
// case 'show':
// updateCandidate = showCommand(updateCandidate, cmd)
// break
// /* Single command */
// case 'search':
// updateCandidate = searchCommand(updateCandidate, cmd)
// break
// default:
// updateCandidate = otherCommand(updateCandidate, cmd, state)
// break
}
}
// console.log(tasksToUpdate, taskToCreate)
if (tasksToUpdate) {
updateTask(tasksToUpdate)
}
if (taskToCreate) {
createTask(taskToCreate)
}
}
return { run }
}

View File

@@ -1,42 +1,43 @@
import { useStore } from './useStore.ts';
import { useCrypto } from './useCrypto.ts';
import { useCrypto } from './useCrypto.ts'
import { useStore } from './useStore.ts'
const BASE_URL = 'https://automation.deep-node.de/webhook';
const BASE_URL = 'https://automation.deep-node.de/webhook'
type Settings = {
username: string;
password: string;
};
interface Settings {
username: string
password: string
}
const isTauri = () =>
typeof window !== 'undefined' && Boolean((window as typeof window & { __TAURI__?: unknown }).__TAURI__);
function isTauri() {
return typeof window !== 'undefined' && Boolean((window as typeof window & { __TAURI__?: unknown }).__TAURI__)
}
async function buildAuthHeader(): Promise<string | undefined> {
const {decrypt} = useCrypto();
const { getValue } = useStore();
const settings = await getValue<Settings>('settings');
if (!settings) return undefined;
let {username, password} = settings;
password = decrypt(password) as string;
const { decrypt } = useCrypto()
const { getValue } = useStore()
const settings = await getValue<Settings>('settings')
if (!settings)
return undefined
let { username, password } = settings
password = decrypt(password) as string
if (username && password) {
const token = btoa(`${username}:${password}`);
return `Basic ${token}`;
const token = btoa(`${username}:${password}`)
return `Basic ${token}`
}
return undefined;
return undefined
}
export function useApi() {
const apiFetch = async (endpoint: string, options: RequestInit = {}) => {
const url = endpoint.startsWith('http') ? endpoint : `${BASE_URL}/${endpoint}`;
const url = endpoint.startsWith('http') ? endpoint : `${BASE_URL}/${endpoint}`
const authHeader = await buildAuthHeader();
console.log('authHeader',authHeader);
const authHeader = await buildAuthHeader()
const headers = {
'Content-Type': 'application/json',
...(authHeader ? { Authorization: authHeader } : {}),
...options.headers,
} as Record<string, string>;
} as Record<string, string>
const response = isTauri()
? await (await import('@tauri-apps/plugin-http')).fetch(url, {
@@ -46,28 +47,28 @@ export function useApi() {
: await fetch(url, {
...options,
headers,
});
})
if (!response.ok) {
throw new Error(`API call failed: ${response.statusText}`);
throw new Error(`API call failed: ${response.statusText}`)
}
return response;
};
return response
}
const get = (endpoint: string, options: RequestInit = {}) =>
apiFetch(endpoint, { ...options, method: 'GET' });
apiFetch(endpoint, { ...options, method: 'GET' })
const post = (endpoint: string, body: unknown, options: RequestInit = {}) =>
apiFetch(endpoint, { ...options, method: 'POST', body: JSON.stringify(body) });
apiFetch(endpoint, { ...options, method: 'POST', body: JSON.stringify(body) })
const put = (endpoint: string, body: unknown, options: RequestInit = {}) =>
apiFetch(endpoint, { ...options, method: 'PUT', body: JSON.stringify(body) });
apiFetch(endpoint, { ...options, method: 'PUT', body: JSON.stringify(body) })
return {
get,
post,
put,
fetch: apiFetch,
};
}
}

View File

@@ -0,0 +1,49 @@
import { computed, onMounted, ref } from 'vue'
import { useStore } from './useStore.ts'
export default function useHistory() {
const { getValue, setValue } = useStore()
const store = ref<string[]>([])
const history = computed<string[]>(() => store.value)
const historyIndex = ref(0)
const resetHistoryIndex = () => {
historyIndex.value = store.value.length - 1
}
onMounted(async () => {
store.value = await getValue('history') || []
resetHistoryIndex()
console.log({ s: store.value })
})
const pushHistory = (item: string) => {
if (store.value.length > 20)
store.value.shift()
store.value.push(item.trim())
setValue('history', store.value)
}
const matchHistory = (item: string) => {
const match = store.value.filter(i => i.startsWith(item.trim())).pop()
if (match) {
historyIndex.value = store.value.indexOf(match)
return match
}
return ''
}
const moveHistory = (direction: 'up' | 'down') => {
if (direction === 'up') {
historyIndex.value = Math.max(0, historyIndex.value - 1)
}
else {
historyIndex.value = Math.min(store.value.length - 1, historyIndex.value + 1)
}
}
const historyItem = computed(() => store.value[historyIndex.value])
return { history, pushHistory, matchHistory, moveHistory, historyItem, resetHistoryIndex }
}

View File

@@ -1,52 +1,51 @@
import type { Store } from '@tauri-apps/plugin-store';
import type { Store } from '@tauri-apps/plugin-store'
let storePromise: Promise<Store> | null = null;
let storePromise: Promise<Store> | null = null
const isTauri = () =>
typeof window !== 'undefined' && Boolean((window as typeof window & { __TAURI__?: unknown }).__TAURI__);
function isTauri() {
return typeof window !== 'undefined' && Boolean((window as typeof window & { __TAURI__?: unknown }).__TAURI__)
}
async function getStore(): Promise<Store> {
if (!storePromise) {
const { load } = await import('@tauri-apps/plugin-store');
const { load } = await import('@tauri-apps/plugin-store')
storePromise = load('store.json', {
autoSave: false,
defaults: {},
});
})
}
return storePromise;
return storePromise
}
export function useStore() {
const setValue = async <T>(key: string, value: T) => {
console.log('setValue',key,value);
if (isTauri()) {
const store = await getStore();
await store.set(key, value);
await store.save();
return;
const store = await getStore()
await store.set(key, value)
await store.save()
return
}
localStorage.setItem(key, JSON.stringify(value));
};
localStorage.setItem(key, JSON.stringify(value))
}
const getValue = async <T>(key: string) => {
console.log('getValue',key);
if (isTauri()) {
const store = await getStore();
return store.get<T>(key);
const store = await getStore()
return store.get<T>(key)
}
const rawValue = localStorage.getItem(key);
if (rawValue === null) return null;
const rawValue = localStorage.getItem(key)
if (rawValue === null)
return null
try {
return JSON.parse(rawValue) as T;
} catch {
return null;
return JSON.parse(rawValue) as T
}
};
catch {
return null
}
}
return {
setValue,
getValue,
};
}
}

View File

@@ -1,6 +1,7 @@
import type { Task } from '../types.ts'
import { useArrayUnique } from '@vueuse/core'
import { ref } from 'vue'
import { TaskStatus } from '../types.ts'
import { useApi } from './useApi.ts'
const tasks = ref<Task[]>([])
@@ -29,22 +30,23 @@ export function useTasks() {
}
}
const createTask = async (taskData: Partial<Task>) => {
const createTask = async (taskData: Pick<Task, 'title' | 'dueDate' | 'tag'>) => {
// Get next ID as per current logic in CreateScreen.vue
const nextId = () => tasks.value.sort((a, b) => a.id_ - b.id_).reduce((acc, task) => {
if (task.id_ === acc + 1)
return acc + 1
return acc
}, 0) + 1
isLoading.value = true
error.value = null
try {
// Get next ID as per current logic in CreateScreen.vue
const nextId = tasks.value.sort((a, b) => b.id_ - a.id_).reduce((acc, task) => {
if (task.id_ === acc + 1)
return acc + 1
return task.id_ + 1
}, 0)
const newTask: Partial<Task> = {
...taskData,
id_: nextId,
const newTask: Omit<Task, 'id'> = {
id_: nextId(),
status: TaskStatus.WAIT,
logs: [],
lastaction: Date.now(),
archived: false,
...taskData,
}
const data = await api.put('e5880167-9322-4d7b-8a38-e06bae8a7734/list', { tasks: [newTask] }).then(res => res.json())
@@ -62,12 +64,12 @@ export function useTasks() {
}
}
const updateTask = async (task: Task) => {
console.log('updateTask', task)
const updateTask = async (task: Task | Task[]) => {
isLoading.value = true
error.value = null
const tasksToUpdate = (Array.isArray(task) ? task : [task]).map(t => ({ ...t, lastaction: Date.now() } as Task))
try {
const data = await api.put('e5880167-9322-4d7b-8a38-e06bae8a7734/list', { tasks: [task] }).then(res => res.json())
const data = await api.put('e5880167-9322-4d7b-8a38-e06bae8a7734/list', { tasks: tasksToUpdate }).then(res => res.json())
if (data.tasks) {
tasks.value = data.tasks
}