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

@@ -26,21 +26,22 @@
"daisyui": "^5.5.18",
"jsencrypt": "^3.5.4",
"luxon": "^3.7.2",
"sherlockjs": "^1.4.2",
"uuid": "^13.0.0",
"vue": "^3.5.28",
"vue-router": "^4.6.4"
},
"devDependencies": {
"@antfu/eslint-config": "^7.4.3",
"@tailwindcss/vite": "^4.2.0",
"@tauri-apps/cli": "^2",
"@vitejs/plugin-vue": "^6.0.4",
"eslint": "^9.39.2",
"eslint-plugin-format": "^1.4.0",
"tailwindcss": "^4.2.0",
"typescript": "~5.9.3",
"vite": "^7.3.1",
"vue-tsc": "^2.1.10",
"@antfu/eslint-config": "^7.4.3",
"eslint": "^9.39.2",
"eslint-plugin-format": "^1.4.0"
"vue-tsc": "^2.1.10"
},
"packageManager": "pnpm@10.27.0+sha512.72d699da16b1179c14ba9e64dc71c9a40988cbdc65c264cb0e489db7de917f20dcf4d64d8723625f2969ba52d4b7e2a1170682d9ac2a5dcaeaab732b7e16f04a"
}

8
pnpm-lock.yaml generated
View File

@@ -38,6 +38,9 @@ importers:
luxon:
specifier: ^3.7.2
version: 3.7.2
sherlockjs:
specifier: ^1.4.2
version: 1.4.2
uuid:
specifier: ^13.0.0
version: 13.0.0
@@ -1947,6 +1950,9 @@ packages:
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
engines: {node: '>=8'}
sherlockjs@1.4.2:
resolution: {integrity: sha512-Liynk2FRTyiHLzMqoe1LvRkT3MhMjUphIBKGa1pRovDKXEaygwpha76om/qV9YTG3qFr1+UIifEuH0VU+KVuRA==}
sisteransi@1.0.5:
resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}
@@ -4094,6 +4100,8 @@ snapshots:
shebang-regex@3.0.0: {}
sherlockjs@1.4.2: {}
sisteransi@1.0.5: {}
source-map-js@1.2.1: {}

View File

