diff --git a/assets/fonts/FreeSans.otf b/assets/fonts/FreeSans.otf
deleted file mode 100755
index 0b04a5d5..00000000
Binary files a/assets/fonts/FreeSans.otf and /dev/null differ
diff --git a/assets/fonts/FreeSans.ttf b/assets/fonts/FreeSans.ttf
deleted file mode 100755
index 9db95853..00000000
Binary files a/assets/fonts/FreeSans.ttf and /dev/null differ
diff --git a/assets/fonts/FreeSansBold.otf b/assets/fonts/FreeSansBold.otf
deleted file mode 100755
index 17865b10..00000000
Binary files a/assets/fonts/FreeSansBold.otf and /dev/null differ
diff --git a/assets/fonts/FreeSansBold.ttf b/assets/fonts/FreeSansBold.ttf
deleted file mode 100755
index 63644e74..00000000
Binary files a/assets/fonts/FreeSansBold.ttf and /dev/null differ
diff --git a/flatpak/com.github.ransome1.sleek.appdata.xml b/flatpak/com.github.ransome1.sleek.appdata.xml
index d8b838e0..5332067b 100755
--- a/flatpak/com.github.ransome1.sleek.appdata.xml
+++ b/flatpak/com.github.ransome1.sleek.appdata.xml
@@ -9,7 +9,7 @@
Robin Ahle
-
+
https://github.com/ransome1/sleek
https://github.com/ransome1/sleek/issues
diff --git a/flatpak/com.github.ransome1.sleek.desktop b/flatpak/com.github.ransome1.sleek.desktop
index 07527a75..ef01419e 100755
--- a/flatpak/com.github.ransome1.sleek.desktop
+++ b/flatpak/com.github.ransome1.sleek.desktop
@@ -1,5 +1,5 @@
[Desktop Entry]
-Version=2.0.7-rc.3
+Version=2.0.7-rc.4
Name=sleek
Exec=sleek
Type=Application
diff --git a/package.json b/package.json
index 4c8a2a98..86d8115b 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "sleek",
- "version": "2.0.7-rc.3",
+ "version": "2.0.7-rc.4",
"main": "./src/main/main.tsx",
"scripts": {
"build": "concurrently \"yarn run peggy\" \"yarn run build:main\" \"yarn run build:renderer\"",
diff --git a/release/app/package.json b/release/app/package.json
index 8dea3b3d..3e8479ee 100644
--- a/release/app/package.json
+++ b/release/app/package.json
@@ -1,6 +1,6 @@
{
"name": "sleek",
- "version": "2.0.7-rc.3",
+ "version": "2.0.7-rc.4",
"description": "todo.txt manager for Linux, Windows and MacOS, free and open-source (FOSS)",
"synopsis": "todo.txt manager for Linux, Windows and MacOS, free and open-source (FOSS)",
"keywords": [
diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml
index 6647aa0d..8fbe16d9 100644
--- a/snap/snapcraft.yaml
+++ b/snap/snapcraft.yaml
@@ -1,6 +1,6 @@
name: sleek
base: core20
-version: "2.0.7-rc.3"
+version: "2.0.7-rc.4"
summary: todo.txt manager for Linux, free and open-source (FOSS)
description: |
sleek is an open-source (FOSS) todo manager based on the todo.txt syntax. Stripped down to only the most necessary features, and with a clean and simple interface, sleek aims to help you focus on getting things done.
diff --git a/src/main/config.tsx b/src/main/config.tsx
index 2da9145b..59db0a65 100644
--- a/src/main/config.tsx
+++ b/src/main/config.tsx
@@ -118,13 +118,13 @@ filter.onDidChange('attributes', async () => {
}
});
-filter.onDidChange('search', async () => {
- try {
- await processDataRequest(searchString);
- } catch(error: any) {
- console.error(error);
- }
-});
+// filter.onDidChange('search', async () => {
+// try {
+// await processDataRequest(searchString);
+// } catch(error: any) {
+// console.error(error);
+// }
+// });
config.onDidAnyChange(async(settings) => {
try {
diff --git a/src/main/modules/Notifications.tsx b/src/main/modules/Notifications.tsx
index bbf68158..50472f2b 100644
--- a/src/main/modules/Notifications.tsx
+++ b/src/main/modules/Notifications.tsx
@@ -30,6 +30,20 @@ function createSpeakingDifference(dueDate: Dayjs) {
return 'Due';
}
+function isNotificationSuppressed(searchFilters, body) {
+ let suppressNotification = false;
+ for (const searchFilter of searchFilters) {
+ if (searchFilter.label && searchFilter.suppress) {
+ const match = checkForSearchMatches(body, searchFilter.label);
+ if (match) {
+ suppressNotification = true;
+ break;
+ }
+ }
+ }
+ return suppressNotification;
+}
+
function handleNotification(due: string | null, body: string, badge: Badge) {
const notificationAllowed = config.get('notificationsAllowed');
@@ -40,35 +54,18 @@ function handleNotification(due: string | null, body: string, badge: Badge) {
const notificationThreshold: number = config.get('notificationThreshold');
const hash = todayString + crypto.createHash('sha256').update(body).digest('hex');
const searchFilters: SearchFilter[] = filter.get('search') || [];
-
- let searchFilterMatch;
- for (const searchFilter of searchFilters) {
- if (searchFilter.notify && searchFilter.label) {
- const match = checkForSearchMatches(body, searchFilter.label);
- if (match) {
- searchFilterMatch = {
- label: searchFilter.label,
- };
- break;
- }
- }
- }
+
+ if(isNotificationSuppressed(searchFilters, body)) return;
- let title: string;
if(dueDate.isToday() || dueDate.isBetween(today, today.add(notificationThreshold, 'day'))) {
- title = createSpeakingDifference(dueDate)
- } else if(searchFilterMatch && searchFilterMatch.label) {
- title = `Matched "${searchFilterMatch.label}"`;
- } else {
- return;
- }
-
- badge.count += 1;
- const notifiedTodoObjects = new Set(notifiedTodoObjectsStorage.get('notifiedTodoObjects', []));
- if(!notifiedTodoObjects.has(hash)) {
- sendNotification(title, body);
- notifiedTodoObjects.add(hash);
- notifiedTodoObjectsStorage.set('notifiedTodoObjects', Array.from(notifiedTodoObjects));
+ badge.count += 1;
+ const title = createSpeakingDifference(dueDate);
+ const notifiedTodoObjects = new Set(notifiedTodoObjectsStorage.get('notifiedTodoObjects', []));
+ if(!notifiedTodoObjects.has(hash)) {
+ sendNotification(title, body);
+ notifiedTodoObjects.add(hash);
+ notifiedTodoObjectsStorage.set('notifiedTodoObjects', Array.from(notifiedTodoObjects));
+ }
}
}
}
diff --git a/src/renderer/App.scss b/src/renderer/App.scss
index ab896018..c1aa2dba 100644
--- a/src/renderer/App.scss
+++ b/src/renderer/App.scss
@@ -1,26 +1,20 @@
@import "Variables.scss";
@import "Coloring.scss";
-@font-face {
- font-family: "FreeSans";
- src: url("../../assets/fonts/FreeSans.otf") format("opentype");
- src: url("../../assets/fonts/FreeSans.ttf") format("truetype");
-}
-@font-face {
- font-family: "FreeSansBold";
- src: url("../../assets/fonts/FreeSansBold.otf") format("opentype");
- src: url("../../assets/fonts/FreeSansBold.ttf") format("truetype");
-}
-
body {
overflow: hidden;
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
+ font-family: "Helvetica, Arial, Sans-Serif";
code {
+ font-size: 0.9em;
background: $lighter-grey;
- color: $mid-grey;
+ color: $dark-grey;
+ border-radius: 0.25em;
+ padding: 0.25em;
+ white-space: nowrap;
}
#root {
.flexContainer {
@@ -62,7 +56,7 @@ body {
}
h1, h2, h3, h4, h5 {
- font-family: $font-family-bold;
+ font-weight: bold;
}
*::-webkit-scrollbar {
diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx
index c8e84e79..ef4fb8e6 100644
--- a/src/renderer/App.tsx
+++ b/src/renderer/App.tsx
@@ -134,6 +134,7 @@ const App = () => {
setSearchString={setSearchString}
settings={settings}
searchFieldRef={searchFieldRef}
+ setPromptItem={setPromptItem}
/>
= memo(({
const handleKeyDown = useCallback(
(event: KeyboardEvent) => {
const isSearchFocused = document.activeElement === searchFieldRef.current;
- if ((event.metaKey || event.ctrlKey) && event.key === 'f' && settings.isSearchOpen && !isSearchFocused) {
+ if ((event.metaKey || event.ctrlKey) && !event.shiftKey && event.key === 'f' && settings.isSearchOpen && !isSearchFocused) {
event.preventDefault();
searchFieldRef.current?.focus();
}
diff --git a/src/renderer/Header/Search.scss b/src/renderer/Header/Search.scss
index 8396ce29..8794654f 100644
--- a/src/renderer/Header/Search.scss
+++ b/src/renderer/Header/Search.scss
@@ -4,11 +4,6 @@
display: flex;
position: relative;
margin-bottom: 0.25em;
- .MuiButton-root {
- position: absolute;
- top: 0.5em;
- right: 3.5em;
- }
.MuiAutocomplete-root {
flex-direction: row;
flex: 1;
diff --git a/src/renderer/Header/Search.tsx b/src/renderer/Header/Search.tsx
index ac4a1691..9273bb16 100644
--- a/src/renderer/Header/Search.tsx
+++ b/src/renderer/Header/Search.tsx
@@ -1,8 +1,14 @@
import React, { useState, useEffect, useCallback, memo, MouseEvent } from 'react';
import TextField from '@mui/material/TextField';
import Autocomplete, { createFilterOptions } from '@mui/material/Autocomplete';
+import InputAdornment from '@mui/material/InputAdornment';
+import IconButton from '@mui/material/IconButton';
+import ClearIcon from '@mui/icons-material/Clear';
+import AddIcon from '@mui/icons-material/Add';
import Button from '@mui/material/Button';
import RemoveCircleIcon from '@mui/icons-material/RemoveCircle';
+import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
+import ArrowDropUpIcon from '@mui/icons-material/ArrowDropUp';
import AddCircleIcon from '@mui/icons-material/AddCircle';
import NotificationsActiveIcon from '@mui/icons-material/NotificationsActive';
import NotificationsOffIcon from '@mui/icons-material/NotificationsOff';
@@ -18,6 +24,7 @@ interface SearchProps extends WithTranslation {
searchString: string;
setSearchString: React.Dispatch>;
searchFieldRef: React.RefObject;
+ setPromptItem: React.Dispatch>;
t: typeof i18n.t;
}
@@ -27,33 +34,45 @@ const Search: React.FC = memo(({
searchString,
setSearchString,
searchFieldRef,
+ setPromptItem,
t,
}) => {
const [searchFilters, setSearchFilters] = useState(store.getFilters('search'));
+ const [isAutocompleteOpen, setIsAutocompleteOpen] = useState(false);
- const toggleNotify = (option) => {
+ const toggleSuppress = (option) => {
const updatedFilters = searchFilters.map(searchFilter => {
if (searchFilter.label === option.label) {
- return { ...searchFilter, notify: !option.notify }; // Set notify to true when removing
+ return { ...searchFilter, suppress: !option.suppress };
}
return searchFilter;
});
setSearchFilters(updatedFilters);
}
- const handleRemoveFilter = useCallback((event: MouseEvent, option: SearchFilter) => {
- event.stopPropagation();
- event.preventDefault();
+ const handleDeleteFilterConfirm = (option) => {
const updatedFilters = searchFilters.filter(searchFilter => searchFilter.label !== option.label);
setSearchFilters(updatedFilters);
+ };
+
+ const handleDeleteFilter = useCallback((event: MouseEvent, option: SearchFilter) => {
+ event.stopPropagation();
+ event.preventDefault();
+ setPromptItem({
+ id: 'confirmSearchFilterDelete',
+ headline: 'Delete search filter',
+ text: `This will delete search filter ${option.label}
`,
+ button1: 'Delete',
+ onButton1: () => handleDeleteFilterConfirm(option),
+ });
}, [searchFilters]);
const handleAddNewFilter = useCallback((event: React.SyntheticEvent, value: string) => {
event.stopPropagation();
event.preventDefault();
const updatedFilters = [
- { label: value, notify: false },
+ { label: value, suppress: false },
...searchFilters.filter(searchFilter => searchFilter.label !== value)
];
setSearchFilters(updatedFilters);
@@ -65,18 +84,6 @@ const Search: React.FC = memo(({
}
}, [searchString]);
- const handleKeyDown = useCallback((event: KeyboardEvent) => {
- const isSearchFocused = document.activeElement === searchFieldRef.current;
- if (searchString && isSearchFocused && event.key === 'Escape') {
- setSearchString('');
- } else if (!searchString && isSearchFocused && event.key === 'Escape') {
- const isSearchOpen = !settings.isSearchOpen;
- store.setConfig('isSearchOpen', isSearchOpen);
- } else if (isSearchFocused && searchString && (event.metaKey || event.ctrlKey) && event.key === 'Enter') {
- handleAddTodo();
- }
- }, [searchFieldRef, searchString, settings.isSearchOpen]);
-
useEffect(() => {
const handleSearch = () => {
ipcRenderer.send('requestData', searchString);
@@ -99,9 +106,34 @@ const Search: React.FC = memo(({
}
}, [settings.isSearchOpen, searchFieldRef]);
+ const handleKeyDown = useCallback((event: KeyboardEvent) => {
+ const isSearchFocused = document.activeElement === searchFieldRef.current;
+ if(!isAutocompleteOpen && isSearchFocused && event.key === 'ArrowDown') {
+ setIsAutocompleteOpen(true);
+ } else if (searchString && isSearchFocused && event.key === 'Escape') {
+ setSearchString('');
+ } else if (!searchString && isSearchFocused && event.key === 'Escape') {
+ const isSearchOpen = !settings.isSearchOpen;
+ store.setConfig('isSearchOpen', isSearchOpen);
+ } else if (isSearchFocused && searchString && (event.metaKey || event.ctrlKey) && event.key === 'Enter') {
+ handleAddTodo();
+ } else if((event.metaKey || event.ctrlKey) && event.shiftKey && event.key === 'f') {
+ searchFieldRef?.current?.focus();
+ setIsAutocompleteOpen(!isAutocompleteOpen);
+ }
+ }, [searchFieldRef, searchString, settings.isSearchOpen, isAutocompleteOpen]);
+
+ const handleKeyUp = useCallback((event: KeyboardEvent) => {
+ if(isAutocompleteOpen && (event.key === 'Escape' || event.key === 'Enter')) {
+ setIsAutocompleteOpen(false);
+ }
+ }, [isAutocompleteOpen]);
+
useEffect(() => {
+ document.addEventListener('keyup', handleKeyUp);
document.addEventListener('keydown', handleKeyDown);
return () => {
+ document.removeEventListener('keyup', handleKeyUp);
document.removeEventListener('keydown', handleKeyDown);
};
}, [handleKeyDown]);
@@ -112,12 +144,17 @@ const Search: React.FC = memo(({
setIsAutocompleteOpen(false)}
onChange={(event, value: string | SearchFilter | null) => {
+ setIsAutocompleteOpen(false)
if (value && typeof value !== 'string' && value.inputValue) {
handleAddNewFilter(event, value.inputValue);
}
}}
- options={searchFilters}
filterOptions={(options: (string | SearchFilter)[], params) => {
const filter = createFilterOptions();
const filtered: SearchFilter[] = filter(options as SearchFilter[], params);
@@ -131,7 +168,6 @@ const Search: React.FC = memo(({
}
return filtered;
}}
- inputValue={searchString}
onInputChange={(_, value) => setSearchString(value)}
getOptionLabel={(option: SearchFilter | string): string => {
if (typeof option === 'string') {
@@ -158,25 +194,25 @@ const Search: React.FC = memo(({
onClick={(event) => {
event.stopPropagation();
if(typeof option !== 'string') {
- handleRemoveFilter(event, option);
+ handleDeleteFilter(event, option);
}
}}
data-testid="header-search-autocomplete-remove"
/>
- {option.notify === false ? (
+ {option.suppress === false ? (
{
event.stopPropagation();
- toggleNotify(option);
+ toggleSuppress(option);
}}
className="greyedOut"
data-testid="header-search-autocomplete-notification-disable"
/>
) : (
- {
event.stopPropagation();
- toggleNotify(option);
+ toggleSuppress(option);
}}
data-testid="header-search-autocomplete-notification-enable"
/>
@@ -187,26 +223,56 @@ const Search: React.FC = memo(({
)}
renderInput={(params) => (
- <>
+ <>
+ {searchFilters.length > 0 || searchString ? (
+ setIsAutocompleteOpen(!isAutocompleteOpen)}
+ data-testid="header-search-clear-icon"
+ >
+ {isAutocompleteOpen ? (
+
+ ) : (
+
+ )}
+
+ ) : (
+
+
+
+ )}
+
+ ),
+ endAdornment: (
+
+ {searchString && searchString.length > 0 && (
+
+ )}
+ setSearchString('')} data-testid="header-search-clear-icon">
+
+
+
+ ),
+ }}
/>
>
)}
/>
- {searchString && searchString.length > 0 && (
- <>
-
- >
- )}
)}
>
diff --git a/src/renderer/Navigation.scss b/src/renderer/Navigation.scss
index a8c0f5b7..f8d60d4f 100644
--- a/src/renderer/Navigation.scss
+++ b/src/renderer/Navigation.scss
@@ -26,7 +26,7 @@
flex-direction: column;
transition: left 0.3s ease;
div {
- font-family: $font-family-bold;
+ font-weight: bold;
line-height: 5em;
text-align: center;
color: white;
diff --git a/src/renderer/Prompt.tsx b/src/renderer/Prompt.tsx
index fca398f0..eedf5830 100644
--- a/src/renderer/Prompt.tsx
+++ b/src/renderer/Prompt.tsx
@@ -1,4 +1,4 @@
-import React, { useEffect } from 'react';
+import React, { useEffect, Fragment } from 'react';
import Dialog from '@mui/material/Dialog';
import DialogTitle from '@mui/material/DialogTitle';
import DialogContent from '@mui/material/DialogContent';
@@ -39,7 +39,7 @@ const Prompt: React.FC = ({
return (