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:
109
src/composables/useActions.ts
Normal file
109
src/composables/useActions.ts
Normal 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 }
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
49
src/composables/useHistory.ts
Normal file
49
src/composables/useHistory.ts
Normal 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 }
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user