From a8269b4ca7b43f4a57c9af1638bfb888ab22d623 Mon Sep 17 00:00:00 2001 From: wallek Date: Tue, 12 May 2026 10:17:13 +0200 Subject: [PATCH 1/3] feat: two-row view for todo display - Add multiLineView setting with migration from older versions - Add settings UI toggle for two-row view - Implement badge rendering in separate row below todo text - Clean badge patterns from todo body text in multiLineView mode - Add localized setting labels for all supported languages - Add CSS layout for row-text and row-badges in multiLineView mode --- src/@types/Settings.ts | 1 + src/locales/cs.json | 1 + src/locales/de.json | 1 + src/locales/en.json | 1 + src/locales/es.json | 1 + src/locales/fr.json | 1 + src/locales/hi.json | 1 + src/locales/hu.json | 1 + src/locales/it.json | 1 + src/locales/jp.json | 1 + src/locales/ko.json | 1 + src/locales/pl.json | 1 + src/locales/pt-br.json | 1 + src/locales/pt.json | 1 + src/locales/ru.json | 1 + src/locales/tr.json | 1 + src/locales/zh.json | 1 + src/main/Stores.ts | 4 + src/renderer/Grid/Renderer.tsx | 246 +++++++++++++++++++++++++---- src/renderer/Grid/Row.scss | 16 ++ src/renderer/Settings/Settings.tsx | 7 +- 21 files changed, 258 insertions(+), 32 deletions(-) diff --git a/src/@types/Settings.ts b/src/@types/Settings.ts index 3c43aa9e..ca516931 100644 --- a/src/@types/Settings.ts +++ b/src/@types/Settings.ts @@ -20,6 +20,7 @@ export interface SettingStore { menuBarVisibility: boolean; disableAnimations: boolean; compact: boolean; + multiLineView: boolean; zoom: number; matomo: boolean; diff --git a/src/locales/cs.json b/src/locales/cs.json index 20599660..9d51e5be 100644 --- a/src/locales/cs.json +++ b/src/locales/cs.json @@ -35,6 +35,7 @@ "settings.6": "Sobota", "settings.menuBarVisibility": "Zobrazit menu v panelu", "settings.compact": "Kompaktní zobrazení", + "settings.multiLineView": "Dvouřádkové zobrazení", "attributes": "Atributy", "filters": "Filtry", "sorting": "Řazení", diff --git a/src/locales/de.json b/src/locales/de.json index 8caef7a8..ebde16a5 100644 --- a/src/locales/de.json +++ b/src/locales/de.json @@ -35,6 +35,7 @@ "settings.6": "Samstag", "settings.menuBarVisibility": "Menüleiste anzeigen", "settings.compact": "Kompakte Ansicht", + "settings.multiLineView": "Zweizeilige Ansicht", "attributes": "Attribute", "filters": "Filter", "sorting": "Sortierung", diff --git a/src/locales/en.json b/src/locales/en.json index e13441af..d5620ba2 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -35,6 +35,7 @@ "settings.6": "Saturday", "settings.menuBarVisibility": "Show menu bar", "settings.compact": "Compact view", + "settings.multiLineView": "Two-row view", "attributes": "Attributes", "filters": "Filters", "sorting": "Sorting", diff --git a/src/locales/es.json b/src/locales/es.json index 7705b71e..75e17331 100644 --- a/src/locales/es.json +++ b/src/locales/es.json @@ -35,6 +35,7 @@ "settings.6": "Sábado", "settings.menuBarVisibility": "Mostrar barra de menú", "settings.compact": "Vista compacta", + "settings.multiLineView": "Vista de dos filas", "attributes": "Atributos", "filters": "Filtros", "sorting": "Ordenación", diff --git a/src/locales/fr.json b/src/locales/fr.json index 21789cef..51c9e8e5 100644 --- a/src/locales/fr.json +++ b/src/locales/fr.json @@ -35,6 +35,7 @@ "settings.6": "Samedi", "settings.menuBarVisibility": "Afficher la barre de menus", "settings.compact": "Vue compacte", + "settings.multiLineView": "Vue sur deux lignes", "attributes": "Attributs", "filters": "Filtres", "sorting": "Tri", diff --git a/src/locales/hi.json b/src/locales/hi.json index 73d59b04..0ad19520 100644 --- a/src/locales/hi.json +++ b/src/locales/hi.json @@ -35,6 +35,7 @@ "settings.6": "शनिवार", "settings.menuBarVisibility": "मेनू बार दिखाएं", "settings.compact": "संकुचित दृश्य", + "settings.multiLineView": "दो-पंक्ति दृश्य", "attributes": "गुण", "filters": "फ़िल्टर", "sorting": "क्रमबद्ध करना", diff --git a/src/locales/hu.json b/src/locales/hu.json index 3b0a1ebf..3cb518b9 100644 --- a/src/locales/hu.json +++ b/src/locales/hu.json @@ -35,6 +35,7 @@ "settings.6": "Szombat", "settings.menuBarVisibility": "Menüsor megjelenítése", "settings.compact": "Komplett nézet", + "settings.multiLineView": "Kétsoros nézet", "attributes": "Tulajdonságok", "filters": "Szűrők", "sorting": "Rendezés", diff --git a/src/locales/it.json b/src/locales/it.json index 10f6743a..48aac2e9 100644 --- a/src/locales/it.json +++ b/src/locales/it.json @@ -35,6 +35,7 @@ "settings.6": "Sabato", "settings.menuBarVisibility": "Mostra la barra dei menu", "settings.compact": "Vista compatta", + "settings.multiLineView": "Vista su due righe", "attributes": "Attributi", "filters": "Filtri", "sorting": "Ordinamento", diff --git a/src/locales/jp.json b/src/locales/jp.json index a2bdafd6..b86db9ca 100644 --- a/src/locales/jp.json +++ b/src/locales/jp.json @@ -35,6 +35,7 @@ "settings.6": "土曜日", "settings.menuBarVisibility": "メニューバーを表示する", "settings.compact": "コンパクトビュー", + "settings.multiLineView": "2行ビュー", "attributes": "属性", "filters": "フィルター", "sorting": "ソート", diff --git a/src/locales/ko.json b/src/locales/ko.json index d7a50699..4db497b7 100644 --- a/src/locales/ko.json +++ b/src/locales/ko.json @@ -35,6 +35,7 @@ "settings.6": "토요일", "settings.menuBarVisibility": "메뉴 바 표시", "settings.compact": "컴팩트 보기", + "settings.multiLineView": "두 줄 보기", "attributes": "속성", "filters": "필터", "sorting": "정렬", diff --git a/src/locales/pl.json b/src/locales/pl.json index ec854949..18093974 100644 --- a/src/locales/pl.json +++ b/src/locales/pl.json @@ -35,6 +35,7 @@ "settings.6": "Sobota", "settings.menuBarVisibility": "Pokaż pasek menu", "settings.compact": "Widok kompaktowy", + "settings.multiLineView": "Widok dwuwierszowy", "attributes": "Atrybuty", "filters": "Filtry", "sorting": "Sortowanie", diff --git a/src/locales/pt-br.json b/src/locales/pt-br.json index ef22d1c5..d11cd6fa 100644 --- a/src/locales/pt-br.json +++ b/src/locales/pt-br.json @@ -35,6 +35,7 @@ "settings.6": "Sábado", "settings.menuBarVisibility": "Mostrar barra de menus", "settings.compact": "Visão compacta", + "settings.multiLineView": "Vista em duas linhas", "attributes": "Atributos", "filters": "Filtros", "sorting": "Classificação", diff --git a/src/locales/pt.json b/src/locales/pt.json index 62a94982..ed61ec24 100644 --- a/src/locales/pt.json +++ b/src/locales/pt.json @@ -35,6 +35,7 @@ "settings.6": "Sábado", "settings.menuBarVisibility": "Mostrar barra de menu", "settings.compact": "Visão compacta", + "settings.multiLineView": "Vista em duas linhas", "attributes": "Atributos", "filters": "Filtros", "sorting": "Ordenação", diff --git a/src/locales/ru.json b/src/locales/ru.json index 8cee60c7..2bdcad1f 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -35,6 +35,7 @@ "settings.6": "Суббота", "settings.menuBarVisibility": "Показать строку меню", "settings.compact": "Компактный вид", + "settings.multiLineView": "Двухстрочный вид", "attributes": "Атрибуты", "filters": "Фильтры", "sorting": "Сортировка", diff --git a/src/locales/tr.json b/src/locales/tr.json index ffffbbb4..45547c5d 100644 --- a/src/locales/tr.json +++ b/src/locales/tr.json @@ -35,6 +35,7 @@ "settings.6": "Cumartesi", "settings.menuBarVisibility": "Menü çubuğunu göster", "settings.compact": "Kompakt görünüm", + "settings.multiLineView": "İki satırlı görünüm", "attributes": "Özellikler", "filters": "Filtreler", "sorting": "Sıralama", diff --git a/src/locales/zh.json b/src/locales/zh.json index 11f4b524..be727d0c 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -35,6 +35,7 @@ "settings.6": "周六", "settings.menuBarVisibility": "显示菜单栏", "settings.compact": "精简视图", + "settings.multiLineView": "双行视图", "attributes": "属性", "filters": "过滤器", "sorting": "排序", diff --git a/src/main/Stores.ts b/src/main/Stores.ts index 5f741f5d..0fda0c5f 100644 --- a/src/main/Stores.ts +++ b/src/main/Stores.ts @@ -145,6 +145,10 @@ const migrations = { config.set("invertTrayColor", false); config.set("startMinimized", false); }, + "2.0.27": (config) => { + console.log("Migrating settings store from 2.0.19 → 2.0.27"); + config.set("multiLineView", config.get("multiLineView", false)); + }, }; const rerenderDefinition = { diff --git a/src/renderer/Grid/Renderer.tsx b/src/renderer/Grid/Renderer.tsx index c30b0948..3d536c6d 100644 --- a/src/renderer/Grid/Renderer.tsx +++ b/src/renderer/Grid/Renderer.tsx @@ -114,7 +114,222 @@ const RendererComponent: React.FC = memo( return uri; }; + const linkOptions: Components = { + a: ({ children, href, ...props }): JSX.Element | null => { + if (!children) return null; + const childrenStr = + typeof children === "string" ? children : String(children); + const match = /([a-zA-Z]+:\/\/\S+)/g.exec(childrenStr); + const maxChars = 40; + const truncatedChildren = + childrenStr.length > maxChars + ? childrenStr.slice(0, maxChars) + "..." + : childrenStr; + + const link = ( + + handleLinkClick(event, match ? childrenStr : href || childrenStr) + } + > + {truncatedChildren} + + + ); + + return childrenStr.length > maxChars ? ( + + {link} + + ) : ( + link + ); + }, + }; + + if (settings.multiLineView) { + const badgePatterns: RegExp[] = []; + if (todoObject.tString) badgePatterns.push(new RegExp(`t:${todoObject.tString.replace(/\s/g, "\\s")}`, "g")); + if (todoObject.dueString) badgePatterns.push(new RegExp(`due:${todoObject.dueString.replace(/\s/g, "\\s")}`, "g")); + badgePatterns.push(/@\S+/g, /(?:^|\s)\+\S+/g, /\bh:1\b/g, /\bpm:\d+/g, /\brec:[^ ]+/g); + + let cleanBody = todoObject.body; + for (const pattern of badgePatterns) { + cleanBody = cleanBody.replace(pattern, ""); + } + cleanBody = cleanBody.replace(/\s+/g, " ").trim(); + + const badgeElements: React.ReactNode[] = []; + let badgeIndex = 0; + + if (todoObject.due) { + badgeIndex++; + badgeElements.push( + + + + ); + } + + if (todoObject.t) { + badgeIndex++; + badgeElements.push( + + + + ); + } + + if (todoObject.projects) { + todoObject.projects.forEach((project) => { + badgeIndex++; + badgeElements.push( + + + + ); + }); + } + + if (todoObject.contexts) { + todoObject.contexts.forEach((context) => { + badgeIndex++; + badgeElements.push( + + + + ); + }); + } + + if (todoObject.rec) { + badgeIndex++; + badgeElements.push( + + + + ); + } + + if (todoObject.pm) { + badgeIndex++; + badgeElements.push( + + + + ); + } + + return ( + <> + + + {cleanBody} + + + {badgeElements.length > 0 && ( + {badgeElements} + )} + + ); + } + const options: Components = { + ...linkOptions, p: ({ children }): JSX.Element | null => { const mappedChildren = React.Children.map(children, (child) => { if (typeof child !== "string") return child; @@ -146,37 +361,6 @@ const RendererComponent: React.FC = memo( }); return mappedChildren ? <>{mappedChildren} : null; }, - a: ({ children, href, ...props }): JSX.Element | null => { - if (!children) return null; - const childrenStr = - typeof children === "string" ? children : String(children); - const match = /([a-zA-Z]+:\/\/\S+)/g.exec(childrenStr); - const maxChars = 40; - const truncatedChildren = - childrenStr.length > maxChars - ? childrenStr.slice(0, maxChars) + "..." - : childrenStr; - - const link = ( - - handleLinkClick(event, match ? childrenStr : href || childrenStr) - } - > - {truncatedChildren} - - - ); - - return childrenStr.length > maxChars ? ( - - {link} - - ) : ( - link - ); - }, }; return ( diff --git a/src/renderer/Grid/Row.scss b/src/renderer/Grid/Row.scss index ef07d02e..e9e1603a 100644 --- a/src/renderer/Grid/Row.scss +++ b/src/renderer/Grid/Row.scss @@ -68,6 +68,22 @@ } } +.multiLineView { + .row { + .row-text { + flex: 1; + min-width: 0; + } + .row-badges { + width: 100%; + display: flex; + flex-wrap: wrap; + gap: 0.25em; + align-items: center; + } + } +} + .dark { .row { border-color: Variables.$dark-grey; diff --git a/src/renderer/Settings/Settings.tsx b/src/renderer/Settings/Settings.tsx index 7bd031da..9b2b4304 100644 --- a/src/renderer/Settings/Settings.tsx +++ b/src/renderer/Settings/Settings.tsx @@ -76,6 +76,9 @@ const visibleSettings: VisibleSettings = { compact: { style: "toggle", }, + multiLineView: { + style: "toggle", + }, notificationsAllowed: { style: "toggle", }, @@ -149,17 +152,19 @@ const SettingsComponent: React.FC = memo( document.body.classList.toggle("disableAnimations", disableAnimations); document.body.classList.toggle("compact", compact); + document.body.classList.toggle("multiLineView", settings.multiLineView); document.body.classList.toggle("dark", shouldUseDarkColors); document.body.classList.toggle("light", !shouldUseDarkColors); return () => { - document.body.classList.remove("dark", "light", "compact"); + document.body.classList.remove("dark", "light", "compact", "multiLineView"); }; }, [ settings.shouldUseDarkColors, settings.zoom, settings.compact, settings.disableAnimations, + settings.multiLineView, ]); const { t } = useTranslation(); From cd6b33621a4fd6d98eb3e4e794b81ed91746dc79 Mon Sep 17 00:00:00 2001 From: wallek Date: Mon, 18 May 2026 11:05:06 +0200 Subject: [PATCH 2/3] Improve multiLineView layout with flexbox wrapper - Wrap row content in flex container for better alignment - Override ReactMarkdown paragraph rendering to prevent extra spacing - Adjust row padding and alignment for multiLineView mode - Add gap between row elements for consistent spacing --- src/renderer/Grid/Renderer.tsx | 9 ++- src/renderer/Grid/Row.scss | 16 +++++ src/renderer/Grid/Row.tsx | 116 +++++++++++++++++---------------- 3 files changed, 83 insertions(+), 58 deletions(-) diff --git a/src/renderer/Grid/Renderer.tsx b/src/renderer/Grid/Renderer.tsx index 3d536c6d..27321ecf 100644 --- a/src/renderer/Grid/Renderer.tsx +++ b/src/renderer/Grid/Renderer.tsx @@ -310,12 +310,19 @@ const RendererComponent: React.FC = memo( ); } + const multiLineComponents: Components = { + ...linkOptions, + p: ({ children }): JSX.Element | null => { + return <>{children}; + }, + }; + return ( <> {cleanBody} diff --git a/src/renderer/Grid/Row.scss b/src/renderer/Grid/Row.scss index e9e1603a..dac11872 100644 --- a/src/renderer/Grid/Row.scss +++ b/src/renderer/Grid/Row.scss @@ -36,6 +36,10 @@ } } +.row-content { + display: contents; +} + .compact { .row { padding: 0.3em; @@ -70,6 +74,18 @@ .multiLineView { .row { + align-items: flex-start; + padding: 0.3em 0.5em; + + .row-content { + display: flex; + flex-wrap: wrap; + flex: 1; + min-width: 0; + gap: 0.25em; + align-items: center; + } + .row-text { flex: 1; min-width: 0; diff --git a/src/renderer/Grid/Row.tsx b/src/renderer/Grid/Row.tsx index 284cb5d7..117e0a2a 100644 --- a/src/renderer/Grid/Row.tsx +++ b/src/renderer/Grid/Row.tsx @@ -174,67 +174,69 @@ const Row: React.FC = memo( onChange={handleCheckboxChange} /> - {(settings.sorting[0].value != "priority" || settings.fileSorting) && - todoObject.priority && ( - - - + + + )} + + {todoObject.completed && ( + + + )} - {todoObject.completed && ( - - - - )} - - {todoObject.created && ( - - - - )} - - {todoObject.hidden && ( - - )} - - + {todoObject.created && ( + + + + )} + + {todoObject.hidden && ( + + )} + + + ); From 820bc763c161ef5e25cc81c5f908f5f14a215110 Mon Sep 17 00:00:00 2001 From: wallek Date: Thu, 21 May 2026 10:37:05 +0200 Subject: [PATCH 3/3] Refactor multiLineView badge rendering and fix migration - Simplify badge pattern matching by reusing existing expressions array - Consolidate date badge rendering (due/t) into single loop - Consolidate list badge rendering (projects/contexts) into single loop - Remove redundant badgeIndex tracking in favor of semantic keys - Simplify paragraph component override in multiLineComponents - Extract multiLineView from settings destructuring for consistency - Fix migration to always set multiLineView to false --- src/main/Stores.ts | 2 +- src/renderer/Grid/Renderer.tsx | 130 +++++++---------------------- src/renderer/Settings/Settings.tsx | 4 +- 3 files changed, 33 insertions(+), 103 deletions(-) diff --git a/src/main/Stores.ts b/src/main/Stores.ts index 0fda0c5f..d8be7127 100644 --- a/src/main/Stores.ts +++ b/src/main/Stores.ts @@ -147,7 +147,7 @@ const migrations = { }, "2.0.27": (config) => { console.log("Migrating settings store from 2.0.19 → 2.0.27"); - config.set("multiLineView", config.get("multiLineView", false)); + config.set("multiLineView", false); }, }; diff --git a/src/renderer/Grid/Renderer.tsx b/src/renderer/Grid/Renderer.tsx index 27321ecf..dbc2f47f 100644 --- a/src/renderer/Grid/Renderer.tsx +++ b/src/renderer/Grid/Renderer.tsx @@ -149,126 +149,63 @@ const RendererComponent: React.FC = memo( }; if (settings.multiLineView) { - const badgePatterns: RegExp[] = []; - if (todoObject.tString) badgePatterns.push(new RegExp(`t:${todoObject.tString.replace(/\s/g, "\\s")}`, "g")); - if (todoObject.dueString) badgePatterns.push(new RegExp(`due:${todoObject.dueString.replace(/\s/g, "\\s")}`, "g")); - badgePatterns.push(/@\S+/g, /(?:^|\s)\+\S+/g, /\bh:1\b/g, /\bpm:\d+/g, /\brec:[^ ]+/g); - let cleanBody = todoObject.body; - for (const pattern of badgePatterns) { - cleanBody = cleanBody.replace(pattern, ""); + for (const { pattern } of expressions) { + cleanBody = cleanBody.replace(new RegExp(pattern.source, "g"), ""); } cleanBody = cleanBody.replace(/\s+/g, " ").trim(); const badgeElements: React.ReactNode[] = []; - let badgeIndex = 0; - if (todoObject.due) { - badgeIndex++; - badgeElements.push( - - - - ); - } - - if (todoObject.t) { - badgeIndex++; + const dateBadges: Array<{ type: "due" | "t"; value: string | null }> = [ + { type: "due", value: todoObject.due }, + { type: "t", value: todoObject.t }, + ]; + dateBadges.forEach(({ type, value }) => { + if (!value) return; badgeElements.push( ); - } + }); - if (todoObject.projects) { - todoObject.projects.forEach((project) => { - badgeIndex++; - badgeElements.push( - - - - ); - }); - } - - if (todoObject.contexts) { - todoObject.contexts.forEach((context) => { - badgeIndex++; + const listBadges: Array<{ type: "projects" | "contexts"; values: string[] | null }> = [ + { type: "projects", values: todoObject.projects }, + { type: "contexts", values: todoObject.contexts }, + ]; + listBadges.forEach(({ type, values }) => { + values?.forEach((value) => { badgeElements.push( ); }); - } + }); if (todoObject.rec) { - badgeIndex++; badgeElements.push( - +