@@ -1,9 +1,14 @@
<script setup lang="ts">
import { PhCheckSquareOffset, PhListChecks, PhSliders } from '@phosphor-icons/vue'
import { useMediaQuery } from '@vueuse/core'
import { computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import CreateInput from './components/CreateInput.vue'
import CreateModal from './components/CreateModal.vue'
import { useTasks } from './composables/useTasks.ts'
const isMobile = useMediaQuery('(pointer: coarse)')
const { fetchTasks } = useTasks()
const router = useRouter()
const currentPath = computed(() => router.currentRoute.value.path)
@@ -11,22 +16,13 @@ onMounted(fetchTasks)
</script>
<template>
<div class="overflow-hidden">
<main class="pb-40 overflow-y-scroll h-screen">
<div>
<main class="h-screen overflow-y-auto">
<RouterView />
</main>
<div class="dock dock-xl inset-shadow-sm">
<RouterLink to="/create" :class="currentPath === '/create' ? 'dock-active' : ''">
<PhCheckSquareOffset :size="32" />
</RouterLink>
<RouterLink to="/" :class="currentPath === '/' ? 'dock-active' : ''">
<PhListChecks :size="32" />
</RouterLink>
<RouterLink to="/settings" :class="currentPath === '/settings' ? 'dock-active' : ''">
<PhSliders :size="32" />
</RouterLink>
</div>
<template v-if="currentPath === '/'">
<CreateModal v-if="isMobile" />
<CreateInput v-else />
</template>
</div>
</template>

View File

@@ -0,0 +1,142 @@
<script setup lang="ts">
import { onClickOutside, onKeyStroke, onStartTyping } from '@vueuse/core'
import { ref, useTemplateRef, watchEffect } from 'vue'
import useActions from '../composables/useActions.ts'
import useHistory from '../composables/useHistory.ts'
import { useTasks } from '../composables/useTasks.ts'
import HelpPanel from './HelpPanel.vue'
const inputComponent = useTemplateRef('inputComponent')
const value = ref('')
const { run } = useActions()
const { pushHistory, matchHistory, historyItem, moveHistory, resetHistoryIndex } = useHistory()
const { tasks } = useTasks()
const inputActive = ref(false)
const showHelp = ref(false)
const suggestion = ref('')
const rememberTemp = ref('')
function resetInput() {
value.value = ''
suggestion.value = ''
rememberTemp.value = value.value = ''
resetHistoryIndex()
inputActive.value = false
inputComponent?.value?.blur()
}
onKeyStroke('Escape', (e) => {
e.preventDefault()
resetInput()
})
onClickOutside(inputComponent, resetInput)
onKeyStroke(' ', (e) => {
if (!inputActive.value) {
e.preventDefault()
showHelp.value = true
inputActive.value = true
inputComponent?.value?.focus()
}
})
onStartTyping(() => {
showHelp.value = false
inputActive.value = true
inputComponent?.value?.focus()
})
function handleSubmit() {
run(value.value)
pushHistory(value.value)
resetInput()
}
onKeyStroke('Tab', (e) => {
if (inputActive.value && suggestion.value.length > 0) {
e.preventDefault()
value.value = suggestion.value
}
})
onKeyStroke('ArrowUp', (e) => {
if (inputActive.value) {
e.preventDefault()
if (suggestion.value.trim().length > 0) {
moveHistory('up')
}
else {
rememberTemp.value = value.value
}
value.value = historyItem.value
}
else {
inputActive.value = true
inputComponent?.value?.focus()
value.value = ''
}
})
onKeyStroke('ArrowDown', (e) => {
if (inputActive.value) {
e.preventDefault()
if (suggestion.value.trim().length > 0) {
const before = historyItem.value
moveHistory('down')
const after = historyItem.value
if (before !== after) {
value.value = after
}
else {
value.value = rememberTemp.value
}
}
}
})
watchEffect(() => {
let match = ''
if (value.value.length > 0) {
const isEditCommand = value.value.match(/^e(?:dit)? (\d+)\s?$/i)
if (isEditCommand && isEditCommand[1]) {
const id = Number.parseInt(isEditCommand[1])
if (id && !Number.isNaN(id)) {
const found = (tasks.value.filter(t => t.id_ === id) || []).pop()
if (found) {
match = `${value.value.trim()} ${found.title}`
}
}
}
else {
match = matchHistory(value.value.trim())
}
}
suggestion.value = match
})
</script>
<template>
<form class="flex bottom-0 right-0 left-0 top-0 justify-center items-center flex-col gap-6 transition-opacity duration-500 bg-primary/10 " :class=" inputActive ? 'fixed' : 'opacity-0' " @submit.prevent="handleSubmit">
<div class="font-mono w-10/12 max-w-md relative h-10">
<div class="absolute top-0 left-0 right-0 bottom-0 bg-white px-4 py-2">
{{ suggestion }}
</div>
<input
ref="inputComponent"
v-model="value" type="text"
class="absolute top-0 left-0 right-0 bottom-0 bg-white/40 px-4 py-2 rounded-lg shadow-2xl focus:outline-none focus:ring-2 focus:ring-primary h-full w-full"
>
</div>
<div v-if="showHelp" class="bg-white/70 backdrop-blur-xs p-6 rounded-lg shadow-xl text-center">
<HelpPanel />
</div>
</form>
</template>
<style scoped>
</style>

View File

@@ -0,0 +1,68 @@
<script setup lang="ts">
import type { Task } from '../types.ts'
import { PhPlus } from '@phosphor-icons/vue'
import { useTemplateRef } from 'vue'
import { useTasks } from '../composables/useTasks.ts'
import { router } from '../router.ts'
const { createTask, tasks } = useTasks()
async function handleSubmit(e: Event) {
const data = new FormData(e.target as HTMLFormElement)
const task: Partial<Task> = Object.fromEntries(data)
await createTask(task)
await router.push('/')
}
const createModal = useTemplateRef('createModal')
</script>
<template>
<div>
<div class="fab">
<button class="btn btn-xl btn-circle btn-secondary" @click="createModal?.showModal()">
<PhPlus size="24" weight="bold" />
</button>
</div>
<dialog ref="createModal" class="modal">
<div class="modal-box">
<form method="dialog">
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">
</button>
</form>
<form @submit.prevent="handleSubmit">
<fieldset class="fieldset">
<legend class="fieldset-legend">
What is your name?
</legend>
<input type="text" class="input" name="title" placeholder="Type here">
</fieldset>
<fieldset class="fieldset">
<legend class="fieldset-legend">
Category
</legend>
<select class="select" name="tag">
<option disabled selected value="@uncategorized">
@uncategorized
</option>
<option value="@home">
@home
</option>
<option value="@work">
Amber
</option>
</select>
</fieldset>
<button class="btn btn-primary">
Submit
</button>
</form>
</div>
</dialog>
</div>
</template>
<style scoped>
</style>

View File

@@ -0,0 +1,92 @@
<script setup lang="ts">
</script>
<template>
<div class="font-mono text-sm text-left">
<p>
<b>
<u>/</u>:
</b>
search for anything
</p>
<p>
<b>
<u>t</u>ask:
</b>&nbsp;
create a new task
</p>
<p>
<b>
<u>e</u>dit:
</b>&nbsp;
edit a task
</p>
<p>
<b>
<u>due</u>:
</b>&nbsp;
set due date or <u>clear</u>
</p>
<p>
<b>
<u>b</u>egin:
</b>&nbsp;
start timer
</p>
<p>
<b>
<u>st</u>op:
</b>&nbsp;
stop timer
</p>
<p>
<b>
<u>fl</u>ag:
</b>&nbsp;
flag a task
</p>
<p>
<b>
<u>move</u>:
</b>&nbsp;
move task to another tag
</p>
<p>
<b>
<u>d</u>elete:
</b>&nbsp;
delete task
</p>
<p>
<b>
<u>a</u>rchive:
</b>&nbsp;
archive a task
</p>
<p>
<b>
<u>re</u>store:
</b>&nbsp;
unarchive a task
</p>
<p>
<b>
<u>sw</u>itch:
</b>&nbsp;
switch the working task
</p>
<p>
<b>
<u>list-archived</u>:
</b>&nbsp;
show archived tasks
</p>
<p>
<b>
<u>today</u>:
</b>&nbsp;
show today overview
</p>
</div>
</template>

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
import type { Task } from '../types.ts'
import { PhCheckSquare, PhDotsThree, PhFlag, PhPause, PhPlay, PhSquare, PhX } from '@phosphor-icons/vue'
import { PhCheckSquare, PhFlag, PhPlay, PhSquare } from '@phosphor-icons/vue'
import { DateTime } from 'luxon'
import { computed, ref } from 'vue'
import { useTasks } from '../composables/useTasks.ts'
@@ -37,48 +37,20 @@ async function handleClick(update: Partial<Task>) {
</script>
<template>
<li class="list-row">
<div class="flex items-center justify-center">
<button class="btn btn-square btn-ghost" @click="statusSelectVisible = !statusSelectVisible">
<PhX v-if="statusSelectVisible" :size="20" />
<template v-else>
<PhSquare v-if="task.status === TaskStatus.WAIT" :size="20" />
<PhCheckSquare v-else-if="task.status === TaskStatus.DONE" :size="20" weight="fill" class="text-success" />
<PhFlag v-else-if="task.status === TaskStatus.FLAG" :size="20" weight="fill" class="text-warning" />
<PhPlay v-else-if="task.status === TaskStatus.WIP" :size="20" weight="fill" class="text-info" />
</template>
</button>
<Transition>
<div v-if="statusSelectVisible" class="">
<template v-if="task.status !== TaskStatus.WIP">
<button v-if="task.status !== TaskStatus.DONE" class="btn btn-square btn-ghost" @click="handleClick({ status: TaskStatus.DONE })">
<PhCheckSquare :size="24" weight="regular" class="text-success" />
</button>
<button v-if="task.status !== TaskStatus.WAIT" class="btn btn-square btn-ghost" @click="handleClick({ status: TaskStatus.WAIT })">
<PhSquare :size="24" weight="regular" />
</button>
<button v-if="task.status !== TaskStatus.FLAG" class="btn btn-square btn-ghost" @click="handleClick({ status: TaskStatus.FLAG })">
<PhFlag :size="24" weight="fill" class="text-warning" />
</button>
<button class="btn btn-square btn-ghost" @click="handleClick({ status: TaskStatus.WIP })">
<PhPlay :size="24" weight="fill" class="text-info" />
</button>
</template>
<button v-else class="btn btn-square btn-ghost" @click="handleClick({ status: TaskStatus.WAIT })">
<PhPause :size="24" weight="fill" class="text-info" />
</button>
</div>
</Transition>
</div>
<div class="flex flex-col justify-center">
<div>{{ task.id_ }} {{ task.title }}</div>
<div v-if="task.dueDate" :class="dueColor">
{{ DateTime.fromMillis(task.dueDate).toFormat('dd/MM/yyyy') }}
<li class="">
<div class="font-mono text-sm flex flex-row justify-start gap-4">
<span>{{ String(task.id_).padStart(3, '&nbsp;') }}</span>
<div class="flex items-center justify-center">
<PhSquare v-if="task.status === TaskStatus.WAIT" :size="16" />
<PhCheckSquare v-else-if="task.status === TaskStatus.DONE" :size="16" weight="fill" class="text-success" />
<PhFlag v-else-if="task.status === TaskStatus.FLAG" :size="16" weight="fill" class="text-warning" />
<PhPlay v-else-if="task.status === TaskStatus.WIP" :size="16" weight="fill" class="text-info" />
</div>
<span>{{ task.title }}</span>
<span v-if="task.dueDate" :class="dueColor">
{{ DateTime.fromMillis(task.dueDate).toFormat('dd/MM/yyyy') }}
</span>
</div>
<button class="btn btn-square btn-ghost">
<PhDotsThree :size="24" weight="regular" />
</button>
</li>
</template>

View File

@@ -0,0 +1,87 @@
<script setup lang="ts">
import type { Task } from '../types.ts'
import { PhCheckSquare, PhDotsThree, PhFlag, PhPause, PhPlay, PhSquare, PhX } from '@phosphor-icons/vue'
import { DateTime } from 'luxon'
import { computed, ref } from 'vue'
import { useTasks } from '../composables/useTasks.ts'
import { TaskStatus } from '../types.ts'
const { task } = defineProps<{ task: Task }>()
const { updateTask } = useTasks()
const dueColor = computed(() => {
const dueDiff = task.dueDate ? DateTime.fromMillis(task.dueDate).diffNow('days').days : undefined
if (!dueDiff)
return ''
if (dueDiff < 0) {
return 'text-error'
}
else if (dueDiff < 2) {
return 'text-warning'
}
else if (dueDiff < 7) {
return 'text-success'
}
else {
return 'text-neutral'
}
})
const statusSelectVisible = ref(false)
async function handleClick(update: Partial<Task>) {
updateTask({ ...task, ...update })
statusSelectVisible.value = false
}
</script>
<template>
<li class="list-row">
<div class="flex items-center justify-center">
<button class="btn btn-square btn-ghost" @click="statusSelectVisible = !statusSelectVisible">
<PhX v-if="statusSelectVisible" :size="20" />
<template v-else>
<PhSquare v-if="task.status === TaskStatus.WAIT" :size="20" />
<PhCheckSquare v-else-if="task.status === TaskStatus.DONE" :size="20" weight="fill" class="text-success" />
<PhFlag v-else-if="task.status === TaskStatus.FLAG" :size="20" weight="fill" class="text-warning" />
<PhPlay v-else-if="task.status === TaskStatus.WIP" :size="20" weight="fill" class="text-info" />
</template>
</button>
<Transition>
<div v-if="statusSelectVisible" class="">
<template v-if="task.status !== TaskStatus.WIP">
<button v-if="task.status !== TaskStatus.DONE" class="btn btn-square btn-ghost" @click="handleClick({ status: TaskStatus.DONE })">
<PhCheckSquare :size="24" weight="regular" class="text-success" />
</button>
<button v-if="task.status !== TaskStatus.WAIT" class="btn btn-square btn-ghost" @click="handleClick({ status: TaskStatus.WAIT })">
<PhSquare :size="24" weight="regular" />
</button>
<button v-if="task.status !== TaskStatus.FLAG" class="btn btn-square btn-ghost" @click="handleClick({ status: TaskStatus.FLAG })">
<PhFlag :size="24" weight="fill" class="text-warning" />
</button>
<button class="btn btn-square btn-ghost" @click="handleClick({ status: TaskStatus.WIP })">
<PhPlay :size="24" weight="fill" class="text-info" />
</button>
</template>
<button v-else class="btn btn-square btn-ghost" @click="handleClick({ status: TaskStatus.WAIT })">
<PhPause :size="24" weight="fill" class="text-info" />
</button>
</div>
</Transition>
</div>
<div class="flex flex-col justify-center">
<div>{{ task.id }} {{ task.id_ }} {{ task.title }}</div>
<div v-if="task.dueDate" :class="dueColor">
{{ DateTime.fromMillis(task.dueDate).toFormat('dd/MM/yyyy') }}
</div>
</div>
<button class="btn btn-square btn-ghost">
<PhDotsThree :size="24" weight="regular" />
</button>
</li>
</template>
<style scoped>
</style>

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
}

View File

@@ -1,13 +1,10 @@
import { createWebHistory, createRouter } from 'vue-router'
import ListScreen from './screens/ListScreen.vue';
import SettingsScreen from './screens/SettingsScreen.vue';
import CreateScreen from './screens/CreateScreen.vue';
import { createRouter, createWebHistory } from 'vue-router'
import ListScreen from './screens/ListScreen.vue'
import SettingsScreen from './screens/SettingsScreen.vue'
const routes = [
{ path: '/', component: ListScreen },
{ path: '/settings', component: SettingsScreen },
{ path: '/create', component: CreateScreen }
]
export const router = createRouter({

View File

@@ -1,35 +0,0 @@
<script setup lang="ts">
import type { Task } from '../types.ts'
import { useTasks } from '../composables/useTasks.ts'
import { router } from '../router.ts'
const { createTask, tasks } = useTasks()
async function handleSubmit(e: Event) {
const data = new FormData(e.target as HTMLFormElement)
const task: Partial<Task> = Object.fromEntries(data)
await createTask(task)
await router.push('/')
}
</script>
<template>
<div>
<form @submit.prevent="handleSubmit">
<fieldset class="fieldset">
<legend class="fieldset-legend">
What is your name?
</legend>
<input type="text" class="input" name="title" placeholder="Type here">
</fieldset>
<button class="btn btn-primary">
Submit
</button>
</form>
</div>
</template>
<style scoped>
</style>

View File

@@ -1,6 +1,5 @@
<script setup lang="ts">
import type { Task } from '../types.ts'
import { PhCaretDown, PhCaretUp } from '@phosphor-icons/vue'
import { computed, onMounted, ref } from 'vue'
import TodoItem from '../components/TodoItem.vue'
@@ -14,12 +13,21 @@ onMounted(async () => {
const visibleTasks = computed<Task[]>(() => tasks.value.filter(task => !task.archived).sort((a, b) => a.id_ - b.id_))
const categorizedTasks = computed(() => visibleTasks.value.reduce((acc, task) => {
const tag = task.tag ?? '@uncategorized'
acc[tag] = acc[tag] ?? []
acc[tag].push(task)
return acc
}, {} as Record<string, Task[]>))
const categorizedTasks = computed(() => {
const unordered: Record<string, Task[]> = visibleTasks.value.reduce((acc, task) => {
const tag = task.tag ?? '@uncategorized'
acc[tag] = acc[tag] ?? []
acc[tag].push(task)
return acc
}, {} as Record<string, Task[]>)
return Object.keys(unordered).sort().reduce(
(obj, key) => {
obj[key] = unordered[key]
return obj
},
{} as Record<string, Task[]>,
)
})
const collapsed = ref<string[]>([])
</script>
@@ -27,22 +35,15 @@ const collapsed = ref<string[]>([])
<template>
<div>
<div class="flex flex-col gap-4">
<div v-for="(tasks, category) in categorizedTasks" :key="category" class="m-4 rounded-box border border-neutral-100 shadow-md">
<div class="m-4 flex justify-between items-center">
<div class="badge badge-xl badge-primary">
{{ category }}
</div>
<button
class="btn btn-square btn-sm"
@click="collapsed.includes(category) ? collapsed.splice(collapsed.indexOf(category), 1) : collapsed.push(category)"
>
<PhCaretDown v-if="collapsed.includes(category)" :size="20" />
<PhCaretUp v-else :size="20" />
<div v-for="(catTasks, category) in categorizedTasks" :key="category" class="m-4 ">
<div class="mb-4">
<button class="px-4 py-1 rounded bg-neutral-200 font-mono text-sm font-bold" @click="collapsed.includes(category) ? collapsed.splice(collapsed.indexOf(category), 1) : collapsed.push(category)">
{{ category }} [{{ catTasks.length }}]
</button>
</div>
<Transition name="fade">
<ul v-if="!collapsed.includes(category)" class="list bg-base-100 rounded-box">
<TodoItem v-for="task in tasks.sort((a, b) => a.id_ - b.id_)" :key="task.id" :task />
<ul v-if="!collapsed.includes(category)" class="">
<TodoItem v-for="task in catTasks" :key="task.id" :task />
</ul>
</Transition>
</div>

View File

@@ -1,4 +1,3 @@
export enum TaskStatus {
NONE,
DONE,
@@ -7,21 +6,21 @@ export enum TaskStatus {
FLAG,
}
export type Worklog = {
start: number;
end: number;
};
export type Task = {
"uuid": string,
"archived": boolean,
"tag": string,
"title": string,
"status": TaskStatus,
"lastaction": number | null,
"logs": Worklog[],
"dueDate": number | null,
"id_": number,
"id": number
export interface Worklog {
start: number
end: number
}
export interface Task {
archived: boolean
tag: string
title: string
status: TaskStatus
lastaction: number | null
logs: Worklog[]
dueDate: number | null
id_: number
id: number
}
export type Actions = 't' | 'c' | 'f'

403
src/utils/actions.ts Normal file
View File

@@ -0,0 +1,403 @@
import type { Task } from '../types.ts'
import type { Command } from './parser.ts'
import { TaskStatus } from '../types.ts'
import { parseDueDate, stopWorkLogging } from './helpers.ts'
export function moveCommand(tasks: Task[], ids: Task['id_'][], cmd: Command) {
return tasks.filter(t => ids.includes(t.id_) && t.tag && cmd?.tag).map(t => ({
...t,
tag: cmd?.tag,
} as Task))
}
export function beginCommand(tasks: Task[], ids: Task['id_'][]) {
return tasks.filter(t => ids.includes(t.id_) && t.status !== TaskStatus.WIP).map((t) => {
t.status = TaskStatus.WIP
t.logs = (t.logs || []).concat({
start: Date.now(),
end: 0,
})
return t
})
}
export function checkCommand(tasks: Task[], ids: Task['id_'][]) {
return tasks.filter(t => ids.includes(t.id_)).map((t) => {
t.status
= t.status === TaskStatus.DONE ? TaskStatus.WAIT : TaskStatus.DONE
if (t.status === TaskStatus.DONE) {
t = stopWorkLogging(t)
}
return t
})
}
export function deleteCommand(ids: Task['id_'][], cmd: Command, tasks: Task[]) {
if (ids.length === 0) {
// Delete by tag
const tag = (cmd?.id?.match(/^(@.*)/) || []).pop()
if (tag) {
return tasks.filter(t => t.tag === tag).map(t => ({
...t,
status: TaskStatus.NONE,
} as Task))
}
// Delete by status
const status = (
cmd?.id?.match(/^(finished|done|flag|ongoing|wip|wait|pending)/)
|| []
).pop()
if (status) {
let taskStatus = null
switch (status) {
case 'finished':
case 'done':
taskStatus = TaskStatus.DONE
break
case 'flag':
case 'flagged':
taskStatus = TaskStatus.FLAG
break
case 'ongoing':
case 'wip':
taskStatus = TaskStatus.WIP
break
case 'wait':
case 'pending':
taskStatus = TaskStatus.WAIT
break
default:
break
}
if (taskStatus) {
return tasks.filter(t => t.status === taskStatus && !t.archived).map(t => ({
...t,
status: TaskStatus.NONE,
} as Task))
}
}
return []
}
else {
// Delete by id
return tasks.filter(t => ids.includes(t.id_)).map(t => ({
...t,
status: TaskStatus.NONE,
} as Task))
}
}
export function flagCommand(tasks: Task[], ids: Task['id_'][]) {
return tasks.filter(t => ids.includes(t.id_)).map((t) => {
t.status
= t.status === TaskStatus.FLAG ? TaskStatus.WAIT : TaskStatus.FLAG
t = stopWorkLogging(t)
return t
})
}
export function stopCommand(tasks: Task[], ids: Task['id_'][]) {
return tasks.filter(t => ids.includes(t.id_) && t.status === TaskStatus.WIP).map((t) => {
t = stopWorkLogging(t)
return {
...t,
status: TaskStatus.WAIT,
} as Task
})
}
export function switchCommand(tasks: Task[], ids: Task['id_'][]) {
if (ids.length === 2) {
const stopId = ids[0]
const startId = ids[1]
return tasks.filter(t => ids.includes(t.id_)).map((t) => {
if (t.id_ === stopId && t.status === TaskStatus.WIP) {
return stopCommand([t], [stopId])[0]
}
if (t.id_ === startId && t.status !== TaskStatus.WIP) {
return beginCommand([t], [startId])[0]
}
return t
})
}
return []
}
export function archiveCommand(ids: Task['id_'][], cmd: Command, tasks: Task[]) {
if (ids.length === 0) {
// Archive by tag
const tag = (cmd?.id?.match(/^(@.*)/) || []).pop()
if (tag) {
return tasks.filter(t => t.tag === tag).map(t => ({
...t,
archived: true,
} as Task))
}
}
else {
// Archive by Ids
return tasks.filter(t => ids.includes(t.id_)).map(t => ({
...t,
archived: true,
} as Task))
}
return []
}
export function restoreCommand(ids: Task['id_'][], cmd: Command, tasks: Task[]) {
if (ids.length === 0) {
// Archive by tag
const tag = (cmd?.id?.match(/^(@.*)/) || []).pop()
if (tag) {
return tasks.filter(t => t.tag === tag).map(t => ({
...t,
archived: false,
} as Task))
}
}
else {
// Archive by Ids
return tasks.filter(t => ids.includes(t.id)).map(t => ({
...t,
archived: false,
} as Task))
}
return []
}
export function insertTaskCommand(cmd: Command) {
const tag = cmd?.tag || '@uncategorized'
const task = cmd?.text
if (task && task.length) {
return {
tag,
title: task,
dueDate: null,
} as Pick<Task, 'title' | 'dueDate' | 'tag'>
}
return null
}
export function editTaskCommand(ids: Task['id_'][], cmd: Command, tasks: Task[]) {
const id = ids[0]
const task = cmd?.text
if (task && task.length) {
return tasks.filter(t => t.id === id).map(t => ({
...t,
title: task,
} as Task))
}
return []
}
export function dueCommand(ids: Task['id_'][], cmd: Command, tasks: Task[]) {
const id = ids && ids.length ? ids[0] : null
const text = (cmd?.text || '').trim()
if (id) {
return tasks.filter(t => t.id_ === id).map((t: Task) => {
if (/^(?:clear|none|remove)$/i.test(text)) {
t.dueDate = null
}
else if (text && text.length) {
t.dueDate = parseDueDate(text)
}
return t
})
}
return []
}
export function tagRenameCommand(cmd: Command, tasks: Task[]) {
const [from, to] = cmd?.tag?.split(' ') || []
if (!from || !to)
return []
return tasks.filter(t => t.tag.match(from)).map(t => ({
...t,
tag: to,
} as Task))
}
// export function hideCommand(updateCandidate, cmd) {
// updateCandidate = (() => {
// switch (cmd.text) {
// case 'finished':
// case 'done':
// return {
// ...updateCandidate,
// taskVisibility: {
// ...updateCandidate.taskVisibility,
// done: false,
// },
// }
// case 'flag':
// case 'flagged':
// return {
// ...updateCandidate,
// taskVisibility: {
// ...updateCandidate.taskVisibility,
// flagged: false,
// },
// }
// case 'ongoing':
// case 'wip':
// return {
// ...updateCandidate,
// taskVisibility: {
// ...updateCandidate.taskVisibility,
// wip: false,
// },
// }
// case 'pending':
// case 'wait':
// return {
// ...updateCandidate,
// taskVisibility: {
// ...updateCandidate.taskVisibility,
// wait: false,
// },
// }
// default:
// return updateCandidate
// }
// })()
// return updateCandidate
// }
//
// export function showCommand(updateCandidate, cmd) {
// updateCandidate = (() => {
// switch (cmd.text) {
// case 'finished':
// case 'done':
// return {
// ...updateCandidate,
// taskVisibility: {
// ...updateCandidate.taskVisibility,
// done: true,
// },
// }
// case 'flag':
// case 'flagged':
// return {
// ...updateCandidate,
// taskVisibility: {
// ...updateCandidate.taskVisibility,
// flagged: true,
// },
// }
// case 'wip':
// case 'ongoing':
// return {
// ...updateCandidate,
// taskVisibility: {
// ...updateCandidate.taskVisibility,
// wip: true,
// },
// }
// case 'pending':
// case 'wait':
// return {
// ...updateCandidate,
// taskVisibility: {
// ...updateCandidate.taskVisibility,
// wait: true,
// },
// }
// default:
// return updateCandidate
// }
// })()
// return updateCandidate
// }
//
// export function searchCommand(updateCandidate: any, cmd) {
// if (cmd.command.match(/search/i)) {
// updateCandidate = {
// ...updateCandidate,
// filterBy: cmd.text,
// }
// }
// return updateCandidate
// }
//
// export function otherCommand(updateCandidate, cmd, tasks: Task[]) {
// updateCandidate = (() => {
// const commandText = cmd.command.toLowerCase()
// if (commandText === 'help') {
// return {
// ...updateCandidate,
// showHelp: true,
// }
// }
// else if (commandText === 'quickhelp') {
// return {
// ...updateCandidate,
// showQuickHelp: true,
// }
// }
// else if (commandText === 'today') {
// return {
// ...updateCandidate,
// showToday: !state.showToday,
// }
// }
// else if (commandText === 'dark') {
// return {
// ...updateCandidate,
// darkMode: true,
// }
// }
// else if (commandText === 'light') {
// return {
// ...updateCandidate,
// darkMode: false,
// }
// }
// else if (commandText === 'setting') {
// return {
// ...updateCandidate,
// showSettings: true,
// }
// }
// else if (commandText === 'customize') {
// return {
// ...updateCandidate,
// showCustomCSS: !updateCandidate.showCustomCSS,
// }
// }
// else if (commandText === 'list-archived') {
// return {
// ...updateCandidate,
// showArchived: !updateCandidate.showArchived,
// }
// }
// else if (commandText === 'login') {
// // OK, Let me explain the weird @demo stuff here:
// // If the user is already has their data on another machine, and
// // they opened this app on a new machine, then login right away,
// // the tasks in the range of 1..12 will be conflict with the demo
// // tasks. So, we will explicitly remove these demo tasks if they're
// // actually a demo, when login.
// return {
// ...updateCandidate,
// tasks: updateCandidate.tasks.filter(t =>
// (t.id - 1) * (t.id - 12) <= 0 ? t.tag !== '@demo' : true,
// ),
// userWantToLogin: true,
// }
// }
// else if (commandText === 'logout') {
// return {
// ...updateCandidate,
// authToken: '',
// userName: '',
// userWantToLogin: true,
// }
// }
// else {
// return updateCandidate
// }
// })()
// return updateCandidate
// }

27
src/utils/helpers.ts Normal file
View File

@@ -0,0 +1,27 @@
import type { Task } from '../types.ts'
import Sherlock from 'sherlockjs'
export function parseDueDate(text: string): number | null {
try {
const parsed = Sherlock.parse(text)
if (parsed && parsed.startDate) {
return parsed.startDate.getTime()
}
}
catch {}
const ts = Date.parse(text)
return Number.isNaN(ts) ? null : ts
}
export function stopWorkLogging(t: Task) {
if (t.logs && t.logs.length) {
const lastLog = t.logs[t.logs.length - 1]
if (lastLog.start && !lastLog.end) {
lastLog.end = Date.now()
}
}
else {
t.logs = []
}
return t
}

212
src/utils/parser.ts Normal file
View File

@@ -0,0 +1,212 @@
export type Command = {
command: string
tag?: string
text?: string
id?: string
} | null
function parseTaskCommand(str: string) {
return str.match(/^(t(?:ask)?)\s(@\S*[0-9a-z'-])?([\s\S]*)/i)
}
function parseEditCommand(str: string) {
return str.match(/^(e(?:dit)?)\s(\d+)([\s\S]*)/i)
}
const parseDueCommand = (str: string) => str.match(/^(due)\s(\d+)([\s\S]*)/i)
function parseMoveCommand(str: string) {
return str.match(/^(mv|move)\s(?:(\d+)\s)+(@\S*[0-9a-z'-])/i)
}
function parseCheckCommand(str: string) {
return str.match(/^(c(?:heck)?)\s([\s\S]*)/i)
}
function parseBeginCommand(str: string) {
return str.match(/^(b(?:egin)?)\s([\s\S]*)/i)
}
function parseDeleteCommand(str: string) {
return str.match(/^(d(?:elete)?)\s([\s\S]*)/i)
}
const parseFlagCommand = (str: string) => str.match(/^(fl(?:ag)?)\s([\s\S]*)/i)
const parseStopCommand = (str: string) => str.match(/^(st(?:op)?)\s([\s\S]*)/i)
function parseSwitchCommand(str: string) {
return str.match(/^(sw(?:itch)?)\s([\s\S]*)/i)
}
function parseArchiveCommand(str: string) {
return str.match(/^(a(?:rchive)?)\s([\s\S]*)/i)
}
function parseRestoreCommand(str: string) {
return str.match(/^(re(?:store)?)\s([\s\S]*)/i)
}
function parseTagRenameCommand(str: string) {
return str.match(
/^(tr|tagre|tagrename)\s(@\S*[0-9a-z'-])\s(@\S*[0-9a-z'-])/i,
)
}
function parseVisibilityCommand(str: string) {
return str.match(
/^(hide|show)\s(done|finished|wait|pending|ongoing|wip|flag|flagged)\b/i,
)
}
function parseOtherCommand(str: string) {
return str.match(
/^(help|quickhelp|today|dark|light|setting|customize|list-archived|login|logout)/i,
)
}
const parseTextFallback = (str: string) => str.match(/(\b\w*\b\S*\s)/)
const parseSearch = (str: string) => str.match(/^\/.*/)
/* ----------------------------------------- */
function compileTaskCommand(input: string) {
const matchTask = parseTaskCommand(input)
if (matchTask) {
return {
command: matchTask[1],
tag: matchTask[2],
text: matchTask[3].trim(),
} as Command
}
return null
}
function compileEditCommand(input: string) {
const matchEdit = parseEditCommand(input)
if (matchEdit) {
return {
command: matchEdit[1],
id: matchEdit[2],
text: matchEdit[3].trim(),
} as Command
}
return null
}
function compileDueCommand(input: string) {
const matchDue = parseDueCommand(input)
if (matchDue) {
return {
command: matchDue[1],
id: matchDue[2],
text: matchDue[3].trim(),
} as Command
}
return null
}
function compileMoveCommand(input: string) {
const matchMove = parseMoveCommand(input)
if (matchMove) {
return {
command: matchMove[1],
id: matchMove[2],
tag: matchMove[3],
} as Command
}
return null
}
function compileTagReCommand(input: string) {
const matchTagRe = parseTagRenameCommand(input)
if (matchTagRe) {
return {
command: matchTagRe[1],
tag: `${matchTagRe[2]} ${matchTagRe[3]}`,
} as Command
}
return null
}
function compileMathOtherCommand(input: string) {
const matchOther
= parseCheckCommand(input)
|| parseBeginCommand(input)
|| parseDeleteCommand(input)
|| parseFlagCommand(input)
|| parseStopCommand(input)
|| parseSwitchCommand(input)
|| parseArchiveCommand(input)
|| parseRestoreCommand(input)
if (matchOther) {
return {
command: matchOther[1],
id: matchOther[2],
}
}
return null
}
function compileVisibilityCommand(input: string) {
const matchVisibility = parseVisibilityCommand(input)
if (matchVisibility) {
return {
command: matchVisibility[1],
text: matchVisibility[2],
} as Command
}
return null
}
function compileHelpCommand(input: string) {
const matchHelp = parseOtherCommand(input)
if (matchHelp) {
return {
command: matchHelp[1],
}
}
return null
}
function compileSearchCommand(input: string) {
if (parseSearch(input)) {
return {
command: 'search',
text: input.replace(/^\//, ''),
}
}
return null
}
/* ----------------------------------------- */
export function parseCommand(input: string): Command {
let ret
ret = compileSearchCommand(input)
if (ret)
return ret
ret = compileTaskCommand(input)
if (ret)
return ret
ret = compileEditCommand(input)
if (ret)
return ret
ret = compileDueCommand(input)
if (ret)
return ret
ret = compileMoveCommand(input)
if (ret)
return ret
ret = compileTagReCommand(input)
if (ret)
return ret
ret = compileMathOtherCommand(input)
if (ret)
return ret
ret = compileVisibilityCommand(input)
if (ret)
return ret
ret = compileHelpCommand(input)
if (ret)
return ret
if (parseTextFallback(input)) {
return compileTaskCommand(`task ${input}`)
}
return null
}

11
src/vite-env.d.ts vendored
View File

@@ -1,7 +1,10 @@
/// <reference types="vite/client" />
declare module "*.vue" {
import type { DefineComponent } from "vue";
const component: DefineComponent<{}, {}, any>;
export default component;
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}
declare module 'sherlockjs';