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:
@@ -26,21 +26,22 @@
|
|||||||
"daisyui": "^5.5.18",
|
"daisyui": "^5.5.18",
|
||||||
"jsencrypt": "^3.5.4",
|
"jsencrypt": "^3.5.4",
|
||||||
"luxon": "^3.7.2",
|
"luxon": "^3.7.2",
|
||||||
|
"sherlockjs": "^1.4.2",
|
||||||
"uuid": "^13.0.0",
|
"uuid": "^13.0.0",
|
||||||
"vue": "^3.5.28",
|
"vue": "^3.5.28",
|
||||||
"vue-router": "^4.6.4"
|
"vue-router": "^4.6.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@antfu/eslint-config": "^7.4.3",
|
||||||
"@tailwindcss/vite": "^4.2.0",
|
"@tailwindcss/vite": "^4.2.0",
|
||||||
"@tauri-apps/cli": "^2",
|
"@tauri-apps/cli": "^2",
|
||||||
"@vitejs/plugin-vue": "^6.0.4",
|
"@vitejs/plugin-vue": "^6.0.4",
|
||||||
|
"eslint": "^9.39.2",
|
||||||
|
"eslint-plugin-format": "^1.4.0",
|
||||||
"tailwindcss": "^4.2.0",
|
"tailwindcss": "^4.2.0",
|
||||||
"typescript": "~5.9.3",
|
"typescript": "~5.9.3",
|
||||||
"vite": "^7.3.1",
|
"vite": "^7.3.1",
|
||||||
"vue-tsc": "^2.1.10",
|
"vue-tsc": "^2.1.10"
|
||||||
"@antfu/eslint-config": "^7.4.3",
|
|
||||||
"eslint": "^9.39.2",
|
|
||||||
"eslint-plugin-format": "^1.4.0"
|
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@10.27.0+sha512.72d699da16b1179c14ba9e64dc71c9a40988cbdc65c264cb0e489db7de917f20dcf4d64d8723625f2969ba52d4b7e2a1170682d9ac2a5dcaeaab732b7e16f04a"
|
"packageManager": "pnpm@10.27.0+sha512.72d699da16b1179c14ba9e64dc71c9a40988cbdc65c264cb0e489db7de917f20dcf4d64d8723625f2969ba52d4b7e2a1170682d9ac2a5dcaeaab732b7e16f04a"
|
||||||
}
|
}
|
||||||
8
pnpm-lock.yaml
generated
8
pnpm-lock.yaml
generated
@@ -38,6 +38,9 @@ importers:
|
|||||||
luxon:
|
luxon:
|
||||||
specifier: ^3.7.2
|
specifier: ^3.7.2
|
||||||
version: 3.7.2
|
version: 3.7.2
|
||||||
|
sherlockjs:
|
||||||
|
specifier: ^1.4.2
|
||||||
|
version: 1.4.2
|
||||||
uuid:
|
uuid:
|
||||||
specifier: ^13.0.0
|
specifier: ^13.0.0
|
||||||
version: 13.0.0
|
version: 13.0.0
|
||||||
@@ -1947,6 +1950,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
|
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
sherlockjs@1.4.2:
|
||||||
|
resolution: {integrity: sha512-Liynk2FRTyiHLzMqoe1LvRkT3MhMjUphIBKGa1pRovDKXEaygwpha76om/qV9YTG3qFr1+UIifEuH0VU+KVuRA==}
|
||||||
|
|
||||||
sisteransi@1.0.5:
|
sisteransi@1.0.5:
|
||||||
resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}
|
resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}
|
||||||
|
|
||||||
@@ -4094,6 +4100,8 @@ snapshots:
|
|||||||
|
|
||||||
shebang-regex@3.0.0: {}
|
shebang-regex@3.0.0: {}
|
||||||
|
|
||||||
|
sherlockjs@1.4.2: {}
|
||||||
|
|
||||||
sisteransi@1.0.5: {}
|
sisteransi@1.0.5: {}
|
||||||
|
|
||||||
source-map-js@1.2.1: {}
|
source-map-js@1.2.1: {}
|
||||||
|
|||||||
28
src/App.vue
28
src/App.vue
@@ -1,9 +1,14 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { PhCheckSquareOffset, PhListChecks, PhSliders } from '@phosphor-icons/vue'
|
import { useMediaQuery } from '@vueuse/core'
|
||||||
import { computed, onMounted } from 'vue'
|
import { computed, onMounted } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
import CreateInput from './components/CreateInput.vue'
|
||||||
|
import CreateModal from './components/CreateModal.vue'
|
||||||
|
|
||||||
import { useTasks } from './composables/useTasks.ts'
|
import { useTasks } from './composables/useTasks.ts'
|
||||||
|
|
||||||
|
const isMobile = useMediaQuery('(pointer: coarse)')
|
||||||
|
|
||||||
const { fetchTasks } = useTasks()
|
const { fetchTasks } = useTasks()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const currentPath = computed(() => router.currentRoute.value.path)
|
const currentPath = computed(() => router.currentRoute.value.path)
|
||||||
@@ -11,22 +16,13 @@ onMounted(fetchTasks)
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="overflow-hidden">
|
<div>
|
||||||
<main class="pb-40 overflow-y-scroll h-screen">
|
<main class="h-screen overflow-y-auto">
|
||||||
<RouterView />
|
<RouterView />
|
||||||
</main>
|
</main>
|
||||||
<div class="dock dock-xl inset-shadow-sm">
|
<template v-if="currentPath === '/'">
|
||||||
<RouterLink to="/create" :class="currentPath === '/create' ? 'dock-active' : ''">
|
<CreateModal v-if="isMobile" />
|
||||||
<PhCheckSquareOffset :size="32" />
|
<CreateInput v-else />
|
||||||
</RouterLink>
|
</template>
|
||||||
|
|
||||||
<RouterLink to="/" :class="currentPath === '/' ? 'dock-active' : ''">
|
|
||||||
<PhListChecks :size="32" />
|
|
||||||
</RouterLink>
|
|
||||||
|
|
||||||
<RouterLink to="/settings" :class="currentPath === '/settings' ? 'dock-active' : ''">
|
|
||||||
<PhSliders :size="32" />
|
|
||||||
</RouterLink>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
142
src/components/CreateInput.vue
Normal file
142
src/components/CreateInput.vue
Normal 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>
|
||||||
68
src/components/CreateModal.vue
Normal file
68
src/components/CreateModal.vue
Normal 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>
|
||||||
92
src/components/HelpPanel.vue
Normal file
92
src/components/HelpPanel.vue
Normal 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>
|
||||||
|
create a new task
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<b>
|
||||||
|
<u>e</u>dit:
|
||||||
|
</b>
|
||||||
|
edit a task
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<b>
|
||||||
|
<u>due</u>:
|
||||||
|
</b>
|
||||||
|
set due date or <u>clear</u>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<b>
|
||||||
|
<u>b</u>egin:
|
||||||
|
</b>
|
||||||
|
start timer
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<b>
|
||||||
|
<u>st</u>op:
|
||||||
|
</b>
|
||||||
|
stop timer
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<b>
|
||||||
|
<u>fl</u>ag:
|
||||||
|
</b>
|
||||||
|
flag a task
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<b>
|
||||||
|
<u>move</u>:
|
||||||
|
</b>
|
||||||
|
move task to another tag
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<b>
|
||||||
|
<u>d</u>elete:
|
||||||
|
</b>
|
||||||
|
delete task
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<b>
|
||||||
|
<u>a</u>rchive:
|
||||||
|
</b>
|
||||||
|
archive a task
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<b>
|
||||||
|
<u>re</u>store:
|
||||||
|
</b>
|
||||||
|
unarchive a task
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<b>
|
||||||
|
<u>sw</u>itch:
|
||||||
|
</b>
|
||||||
|
switch the working task
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<b>
|
||||||
|
<u>list-archived</u>:
|
||||||
|
</b>
|
||||||
|
show archived tasks
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<b>
|
||||||
|
<u>today</u>:
|
||||||
|
</b>
|
||||||
|
show today overview
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Task } from '../types.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 { DateTime } from 'luxon'
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { useTasks } from '../composables/useTasks.ts'
|
import { useTasks } from '../composables/useTasks.ts'
|
||||||
@@ -37,48 +37,20 @@ async function handleClick(update: Partial<Task>) {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<li class="list-row">
|
<li class="">
|
||||||
<div class="flex items-center justify-center">
|
<div class="font-mono text-sm flex flex-row justify-start gap-4">
|
||||||
<button class="btn btn-square btn-ghost" @click="statusSelectVisible = !statusSelectVisible">
|
<span>{{ String(task.id_).padStart(3, ' ') }}</span>
|
||||||
<PhX v-if="statusSelectVisible" :size="20" />
|
<div class="flex items-center justify-center">
|
||||||
<template v-else>
|
<PhSquare v-if="task.status === TaskStatus.WAIT" :size="16" />
|
||||||
<PhSquare v-if="task.status === TaskStatus.WAIT" :size="20" />
|
<PhCheckSquare v-else-if="task.status === TaskStatus.DONE" :size="16" weight="fill" class="text-success" />
|
||||||
<PhCheckSquare v-else-if="task.status === TaskStatus.DONE" :size="20" weight="fill" class="text-success" />
|
<PhFlag v-else-if="task.status === TaskStatus.FLAG" :size="16" weight="fill" class="text-warning" />
|
||||||
<PhFlag v-else-if="task.status === TaskStatus.FLAG" :size="20" weight="fill" class="text-warning" />
|
<PhPlay v-else-if="task.status === TaskStatus.WIP" :size="16" weight="fill" class="text-info" />
|
||||||
<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') }}
|
|
||||||
</div>
|
</div>
|
||||||
|
<span>{{ task.title }}</span>
|
||||||
|
<span v-if="task.dueDate" :class="dueColor">
|
||||||
|
{{ DateTime.fromMillis(task.dueDate).toFormat('dd/MM/yyyy') }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-square btn-ghost">
|
|
||||||
<PhDotsThree :size="24" weight="regular" />
|
|
||||||
</button>
|
|
||||||
</li>
|
</li>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
87
src/components/TodoItemTouch.vue
Normal file
87
src/components/TodoItemTouch.vue
Normal 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>
|
||||||
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 = {
|
interface Settings {
|
||||||
username: string;
|
username: string
|
||||||
password: string;
|
password: string
|
||||||
};
|
}
|
||||||
|
|
||||||
const isTauri = () =>
|
function isTauri() {
|
||||||
typeof window !== 'undefined' && Boolean((window as typeof window & { __TAURI__?: unknown }).__TAURI__);
|
return typeof window !== 'undefined' && Boolean((window as typeof window & { __TAURI__?: unknown }).__TAURI__)
|
||||||
|
}
|
||||||
|
|
||||||
async function buildAuthHeader(): Promise<string | undefined> {
|
async function buildAuthHeader(): Promise<string | undefined> {
|
||||||
const {decrypt} = useCrypto();
|
const { decrypt } = useCrypto()
|
||||||
const { getValue } = useStore();
|
const { getValue } = useStore()
|
||||||
const settings = await getValue<Settings>('settings');
|
const settings = await getValue<Settings>('settings')
|
||||||
if (!settings) return undefined;
|
if (!settings)
|
||||||
let {username, password} = settings;
|
return undefined
|
||||||
password = decrypt(password) as string;
|
let { username, password } = settings
|
||||||
|
password = decrypt(password) as string
|
||||||
|
|
||||||
if (username && password) {
|
if (username && password) {
|
||||||
const token = btoa(`${username}:${password}`);
|
const token = btoa(`${username}:${password}`)
|
||||||
return `Basic ${token}`;
|
return `Basic ${token}`
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useApi() {
|
export function useApi() {
|
||||||
const apiFetch = async (endpoint: string, options: RequestInit = {}) => {
|
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();
|
const authHeader = await buildAuthHeader()
|
||||||
console.log('authHeader',authHeader);
|
|
||||||
const headers = {
|
const headers = {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
...(authHeader ? { Authorization: authHeader } : {}),
|
...(authHeader ? { Authorization: authHeader } : {}),
|
||||||
...options.headers,
|
...options.headers,
|
||||||
} as Record<string, string>;
|
} as Record<string, string>
|
||||||
|
|
||||||
const response = isTauri()
|
const response = isTauri()
|
||||||
? await (await import('@tauri-apps/plugin-http')).fetch(url, {
|
? await (await import('@tauri-apps/plugin-http')).fetch(url, {
|
||||||
@@ -46,28 +47,28 @@ export function useApi() {
|
|||||||
: await fetch(url, {
|
: await fetch(url, {
|
||||||
...options,
|
...options,
|
||||||
headers,
|
headers,
|
||||||
});
|
})
|
||||||
|
|
||||||
if (!response.ok) {
|
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 = {}) =>
|
const get = (endpoint: string, options: RequestInit = {}) =>
|
||||||
apiFetch(endpoint, { ...options, method: 'GET' });
|
apiFetch(endpoint, { ...options, method: 'GET' })
|
||||||
|
|
||||||
const post = (endpoint: string, body: unknown, options: RequestInit = {}) =>
|
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 = {}) =>
|
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 {
|
return {
|
||||||
get,
|
get,
|
||||||
post,
|
post,
|
||||||
put,
|
put,
|
||||||
fetch: apiFetch,
|
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 = () =>
|
function isTauri() {
|
||||||
typeof window !== 'undefined' && Boolean((window as typeof window & { __TAURI__?: unknown }).__TAURI__);
|
return typeof window !== 'undefined' && Boolean((window as typeof window & { __TAURI__?: unknown }).__TAURI__)
|
||||||
|
}
|
||||||
|
|
||||||
async function getStore(): Promise<Store> {
|
async function getStore(): Promise<Store> {
|
||||||
if (!storePromise) {
|
if (!storePromise) {
|
||||||
const { load } = await import('@tauri-apps/plugin-store');
|
const { load } = await import('@tauri-apps/plugin-store')
|
||||||
storePromise = load('store.json', {
|
storePromise = load('store.json', {
|
||||||
autoSave: false,
|
autoSave: false,
|
||||||
defaults: {},
|
defaults: {},
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
return storePromise;
|
return storePromise
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useStore() {
|
export function useStore() {
|
||||||
const setValue = async <T>(key: string, value: T) => {
|
const setValue = async <T>(key: string, value: T) => {
|
||||||
console.log('setValue',key,value);
|
|
||||||
if (isTauri()) {
|
if (isTauri()) {
|
||||||
const store = await getStore();
|
const store = await getStore()
|
||||||
await store.set(key, value);
|
await store.set(key, value)
|
||||||
await store.save();
|
await store.save()
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
localStorage.setItem(key, JSON.stringify(value));
|
localStorage.setItem(key, JSON.stringify(value))
|
||||||
};
|
}
|
||||||
|
|
||||||
const getValue = async <T>(key: string) => {
|
const getValue = async <T>(key: string) => {
|
||||||
console.log('getValue',key);
|
|
||||||
if (isTauri()) {
|
if (isTauri()) {
|
||||||
const store = await getStore();
|
const store = await getStore()
|
||||||
return store.get<T>(key);
|
return store.get<T>(key)
|
||||||
}
|
}
|
||||||
const rawValue = localStorage.getItem(key);
|
const rawValue = localStorage.getItem(key)
|
||||||
if (rawValue === null) return null;
|
if (rawValue === null)
|
||||||
|
return null
|
||||||
try {
|
try {
|
||||||
return JSON.parse(rawValue) as T;
|
return JSON.parse(rawValue) as T
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
};
|
catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
setValue,
|
setValue,
|
||||||
getValue,
|
getValue,
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { Task } from '../types.ts'
|
import type { Task } from '../types.ts'
|
||||||
import { useArrayUnique } from '@vueuse/core'
|
import { useArrayUnique } from '@vueuse/core'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
import { TaskStatus } from '../types.ts'
|
||||||
import { useApi } from './useApi.ts'
|
import { useApi } from './useApi.ts'
|
||||||
|
|
||||||
const tasks = ref<Task[]>([])
|
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
|
isLoading.value = true
|
||||||
error.value = null
|
error.value = null
|
||||||
try {
|
try {
|
||||||
// Get next ID as per current logic in CreateScreen.vue
|
const newTask: Omit<Task, 'id'> = {
|
||||||
const nextId = tasks.value.sort((a, b) => b.id_ - a.id_).reduce((acc, task) => {
|
id_: nextId(),
|
||||||
if (task.id_ === acc + 1)
|
status: TaskStatus.WAIT,
|
||||||
return acc + 1
|
|
||||||
return task.id_ + 1
|
|
||||||
}, 0)
|
|
||||||
|
|
||||||
const newTask: Partial<Task> = {
|
|
||||||
...taskData,
|
|
||||||
id_: nextId,
|
|
||||||
logs: [],
|
logs: [],
|
||||||
|
lastaction: Date.now(),
|
||||||
archived: false,
|
archived: false,
|
||||||
|
...taskData,
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await api.put('e5880167-9322-4d7b-8a38-e06bae8a7734/list', { tasks: [newTask] }).then(res => res.json())
|
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) => {
|
const updateTask = async (task: Task | Task[]) => {
|
||||||
console.log('updateTask', task)
|
|
||||||
isLoading.value = true
|
isLoading.value = true
|
||||||
error.value = null
|
error.value = null
|
||||||
|
const tasksToUpdate = (Array.isArray(task) ? task : [task]).map(t => ({ ...t, lastaction: Date.now() } as Task))
|
||||||
try {
|
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) {
|
if (data.tasks) {
|
||||||
tasks.value = data.tasks
|
tasks.value = data.tasks
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
import { createWebHistory, createRouter } from 'vue-router'
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
import ListScreen from './screens/ListScreen.vue';
|
import ListScreen from './screens/ListScreen.vue'
|
||||||
import SettingsScreen from './screens/SettingsScreen.vue';
|
import SettingsScreen from './screens/SettingsScreen.vue'
|
||||||
import CreateScreen from './screens/CreateScreen.vue';
|
|
||||||
|
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{ path: '/', component: ListScreen },
|
{ path: '/', component: ListScreen },
|
||||||
{ path: '/settings', component: SettingsScreen },
|
{ path: '/settings', component: SettingsScreen },
|
||||||
{ path: '/create', component: CreateScreen }
|
|
||||||
]
|
]
|
||||||
|
|
||||||
export const router = createRouter({
|
export const router = createRouter({
|
||||||
|
|||||||
@@ -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>
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Task } from '../types.ts'
|
import type { Task } from '../types.ts'
|
||||||
import { PhCaretDown, PhCaretUp } from '@phosphor-icons/vue'
|
|
||||||
|
|
||||||
import { computed, onMounted, ref } from 'vue'
|
import { computed, onMounted, ref } from 'vue'
|
||||||
import TodoItem from '../components/TodoItem.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 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 categorizedTasks = computed(() => {
|
||||||
const tag = task.tag ?? '@uncategorized'
|
const unordered: Record<string, Task[]> = visibleTasks.value.reduce((acc, task) => {
|
||||||
acc[tag] = acc[tag] ?? []
|
const tag = task.tag ?? '@uncategorized'
|
||||||
acc[tag].push(task)
|
acc[tag] = acc[tag] ?? []
|
||||||
return acc
|
acc[tag].push(task)
|
||||||
}, {} as Record<string, 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[]>([])
|
const collapsed = ref<string[]>([])
|
||||||
</script>
|
</script>
|
||||||
@@ -27,22 +35,15 @@ const collapsed = ref<string[]>([])
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="flex flex-col gap-4">
|
<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 v-for="(catTasks, category) in categorizedTasks" :key="category" class="m-4 ">
|
||||||
<div class="m-4 flex justify-between items-center">
|
<div class="mb-4">
|
||||||
<div class="badge badge-xl badge-primary">
|
<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 }}
|
{{ category }} [{{ catTasks.length }}]
|
||||||
</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" />
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<Transition name="fade">
|
<Transition name="fade">
|
||||||
<ul v-if="!collapsed.includes(category)" class="list bg-base-100 rounded-box">
|
<ul v-if="!collapsed.includes(category)" class="">
|
||||||
<TodoItem v-for="task in tasks.sort((a, b) => a.id_ - b.id_)" :key="task.id" :task />
|
<TodoItem v-for="task in catTasks" :key="task.id" :task />
|
||||||
</ul>
|
</ul>
|
||||||
</Transition>
|
</Transition>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
35
src/types.ts
35
src/types.ts
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
export enum TaskStatus {
|
export enum TaskStatus {
|
||||||
NONE,
|
NONE,
|
||||||
DONE,
|
DONE,
|
||||||
@@ -7,21 +6,21 @@ export enum TaskStatus {
|
|||||||
FLAG,
|
FLAG,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Worklog {
|
||||||
export type Worklog = {
|
start: number
|
||||||
start: number;
|
end: 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 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
403
src/utils/actions.ts
Normal 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
27
src/utils/helpers.ts
Normal 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
212
src/utils/parser.ts
Normal 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
11
src/vite-env.d.ts
vendored
@@ -1,7 +1,10 @@
|
|||||||
/// <reference types="vite/client" />
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
declare module "*.vue" {
|
declare module '*.vue' {
|
||||||
import type { DefineComponent } from "vue";
|
import type { DefineComponent } from 'vue'
|
||||||
const component: DefineComponent<{}, {}, any>;
|
|
||||||
export default component;
|
const component: DefineComponent<{}, {}, any>
|
||||||
|
export default component
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare module 'sherlockjs';
|
||||||
|
|||||||
Reference in New Issue
Block a user