From 4e381127a4501f28045f486faf58a5cec78d82e3 Mon Sep 17 00:00:00 2001 From: riiimparm Date: Sun, 28 Jun 2026 15:52:28 +0900 Subject: [PATCH 1/5] =?UTF-8?q?feat:=20=E6=A4=9C=E7=B4=A2,=20=E3=82=A2?= =?UTF-8?q?=E3=83=83=E3=83=97=E3=83=87=E3=83=BC=E3=83=88=E3=83=95=E3=82=A7?= =?UTF-8?q?=E3=83=83=E3=83=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 4 +- package.json | 2 +- scripts/patch-dmg.sh | 2 +- src-tauri/Cargo.lock | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/tauri.conf.json | 2 +- src/App.tsx | 147 +++++++++++++++++++++++++++--- src/components/SettingsModal.tsx | 11 ++- src/components/ShortcutsModal.tsx | 2 + src/components/TaskInput.tsx | 88 ++++++++++++------ src/components/TaskItem.tsx | 87 +++++++++--------- src/components/TaskList.tsx | 48 ++++++++-- src/context/UiContext.tsx | 10 ++ src/i18n.ts | 6 ++ src/types.ts | 4 +- src/utils/updater.ts | 41 +++++++++ 16 files changed, 355 insertions(+), 103 deletions(-) create mode 100644 src/utils/updater.ts diff --git a/package-lock.json b/package-lock.json index cfaff2c..f610490 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "FastTask", - "version": "0.1.0", + "version": "1.1.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "FastTask", - "version": "0.1.0", + "version": "1.1.1", "dependencies": { "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", diff --git a/package.json b/package.json index 191171c..5bfee82 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "FastTask", "private": true, - "version": "0.1.0", + "version": "1.1.1", "type": "module", "scripts": { "dev": "vite", diff --git a/scripts/patch-dmg.sh b/scripts/patch-dmg.sh index a26244c..46ece65 100755 --- a/scripts/patch-dmg.sh +++ b/scripts/patch-dmg.sh @@ -2,7 +2,7 @@ # ビルド済み DMG に修正スクリプトを追加する set -e -DMG_SRC="src-tauri/target/release/bundle/dmg/FastTask_0.1.0_aarch64.dmg" +DMG_SRC="src-tauri/target/release/bundle/dmg/FastTask_1.1.0_aarch64.dmg" COMMAND_FILE="src-tauri/dmg-resources/FastTaskを修正する.command" TMP_RW="/tmp/fasttask_rw.dmg" MOUNT_POINT="/tmp/fasttask_mount" diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 72aea44..492f07f 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -4,7 +4,7 @@ version = 4 [[package]] name = "FastTask" -version = "0.1.0" +version = "1.1.1" dependencies = [ "serde", "serde_json", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index aff9b31..f5e8ce6 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "FastTask" -version = "0.1.0" +version = "1.1.1" description = "DailyTask" authors = ["you"] edition = "2021" diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index d61bbce..cfa11b3 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "FastTask", - "version": "0.1.0", + "version": "1.1.1", "identifier": "com.dailytask.app", "build": { "beforeDevCommand": "pnpm dev", diff --git a/src/App.tsx b/src/App.tsx index bfa74b4..9a316cb 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -13,12 +13,16 @@ import { osNotify, osNotifyWithAction, setupFocusTimerActions, setFocusTimerActi import { t } from "./i18n"; import { buildDfsOrder, getDepth, hasUndoneDescendants } from "./utils/taskTree"; import { getCurrentWindow } from "@tauri-apps/api/window"; +import { openUrl } from "@tauri-apps/plugin-opener"; import { CloseConfirmModal } from "./components/CloseConfirmModal"; +import { checkForUpdate, UpdateInfo } from "./utils/updater"; function AppInner() { const init = useStore((s) => s.init); const loaded = useStore((s) => s.loaded); const grouping = useStore((s) => s.settings.groupingEnabled); + const tagsEnabled = useStore((s) => s.settings.tagsEnabled ?? false); + const tags = useStore((s) => s.tags); const lang = useStore((s) => s.settings.language); const lastFocusMinutes = useStore((s) => s.settings.lastFocusMinutes); const updateSettings = useStore((s) => s.updateSettings); @@ -35,6 +39,9 @@ function AppInner() { const [showCloseConfirm, setShowCloseConfirm] = useState(false); const [showSettings, setShowSettings] = useState(false); const [showShortcuts, setShowShortcuts] = useState(false); + const [tagFilterEnabled, setTagFilterEnabled] = useState(false); + const [tagFilterIds, setTagFilterIds] = useState>(new Set()); + const [updateInfo, setUpdateInfo] = useState(null); const { selectedTaskId, @@ -52,6 +59,9 @@ function AppInner() { setFocusMinutes, mainInputRef, triggerVibration, + searchMode, + searchQuery, + setSearchMode, } = useUiContext(); const [timerActive, setTimerActive] = useState(false); @@ -124,8 +134,25 @@ function AppInner() { // eslint-disable-next-line react-hooks/exhaustive-deps }, [lang]); + const searchKeywords = useMemo(() => { + if (!searchMode || !searchQuery.trim()) return []; + return searchQuery.trim().toLowerCase().split(/\s+/); + }, [searchMode, searchQuery]); + const todoTasks = useMemo(() => { - const dfs = buildDfsOrder(tasks.filter((t) => t.status === "todo")); + let dfs = buildDfsOrder(tasks.filter((t) => t.status === "todo")); + if (searchKeywords.length > 0) { + dfs = dfs.filter((task) => { + const body = task.projectName && task.title.startsWith(`:${task.projectName} `) + ? task.title.slice(task.projectName.length + 2) + : task.title; + const text = body.toLowerCase(); + const tagText = tagsEnabled + ? task.tags.map((id) => tags.find((t) => t.id === id)?.name ?? "").join(" ").toLowerCase() + : ""; + return searchKeywords.every((kw) => text.includes(kw) || tagText.includes(kw)); + }); + } if (!grouping) return dfs; // グルーピング ON 時は ProjectSection の表示順に合わせる const NONE_KEY = "__none__"; @@ -137,12 +164,17 @@ function AppInner() { sectionsMap.get(key)!.push(t); } return sectionOrder.flatMap((k) => sectionsMap.get(k)!); - }, [tasks, grouping]); + }, [tasks, grouping, searchKeywords, tagsEnabled, tags]); useEffect(() => { init(); }, [init]); + useEffect(() => { + if (!loaded) return; + checkForUpdate().then((info) => { if (info) setUpdateInfo(info); }); + }, [loaded]); + useEffect(() => { if (!toast) return; const timer = setTimeout(() => setToast(undefined), 3500); @@ -199,6 +231,14 @@ function AppInner() { return; } + // Cmd+F: 検索モードへ + if ((e.ctrlKey || e.metaKey) && e.key === "f") { + e.preventDefault(); + setSearchMode(true); + mainInputRef.current?.focus(); + return; + } + // ? はどこでも有効(入力欄以外) if (!isInput && e.key === "?") { e.preventDefault(); @@ -534,6 +574,8 @@ function AppInner() { setFocusElapsed, setShowFocusEnd, setFocusPhase, + searchMode, + setSearchMode, ], ); @@ -553,19 +595,93 @@ function AppInner() { return (
setShowSettings(true)} /> + {updateInfo && ( +
+ + {lang === "ja" + ? `v${updateInfo.version} が利用可能です` + : `v${updateInfo.version} is available`} + +
+ + +
+
+ )}
- +
+ + {tagsEnabled && tags.length > 0 && ( + + )} + {tagFilterEnabled && tags.length > 0 && ( +
+ {tags.map((tag) => { + const active = tagFilterIds.has(tag.id); + return ( + + ); + })} +
+ )} +
{isReorderMode && ( ↕ {lang === "ja" ? "並び替えモード" : "Reorder mode"} @@ -581,7 +697,10 @@ function AppInner() { )}
- + 0 ? tagFilterIds : undefined} + searchKeywords={searchKeywords} + /> {!focusedTaskId && }
setShowSettings(false)} /> diff --git a/src/components/SettingsModal.tsx b/src/components/SettingsModal.tsx index ada6b8f..fd8f978 100644 --- a/src/components/SettingsModal.tsx +++ b/src/components/SettingsModal.tsx @@ -10,7 +10,7 @@ interface Props { onClose: () => void; } -type ToggleKey = "copyIncludeUrl" | "groupingEnabled" | "copyGroupingEnabled" | "autoDeleteOldCompleted" | "showDueDate"; +type ToggleKey = "copyIncludeUrl" | "groupingEnabled" | "copyGroupingEnabled" | "autoDeleteOldCompleted" | "showDueDate" | "tagsEnabled"; const TOGGLES: Array<{ key: ToggleKey; label: TKey; desc: TKey }> = [ { key: "copyIncludeUrl", label: "setting_copyIncludeUrl", desc: "setting_copyIncludeUrl_desc" }, @@ -18,6 +18,7 @@ const TOGGLES: Array<{ key: ToggleKey; label: TKey; desc: TKey }> = [ { key: "copyGroupingEnabled", label: "setting_copyGroupingEnabled", desc: "setting_copyGroupingEnabled_desc" }, { key: "autoDeleteOldCompleted", label: "setting_autoDeleteOldCompleted", desc: "setting_autoDeleteOldCompleted_desc" }, { key: "showDueDate", label: "setting_showDueDate", desc: "setting_showDueDate_desc" }, + { key: "tagsEnabled", label: "setting_tagsEnabled", desc: "setting_tagsEnabled_desc" }, ]; function Toggle({ value, onChange }: { value: boolean; onChange: (v: boolean) => void }) { @@ -287,9 +288,11 @@ export function SettingsModal({ open, onClose }: Props) { )}
-
- -
+ {settings.tagsEnabled && ( +
+ +
+ )}
+ {searchMode ? ( + + + + + + + ) : ( + + )}
{projectCandidates.length > 0 && (
@@ -220,7 +252,7 @@ export function TaskInput() {
)} - {showDueDate && ( + {showDueDate && !searchMode && (
- setShowTags(false)} anchorRef={tagsBtn}> -
- {tags.length === 0 && ( -
{t(lang, "noTags")}
- )} - {tags.map((tag) => { - const active = task.tags.includes(tag.id); - return ( - - ); - })} -
-
-
+ {tagsEnabled && ( +
+ + setShowTags(false)} anchorRef={tagsBtn}> +
+ {tags.length === 0 && ( +
{t(lang, "noTags")}
+ )} + {tags.map((tag) => { + const active = task.tags.includes(tag.id); + return ( + + ); + })} +
+
+
+ )}
)} +
+
@@ -733,6 +751,13 @@ function AppInner() { /> {!focusedTaskId && } +
+ {calendarEnabled && ( +
+ +
+ )} +
setShowSettings(false)} /> {showShortcuts && setShowShortcuts(false)} />} {focusPhase === "setup" && ( diff --git a/src/components/CalendarPanel.tsx b/src/components/CalendarPanel.tsx new file mode 100644 index 0000000..edceaee --- /dev/null +++ b/src/components/CalendarPanel.tsx @@ -0,0 +1,290 @@ +import { useEffect, useMemo, useRef, useState } from "react"; +import { useStore } from "../store"; +import { useUiContext } from "../context/UiContext"; +import { t } from "../i18n"; +import { dateToIso, DAY_NAMES_JA, DAY_NAMES_EN_SHORT } from "../utils/parseDate"; + +type ViewMode = "month" | "week"; + +function startOfWeek(date: Date): Date { + const d = new Date(date); + d.setHours(0, 0, 0, 0); + d.setDate(d.getDate() - d.getDay()); + return d; +} + +function addDays(date: Date, days: number): Date { + const d = new Date(date); + d.setDate(d.getDate() + days); + return d; +} + +function addMonths(date: Date, months: number): Date { + const d = new Date(date); + d.setMonth(d.getMonth() + months); + return d; +} + +function startOfMonth(date: Date): Date { + return new Date(date.getFullYear(), date.getMonth(), 1); +} + +function endOfMonth(date: Date): Date { + return new Date(date.getFullYear(), date.getMonth() + 1, 0); +} + +function isSameDay(a: Date, b: Date): boolean { + return ( + a.getFullYear() === b.getFullYear() && + a.getMonth() === b.getMonth() && + a.getDate() === b.getDate() + ); +} + +export function CalendarPanel() { + const tasks = useStore((s) => s.tasks); + const addTask = useStore((s) => s.addTask); + const toggleTask = useStore((s) => s.toggleTask); + const lang = useStore((s) => s.settings.language); + const { setSelectedTaskId } = useUiContext(); + + const [anchorDate, setAnchorDate] = useState(() => new Date()); + const [viewMode, setViewMode] = useState("month"); + const [weekdaysOnly, setWeekdaysOnly] = useState(false); + const [creatingIso, setCreatingIso] = useState(null); + const [draft, setDraft] = useState(""); + const inputRef = useRef(null); + const today = useMemo(() => new Date(), []); + + useEffect(() => { + if (creatingIso) inputRef.current?.focus(); + }, [creatingIso]); + + const tasksByDate = useMemo(() => { + const map = new Map(); + for (const task of tasks) { + if (!task.dueDate) continue; + const list = map.get(task.dueDate) ?? []; + list.push(task); + map.set(task.dueDate, list); + } + return map; + }, [tasks]); + + const dayNames = + lang === "ja" + ? DAY_NAMES_JA + : DAY_NAMES_EN_SHORT.map((d) => d[0].toUpperCase() + d.slice(1)); + const visibleDayIndexes = weekdaysOnly ? [1, 2, 3, 4, 5] : [0, 1, 2, 3, 4, 5, 6]; + + const weeks = useMemo(() => { + if (viewMode === "week") { + return [Array.from({ length: 7 }, (_, i) => addDays(startOfWeek(anchorDate), i))]; + } + const monthStart = startOfMonth(anchorDate); + const monthEnd = endOfMonth(anchorDate); + const gridStart = startOfWeek(monthStart); + const gridEnd = addDays(startOfWeek(monthEnd), 6); + const days: Date[] = []; + for (let cur = gridStart; cur <= gridEnd; cur = addDays(cur, 1)) { + days.push(cur); + } + const rows: Date[][] = []; + for (let i = 0; i < days.length; i += 7) rows.push(days.slice(i, i + 7)); + return rows; + }, [viewMode, anchorDate]); + + function goPrev() { + setAnchorDate((d) => (viewMode === "month" ? addMonths(d, -1) : addDays(d, -7))); + } + function goNext() { + setAnchorDate((d) => (viewMode === "month" ? addMonths(d, 1) : addDays(d, 7))); + } + function goToday() { + setAnchorDate(new Date()); + } + + function submitDraft(iso: string) { + if (!draft.trim()) return; + const id = addTask(draft, iso); + if (id) setSelectedTaskId(id); + setDraft(""); + inputRef.current?.focus(); + } + + const headerLabel = + viewMode === "month" + ? lang === "ja" + ? `${anchorDate.getFullYear()}年${anchorDate.getMonth() + 1}月` + : anchorDate.toLocaleDateString("en-US", { month: "long", year: "numeric" }) + : (() => { + const s = startOfWeek(anchorDate); + const e = addDays(s, 6); + return `${s.getMonth() + 1}/${s.getDate()} - ${e.getMonth() + 1}/${e.getDate()}`; + })(); + + return ( +
+
+
+
{headerLabel}
+
+ + + +
+
+
+
+ {(["month", "week"] as ViewMode[]).map((m) => ( + + ))} +
+ +
+
+ +
+ {visibleDayIndexes.map((i) => ( +
+ {dayNames[i]} +
+ ))} +
+ +
+ {weeks.map((week, wi) => ( +
+ {visibleDayIndexes.map((di) => { + const day = week[di]; + const iso = dateToIso(day); + const isCurrentMonth = viewMode === "week" || day.getMonth() === anchorDate.getMonth(); + const isToday = isSameDay(day, today); + const dayTasks = isToday + ? [ + ...(tasksByDate.get(iso) ?? []), + ...tasks.filter((tk) => tk.isMinimum && tk.dueDate !== iso), + ] + : tasksByDate.get(iso) ?? []; + const isCreating = creatingIso === iso; + const visibleTasks = dayTasks.slice(0, 3); + const overflow = dayTasks.length - visibleTasks.length; + + return ( +
{ + if (!isCreating) setCreatingIso(iso); + }} + className={`min-h-[64px] rounded-md p-1 text-left cursor-text transition-colors ${ + isCreating ? "bg-accent/10 ring-1 ring-accent/40" : "hover:bg-black/[0.03] dark:hover:bg-white/[0.04]" + } ${isCurrentMonth ? "" : "opacity-40"}`} + > +
+ {day.getDate()} +
+
+ {visibleTasks.map((task) => ( +
e.stopPropagation()} className="flex items-center gap-1"> + + + {task.title} + +
+ ))} + {overflow > 0 &&
+{overflow}
} + {isCreating && ( + e.stopPropagation()} + onChange={(e) => setDraft(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter") { + e.preventDefault(); + submitDraft(iso); + } + if (e.key === "Escape") { + e.preventDefault(); + setCreatingIso(null); + setDraft(""); + } + }} + onBlur={() => { + setCreatingIso(null); + setDraft(""); + }} + placeholder={t(lang, "calendarAddPlaceholder")} + className="w-full text-[9.5px] bg-transparent outline-none border-b border-accent/40 placeholder:text-subink/50" + /> + )} +
+
+ ); + })} +
+ ))} +
+
+ ); +} diff --git a/src/components/SettingsModal.tsx b/src/components/SettingsModal.tsx index fd8f978..dbc5c4d 100644 --- a/src/components/SettingsModal.tsx +++ b/src/components/SettingsModal.tsx @@ -10,15 +10,15 @@ interface Props { onClose: () => void; } -type ToggleKey = "copyIncludeUrl" | "groupingEnabled" | "copyGroupingEnabled" | "autoDeleteOldCompleted" | "showDueDate" | "tagsEnabled"; +type ToggleKey = "copyIncludeUrl" | "copyGroupingEnabled" | "autoDeleteOldCompleted" | "showDueDate" | "tagsEnabled" | "calendarEnabled"; const TOGGLES: Array<{ key: ToggleKey; label: TKey; desc: TKey }> = [ { key: "copyIncludeUrl", label: "setting_copyIncludeUrl", desc: "setting_copyIncludeUrl_desc" }, - { key: "groupingEnabled", label: "setting_groupingEnabled", desc: "setting_groupingEnabled_desc" }, { key: "copyGroupingEnabled", label: "setting_copyGroupingEnabled", desc: "setting_copyGroupingEnabled_desc" }, { key: "autoDeleteOldCompleted", label: "setting_autoDeleteOldCompleted", desc: "setting_autoDeleteOldCompleted_desc" }, { key: "showDueDate", label: "setting_showDueDate", desc: "setting_showDueDate_desc" }, { key: "tagsEnabled", label: "setting_tagsEnabled", desc: "setting_tagsEnabled_desc" }, + { key: "calendarEnabled", label: "setting_calendarEnabled", desc: "setting_calendarEnabled_desc" }, ]; function Toggle({ value, onChange }: { value: boolean; onChange: (v: boolean) => void }) { @@ -215,7 +215,9 @@ export function SettingsModal({ open, onClose }: Props) { {TOGGLES.map((it) => (
-
{t(lang, it.label)}
+
+ {t(lang, it.label)} +
{t(lang, it.desc)}
{lang === "ja" ? "ウィンドウフォーカスショートカット" : "Window focus shortcut"}
-
- {lang === "ja" - ? "修飾キー(⌘/⌃/⌥)を含むキー組み合わせ" - : "Key combo including a modifier (⌘/⌃/⌥)"} -
{capturing ? ( {shortcutError}
)} +
+ {lang === "ja" ? "ショートカット一覧は「?」から確認できます" : "Press \"?\" to view the shortcut list"} +
{settings.tagsEnabled && ( diff --git a/src/components/TaskInput.tsx b/src/components/TaskInput.tsx index be51c83..7645f84 100644 --- a/src/components/TaskInput.tsx +++ b/src/components/TaskInput.tsx @@ -1,8 +1,6 @@ import { useEffect, useMemo, useRef, useState } from "react"; -import { DayPicker } from "react-day-picker"; import { useStore } from "../store"; import { useUiContext } from "../context/UiContext"; -import { Popover } from "./Popover"; import { parseProjectFromInput } from "../utils/project"; import { parseDateFromText, formatDateShort, dateToIso } from "../utils/parseDate"; import { buildDfsOrder } from "../utils/taskTree"; @@ -16,11 +14,9 @@ export function TaskInput() { const showDueDate = useStore((s) => s.settings.showDueDate ?? false); const tagsEnabled = useStore((s) => s.settings.tagsEnabled ?? false); const [value, setValue] = useState(""); - const [due, setDue] = useState(undefined); - const [showDate, setShowDate] = useState(false); const [highlight, setHighlight] = useState(0); + const [isFocused, setIsFocused] = useState(false); const inputRef = useRef(null); - const dateBtnRef = useRef(null); const { mainInputRef, setSelectedTaskId, searchMode, searchQuery, setSearchMode, setSearchQuery } = useUiContext(); @@ -31,6 +27,7 @@ export function TaskInput() { useEffect(() => { inputRef.current?.focus(); + setIsFocused(true); }, []); useEffect(() => { @@ -79,28 +76,28 @@ export function TaskInput() { } const detectedDate = useMemo(() => { - if (!showDueDate || due || !value.trim()) return null; + if (!showDueDate || !value.trim()) return null; const { body } = parseProjectFromInput(value); return parseDateFromText(body, new Date()); - }, [showDueDate, due, value]); + }, [showDueDate, value]); const splitHintVisible = useMemo(() => { if (!value.trim()) return false; const parts = value.split(",").map((s) => s.trim()).filter(Boolean); return parts.some((part) => { - const withoutUrl = part.replace(/https?:\/\/\S+/g, "").trim(); + const withoutUrl = part.replace(/(?:https?|file):\/\/\S+/g, "").trim(); return withoutUrl.length >= 30; }); }, [value]); function submit() { if (!value.trim()) return; - const effectiveDue = due ?? detectedDate?.date ?? undefined; + const effectiveDue = detectedDate?.date ?? undefined; const iso = effectiveDue ? dateToIso(effectiveDue) : undefined; // 日付検出時はテキストから日付部分を除去してプロジェクトプレフィックスを再結合 let submitValue = value; - if (!due && detectedDate) { + if (detectedDate) { const { projectName } = parseProjectFromInput(value); const prefix = projectName ? `:${projectName} ` : ""; submitValue = prefix + detectedDate.textWithoutDate; @@ -108,15 +105,16 @@ export function TaskInput() { const parts = submitValue.split(",").map((s) => s.trim()).filter(Boolean); const { projectName: firstProject } = parseProjectFromInput(parts[0] ?? ""); + let lastId = ""; parts.forEach((part, i) => { - if (i > 0 && firstProject && !part.startsWith(":")) { - addTask(`:${firstProject} ${part}`, iso); - } else { - addTask(part, iso); - } + const id = + i > 0 && firstProject && !part.startsWith(":") + ? addTask(`:${firstProject} ${part}`, iso) + : addTask(part, iso); + if (id) lastId = id; }); setValue(""); - setDue(undefined); + if (lastId) setSelectedTaskId(lastId); requestAnimationFrame(() => inputRef.current?.focus()); } @@ -199,7 +197,7 @@ export function TaskInput() { const { projectName: parsedProject } = parseProjectFromInput(value); const detectedUrl = useMemo(() => { - const m = value.match(/https?:\/\/\S+/); + const m = value.match(/(?:https?|file):\/\/\S+/); return m ? m[0] : null; }, [value]); @@ -218,8 +216,11 @@ export function TaskInput() { if (searchMode) setSearchQuery(e.target.value); }} onKeyDown={onKeyDown} - placeholder={searchMode ? (lang === "ja" ? "キーワードで検索..." : "Search...") : t(lang, "placeholder")} - className={`w-full pl-4 pr-10 py-2.5 rounded-xl border-0 outline-none transition-all text-[13px] placeholder:text-black/30 dark:placeholder:text-white/28${bulkCount >= 2 && !searchMode ? " pr-16" : ""} ${searchMode ? "bg-blue-50/60 dark:!bg-blue-950/25 shadow-[0_0_0_2px_rgba(59,130,246,0.45)]" : "bg-black/[0.055] dark:!bg-white/[0.08]"}`} + onFocus={() => setIsFocused(true)} + onBlur={() => setIsFocused(false)} + placeholder={searchMode || !isFocused ? "" : t(lang, showDueDate ? "placeholderWithDate" : "placeholder")} + style={{ transition: "background-color 0.5s ease-out, box-shadow 0.7s ease-in" }} + className={`w-full pl-4 pr-10 py-2.5 rounded-xl border-0 outline-none text-[13px] placeholder:text-black/30 dark:placeholder:text-white/28 bg-black/[0.055] dark:!bg-white/[0.08]${bulkCount >= 2 && !searchMode ? " pr-16" : ""} ${searchMode ? "!shadow-[inset_0_12px_16px_-6px_rgba(0,0,0,0.18)] dark:!shadow-[inset_0_16px_22px_-6px_rgba(255,166,64,0.24)]" : ""}`} /> {bulkCount >= 2 ? ( @@ -227,7 +228,7 @@ export function TaskInput() { ) : null} {searchMode ? ( - + @@ -270,45 +271,6 @@ export function TaskInput() {
)} - {showDueDate && !searchMode && ( -
- - setShowDate(false)} anchorRef={dateBtnRef}> - { - setDue(d); - setShowDate(false); - }} - /> - {due && ( - - )} - -
- )} {/* プロジェクト表示とタグヒント */} {!searchMode && (parsedProject || matchedTags.length > 0) && ( diff --git a/src/components/TaskItem.tsx b/src/components/TaskItem.tsx index cf13699..caed7f7 100644 --- a/src/components/TaskItem.tsx +++ b/src/components/TaskItem.tsx @@ -1,13 +1,13 @@ import { useEffect, useMemo, useRef, useState } from "react"; import { useSortable } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; -import { DayPicker } from "react-day-picker"; import { openUrl } from "@tauri-apps/plugin-opener"; import { useStore } from "../store"; import { useUiContext } from "../context/UiContext"; import { Tag, Task } from "../types"; import { Popover } from "./Popover"; -import { todayIso } from "../utils/project"; +import { todayIso, parseProjectFromInput } from "../utils/project"; +import { parseDateFromText, dateToIso } from "../utils/parseDate"; import { t } from "../i18n"; import { getDepth, hasUndoneDescendants } from "../utils/taskTree"; @@ -57,12 +57,10 @@ export function TaskItem({ task, draggable = true }: Props) { const [showTags, setShowTags] = useState(false); const [showUrl, setShowUrl] = useState(false); - const [showDate, setShowDate] = useState(false); const [urlInput, setUrlInput] = useState(task.url ?? ""); const tagsBtn = useRef(null); const urlBtn = useRef(null); - const dateBtn = useRef(null); const tagsById = new Map(tags.map((t) => [t.id, t])); @@ -83,7 +81,22 @@ export function TaskItem({ task, draggable = true }: Props) { } function commitEdit() { - if (editValue.trim()) updateTask(task.id, { title: editValue.trim() }); + const trimmed = editValue.trim(); + if (trimmed) { + let title = trimmed; + const updates: { title: string; dueDate?: string } = { title }; + if (showDueDate) { + const { projectName, body } = parseProjectFromInput(trimmed); + const detected = parseDateFromText(body, new Date()); + if (detected) { + const prefix = projectName ? `:${projectName} ` : ""; + title = (prefix + detected.textWithoutDate).trim(); + updates.title = title; + updates.dueDate = dateToIso(detected.date); + } + } + updateTask(task.id, updates); + } setEditing(false); } @@ -110,6 +123,11 @@ export function TaskItem({ task, draggable = true }: Props) { } + const rowRef = useRef(null); + useEffect(() => { + if (isSelected) rowRef.current?.scrollIntoView({ block: "nearest" }); + }, [isSelected]); + const due = task.dueDate; const dueDate = due ? new Date(due + "T00:00:00") : undefined; const today = todayIso(); @@ -123,7 +141,10 @@ export function TaskItem({ task, draggable = true }: Props) { return (
{ + sortable.setNodeRef(el); + rowRef.current = el; + }} style={{ ...style, paddingLeft: depth > 0 ? `${depth * 20 + 8}px` : undefined }} data-project={task.projectName || undefined} onClick={() => setSelectedTaskId(task.id)} @@ -221,7 +242,7 @@ export function TaskItem({ task, draggable = true }: Props) { - AWAITING... + PENDING... )}
@@ -258,9 +279,13 @@ export function TaskItem({ task, draggable = true }: Props) { )} {showDueDate && due && ( - + )}
@@ -354,48 +379,6 @@ export function TaskItem({ task, draggable = true }: Props) {
- {showDueDate && ( -
- - setShowDate(false)} anchorRef={dateBtn}> - { - const iso = d - ? `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}` - : undefined; - updateTask(task.id, { dueDate: iso }); - setShowDate(false); - }} - /> - {task.dueDate && ( - - )} - -
- )} -