(
62 | collection: T[],
63 | payload: { id: string; clipboard: string },
64 | fnUpdate: (item: T, text: string) => void,
65 | fnNew: (after: T, text: string) => T,
66 | ) {
67 | const [first, ...rest] = payload.clipboard
68 | .split('\n')
69 | .map(s => s.replace(/^[\W]*(-|\*) (\[( |x)\] )?/, ''))
70 | .map(s => s.trim())
71 | .filter(s => !!s);
72 |
73 | const item = find(collection, payload);
74 |
75 | fnUpdate(item, first);
76 |
77 | let after = item;
78 | rest.reverse().forEach(text => {
79 | insertAfter(collection, after, fnNew(after, text));
80 | });
81 | }
82 |
--------------------------------------------------------------------------------
/app/src/modules/tasks/index.ts:
--------------------------------------------------------------------------------
1 | import taskReducer, { ITask } from '../task';
2 | import generateId from 'utils/generateId';
3 |
4 | type IState = {
5 | tasks: { [key: string]: ITask };
6 | selected: string;
7 | undo: {
8 | lastAction: string | null;
9 | past: ITask[];
10 | future: ITask[];
11 | };
12 | };
13 |
14 | interface IAction {
15 | type: string;
16 | payload?: any;
17 | }
18 |
19 | export default function tasksReducer(
20 | state: IState | undefined,
21 | action: IAction,
22 | ): IState {
23 | if (state === undefined) {
24 | state = ACTION_HANDLERS['tasks/new'](state);
25 | }
26 |
27 | const handler = ACTION_HANDLERS[action.type] || forwardAction;
28 | return handler(state, action);
29 | }
30 |
31 | const UNDO_EMPTY = {
32 | lastAction: null,
33 | past: [],
34 | future: [],
35 | };
36 |
37 | function createNewTask(title?: string) {
38 | return {
39 | id: generateId('task'),
40 | title: title || '',
41 | todos: [],
42 | bookmarks: [],
43 | note: '',
44 | };
45 | }
46 |
47 | const ACTION_HANDLERS: any = {
48 | 'tasks/new': (state: IState | undefined) => {
49 | const newTask = createNewTask();
50 |
51 | return {
52 | selected: newTask.id,
53 | tasks: {
54 | ...(state?.tasks || {}),
55 | [newTask.id]: newTask,
56 | },
57 | undo: UNDO_EMPTY,
58 | };
59 | },
60 |
61 | 'tasks/select': (state: IState, action: IAction) => {
62 | const taskId = action.payload.task.id;
63 |
64 | if (!state.tasks[taskId]) {
65 | return state;
66 | }
67 |
68 | return {
69 | selected: taskId,
70 | tasks: state.tasks,
71 | undo: UNDO_EMPTY,
72 | };
73 | },
74 |
75 | 'tasks/next': (state: IState) => {
76 | const tasks = Object.values(state.tasks);
77 |
78 | if (tasks.length < 2) {
79 | return state;
80 | }
81 |
82 | const current = tasks.findIndex((t) => t.id === state.selected);
83 | if (current === -1) {
84 | return state;
85 | }
86 |
87 | const next = tasks[current + 1] || tasks[0];
88 | if (!next) {
89 | return state;
90 | }
91 |
92 | return {
93 | selected: next.id,
94 | tasks: state.tasks,
95 | undo: UNDO_EMPTY,
96 | };
97 | },
98 |
99 | 'tasks/delete': (state: IState, action: IAction) => {
100 | const taskId = action.payload.task.id;
101 |
102 | if (!state.tasks[taskId]) {
103 | return state;
104 | }
105 |
106 | const tasks = { ...state.tasks };
107 |
108 | Reflect.deleteProperty(tasks, taskId);
109 |
110 | let selected = state.selected;
111 |
112 | if (selected === taskId) {
113 | selected = Object.keys(tasks)[0];
114 |
115 | if (!selected) {
116 | const newTask = createNewTask('Focus');
117 | selected = newTask.id;
118 | tasks[selected] = newTask;
119 | }
120 | }
121 |
122 | return {
123 | selected,
124 | tasks,
125 | undo: UNDO_EMPTY,
126 | };
127 | },
128 |
129 | 'tasks/import': (state: IState, action: IAction) => {
130 | const task = { ...createNewTask(), ...action.payload.task };
131 |
132 | return {
133 | selected: task.id,
134 | tasks: {
135 | ...state.tasks,
136 | [task.id]: task,
137 | },
138 | undo: UNDO_EMPTY,
139 | };
140 | },
141 |
142 | 'tasks/undo': (state: IState, action: IAction) => {
143 | const { past, future } = state.undo;
144 |
145 | if (past.length === 0) {
146 | return state;
147 | }
148 |
149 | const previous = past[past.length - 1];
150 | const newPast = past.slice(0, past.length - 1);
151 |
152 | return {
153 | selected: state.selected,
154 | tasks: {
155 | ...state.tasks,
156 | [state.selected]: previous,
157 | },
158 |
159 | undo: {
160 | lastAction: action.type,
161 | past: newPast,
162 | future: [state.tasks[state.selected], ...future],
163 | },
164 | };
165 | },
166 |
167 | 'tasks/redo': (state: IState, action: IAction) => {
168 | const { past, future } = state.undo;
169 |
170 | if (future.length === 0) {
171 | return state;
172 | }
173 |
174 | const next = future[0];
175 | const newFuture = future.slice(1);
176 |
177 | return {
178 | selected: state.selected,
179 | tasks: {
180 | ...state.tasks,
181 | [state.selected]: next,
182 | },
183 |
184 | undo: {
185 | lastAction: action.type,
186 | past: [...past, state.tasks[state.selected]],
187 | future: newFuture,
188 | },
189 | };
190 | },
191 | };
192 |
193 | const MAX_UNDO = 100;
194 |
195 | const UNDO_SKIP_ACTIONS = {
196 | 'task/updateTodoText': 'task/newTodo',
197 | 'task/updateBookmark': 'task/newBookmark',
198 | } as any;
199 |
200 | function forwardAction(state: IState, action: IAction) {
201 | const presentTask = state.tasks[state.selected];
202 |
203 | if (!presentTask) {
204 | return state;
205 | }
206 |
207 | const updatedTask = taskReducer(presentTask, action);
208 |
209 | if (updatedTask === presentTask) {
210 | return state;
211 | }
212 |
213 | let undo = state.undo;
214 |
215 | if (
216 | UNDO_SKIP_ACTIONS[action.type] &&
217 | UNDO_SKIP_ACTIONS[action.type] === state.undo.lastAction
218 | ) {
219 | undo = {
220 | ...undo,
221 | lastAction: action.type,
222 | };
223 | } else {
224 | undo = {
225 | lastAction: action.type,
226 | past: [
227 | ...(undo.past.length > MAX_UNDO
228 | ? undo.past.slice(1, MAX_UNDO)
229 | : undo.past),
230 | presentTask,
231 | ],
232 | future: [],
233 | };
234 | }
235 |
236 | return {
237 | selected: updatedTask.id,
238 | tasks: {
239 | ...state.tasks,
240 | [updatedTask.id]: updatedTask,
241 | },
242 | undo,
243 | };
244 | }
245 |
--------------------------------------------------------------------------------
/app/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/app/src/screens/about/AutoUpdateStatus.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import styles from './styles.module.css';
3 | import { autoUpdateSubscribe } from 'utils/electron';
4 |
5 | export default function AutoUpdateStatus() {
6 | const status = useAutoUpdateStatus();
7 |
8 | if (!status) {
9 | return null;
10 | }
11 |
12 | return {status};
13 | }
14 |
15 | function useAutoUpdateStatus() {
16 | const [status, setStatus] = React.useState('');
17 |
18 | React.useEffect(() => {
19 | return autoUpdateSubscribe(m => {
20 | setStatus(m);
21 | });
22 | }, [setStatus]);
23 |
24 | return status;
25 | }
26 |
--------------------------------------------------------------------------------
/app/src/screens/about/TextButton.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import styles from './styles.module.css';
3 |
4 | interface IProps {
5 | onClick: () => void;
6 | children: React.ReactNode;
7 | }
8 |
9 | export default function TextButton({ onClick, children }: IProps) {
10 | return (
11 | {
14 | e.preventDefault();
15 | onClick();
16 | }}>
17 | {children}
18 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/app/src/screens/about/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import Stack from 'components/Stack';
3 | import BackButton from 'components/BackButton';
4 | import styles from './styles.module.css';
5 | import ExternalLink from 'components/ExternalLink';
6 | import logo from 'icons/logo.png';
7 | import { appVersion } from 'utils/electron';
8 | import { autoUpdateRequest } from 'utils/electron';
9 | import AutoUpdateStatus from './AutoUpdateStatus';
10 | import TextButton from './TextButton';
11 |
12 | export default function About() {
13 | return (
14 | <>
15 |
16 |
21 |
22 | Focused Task
23 | Version {appVersion()}
24 |
25 | Copyright ©{' '}
26 |
27 | Radoslav Stankov
28 |
29 |
30 |
31 |
32 | Source Code
33 | {' '}
34 | |{' '}
35 | Check for Updates
36 |
37 |
38 |
39 | >
40 | );
41 | }
42 |
--------------------------------------------------------------------------------
/app/src/screens/about/styles.module.css:
--------------------------------------------------------------------------------
1 | .about {
2 | margin: 40px 0;
3 | }
4 |
5 | .about a {
6 | color: inherit;
7 | text-decoration: none;
8 | }
9 |
10 | .about a:hover {
11 | text-decoration: underline;
12 | }
13 |
14 | .autoupdate {
15 | color: var(--silent);
16 | }
17 |
18 | .button {
19 | cursor: pointer;
20 | }
21 |
22 | .button:hover {
23 | text-decoration: underline;
24 | }
25 |
--------------------------------------------------------------------------------
/app/src/screens/changelog/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import Stack from 'components/Stack';
3 | import BackButton from 'components/BackButton';
4 | import Title from 'components/Title';
5 | import ReactMarkdown from 'react-markdown';
6 | import styles from './styles.module.css';
7 | import raw from 'raw.macro';
8 |
9 | const markdown = raw('../../../../CHANGELOG.md').replace(/^# .*\n\n/, '');
10 |
11 | export default function ChangeLog() {
12 |
13 |
14 | return (
15 | <>
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | >
24 | );
25 | }
26 |
--------------------------------------------------------------------------------
/app/src/screens/changelog/styles.module.css:
--------------------------------------------------------------------------------
1 | .markdown ul {
2 | list-style-type: none;
3 | padding: 0;
4 | margin: 0;
5 | }
6 |
7 | .markdown ul li + li {
8 | margin-top: var(--m);
9 | }
10 |
11 | .markdown strong {
12 | background: var(--box-background);
13 | border: var(--border);
14 | box-sizing: border-box;
15 | border-radius: var(--radius);
16 | font-size: 10px;
17 | line-height: 14px;
18 | padding: 4px 5px 4px;
19 | text-transform: uppercase;
20 | }
21 |
22 | .markdown h2 {
23 | font-weight: bold;
24 | font-size: var(--font-subtitle);
25 | line-height: var(--font-subtitle-height);
26 | margin-top: var(--l);
27 | margin-bottom: var(--s);
28 | }
29 |
--------------------------------------------------------------------------------
/app/src/screens/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import useSelector from 'hooks/useSelector';
3 | import { getSelectedScreen } from 'modules/selectors';
4 | import { Screens, IScreens } from './screens';
5 | import useShortcuts from 'hooks/useShortcuts';
6 | import useTheme from 'hooks/useTheme';
7 |
8 | export default function App() {
9 | useTheme();
10 | useShortcuts();
11 |
12 | const selectedScreen: IScreens = useSelector(getSelectedScreen);
13 | const Screen = Screens[selectedScreen];
14 |
15 | return ;
16 | }
17 |
--------------------------------------------------------------------------------
/app/src/screens/preferences/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import Section from 'components/Section';
3 | import Stack from 'components/Stack';
4 | import styles from './styles.module.css';
5 | import BackButton from 'components/BackButton';
6 | import Title from 'components/Title';
7 | import useSelector from 'hooks/useSelector';
8 | import useDispatch from 'hooks/useDispatch';
9 | import { getTheme } from 'modules/selectors';
10 | import { themes, ITheme, changeTheme } from 'modules/preferences';
11 | import Table from 'components/Table';
12 |
13 | export default function Preferences() {
14 | return (
15 | <>
16 |
17 |
18 |
19 |
26 |
27 | >
28 | );
29 | }
30 |
31 | function ThemeDropdown() {
32 | const theme = useSelector(getTheme);
33 | const dispatch = useDispatch();
34 |
35 | return (
36 |
37 |
50 |
51 | );
52 | }
53 |
--------------------------------------------------------------------------------
/app/src/screens/preferences/styles.module.css:
--------------------------------------------------------------------------------
1 | .select {
2 | color: var(--text-color);
3 | background-color: var(--background);
4 | }
5 |
--------------------------------------------------------------------------------
/app/src/screens/screens.tsx:
--------------------------------------------------------------------------------
1 | import about from './about';
2 | import changelog from './changelog';
3 | import preferences from './preferences';
4 | import shortcuts from './shortcuts';
5 | import task from './task';
6 |
7 | export const Screens = {
8 | about,
9 | changelog,
10 | preferences,
11 | shortcuts,
12 | task,
13 | };
14 |
15 | export type IScreens = keyof typeof Screens
16 |
--------------------------------------------------------------------------------
/app/src/screens/shortcuts/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import Section from 'components/Section';
3 | import Stack from 'components/Stack';
4 | import styles from './styles.module.css';
5 | import BackButton from 'components/BackButton';
6 | import Title from 'components/Title';
7 | import keyCodes from 'utils/keyCodes';
8 | import { updateGlobalShortcutKey, getGlobalShortcutKey } from 'utils/electron';
9 | import isAccelerator from 'electron-is-accelerator';
10 | import classNames from 'classnames';
11 | import Button from 'components/Button';
12 | import Table from 'components/Table';
13 |
14 | export default function Shortcuts() {
15 | return (
16 | <>
17 |
18 |
19 |
20 |
25 |
26 |
27 |
28 | + h
29 |
30 |
31 | Esc
32 |
33 |
34 |
35 |
36 |
37 |
38 | + e
39 |
40 |
41 | + t
42 |
43 |
44 | + + t
45 |
46 |
47 | + b
48 |
49 |
50 | + n
51 |
52 |
53 | + 0
54 |
55 |
56 | + [1-9]
57 |
58 |
59 | + z
60 |
61 |
62 | + + z
63 |
64 |
65 | Tab
66 |
67 |
68 | + `
69 |
70 |
71 |
72 |
73 |
74 |
75 | Enter
76 |
77 |
78 | Backspace
79 |
80 |
81 | + Backspace
82 |
83 |
84 | Esc
85 |
86 |
87 | ↑
88 |
89 |
90 | ↓
91 |
92 |
93 | + ↑
94 |
95 |
96 | + ↓
97 |
98 |
99 | + [
100 |
101 |
102 | + ]
103 |
104 |
105 | + click
106 |
107 |
108 | + + c
109 |
110 |
111 |
112 |
113 |
114 |
115 | ⏎ Enter
116 |
117 |
118 | Backspace
119 |
120 |
121 | + Backspace
122 |
123 |
124 | Esc
125 |
126 |
127 | ↑
128 |
129 |
130 | ↓
131 |
132 |
133 | + ↑
134 |
135 |
136 | + ↓
137 |
138 |
139 | + click
140 |
141 |
142 |
143 |
144 | >
145 | );
146 | }
147 |
148 | function Cmd() {
149 | return ⌘ Cmd;
150 | }
151 |
152 | function Shift() {
153 | return Shift;
154 | }
155 |
156 | function Key({ children }: { children: React.ReactNode }) {
157 | return {children};
158 | }
159 |
160 | function ShortcutGlobal() {
161 | const {
162 | key,
163 | setKey,
164 | isEditing,
165 | edit,
166 | cancelChanges,
167 | saveChanges,
168 | } = useGlobalShortcutForm();
169 |
170 | return (
171 |
175 | {
185 | if (e.keyCode === keyCodes.enter) {
186 | saveChanges();
187 | }
188 | }}
189 | />
190 |
191 |
192 |
193 | ) : (
194 | <>
195 | Open Focused Task (global)
196 | >
197 | )
198 | }>
199 | + {key}
200 |
201 | );
202 | }
203 |
204 | export function useGlobalShortcutForm() {
205 | const [isEditing, setIsEditing] = React.useState(false);
206 | const [key, setKey] = React.useState(getGlobalShortcutKey);
207 |
208 | return {
209 | key,
210 | setKey(e: any) {
211 | setKey(e.target.value);
212 | },
213 | isEditing,
214 | edit() {
215 | setIsEditing(true);
216 | },
217 | cancelChanges() {
218 | setKey(getGlobalShortcutKey());
219 | setIsEditing(false);
220 | },
221 | saveChanges() {
222 | if (isKeyAcceptable(key)) {
223 | updateGlobalShortcutKey(key);
224 | setIsEditing(false);
225 | }
226 | },
227 | };
228 | }
229 |
230 | function isKeyAcceptable(key: string) {
231 | return isAccelerator(`CommandOrControl+${key}`);
232 | }
233 |
--------------------------------------------------------------------------------
/app/src/screens/shortcuts/styles.module.css:
--------------------------------------------------------------------------------
1 | .key {
2 | display: inline-block;
3 | padding: 0px 5px;
4 | font-weight: 600;
5 | font-size: 12px;
6 | line-height: 20px;
7 | vertical-align: 1px;
8 | color: var(--box-background);
9 | background: var(--text-color);
10 | border-radius: var(--radius);
11 | min-width: 10px;
12 | text-align: center;
13 | }
14 |
15 | .input {
16 | width: 30px;
17 | text-align: center;
18 | background: var(--box-background);
19 | border: var(--border);
20 | color: var(--text-color);
21 | border-radius: var(--radius);
22 | padding: 3px 5px 3px;
23 | }
24 |
25 | .input.error {
26 | outline: 1px solid red;
27 | }
28 |
--------------------------------------------------------------------------------
/app/src/screens/task/Bookmarks/OpenLink/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import styles from './styles.module.css';
3 | import ExternalLink from 'components/ExternalLink';
4 | import { bookmarkUri } from 'utils/bookmarks';
5 |
6 | interface IProps {
7 | bookmark: {
8 | uri: string;
9 | };
10 | index: number;
11 | }
12 |
13 | export default function BookmarkOpenLink({ bookmark, index }: IProps) {
14 | const uri = bookmarkUri(bookmark);
15 |
16 | if (!uri) {
17 | return ;
18 | }
19 |
20 | return (
21 |
22 | {index}
23 |
24 | ↗️
25 |
26 |
27 | );
28 | }
29 |
--------------------------------------------------------------------------------
/app/src/screens/task/Bookmarks/OpenLink/styles.module.css:
--------------------------------------------------------------------------------
1 | .link,
2 | .inactive {
3 | align-items: center;
4 | display: inline-flex;
5 | justify-content: center;
6 | text-decoration: none;
7 |
8 | user-select: none;
9 | cursor: pointer;
10 |
11 | color: inherit;
12 | font-size: 10px;
13 |
14 | width: var(--box-size);
15 | height: var(--box-size);
16 | min-width: var(--box-size);
17 | min-height: var(--box-size);
18 |
19 | background: var(--box-background);
20 | border: var(--border);
21 | box-sizing: border-box;
22 | border-radius: var(--radius);
23 | }
24 |
25 | .link:hover {
26 | border-color: var(--hover);
27 | background: (--icon-hover-background)
28 | }
29 |
30 | .link:hover .label {
31 | display: none;
32 | }
33 |
34 | .link:hover .emoji {
35 | display: inline-block;
36 | }
37 |
38 | .emoji {
39 | display: none;
40 | font-size: 16px;
41 | }
42 |
43 |
44 |
--------------------------------------------------------------------------------
/app/src/screens/task/Bookmarks/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import Stack from 'components/Stack';
3 | import keyCodes from 'utils/keyCodes';
4 | import { focusOnBookmarkWithIndex } from 'utils/focusOn';
5 | import useSelector from 'hooks/useSelector';
6 | import useDispatch from 'hooks/useDispatch';
7 | import { openURI } from 'utils/electron';
8 | import Sortable from 'components/Sortable';
9 | import Input from 'components/Input';
10 | import isURI from 'utils/isURI';
11 | import { getBookmarks } from 'modules/selectors';
12 | import BookmarkOpenLink from './OpenLink';
13 | import AddButton from 'components/AddButton';
14 |
15 | import {
16 | updateBookmark,
17 | removeBookmark,
18 | newBookmark,
19 | pasteBookmarks,
20 | moveBookmark,
21 | } from 'modules/task';
22 |
23 | export default function TaskBookmarks() {
24 | const bookmarks = useSelector(getBookmarks);
25 | const dispatch = useDispatch();
26 |
27 | return (
28 |
29 |
32 | dispatch(
33 | moveBookmark({
34 | id: bookmarks[oldIndex]!.id,
35 | by: newIndex - oldIndex,
36 | }),
37 | )
38 | }>
39 | {bookmarks.map((bookmark, i) => (
40 | {
46 | if (e.metaKey && isURI(bookmark.uri)) {
47 | e.preventDefault();
48 | e.stopPropagation();
49 | openURI(bookmark.uri);
50 | }
51 | }}>
52 |
53 |
59 | dispatch(updateBookmark({ id: bookmark.id, uri: value }))
60 | }
61 | onPaste={(clipboard) => {
62 | dispatch(pasteBookmarks({ id: bookmark.id, clipboard }));
63 | }}
64 | onKeyDown={(e) => {
65 | if (
66 | e.keyCode === keyCodes.backspace &&
67 | (e.target.value === '' || e.metaKey)
68 | ) {
69 | e.preventDefault();
70 | dispatch(removeBookmark(bookmark));
71 | focusOnBookmarkWithIndex(i - 1);
72 | } else if (e.keyCode === keyCodes.enter) {
73 | dispatch(newBookmark({ after: bookmark }));
74 | } else if (e.keyCode === keyCodes.esc) {
75 | e.target.blur();
76 | } else if (!e.metaKey && e.keyCode === keyCodes.up) {
77 | focusOnBookmarkWithIndex(i - 1);
78 | } else if (!e.metaKey && e.keyCode === keyCodes.down) {
79 | focusOnBookmarkWithIndex(i + 1);
80 | } else if (e.metaKey && e.keyCode === keyCodes.up) {
81 | dispatch(moveBookmark({ id: bookmark.id, by: -1 }));
82 | } else if (e.metaKey && e.keyCode === keyCodes.down) {
83 | dispatch(moveBookmark({ id: bookmark.id, by: +1 }));
84 | }
85 | }}
86 | />
87 |
88 | ))}
89 |
90 | dispatch(newBookmark())}
92 | subject={bookmarks.length === 0 ? 'bookmark' : 'another'}
93 | />
94 |
95 | );
96 | }
97 |
--------------------------------------------------------------------------------
/app/src/screens/task/DragFileMessage/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import styles from './styles.module.css';
3 |
4 | export default function DragFileMessage() {
5 | return Drop to add a bookmark
;
6 | }
7 |
--------------------------------------------------------------------------------
/app/src/screens/task/DragFileMessage/styles.module.css:
--------------------------------------------------------------------------------
1 | .dragFileMessage {
2 | display: flex;
3 | align-items: center;
4 | justify-content: center;
5 |
6 | font-size: var(--font-title);
7 | line-height: var(--font-title-height);
8 | font-weight: bold;
9 | background: rgba(0, 0, 0, 0.5);
10 | color: white;
11 |
12 | padding: var(--m);
13 |
14 | position: absolute;
15 | left: 0;
16 | bottom: 0;
17 | right: 0;
18 | top: 0;
19 | z-index: 1;
20 | }
21 |
--------------------------------------------------------------------------------
/app/src/screens/task/Note/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import Input from 'components/Input';
3 | import keyCodes from 'utils/keyCodes';
4 | import { updateNote } from 'modules/task';
5 | import { getNote } from 'modules/selectors';
6 | import useSelector from 'hooks/useSelector';
7 | import useDispatch from 'hooks/useDispatch';
8 | import Stack from 'components/Stack';
9 | import AddButton from 'components/AddButton';
10 | import focusOn from 'utils/focusOn';
11 |
12 | export default function TaskNote() {
13 | const note = useSelector(getNote);
14 | const dispatch = useDispatch();
15 | const [showNewButton, setShowNewButton] = React.useState(!note);
16 |
17 | return (
18 | <>
19 | {showNewButton && (
20 | focusOn('note-text')} subject="note" />
21 | )}
22 |
23 | {
29 | setShowNewButton(false);
30 | }}
31 | onChange={(value) => {
32 | dispatch(updateNote(value));
33 | setShowNewButton(!value);
34 | }}
35 | onKeyDown={(e) => {
36 | if (e.keyCode === keyCodes.esc) {
37 | e.target.blur();
38 | }
39 | }}
40 | />
41 |
42 | >
43 | );
44 | }
45 |
--------------------------------------------------------------------------------
/app/src/screens/task/Title/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import Stack from 'components/Stack';
3 | import styles from './styles.module.css';
4 | import { updateTaskTitle } from 'modules/task';
5 | import { getTitle, getSelectedTaskId } from 'modules/selectors';
6 | import Input from 'components/Input';
7 | import useSelector from 'hooks/useSelector';
8 | import useDispatch from 'hooks/useDispatch';
9 | import keyCodes from 'utils/keyCodes';
10 |
11 | export default function TaskTitle() {
12 | const title = useSelector(getTitle);
13 | const id = useSelector(getSelectedTaskId);
14 | const dispatch = useDispatch();
15 |
16 | return (
17 |
18 | dispatch(updateTaskTitle(value || 'Untitled'))}
24 | onKeyDown={(e) => {
25 | if (e.keyCode === keyCodes.esc) {
26 | e.target.blur();
27 | }
28 | }}
29 | tabIndex={-1}
30 | placeholder="Title..."
31 | />
32 |
33 | );
34 | }
35 |
--------------------------------------------------------------------------------
/app/src/screens/task/Title/styles.module.css:
--------------------------------------------------------------------------------
1 | .title {
2 | font-size: var(--font-title) !important;
3 | line-height: var(--font-title-height) !important;
4 | top: 0 !important;
5 | font-weight: bold;
6 | margin-right: 30px;
7 | }
8 |
--------------------------------------------------------------------------------
/app/src/screens/task/Todos/Checkbox/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import styles from './styles.module.css';
3 |
4 | interface IProps {
5 | isChecked: boolean;
6 | onClick: () => void;
7 | }
8 |
9 | export default function TodoCheckbox({ isChecked, onClick }: IProps) {
10 | function handleClick(e: React.SyntheticEvent) {
11 | e.stopPropagation();
12 | onClick();
13 | }
14 |
15 | if (isChecked) {
16 | return (
17 |
22 | ✅
23 |
24 | );
25 | }
26 |
27 | return ;
28 | }
29 |
--------------------------------------------------------------------------------
/app/src/screens/task/Todos/Checkbox/styles.module.css:
--------------------------------------------------------------------------------
1 | .unchecked {
2 | cursor: pointer;
3 | user-select: none;
4 |
5 | width: var(--box-size);
6 | height: var(--box-size);
7 | min-width: var(--box-size);
8 | min-height: var(--box-size);
9 |
10 | background: var(--box-background);
11 | border: var(--border);
12 | box-sizing: border-box;
13 | border-radius: var(--radius);
14 | }
15 |
16 | .unchecked:hover {
17 | border-color: var(--hover);
18 | background: (--icon-hover-background)
19 | }
20 |
21 | .checked {
22 | cursor: pointer;
23 | user-select: none;
24 | font-size: var(--box-size);
25 | line-height: var(--box-size);
26 | margin-right: -3px;
27 | }
28 |
29 | .checked:hover {
30 | opacity: 0.8;
31 | }
32 |
--------------------------------------------------------------------------------
/app/src/screens/task/Todos/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import Input from 'components/Input';
3 | import Sortable from 'components/Sortable';
4 | import Stack from 'components/Stack';
5 | import { focusOnTodoWithIndex } from 'utils/focusOn';
6 | import keyCodes from 'utils/keyCodes';
7 | import styles from './styles.module.css';
8 | import useDispatch from 'hooks/useDispatch';
9 | import useSelector from 'hooks/useSelector';
10 | import TodoCheckbox from './Checkbox';
11 | import AddButton from 'components/AddButton';
12 |
13 | import { getTodos } from 'modules/selectors';
14 |
15 | import {
16 | moveTodo,
17 | newTodo,
18 | pasteTasks,
19 | removeTodo,
20 | toggleTodo,
21 | updateTodoIdent,
22 | updateTodoText,
23 | } from 'modules/task';
24 |
25 | export default function TaskTodos() {
26 | const todos = useSelector(getTodos);
27 | const dispatch = useDispatch();
28 |
29 | return (
30 |
31 | {
34 | const oldIndexBroughtInRange = Math.min(oldIndex, todos.length - 1)
35 |
36 | dispatch(
37 | moveTodo({
38 | id: todos[oldIndexBroughtInRange].id,
39 | by: newIndex - oldIndexBroughtInRange,
40 | }),
41 | )
42 | }
43 | }>
44 | {todos.map((todo, i) => (
45 | {
52 | if (e.metaKey) {
53 | e.preventDefault();
54 | e.stopPropagation();
55 | dispatch(toggleTodo(todo));
56 | }
57 | }}>
58 | dispatch(toggleTodo(todo))}
61 | />
62 | {
67 | if (!text || text !== todo.text) {
68 | dispatch(
69 | updateTodoText({
70 | id: todo.id,
71 | text,
72 | }),
73 | );
74 | }
75 | }}
76 | onPaste={(clipboard) => {
77 | dispatch(pasteTasks({ id: todo.id, clipboard }));
78 | }}
79 | onKeyDown={(e) => {
80 | if (
81 | e.keyCode === keyCodes.backspace &&
82 | (e.target.value === '' || e.metaKey)
83 | ) {
84 | e.preventDefault();
85 | dispatch(removeTodo(todo));
86 | focusOnTodoWithIndex(i - 1);
87 | } else if (e.keyCode === keyCodes.enter) {
88 | dispatch(newTodo({ after: todo }));
89 | } else if (e.keyCode === keyCodes.esc) {
90 | e.target.blur();
91 | } else if (!e.metaKey && e.keyCode === keyCodes.up) {
92 | focusOnTodoWithIndex(i - 1);
93 | } else if (!e.metaKey && e.keyCode === keyCodes.down) {
94 | focusOnTodoWithIndex(i + 1);
95 | } else if (e.metaKey && e.keyCode === keyCodes.up) {
96 | dispatch(moveTodo({ id: todo.id, by: -1 }));
97 | } else if (e.metaKey && e.keyCode === keyCodes.down) {
98 | dispatch(moveTodo({ id: todo.id, by: +1 }));
99 | } else if (e.metaKey && e.keyCode === keyCodes['[']) {
100 | dispatch(updateTodoIdent({ id: todo.id, by: -1 }));
101 | } else if (e.metaKey && e.keyCode === keyCodes[']']) {
102 | dispatch(updateTodoIdent({ id: todo.id, by: 1 }));
103 | } else if (
104 | e.metaKey &&
105 | e.shiftKey &&
106 | e.keyCode === keyCodes.c
107 | ) {
108 | dispatch(toggleTodo(todo));
109 | }
110 | }}
111 | />
112 |
113 | ))}
114 |
115 | dispatch(newTodo())}
117 | subject={todos.length === 0 ? 'todo' : 'another'}
118 | />
119 |
120 | );
121 | }
122 |
--------------------------------------------------------------------------------
/app/src/screens/task/Todos/styles.module.css:
--------------------------------------------------------------------------------
1 | .completed {
2 | color: var(--line);
3 | text-decoration: line-through;
4 | }
5 |
--------------------------------------------------------------------------------
/app/src/screens/task/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import Section from 'components/Section';
3 | import Stack from 'components/Stack';
4 | import TaskTitle from './Title';
5 | import TaskTodos from './Todos';
6 | import TaskBookmarks from './Bookmarks';
7 | import TaskNote from './Note';
8 | import AppMenu from 'components/AppMenu';
9 | import useDragAndDropFiles from 'hooks/useDragAndDropFiles';
10 | import DragFileMessage from './DragFileMessage';
11 |
12 | export default function Task() {
13 | const isDraggingFile = useDragAndDropFiles();
14 |
15 | return (
16 | <>
17 | {isDraggingFile && }
18 |
19 |
20 |
21 |
24 |
27 |
30 |
31 | >
32 | );
33 | }
34 |
--------------------------------------------------------------------------------
/app/src/setupTests.tsx:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom/extend-expect';
6 |
--------------------------------------------------------------------------------
/app/src/types/dom.d.ts:
--------------------------------------------------------------------------------
1 | declare class ResizeObserver {
2 | constructor(callback: () => void);
3 |
4 | observe: (target: Element, options?: ResizeObserverObserveOptions) => void;
5 | }
6 |
7 | interface ResizeObserverObserveOptions {
8 | box?: 'content-box' | 'border-box';
9 | }
10 |
--------------------------------------------------------------------------------
/app/src/types/raw.micro.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'raw.macro';
2 |
--------------------------------------------------------------------------------
/app/src/utils/bookmarks.test.ts:
--------------------------------------------------------------------------------
1 | import { bookmarkUri, bookmarkTitle } from './bookmarks';
2 |
3 | describe(bookmarkUri.name, () => {
4 | it('returns null when invalid url', () => {
5 | expect(bookmarkUri({ uri: '' })).toEqual(null);
6 | expect(bookmarkUri({ uri: 'invalid' })).toEqual(null);
7 | });
8 |
9 | it('returns URI when valid', () => {
10 | expect(bookmarkUri({ uri: 'https://site.com' })).toEqual(
11 | 'https://site.com',
12 | );
13 | expect(bookmarkUri({ uri: '/Users/test.png' })).toEqual('/Users/test.png');
14 | });
15 |
16 | it('handles spaces', () => {
17 | expect(bookmarkUri({ uri: 'Personal Site https://site.com' })).toEqual(
18 | 'https://site.com',
19 | );
20 | expect(bookmarkUri({ uri: 'Test File /Users/test.png' })).toEqual(
21 | '/Users/test.png',
22 | );
23 | });
24 |
25 | it('handles file path with spaces', () => {
26 | expect(bookmarkUri({ uri: '/file/path with spaces.png' })).toEqual(
27 | '/file/path with spaces.png',
28 | );
29 | expect(
30 | bookmarkUri({ uri: 'spaces before /file/path with spaces.png' }),
31 | ).toEqual('/file/path with spaces.png');
32 | });
33 | });
34 |
35 | describe(bookmarkTitle.name, () => {
36 | it('renders text', () => {
37 | expect(bookmarkTitle('text')).toEqual('text');
38 | });
39 |
40 | it('renders URI', () => {
41 | expect(bookmarkTitle('https://site.com')).toEqual('https://site.com');
42 | expect(bookmarkTitle('https://localhost')).toEqual('https://localhost');
43 | expect(bookmarkTitle('/Users/test.png')).toEqual('/Users/test.png');
44 | });
45 |
46 | it('hides URI when title is present', () => {
47 | expect(bookmarkTitle('Some Title https://site.com')).toEqual(
48 | 'Some Title ↗️',
49 | );
50 | expect(bookmarkTitle('Some Title https://localhost')).toEqual(
51 | 'Some Title ↗️',
52 | );
53 | expect(bookmarkTitle('Some Title /Users/test.png')).toEqual(
54 | 'Some Title ↗️',
55 | );
56 | });
57 |
58 | it('handles file path with spaces', () => {
59 | expect(bookmarkTitle('spaces before /file/path with spaces.png')).toEqual(
60 | 'spaces before ↗️',
61 | );
62 | });
63 | });
64 |
--------------------------------------------------------------------------------
/app/src/utils/bookmarks.ts:
--------------------------------------------------------------------------------
1 | import { openURI } from 'utils/electron';
2 | import isURI from 'utils/isURI';
3 | import { last } from 'lodash';
4 |
5 | interface IBookmark {
6 | uri: string;
7 | }
8 |
9 | export function openBookmark(bookmark?: IBookmark) {
10 | if (!bookmark) {
11 | return;
12 | }
13 |
14 | const uri = bookmarkUri(bookmark);
15 |
16 | if (uri) {
17 | openURI(uri);
18 | }
19 | }
20 |
21 | const FILE_PATH = /(^| )\/.*$/;
22 |
23 | export function bookmarkUri({ uri }: IBookmark) {
24 | const fileMatch = uri.match(FILE_PATH);
25 | if (fileMatch) {
26 | return fileMatch[0].trim();
27 | }
28 |
29 | if (uri.includes(' ')) {
30 | uri = last(uri.split(' ')) || '';
31 | }
32 |
33 | if (isURI(uri)) {
34 | return uri;
35 | }
36 |
37 | return null;
38 | }
39 |
40 | export function bookmarkTitle(text: string) {
41 | const fileMatch = text.match(FILE_PATH);
42 | if (fileMatch) {
43 | if (fileMatch[0][0] === ' ') {
44 | return text.replace(fileMatch[0], ' ↗️');
45 | }
46 | }
47 |
48 | if (text.includes(' ')) {
49 | const chunks = text.split(' ');
50 | const last = chunks.splice(-1)[0] || '';
51 | if (isURI(last)) {
52 | text = chunks.join(' ') + ' ↗️';
53 | }
54 | }
55 |
56 | return text;
57 | }
58 |
--------------------------------------------------------------------------------
/app/src/utils/electron/index.tsx:
--------------------------------------------------------------------------------
1 | import electron from './shim';
2 | import { IStoreState } from 'modules';
3 | import { exportTask, importTask } from 'utils/stateRestore';
4 | import isURI, { isFilePathUri } from 'utils/isURI';
5 | import { getTitle } from 'modules/selectors';
6 |
7 | export const isElectron = !!electron;
8 |
9 | export function closeApp() {
10 | if (!isElectron) {
11 | return null;
12 | }
13 |
14 | electron.remote.getCurrentWindow().close();
15 | }
16 |
17 | export function hideApp() {
18 | if (!isElectron) {
19 | return null;
20 | }
21 |
22 | electron.remote.getCurrentWindow().hide();
23 | }
24 |
25 | export function resizeBasedOnContent() {
26 | if (!isElectron) {
27 | return;
28 | }
29 |
30 | const bodyStyle = window.getComputedStyle(document.body) as any;
31 | const padding =
32 | parseInt(bodyStyle['margin-top'], 10) +
33 | parseInt(bodyStyle['margin-bottom'], 10) +
34 | parseInt(bodyStyle['padding-top'], 10) +
35 | parseInt(bodyStyle['padding-bottom'], 10);
36 |
37 | const observer = new ResizeObserver(() => {
38 | const height = Math.min(900, document.body.offsetHeight + padding);
39 |
40 | const bounds = electron.remote
41 | .getCurrentWindow()
42 | .webContents.getOwnerBrowserWindow()
43 | .getBounds();
44 |
45 | if (bounds.height === height) {
46 | return;
47 | }
48 |
49 | electron.ipcRenderer.send('resize', bounds.width, height);
50 | });
51 | observer.observe(document.body);
52 | }
53 |
54 | const fs = window.require && window.require('fs').promises;
55 |
56 | export async function writeTaskToFile(store: IStoreState) {
57 | if (!isElectron) {
58 | return;
59 | }
60 |
61 | const { filePath } = await electron.remote.dialog.showSaveDialog({
62 | defaultPath:
63 | '~/Desktop/' +
64 | getTitle(store).toLocaleLowerCase().replace(/\W+/g, '_') +
65 | '.json',
66 | });
67 |
68 | if (!filePath) {
69 | return;
70 | }
71 |
72 | try {
73 | await fs.writeFile(filePath, exportTask(store));
74 | } catch (e) {
75 | alert(`An error occurred while writing the file: ${e.message}`);
76 | }
77 | }
78 |
79 | export async function readTaskFromFile() {
80 | if (!isElectron) {
81 | return;
82 | }
83 |
84 | const { filePaths } = await electron.remote.dialog.showOpenDialog({
85 | properties: ['openFile'],
86 | filters: [{ name: 'JSON', extensions: ['json'] }],
87 | });
88 |
89 | if (!filePaths || filePaths.length === 0) {
90 | return;
91 | }
92 |
93 | const [filePath] = filePaths;
94 |
95 | try {
96 | const data = await fs.readFile(filePath, 'utf-8');
97 |
98 | return importTask(data);
99 | } catch (e) {
100 | alert(`An error occurred while reading the file: ${e.message}`);
101 | }
102 |
103 | return null;
104 | }
105 |
106 | export type IMenuItem =
107 | | {
108 | label: string;
109 | click?: () => void;
110 | accelerator?: any;
111 | submenu?: IMenuItem[];
112 | type?: 'checkbox';
113 | checked?: boolean;
114 | }
115 | | { type: 'separator' };
116 |
117 | export function openMenu(items: IMenuItem[]) {
118 | if (!isElectron) {
119 | return;
120 | }
121 |
122 | const remote = electron.remote;
123 | const Menu = remote.Menu;
124 | const MenuItem = remote.MenuItem;
125 |
126 | const menu = new Menu();
127 |
128 | items.forEach((item) => menu.append(new MenuItem(item)));
129 |
130 | menu.popup(remote.getCurrentWindow());
131 | }
132 |
133 | export function openURI(uri: string) {
134 | if (!isElectron) {
135 | return;
136 | }
137 |
138 | if (!isURI(uri)) {
139 | return;
140 | }
141 |
142 | if (isFilePathUri(uri)) {
143 | electron.remote.shell.openItem(uri);
144 | } else {
145 | electron.remote.shell.openExternal(uri);
146 | }
147 | }
148 |
149 | export async function confirm({
150 | message,
151 | detail,
152 | fn,
153 | }: {
154 | message: string;
155 | detail?: string;
156 | fn: () => void;
157 | }) {
158 | if (!isElectron) {
159 | return;
160 | }
161 |
162 | const { response } = await electron.remote.dialog.showMessageBox({
163 | buttons: ['Yes', 'No', 'Cancel'],
164 | message,
165 | detail,
166 | });
167 |
168 | if (response === 0) {
169 | fn();
170 | }
171 | }
172 |
173 | export function getGlobalShortcutKey() {
174 | if (!isElectron) {
175 | return;
176 | }
177 |
178 | return electron.remote.getGlobal('globalShortcutKey');
179 | }
180 |
181 | export function updateGlobalShortcutKey(key: string) {
182 | if (!isElectron) {
183 | return;
184 | }
185 |
186 | if (key.length !== 1) {
187 | return;
188 | }
189 |
190 | electron.ipcRenderer.send('updateGlobalShortcutKey', key);
191 | }
192 |
193 | export function appVersion() {
194 | if (!isElectron) {
195 | return null;
196 | }
197 |
198 | return electron.remote.app.getVersion();
199 | }
200 |
201 | export function autoUpdateRequest() {
202 | electron.ipcRenderer.send('autoUpdateRequest');
203 | }
204 |
205 | export function autoUpdateSubscribe(handler: (message: string) => void) {
206 | const listener = (_e: any, message: string) => handler(message);
207 |
208 | electron.ipcRenderer.on('autoUpdateEvent', listener);
209 | electron.ipcRenderer.send('autoUpdateSubscribe');
210 |
211 | return () => {
212 | electron.ipcRenderer.removeListener('autoUpdateEvent', listener);
213 | electron.ipcRenderer.send('autoUpdateUnsubscribe');
214 | };
215 | }
216 |
217 | export function taskSwitchSubscribe(handler: () => void) {
218 | electron.ipcRenderer.on('switchTaskShortcut', handler);
219 |
220 | return () => {
221 | electron.ipcRenderer.removeListener('switchTaskShortcut', handler);
222 | };
223 | }
224 |
--------------------------------------------------------------------------------
/app/src/utils/electron/shim.tsx:
--------------------------------------------------------------------------------
1 | let electron: any = null;
2 |
3 | if (window.require) {
4 | electron = window.require('electron');
5 | }
6 |
7 | export default electron;
8 |
--------------------------------------------------------------------------------
/app/src/utils/focusOn.tsx:
--------------------------------------------------------------------------------
1 | export default function focusOn(id: string) {
2 | document.getElementById(id)?.click();
3 | }
4 |
5 | export function focusOnTodoWithIndex(index: number) {
6 | focusOn(`todo-text-${index}`);
7 | }
8 |
9 | export function focusOnBookmarkWithIndex(index: number) {
10 | focusOn(`bookmark-${index}`);
11 | }
12 |
--------------------------------------------------------------------------------
/app/src/utils/generateId.ts:
--------------------------------------------------------------------------------
1 | import { uniqueId } from 'lodash';
2 |
3 | export default function generateId(name: string) {
4 | return uniqueId(`${name}-`) + +new Date();
5 | }
6 |
--------------------------------------------------------------------------------
/app/src/utils/isURI.test.ts:
--------------------------------------------------------------------------------
1 | import isURI from './isURI';
2 |
3 | describe(isURI.name, () => {
4 | it('handles web urls', () => {
5 | expect(isURI('https://rstankov.com')).toEqual(true);
6 | expect(isURI('https://rstankov.com/about')).toEqual(true);
7 | expect(isURI('https://blog.rstankov.com/about')).toEqual(true);
8 | });
9 |
10 | it('handles localhost', () => {
11 | expect(isURI('https://localhost')).toEqual(true);
12 | });
13 |
14 | it('handles x-callbacks', () => {
15 | expect(isURI('bear://x-callback-url/open-note?id=12345')).toEqual(true);
16 | });
17 |
18 | it('handles files', () => {
19 | expect(isURI('/Users/rstankov/Desktop/test.png')).toEqual(true);
20 | expect(isURI('/Users/rstankov/Desktop/test with space.png')).toEqual(true);
21 | });
22 |
23 | it('handles invalid', () => {
24 | expect(isURI('')).toEqual(false);
25 | expect(isURI('invalid')).toEqual(false);
26 | expect(isURI('https://a')).toEqual(false);
27 | expect(isURI('https://a.com bsdadasdas')).toEqual(false);
28 | });
29 | });
30 |
--------------------------------------------------------------------------------
/app/src/utils/isURI.tsx:
--------------------------------------------------------------------------------
1 | const IS_URL = /^https?:\/\/[^ ]+\.[^ ]+$/;
2 | const IS_LOCALHOST = /^https?:\/\/localhost.*$/;
3 | const IS_FILE_PATH = /^\/.*$/;
4 | const IS_X_CALLBACK = /^[a-z]+:\/\/x-callback-url\/.*$/;
5 |
6 | const MATCHERS = [IS_URL, IS_LOCALHOST, IS_X_CALLBACK, IS_FILE_PATH];
7 |
8 | export default function isURI(uri: string) {
9 | return !!uri && !!MATCHERS.find((matcher) => !!uri.match(matcher));
10 | }
11 |
12 | export function isFilePathUri(uri: string) {
13 | return !!uri.match(IS_FILE_PATH);
14 | }
15 |
--------------------------------------------------------------------------------
/app/src/utils/keyCodes.tsx:
--------------------------------------------------------------------------------
1 | export default {
2 | esc: 27,
3 | backspace: 8,
4 | enter: 13,
5 | up: 38,
6 | down: 40,
7 | left: 37,
8 | right: 39,
9 | tab: 9,
10 | '[': 219,
11 | ']': 221,
12 | '/': 191,
13 | e: 69,
14 | h: 72,
15 | c: 67,
16 | t: 84,
17 | b: 66,
18 | n: 78,
19 | z: 90,
20 | '0': 48,
21 | '1': 49,
22 | '2': 50,
23 | '3': 51,
24 | '4': 52,
25 | '5': 53,
26 | '6': 54,
27 | '7': 55,
28 | '8': 56,
29 | '9': 57,
30 | };
31 |
--------------------------------------------------------------------------------
/app/src/utils/stateRestore.tsx:
--------------------------------------------------------------------------------
1 | import storage from 'utils/storage';
2 | import generateId from 'utils/generateId';
3 | import { getSelectedTask } from 'modules/selectors';
4 |
5 | export const VERSION = 3;
6 |
7 | export function preloadStore() {
8 | const version = storage.get('reduxStoreVersion', 1);
9 | const store = storage.get('reduxStore');
10 |
11 | if (!store) {
12 | return undefined;
13 | }
14 |
15 | if (version === VERSION) {
16 | return store;
17 | }
18 |
19 | const convert = STORE_CONVERT[version];
20 |
21 | if (convert) {
22 | return convert(store);
23 | }
24 |
25 | return undefined;
26 | }
27 |
28 | export function saveStore(store: any) {
29 | storage.set('reduxStoreVersion', VERSION);
30 | storage.set('reduxStore', store);
31 | }
32 |
33 | export function exportTask(store: any) {
34 | return JSON.stringify({
35 | version: VERSION,
36 | date: new Date(),
37 | task: getSelectedTask(store),
38 | });
39 | }
40 |
41 | export function importTask(json: any) {
42 | const data = JSON.parse(json);
43 |
44 | if (!data) {
45 | return null;
46 | }
47 |
48 | const convert = IMPORT_CONVERT[data.version];
49 |
50 | if (convert) {
51 | return convert(data);
52 | }
53 |
54 | return undefined;
55 | }
56 |
57 | const IMPORT_CONVERT: any = {
58 | 1: ({ store }: any) => ({ ...store.task, id: generateId('task') }),
59 | 2: ({ store }: any) => ({ ...store.task.present, id: generateId('task') }),
60 | 3: ({ task }: any) => task,
61 | };
62 |
63 | const STORE_CONVERT: any = {
64 | 1: (store: any) => {
65 | let task = { ...store.task, id: generateId('task') };
66 |
67 | return {
68 | selectedScreen: store.selectedScreen || 'task',
69 | tasks: {
70 | select: task.id,
71 | tasks: {
72 | [task.id]: task,
73 | },
74 | undo: {
75 | lastAction: null,
76 | past: [],
77 | future: [],
78 | },
79 | },
80 | };
81 | },
82 | 2: (store: any) => {
83 | let task = { ...store.task.present, id: generateId('task') };
84 |
85 | return {
86 | selectedScreen: store.selectedScreen || 'task',
87 | tasks: {
88 | selected: task.id,
89 | tasks: {
90 | [task.id]: task,
91 | },
92 | undo: {
93 | lastAction: null,
94 | past: [],
95 | future: [],
96 | },
97 | },
98 | };
99 | },
100 | 3: (store: any) => store,
101 | };
102 |
--------------------------------------------------------------------------------
/app/src/utils/storage.tsx:
--------------------------------------------------------------------------------
1 | export default {
2 | get(key: string, defaultValue: any = undefined): any {
3 | const value = window.localStorage.getItem(key);
4 |
5 | if (value) {
6 | return JSON.parse(value);
7 | }
8 |
9 | return defaultValue;
10 | },
11 |
12 | set(key: string, value: any) {
13 | window.localStorage.setItem(key, JSON.stringify(value));
14 | },
15 | };
16 |
--------------------------------------------------------------------------------
/app/tests/setup.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RStankov/FocusedTask/2532371d6b6a3d097cd8e23588c7786390b4e268/app/tests/setup.js
--------------------------------------------------------------------------------
/app/tests/styleMock.js:
--------------------------------------------------------------------------------
1 | module.exports = {};
2 |
--------------------------------------------------------------------------------
/app/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": false,
3 | "compilerOptions": {
4 | "target": "esnext",
5 | "module": "esnext",
6 | "resolveJsonModule": true,
7 | "jsx": "preserve",
8 | "allowJs": true,
9 | "moduleResolution": "node",
10 | "esModuleInterop": true,
11 | "allowSyntheticDefaultImports": true,
12 | "strict": true,
13 | "noEmit": true,
14 | "noImplicitAny": true,
15 | "noImplicitThis": true,
16 | "noUnusedLocals": true,
17 | "noUnusedParameters": true,
18 | "removeComments": false,
19 | "preserveConstEnums": true,
20 | "sourceMap": true,
21 | "skipLibCheck": true,
22 | "experimentalDecorators": true,
23 | "emitDecoratorMetadata": true,
24 | "baseUrl": "./src",
25 | "typeRoots": [
26 | "./node_modules/@types",
27 | "./types/"
28 | ],
29 | "lib": [
30 | "dom",
31 | "es2015",
32 | "es2016",
33 | "esnext.asynciterable"
34 | ],
35 | "forceConsistentCasingInFileNames": true,
36 | "isolatedModules": true
37 | },
38 | "include": [
39 | "./src"
40 | ],
41 | "exclude": [
42 | "node_modules/**/*"
43 | ]
44 | }
45 |
--------------------------------------------------------------------------------
/assets/Icon.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RStankov/FocusedTask/2532371d6b6a3d097cd8e23588c7786390b4e268/assets/Icon.icns
--------------------------------------------------------------------------------
/assets/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RStankov/FocusedTask/2532371d6b6a3d097cd8e23588c7786390b4e268/assets/icon.png
--------------------------------------------------------------------------------
/assets/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RStankov/FocusedTask/2532371d6b6a3d097cd8e23588c7786390b4e268/assets/screenshot.png
--------------------------------------------------------------------------------
/bin/bootstrap:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -e
4 |
5 | yarn install
6 |
7 | cd app && yarn install && cd ..
8 |
9 | cd shell && yarn install && cd ..
10 |
11 |
--------------------------------------------------------------------------------
/bin/electron-package.js:
--------------------------------------------------------------------------------
1 | const packager = require('electron-packager');
2 | const setLanguages = require('electron-packager-languages');
3 | const createZip = require('electron-installer-zip');
4 | const version = require('../shell/package.json').version;
5 | const fs = require('fs');
6 |
7 | const distPath = './dist';
8 | const name = 'FocusedTask';
9 |
10 | const credentials = require('../credentials.json');
11 |
12 | packager({
13 | dir: './shell',
14 | overwrite: true,
15 | out: distPath,
16 | afterCopy: [setLanguages(['en', 'en_GB'])],
17 | name,
18 | productName: 'Focused Task',
19 | appBundleId: 'com.rstankov.focused-task',
20 | appCopyright: 'Copyright (C) 2020 Radoslav Stankov',
21 | appVersion: version,
22 | appCategoryType: 'public.app-category.productivity',
23 | icon: './assets/Icon.icns',
24 | osxSign: {
25 | identity: credentials.identity,
26 | 'hardened-runtime': true,
27 | entitlements: 'entitlements.plist',
28 | 'entitlements-inherit': 'entitlements.plist',
29 | 'signature-flags': 'library',
30 | },
31 | osxNotarize: credentials.osxNotarize,
32 | }).then(() => {
33 | fs.unlinkSync(`${distPath}/${name}-darwin-x64/LICENSE`);
34 | fs.unlinkSync(`${distPath}/${name}-darwin-x64/LICENSES.chromium.html`);
35 | fs.unlinkSync(`${distPath}/${name}-darwin-x64/version`);
36 |
37 | console.log('Creating the zip package');
38 | createZip(
39 | {
40 | dir: `${distPath}/${name}-darwin-x64/${name}.app`,
41 | out: `${distPath}/${name}-${version}.zip`,
42 | },
43 | error => {
44 | if (error) {
45 | console.error(error);
46 | } else {
47 | console.log('Packaging completed successfully');
48 | }
49 | },
50 | );
51 | });
52 |
--------------------------------------------------------------------------------
/bin/electron-start:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -e
4 |
5 | cd shell && yarn start
6 |
--------------------------------------------------------------------------------
/bin/react-build:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -e
4 |
5 | if [ -d "./shell/build" ]; then rm -r ./shell/build; fi
6 |
7 | cd app && yarn build && mv ./build ../shell
8 |
--------------------------------------------------------------------------------
/bin/react-start:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -e
4 |
5 | cd app && yarn start
6 |
--------------------------------------------------------------------------------
/bin/test:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -e
4 |
5 | cd app && yarn jest --watch
6 |
--------------------------------------------------------------------------------
/credentials.json.example:
--------------------------------------------------------------------------------
1 | {
2 | "identity": "",
3 | "osxNotarize": {
4 | "appleId": "",
5 | "appleIdPassword": "",
6 | "ascProvider": ""
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/entitlements.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.cs.allow-jit
6 |
7 | com.apple.security.cs.allow-unsigned-executable-memory
8 |
9 | com.apple.security.cs.debugger
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "focused-task",
3 | "private": true,
4 | "license": "MIT",
5 | "dependencies": {},
6 | "devDependencies": {
7 | "concurrently": "6.0.0",
8 | "electron-installer-zip": "^0.1.2",
9 | "electron-packager": "15.2.0",
10 | "electron-packager-languages": "0.4.1"
11 | },
12 | "main": "./main.js",
13 | "scripts": {
14 | "dev-react": "./bin/react-start",
15 | "dev-electron": "ELECTRON_START_URL=http://localhost:3000 ./bin/electron-start .",
16 | "dev": "BROWSER=none concurrently \"yarn dev-react\" \"yarn dev-electron\" --kill-others",
17 | "app": "./bin/react-build && ./bin/electron-start",
18 | "test": "./bin/test",
19 | "build": "./bin/react-build && node ./bin/electron-package.js"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/releases.json:
--------------------------------------------------------------------------------
1 | {
2 | "currentRelease": "0.5.1",
3 | "releases": [
4 | {
5 | "version": "0.5.1",
6 | "updateTo": {
7 | "version": "0.5.1",
8 | "name": "0.5.1",
9 | "pub_date": "2021-03-28T00:00:00.0Z",
10 | "notes": "New release",
11 | "url": "https://focused-tasks.s3.eu-central-1.amazonaws.com/FocusedTask-0.5.1.zip"
12 | }
13 | },
14 | {
15 | "version": "0.4.0",
16 | "updateTo": {
17 | "version": "0.4.0",
18 | "name": "0.4.0",
19 | "pub_date": "2020-11-11T00:00:00.0Z",
20 | "notes": "New release",
21 | "url": "https://focused-tasks.s3.eu-central-1.amazonaws.com/FocusedTask-0.4.0.zip"
22 | }
23 | },
24 | {
25 | "version": "0.3.1",
26 | "updateTo": {
27 | "version": "0.3.1",
28 | "name": "0.3.1",
29 | "pub_date": "2020-11-04T01:02:03.0Z",
30 | "notes": "New release",
31 | "url": "https://focused-tasks.s3.eu-central-1.amazonaws.com/FocusedTask-0.3.1.zip"
32 | }
33 | },
34 | {
35 | "version": "0.3",
36 | "updateTo": {
37 | "version": "0.3",
38 | "name": "0.3",
39 | "pub_date": "2020-11-04T00:00:00.0Z",
40 | "notes": "New release",
41 | "url": "https://focused-tasks.s3.eu-central-1.amazonaws.com/FocusedTask-0.3.zip"
42 | }
43 | },
44 | {
45 | "version": "0.2.1",
46 | "updateTo": {
47 | "version": "0.2.1",
48 | "name": "0.2.1",
49 | "pub_date": "2020-10-18T00:00:00.0Z",
50 | "notes": "New release",
51 | "url": "https://focused-tasks.s3.eu-central-1.amazonaws.com/FocusedTask-0.2.1.zip"
52 | }
53 | },
54 | {
55 | "version": "0.2.0",
56 | "updateTo": {
57 | "version": "0.2.0",
58 | "name": "0.2.0",
59 | "pub_date": "2020-09-07T00:00:00.0Z",
60 | "notes": "New release",
61 | "url": "https://focused-tasks.s3.eu-central-1.amazonaws.com/FocusedTask-0.2.0.zip"
62 | }
63 | },
64 | {
65 | "version": "0.1.3",
66 | "updateTo": {
67 | "version": "0.1.3",
68 | "name": "0.1.3",
69 | "pub_date": "2020-09-01T00:00:00.0Z",
70 | "notes": "New release",
71 | "url": "https://focused-tasks.s3.eu-central-1.amazonaws.com/FocusedTask-0.1.3.zip"
72 | }
73 | },
74 | {
75 | "version": "0.1.2",
76 | "updateTo": {
77 | "version": "0.1.2",
78 | "name": "0.1.2",
79 | "pub_date": "2020-08-16T00:00:00.0Z",
80 | "notes": "New release",
81 | "url": "https://focused-tasks.s3.eu-central-1.amazonaws.com/FocusedTask-0.1.2.zip"
82 | }
83 | },
84 | {
85 | "version": "0.1.1",
86 | "updateTo": {
87 | "version": "0.1.1",
88 | "name": "0.1.1",
89 | "pub_date": "2020-08-09T00:00:00.0Z",
90 | "notes": "New release",
91 | "url": "https://focused-tasks.s3.eu-central-1.amazonaws.com/FocusedTask-0.1.1.zip"
92 | }
93 | },
94 | {
95 | "version": "0.1.0",
96 | "updateTo": {
97 | "version": "0.1.0",
98 | "name": "0.1.0",
99 | "pub_date": "2020-07-09T00:00:00.0Z",
100 | "notes": "Initial release",
101 | "url": "https://focused-tasks.s3.eu-central-1.amazonaws.com/FocusedTask-0.1.0.zip"
102 | }
103 | }
104 | ]
105 | }
106 |
--------------------------------------------------------------------------------
/shell/assets/MenuBarIconTemplate.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RStankov/FocusedTask/2532371d6b6a3d097cd8e23588c7786390b4e268/shell/assets/MenuBarIconTemplate.png
--------------------------------------------------------------------------------
/shell/assets/MenuBarIconTemplate@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RStankov/FocusedTask/2532371d6b6a3d097cd8e23588c7786390b4e268/shell/assets/MenuBarIconTemplate@2x.png
--------------------------------------------------------------------------------
/shell/main.js:
--------------------------------------------------------------------------------
1 | const { menubar } = require('menubar');
2 | const electron = require('electron');
3 | const url = require('url');
4 | const path = require('path');
5 | const settings = require('./utils/settings');
6 | const autoUpdate = require('./utils/autoupdate');
7 | const switchTask = require('./utils/switchTask');
8 |
9 | const isDev = !!process.env.ELECTRON_START_URL;
10 |
11 | const mb = menubar({
12 | index:
13 | process.env.ELECTRON_START_URL ||
14 | url.format({
15 | pathname: path.join(__dirname, './build/index.html'),
16 | protocol: 'file:',
17 | slashes: true,
18 | }),
19 | icon: path.join(__dirname, 'assets/MenuBarIconTemplate.png'),
20 | browserWindow: {
21 | width: 500,
22 | height: 600,
23 | minWidth: 300,
24 | maxHeight: 900,
25 | minHeight: 600,
26 | backgroundColor: '#FAFAFA',
27 | webPreferences: {
28 | nodeIntegration: true,
29 | scrollBounce: true,
30 | },
31 | },
32 | });
33 |
34 | mb.on('after-create-window', () => {
35 | settings.trackWindowBounds(mb);
36 | });
37 |
38 | mb.app.on('ready', () => {
39 | settings.setWindowBounds(mb);
40 | settings.setGlobalShortcut(mb);
41 |
42 | if (!isDev) {
43 | autoUpdate();
44 | }
45 | });
46 |
47 | mb.app.on('will-quit', () => {
48 | electron.globalShortcut.unregisterAll();
49 | });
50 |
51 | mb.app.allowRendererProcessReuse = false;
52 |
53 | mb.app.on('web-contents-created', (e, contents) => {
54 | const openExternal = (e, url) => {
55 | e.preventDefault();
56 | electron.shell.openExternal(url);
57 | };
58 |
59 | contents.on('new-window', openExternal);
60 | contents.on('will-navigate', (e, url) => {
61 | if (url !== contents.getURL()) {
62 | openExternal(e, url);
63 | }
64 | });
65 | });
66 |
67 | electron.ipcMain.on('resize', function (_e, width, height) {
68 | mb.window.setSize(width, height);
69 | });
70 |
71 | electron.ipcMain.on('updateGlobalShortcutKey', function (_e, key) {
72 | settings.updateGlobalShortcutKey(mb, key);
73 | });
74 |
75 | switchTask.register(mb);
76 |
--------------------------------------------------------------------------------
/shell/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "license": "MIT",
3 | "main": "./main.js",
4 | "version": "0.5.1",
5 | "dependencies": {
6 | "electron-settings": "4.0.2",
7 | "lodash": "4.17.21",
8 | "menubar": "9.0.3",
9 | "uuid": "8.3.2"
10 | },
11 | "devDependencies": {
12 | "electron": "8.5.2"
13 | },
14 | "scripts": {
15 | "start": "electron ."
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/shell/utils/autoupdate.js:
--------------------------------------------------------------------------------
1 | const electron = require('electron');
2 | const settings = require('./settings');
3 |
4 | module.exports = async function () {
5 | const installationId = await settings.getInstallationId();
6 | const autoUpdater = electron.autoUpdater;
7 |
8 | // NOTE(rstankov): More info here
9 | // https://www.electronjs.org/docs/api/auto-updater
10 | autoUpdater.setFeedURL({
11 | url: 'https://focused-task.herokuapp.com/releases',
12 | headers: {
13 | 'X-INSTALLATION': installationId,
14 | },
15 | serverType: 'json',
16 | });
17 |
18 | autoUpdater.on('error', (error) => {
19 | autoUpdateEvent(`${error}`);
20 | });
21 |
22 | autoUpdater.on('checking-for-update', () => {
23 | autoUpdateEvent('Checking for new update...');
24 | });
25 |
26 | autoUpdater.on('update-available', (error) => {
27 | autoUpdateEvent('New version is downloading...');
28 | });
29 |
30 | autoUpdater.on('update-not-available', (error) => {
31 | autoUpdateEvent('Running on newest version.');
32 | });
33 |
34 | autoUpdater.once('update-downloaded', async () => {
35 | autoUpdateEvent('New version downloaded.');
36 |
37 | const returnValue = await electron.dialog.showMessageBox({
38 | type: 'info',
39 | buttons: ['Restart', 'Later'],
40 | title: 'Application Update',
41 | message: 'Application Update',
42 | detail:
43 | 'A new version has been downloaded. Restart the application to apply the updates.',
44 | });
45 |
46 | if (returnValue.response === 0) {
47 | try {
48 | await autoUpdater.quitAndInstall();
49 | } catch (e) {
50 | // NOTE(rstankov): Handles:
51 | // "Cannot update while running on a read-only volume"
52 | autoUpdateEvent(`Newest version cant be installed due to ${error}`);
53 | }
54 | }
55 | });
56 |
57 | function autoUpdateRequest() {
58 | autoUpdateEvent('Checking for new update..');
59 |
60 | try {
61 | autoUpdater.checkForUpdates();
62 | } catch (error) {
63 | // NOTE(rstankov): Handles:
64 | // "Network errors"
65 | autoUpdateEvent(`Auto update error - ${error}`);
66 | }
67 | }
68 |
69 | electron.ipcMain.on('autoUpdateRequest', autoUpdateRequest);
70 |
71 | autoUpdateRequest();
72 |
73 | setInterval(autoUpdateRequest, 12 * 60 * 1000);
74 | };
75 |
76 | // NOTE(rstankov): Placeholder for the render process
77 | // Needed because we don't always have one
78 | var renderIpc = null;
79 |
80 | // NOTE(rstankov): Keep latest message, so when subscribe we can resend
81 | var latestMessage = null;
82 |
83 | function autoUpdateEvent(message) {
84 | latestMessage = message;
85 |
86 | if (!renderIpc) {
87 | return;
88 | }
89 |
90 | try {
91 | renderIpc.send('autoUpdateEvent', message || '');
92 | } catch (e) {
93 | console.log(e);
94 | }
95 | }
96 |
97 | electron.ipcMain.on('autoUpdateSubscribe', (e) => {
98 | renderIpc = e.sender;
99 | autoUpdateEvent(latestMessage);
100 | });
101 |
102 | electron.ipcMain.on('autoUpdateUnsubscribe', (e) => {
103 | renderIpc = null;
104 | });
105 |
--------------------------------------------------------------------------------
/shell/utils/settings.js:
--------------------------------------------------------------------------------
1 | const settings = require('electron-settings');
2 | const electron = require('electron');
3 | const _ = require('lodash');
4 | const uuid = require('uuid');
5 |
6 | const BOUNDS_KEY = 'windowBounds';
7 | const SHORTCUT_KEY = 'globalShortcut';
8 | const INSTALLATION_ID_KEY = 'installationId';
9 |
10 | module.exports = {
11 | async setWindowBounds(menubar) {
12 | const storedSettings = (await settings.get(BOUNDS_KEY)) || {};
13 |
14 | menubar.setOption('browserWindow', {
15 | ...menubar.getOption('browserWindow'),
16 | ...(storedSettings || {}),
17 | });
18 | },
19 |
20 | trackWindowBounds(menubar) {
21 | const win = menubar.window;
22 |
23 | const handler = _.debounce(() => {
24 | menubar.setOption('browserWindow', {
25 | ...menubar.getOption('browserWindow'),
26 | ...win.getBounds(),
27 | });
28 |
29 | settings.set(BOUNDS_KEY, win.getBounds());
30 | }, 500);
31 |
32 | win.on('resize', handler);
33 | win.on('move', handler);
34 | },
35 |
36 | async setGlobalShortcut(menubar) {
37 | const key = (await settings.get(SHORTCUT_KEY)) || "'";
38 |
39 | global.globalShortcutKey = key;
40 |
41 | electron.globalShortcut.register(`CommandOrControl+${key}`, () => {
42 | if (menubar.window && menubar.window.isVisible()) {
43 | menubar.hideWindow();
44 | } else {
45 | menubar.showWindow();
46 | }
47 | });
48 | },
49 |
50 | updateGlobalShortcutKey(menubar, key) {
51 | settings.set(SHORTCUT_KEY, key);
52 | electron.globalShortcut.unregisterAll();
53 | this.setGlobalShortcut(menubar);
54 | },
55 |
56 | async getInstallationId() {
57 | let id = await settings.get(INSTALLATION_ID_KEY);
58 | if (!id) {
59 | id = uuid.v1();
60 | settings.set(INSTALLATION_ID_KEY, id);
61 | }
62 |
63 | return id;
64 | },
65 | };
66 |
--------------------------------------------------------------------------------
/shell/utils/switchTask.js:
--------------------------------------------------------------------------------
1 | const electron = require('electron');
2 |
3 | const accelerator = 'CommandOrControl+`';
4 |
5 | module.exports = {
6 | register(mb) {
7 | mb.on('after-show', () => {
8 | electron.globalShortcut.register(accelerator, () => {
9 | const win = mb.window;
10 | if (win) {
11 | win.send('switchTaskShortcut');
12 | }
13 | });
14 | });
15 |
16 | mb.on('after-hide', () => {
17 | electron.globalShortcut.unregister(accelerator);
18 | });
19 | },
20 | };
21 |
--------------------------------------------------------------------------------
/shell/yarn.lock:
--------------------------------------------------------------------------------
1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
2 | # yarn lockfile v1
3 |
4 |
5 | "@electron/get@^1.0.1":
6 | version "1.12.2"
7 | resolved "https://registry.yarnpkg.com/@electron/get/-/get-1.12.2.tgz#6442066afb99be08cefb9a281e4b4692b33764f3"
8 | integrity sha512-vAuHUbfvBQpYTJ5wB7uVIDq5c/Ry0fiTBMs7lnEYAo/qXXppIVcWdfBr57u6eRnKdVso7KSiH6p/LbQAG6Izrg==
9 | dependencies:
10 | debug "^4.1.1"
11 | env-paths "^2.2.0"
12 | fs-extra "^8.1.0"
13 | got "^9.6.0"
14 | progress "^2.0.3"
15 | sanitize-filename "^1.6.2"
16 | sumchecker "^3.0.1"
17 | optionalDependencies:
18 | global-agent "^2.0.2"
19 | global-tunnel-ng "^2.7.1"
20 |
21 | "@sindresorhus/is@^0.14.0":
22 | version "0.14.0"
23 | resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea"
24 | integrity sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==
25 |
26 | "@szmarczak/http-timer@^1.1.2":
27 | version "1.1.2"
28 | resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-1.1.2.tgz#b1665e2c461a2cd92f4c1bbf50d5454de0d4b421"
29 | integrity sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==
30 | dependencies:
31 | defer-to-connect "^1.0.1"
32 |
33 | "@types/node@^12.0.12":
34 | version "12.12.54"
35 | resolved "https://registry.yarnpkg.com/@types/node/-/node-12.12.54.tgz#a4b58d8df3a4677b6c08bfbc94b7ad7a7a5f82d1"
36 | integrity sha512-ge4xZ3vSBornVYlDnk7yZ0gK6ChHf/CHB7Gl1I0Jhah8DDnEQqBzgohYG4FX4p81TNirSETOiSyn+y1r9/IR6w==
37 |
38 | boolean@^3.0.0, boolean@^3.0.1:
39 | version "3.0.1"
40 | resolved "https://registry.yarnpkg.com/boolean/-/boolean-3.0.1.tgz#35ecf2b4a2ee191b0b44986f14eb5f052a5cbb4f"
41 | integrity sha512-HRZPIjPcbwAVQvOTxR4YE3o8Xs98NqbbL1iEZDCz7CL8ql0Lt5iOyJFxfnAB0oFs8Oh02F/lLlg30Mexv46LjA==
42 |
43 | buffer-crc32@~0.2.3:
44 | version "0.2.13"
45 | resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242"
46 | integrity sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=
47 |
48 | buffer-from@^1.0.0:
49 | version "1.1.1"
50 | resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef"
51 | integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==
52 |
53 | cacheable-request@^6.0.0:
54 | version "6.1.0"
55 | resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-6.1.0.tgz#20ffb8bd162ba4be11e9567d823db651052ca912"
56 | integrity sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==
57 | dependencies:
58 | clone-response "^1.0.2"
59 | get-stream "^5.1.0"
60 | http-cache-semantics "^4.0.0"
61 | keyv "^3.0.0"
62 | lowercase-keys "^2.0.0"
63 | normalize-url "^4.1.0"
64 | responselike "^1.0.2"
65 |
66 | clone-response@^1.0.2:
67 | version "1.0.2"
68 | resolved "https://registry.yarnpkg.com/clone-response/-/clone-response-1.0.2.tgz#d1dc973920314df67fbeb94223b4ee350239e96b"
69 | integrity sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=
70 | dependencies:
71 | mimic-response "^1.0.0"
72 |
73 | concat-stream@^1.6.2:
74 | version "1.6.2"
75 | resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34"
76 | integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==
77 | dependencies:
78 | buffer-from "^1.0.0"
79 | inherits "^2.0.3"
80 | readable-stream "^2.2.2"
81 | typedarray "^0.0.6"
82 |
83 | config-chain@^1.1.11:
84 | version "1.1.12"
85 | resolved "https://registry.yarnpkg.com/config-chain/-/config-chain-1.1.12.tgz#0fde8d091200eb5e808caf25fe618c02f48e4efa"
86 | integrity sha512-a1eOIcu8+7lUInge4Rpf/n4Krkf3Dd9lqhljRzII1/Zno/kRtUWnznPO3jOKBmTEktkt3fkxisUcivoj0ebzoA==
87 | dependencies:
88 | ini "^1.3.4"
89 | proto-list "~1.2.1"
90 |
91 | core-js@^3.6.5:
92 | version "3.6.5"
93 | resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.6.5.tgz#7395dc273af37fb2e50e9bd3d9fe841285231d1a"
94 | integrity sha512-vZVEEwZoIsI+vPEuoF9Iqf5H7/M3eeQqWlQnYa8FSKKePuYTf5MWnxb5SDAzCa60b3JBRS5g9b+Dq7b1y/RCrA==
95 |
96 | core-util-is@~1.0.0:
97 | version "1.0.2"
98 | resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
99 | integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=
100 |
101 | debug@^2.6.9:
102 | version "2.6.9"
103 | resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
104 | integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
105 | dependencies:
106 | ms "2.0.0"
107 |
108 | debug@^4.1.0, debug@^4.1.1:
109 | version "4.1.1"
110 | resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791"
111 | integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==
112 | dependencies:
113 | ms "^2.1.1"
114 |
115 | decompress-response@^3.3.0:
116 | version "3.3.0"
117 | resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-3.3.0.tgz#80a4dd323748384bfa248083622aedec982adff3"
118 | integrity sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=
119 | dependencies:
120 | mimic-response "^1.0.0"
121 |
122 | defer-to-connect@^1.0.1:
123 | version "1.1.3"
124 | resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-1.1.3.tgz#331ae050c08dcf789f8c83a7b81f0ed94f4ac591"
125 | integrity sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==
126 |
127 | define-properties@^1.1.3:
128 | version "1.1.3"
129 | resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1"
130 | integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==
131 | dependencies:
132 | object-keys "^1.0.12"
133 |
134 | detect-node@^2.0.4:
135 | version "2.0.4"
136 | resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.0.4.tgz#014ee8f8f669c5c58023da64b8179c083a28c46c"
137 | integrity sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw==
138 |
139 | duplexer3@^0.1.4:
140 | version "0.1.4"
141 | resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2"
142 | integrity sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=
143 |
144 | electron-positioner@^4.1.0:
145 | version "4.1.0"
146 | resolved "https://registry.yarnpkg.com/electron-positioner/-/electron-positioner-4.1.0.tgz#e158f8f6aabd6725a8a9b4f2279b9504bcbea1b0"
147 | integrity sha512-726DfbI9ZNoCg+Fcu6XLuTKTnzf+6nFqv7h+K/V6Ug7IbaPMI7s9S8URnGtWFCy5N5PL4HSzRFF2mXuinftDdg==
148 |
149 | electron-settings@4.0.2:
150 | version "4.0.2"
151 | resolved "https://registry.yarnpkg.com/electron-settings/-/electron-settings-4.0.2.tgz#26ef242397393e0e69119f6fb879fc2287d0f508"
152 | integrity sha512-WnUlrnBsO784oXcag0ym+A3ySoIwonz5GhYFsWroMHVzslzmsP+81f/Fof41T9UrRUxuPPKiZPZMwGO+yvWChg==
153 | dependencies:
154 | lodash.get "^4.4.2"
155 | lodash.has "^4.5.2"
156 | lodash.set "^4.3.2"
157 | lodash.unset "^4.5.2"
158 | mkdirp "^1.0.4"
159 | write-file-atomic "^3.0.3"
160 |
161 | electron@8.5.2:
162 | version "8.5.2"
163 | resolved "https://registry.yarnpkg.com/electron/-/electron-8.5.2.tgz#7b0246c6676a39df0e5e384b11cfe854fe5917f0"
164 | integrity sha512-VU+zZnmCzxoZ5UfBg2UGVm+nyxlNlQOQkotMLfk7FCtnkIOhX+sosl618OCxUWjOvPc+Mpg5MEkEmxPU5ziW4Q==
165 | dependencies:
166 | "@electron/get" "^1.0.1"
167 | "@types/node" "^12.0.12"
168 | extract-zip "^1.0.3"
169 |
170 | encodeurl@^1.0.2:
171 | version "1.0.2"
172 | resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
173 | integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=
174 |
175 | end-of-stream@^1.1.0:
176 | version "1.4.4"
177 | resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0"
178 | integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==
179 | dependencies:
180 | once "^1.4.0"
181 |
182 | env-paths@^2.2.0:
183 | version "2.2.0"
184 | resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.0.tgz#cdca557dc009152917d6166e2febe1f039685e43"
185 | integrity sha512-6u0VYSCo/OW6IoD5WCLLy9JUGARbamfSavcNXry/eu8aHVFei6CD3Sw+VGX5alea1i9pgPHW0mbu6Xj0uBh7gA==
186 |
187 | es6-error@^4.1.1:
188 | version "4.1.1"
189 | resolved "https://registry.yarnpkg.com/es6-error/-/es6-error-4.1.1.tgz#9e3af407459deed47e9a91f9b885a84eb05c561d"
190 | integrity sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==
191 |
192 | escape-string-regexp@^4.0.0:
193 | version "4.0.0"
194 | resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34"
195 | integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==
196 |
197 | extract-zip@^1.0.3:
198 | version "1.7.0"
199 | resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.7.0.tgz#556cc3ae9df7f452c493a0cfb51cc30277940927"
200 | integrity sha512-xoh5G1W/PB0/27lXgMQyIhP5DSY/LhoCsOyZgb+6iMmRtCwVBo55uKaMoEYrDCKQhWvqEip5ZPKAc6eFNyf/MA==
201 | dependencies:
202 | concat-stream "^1.6.2"
203 | debug "^2.6.9"
204 | mkdirp "^0.5.4"
205 | yauzl "^2.10.0"
206 |
207 | fd-slicer@~1.1.0:
208 | version "1.1.0"
209 | resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e"
210 | integrity sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=
211 | dependencies:
212 | pend "~1.2.0"
213 |
214 | fs-extra@^8.1.0:
215 | version "8.1.0"
216 | resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0"
217 | integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==
218 | dependencies:
219 | graceful-fs "^4.2.0"
220 | jsonfile "^4.0.0"
221 | universalify "^0.1.0"
222 |
223 | get-stream@^4.1.0:
224 | version "4.1.0"
225 | resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5"
226 | integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==
227 | dependencies:
228 | pump "^3.0.0"
229 |
230 | get-stream@^5.1.0:
231 | version "5.1.0"
232 | resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.1.0.tgz#01203cdc92597f9b909067c3e656cc1f4d3c4dc9"
233 | integrity sha512-EXr1FOzrzTfGeL0gQdeFEvOMm2mzMOglyiOXSTpPC+iAjAKftbr3jpCMWynogwYnM+eSj9sHGc6wjIcDvYiygw==
234 | dependencies:
235 | pump "^3.0.0"
236 |
237 | global-agent@^2.0.2:
238 | version "2.1.12"
239 | resolved "https://registry.yarnpkg.com/global-agent/-/global-agent-2.1.12.tgz#e4ae3812b731a9e81cbf825f9377ef450a8e4195"
240 | integrity sha512-caAljRMS/qcDo69X9BfkgrihGUgGx44Fb4QQToNQjsiWh+YlQ66uqYVAdA8Olqit+5Ng0nkz09je3ZzANMZcjg==
241 | dependencies:
242 | boolean "^3.0.1"
243 | core-js "^3.6.5"
244 | es6-error "^4.1.1"
245 | matcher "^3.0.0"
246 | roarr "^2.15.3"
247 | semver "^7.3.2"
248 | serialize-error "^7.0.1"
249 |
250 | global-tunnel-ng@^2.7.1:
251 | version "2.7.1"
252 | resolved "https://registry.yarnpkg.com/global-tunnel-ng/-/global-tunnel-ng-2.7.1.tgz#d03b5102dfde3a69914f5ee7d86761ca35d57d8f"
253 | integrity sha512-4s+DyciWBV0eK148wqXxcmVAbFVPqtc3sEtUE/GTQfuU80rySLcMhUmHKSHI7/LDj8q0gDYI1lIhRRB7ieRAqg==
254 | dependencies:
255 | encodeurl "^1.0.2"
256 | lodash "^4.17.10"
257 | npm-conf "^1.1.3"
258 | tunnel "^0.0.6"
259 |
260 | globalthis@^1.0.1:
261 | version "1.0.1"
262 | resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.1.tgz#40116f5d9c071f9e8fb0037654df1ab3a83b7ef9"
263 | integrity sha512-mJPRTc/P39NH/iNG4mXa9aIhNymaQikTrnspeCa2ZuJ+mH2QN/rXwtX3XwKrHqWgUQFbNZKtHM105aHzJalElw==
264 | dependencies:
265 | define-properties "^1.1.3"
266 |
267 | got@^9.6.0:
268 | version "9.6.0"
269 | resolved "https://registry.yarnpkg.com/got/-/got-9.6.0.tgz#edf45e7d67f99545705de1f7bbeeeb121765ed85"
270 | integrity sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==
271 | dependencies:
272 | "@sindresorhus/is" "^0.14.0"
273 | "@szmarczak/http-timer" "^1.1.2"
274 | cacheable-request "^6.0.0"
275 | decompress-response "^3.3.0"
276 | duplexer3 "^0.1.4"
277 | get-stream "^4.1.0"
278 | lowercase-keys "^1.0.1"
279 | mimic-response "^1.0.1"
280 | p-cancelable "^1.0.0"
281 | to-readable-stream "^1.0.0"
282 | url-parse-lax "^3.0.0"
283 |
284 | graceful-fs@^4.1.6, graceful-fs@^4.2.0:
285 | version "4.2.4"
286 | resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb"
287 | integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==
288 |
289 | http-cache-semantics@^4.0.0:
290 | version "4.1.1"
291 | resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz#abe02fcb2985460bf0323be664436ec3476a6d5a"
292 | integrity sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==
293 |
294 | imurmurhash@^0.1.4:
295 | version "0.1.4"
296 | resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
297 | integrity sha1-khi5srkoojixPcT7a21XbyMUU+o=
298 |
299 | inherits@^2.0.3, inherits@~2.0.3:
300 | version "2.0.4"
301 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
302 | integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
303 |
304 | ini@^1.3.4:
305 | version "1.3.8"
306 | resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c"
307 | integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==
308 |
309 | is-typedarray@^1.0.0:
310 | version "1.0.0"
311 | resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
312 | integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=
313 |
314 | isarray@~1.0.0:
315 | version "1.0.0"
316 | resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
317 | integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=
318 |
319 | json-buffer@3.0.0:
320 | version "3.0.0"
321 | resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898"
322 | integrity sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=
323 |
324 | json-stringify-safe@^5.0.1:
325 | version "5.0.1"
326 | resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
327 | integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=
328 |
329 | jsonfile@^4.0.0:
330 | version "4.0.0"
331 | resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb"
332 | integrity sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=
333 | optionalDependencies:
334 | graceful-fs "^4.1.6"
335 |
336 | keyv@^3.0.0:
337 | version "3.1.0"
338 | resolved "https://registry.yarnpkg.com/keyv/-/keyv-3.1.0.tgz#ecc228486f69991e49e9476485a5be1e8fc5c4d9"
339 | integrity sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==
340 | dependencies:
341 | json-buffer "3.0.0"
342 |
343 | lodash.get@^4.4.2:
344 | version "4.4.2"
345 | resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99"
346 | integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=
347 |
348 | lodash.has@^4.5.2:
349 | version "4.5.2"
350 | resolved "https://registry.yarnpkg.com/lodash.has/-/lodash.has-4.5.2.tgz#d19f4dc1095058cccbe2b0cdf4ee0fe4aa37c862"
351 | integrity sha1-0Z9NwQlQWMzL4rDN9O4P5Ko3yGI=
352 |
353 | lodash.set@^4.3.2:
354 | version "4.3.2"
355 | resolved "https://registry.yarnpkg.com/lodash.set/-/lodash.set-4.3.2.tgz#d8757b1da807dde24816b0d6a84bea1a76230b23"
356 | integrity sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM=
357 |
358 | lodash.unset@^4.5.2:
359 | version "4.5.2"
360 | resolved "https://registry.yarnpkg.com/lodash.unset/-/lodash.unset-4.5.2.tgz#370d1d3e85b72a7e1b0cdf2d272121306f23e4ed"
361 | integrity sha1-Nw0dPoW3Kn4bDN8tJyEhMG8j5O0=
362 |
363 | lodash@4.17.21:
364 | version "4.17.21"
365 | resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
366 | integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
367 |
368 | lodash@^4.17.10:
369 | version "4.17.19"
370 | resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b"
371 | integrity sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==
372 |
373 | lowercase-keys@^1.0.0, lowercase-keys@^1.0.1:
374 | version "1.0.1"
375 | resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f"
376 | integrity sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==
377 |
378 | lowercase-keys@^2.0.0:
379 | version "2.0.0"
380 | resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479"
381 | integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==
382 |
383 | lru-cache@^6.0.0:
384 | version "6.0.0"
385 | resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
386 | integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==
387 | dependencies:
388 | yallist "^4.0.0"
389 |
390 | matcher@^3.0.0:
391 | version "3.0.0"
392 | resolved "https://registry.yarnpkg.com/matcher/-/matcher-3.0.0.tgz#bd9060f4c5b70aa8041ccc6f80368760994f30ca"
393 | integrity sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==
394 | dependencies:
395 | escape-string-regexp "^4.0.0"
396 |
397 | menubar@9.0.3:
398 | version "9.0.3"
399 | resolved "https://registry.yarnpkg.com/menubar/-/menubar-9.0.3.tgz#ba884a0ab0003d9b0f2a18bfda9da09b4d212eae"
400 | integrity sha512-J5MVygpC5L9cCdZQuCSSdorSidKVqwHKabHcL+oavl2wScWL1jVWFYikxA03L1k3Q58QFF2szFXTceJpMshJSA==
401 | dependencies:
402 | electron-positioner "^4.1.0"
403 |
404 | mimic-response@^1.0.0, mimic-response@^1.0.1:
405 | version "1.0.1"
406 | resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b"
407 | integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==
408 |
409 | minimist@^1.2.5:
410 | version "1.2.6"
411 | resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
412 | integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
413 |
414 | mkdirp@^0.5.4:
415 | version "0.5.5"
416 | resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def"
417 | integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==
418 | dependencies:
419 | minimist "^1.2.5"
420 |
421 | mkdirp@^1.0.4:
422 | version "1.0.4"
423 | resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
424 | integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
425 |
426 | ms@2.0.0:
427 | version "2.0.0"
428 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
429 | integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=
430 |
431 | ms@^2.1.1:
432 | version "2.1.2"
433 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
434 | integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
435 |
436 | normalize-url@^4.1.0:
437 | version "4.5.1"
438 | resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.5.1.tgz#0dd90cf1288ee1d1313b87081c9a5932ee48518a"
439 | integrity sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA==
440 |
441 | npm-conf@^1.1.3:
442 | version "1.1.3"
443 | resolved "https://registry.yarnpkg.com/npm-conf/-/npm-conf-1.1.3.tgz#256cc47bd0e218c259c4e9550bf413bc2192aff9"
444 | integrity sha512-Yic4bZHJOt9RCFbRP3GgpqhScOY4HH3V2P8yBj6CeYq118Qr+BLXqT2JvpJ00mryLESpgOxf5XlFv4ZjXxLScw==
445 | dependencies:
446 | config-chain "^1.1.11"
447 | pify "^3.0.0"
448 |
449 | object-keys@^1.0.12:
450 | version "1.1.1"
451 | resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
452 | integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==
453 |
454 | once@^1.3.1, once@^1.4.0:
455 | version "1.4.0"
456 | resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
457 | integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E=
458 | dependencies:
459 | wrappy "1"
460 |
461 | p-cancelable@^1.0.0:
462 | version "1.1.0"
463 | resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-1.1.0.tgz#d078d15a3af409220c886f1d9a0ca2e441ab26cc"
464 | integrity sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==
465 |
466 | pend@~1.2.0:
467 | version "1.2.0"
468 | resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50"
469 | integrity sha1-elfrVQpng/kRUzH89GY9XI4AelA=
470 |
471 | pify@^3.0.0:
472 | version "3.0.0"
473 | resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176"
474 | integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=
475 |
476 | prepend-http@^2.0.0:
477 | version "2.0.0"
478 | resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897"
479 | integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=
480 |
481 | process-nextick-args@~2.0.0:
482 | version "2.0.1"
483 | resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
484 | integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==
485 |
486 | progress@^2.0.3:
487 | version "2.0.3"
488 | resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8"
489 | integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==
490 |
491 | proto-list@~1.2.1:
492 | version "1.2.4"
493 | resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849"
494 | integrity sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=
495 |
496 | pump@^3.0.0:
497 | version "3.0.0"
498 | resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64"
499 | integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==
500 | dependencies:
501 | end-of-stream "^1.1.0"
502 | once "^1.3.1"
503 |
504 | readable-stream@^2.2.2:
505 | version "2.3.7"
506 | resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57"
507 | integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==
508 | dependencies:
509 | core-util-is "~1.0.0"
510 | inherits "~2.0.3"
511 | isarray "~1.0.0"
512 | process-nextick-args "~2.0.0"
513 | safe-buffer "~5.1.1"
514 | string_decoder "~1.1.1"
515 | util-deprecate "~1.0.1"
516 |
517 | responselike@^1.0.2:
518 | version "1.0.2"
519 | resolved "https://registry.yarnpkg.com/responselike/-/responselike-1.0.2.tgz#918720ef3b631c5642be068f15ade5a46f4ba1e7"
520 | integrity sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec=
521 | dependencies:
522 | lowercase-keys "^1.0.0"
523 |
524 | roarr@^2.15.3:
525 | version "2.15.3"
526 | resolved "https://registry.yarnpkg.com/roarr/-/roarr-2.15.3.tgz#65248a291a15af3ebfd767cbf7e44cb402d1d836"
527 | integrity sha512-AEjYvmAhlyxOeB9OqPUzQCo3kuAkNfuDk/HqWbZdFsqDFpapkTjiw+p4svNEoRLvuqNTxqfL+s+gtD4eDgZ+CA==
528 | dependencies:
529 | boolean "^3.0.0"
530 | detect-node "^2.0.4"
531 | globalthis "^1.0.1"
532 | json-stringify-safe "^5.0.1"
533 | semver-compare "^1.0.0"
534 | sprintf-js "^1.1.2"
535 |
536 | safe-buffer@~5.1.0, safe-buffer@~5.1.1:
537 | version "5.1.2"
538 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
539 | integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
540 |
541 | sanitize-filename@^1.6.2:
542 | version "1.6.3"
543 | resolved "https://registry.yarnpkg.com/sanitize-filename/-/sanitize-filename-1.6.3.tgz#755ebd752045931977e30b2025d340d7c9090378"
544 | integrity sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==
545 | dependencies:
546 | truncate-utf8-bytes "^1.0.0"
547 |
548 | semver-compare@^1.0.0:
549 | version "1.0.0"
550 | resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc"
551 | integrity sha1-De4hahyUGrN+nvsXiPavxf9VN/w=
552 |
553 | semver@^7.3.2:
554 | version "7.5.3"
555 | resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.3.tgz#161ce8c2c6b4b3bdca6caadc9fa3317a4c4fe88e"
556 | integrity sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==
557 | dependencies:
558 | lru-cache "^6.0.0"
559 |
560 | serialize-error@^7.0.1:
561 | version "7.0.1"
562 | resolved "https://registry.yarnpkg.com/serialize-error/-/serialize-error-7.0.1.tgz#f1360b0447f61ffb483ec4157c737fab7d778e18"
563 | integrity sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==
564 | dependencies:
565 | type-fest "^0.13.1"
566 |
567 | signal-exit@^3.0.2:
568 | version "3.0.3"
569 | resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c"
570 | integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==
571 |
572 | sprintf-js@^1.1.2:
573 | version "1.1.2"
574 | resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.2.tgz#da1765262bf8c0f571749f2ad6c26300207ae673"
575 | integrity sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==
576 |
577 | string_decoder@~1.1.1:
578 | version "1.1.1"
579 | resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8"
580 | integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==
581 | dependencies:
582 | safe-buffer "~5.1.0"
583 |
584 | sumchecker@^3.0.1:
585 | version "3.0.1"
586 | resolved "https://registry.yarnpkg.com/sumchecker/-/sumchecker-3.0.1.tgz#6377e996795abb0b6d348e9b3e1dfb24345a8e42"
587 | integrity sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==
588 | dependencies:
589 | debug "^4.1.0"
590 |
591 | to-readable-stream@^1.0.0:
592 | version "1.0.0"
593 | resolved "https://registry.yarnpkg.com/to-readable-stream/-/to-readable-stream-1.0.0.tgz#ce0aa0c2f3df6adf852efb404a783e77c0475771"
594 | integrity sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==
595 |
596 | truncate-utf8-bytes@^1.0.0:
597 | version "1.0.2"
598 | resolved "https://registry.yarnpkg.com/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz#405923909592d56f78a5818434b0b78489ca5f2b"
599 | integrity sha1-QFkjkJWS1W94pYGENLC3hInKXys=
600 | dependencies:
601 | utf8-byte-length "^1.0.1"
602 |
603 | tunnel@^0.0.6:
604 | version "0.0.6"
605 | resolved "https://registry.yarnpkg.com/tunnel/-/tunnel-0.0.6.tgz#72f1314b34a5b192db012324df2cc587ca47f92c"
606 | integrity sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==
607 |
608 | type-fest@^0.13.1:
609 | version "0.13.1"
610 | resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.13.1.tgz#0172cb5bce80b0bd542ea348db50c7e21834d934"
611 | integrity sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==
612 |
613 | typedarray-to-buffer@^3.1.5:
614 | version "3.1.5"
615 | resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080"
616 | integrity sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==
617 | dependencies:
618 | is-typedarray "^1.0.0"
619 |
620 | typedarray@^0.0.6:
621 | version "0.0.6"
622 | resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
623 | integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
624 |
625 | universalify@^0.1.0:
626 | version "0.1.2"
627 | resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
628 | integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==
629 |
630 | url-parse-lax@^3.0.0:
631 | version "3.0.0"
632 | resolved "https://registry.yarnpkg.com/url-parse-lax/-/url-parse-lax-3.0.0.tgz#16b5cafc07dbe3676c1b1999177823d6503acb0c"
633 | integrity sha1-FrXK/Afb42dsGxmZF3gj1lA6yww=
634 | dependencies:
635 | prepend-http "^2.0.0"
636 |
637 | utf8-byte-length@^1.0.1:
638 | version "1.0.4"
639 | resolved "https://registry.yarnpkg.com/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz#f45f150c4c66eee968186505ab93fcbb8ad6bf61"
640 | integrity sha1-9F8VDExm7uloGGUFq5P8u4rWv2E=
641 |
642 | util-deprecate@~1.0.1:
643 | version "1.0.2"
644 | resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
645 | integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
646 |
647 | uuid@8.3.2:
648 | version "8.3.2"
649 | resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
650 | integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
651 |
652 | wrappy@1:
653 | version "1.0.2"
654 | resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
655 | integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
656 |
657 | write-file-atomic@^3.0.3:
658 | version "3.0.3"
659 | resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-3.0.3.tgz#56bd5c5a5c70481cd19c571bd39ab965a5de56e8"
660 | integrity sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==
661 | dependencies:
662 | imurmurhash "^0.1.4"
663 | is-typedarray "^1.0.0"
664 | signal-exit "^3.0.2"
665 | typedarray-to-buffer "^3.1.5"
666 |
667 | yallist@^4.0.0:
668 | version "4.0.0"
669 | resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
670 | integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
671 |
672 | yauzl@^2.10.0:
673 | version "2.10.0"
674 | resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9"
675 | integrity sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=
676 | dependencies:
677 | buffer-crc32 "~0.2.3"
678 | fd-slicer "~1.1.0"
679 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | app/tsconfig.json
--------------------------------------------------------------------------------
/updater/Procfile:
--------------------------------------------------------------------------------
1 | web: yarn start
2 | release: yarn db-migrate
3 |
--------------------------------------------------------------------------------
/updater/README.md:
--------------------------------------------------------------------------------
1 | # Updater
2 |
3 | This is the server used for the auto-update functionality of FocusedTask.
4 |
5 | Works with [Squirrel Update File JSON Format](https://github.com/Squirrel/Squirrel.Mac#update-file-json-format).
6 | Counts downloads and updates anonymously.
7 | Designed to be deployed on [Heroku](https://heroku.com/)
8 |
9 | ## Installation
10 |
11 | Requires [yarn](https://yarnpkg.com/) and [docker-compose](https://docs.docker.com/compose/)
12 |
13 | ```
14 | yarn install
15 | docker-compose up -d --no-recreate
16 | yarn db-migrate
17 | ```
18 |
19 | ## Running
20 |
21 | ```
22 | yarn dev
23 | ```
24 |
--------------------------------------------------------------------------------
/updater/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '2'
2 | services:
3 | postgres:
4 | image: postgres:9.6.3
5 | ports:
6 | - '5432:5432'
7 | volumes:
8 | - /var/lib/postgresql/data
9 | environment:
10 | POSTGRES_USER: focusedtask
11 | POSTGRES_PASSWORD: focusedtask
12 | POSTGRES_DB: focusedtask
13 |
--------------------------------------------------------------------------------
/updater/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": "Update server for FocusTask",
3 | "license": "MIT",
4 | "scripts": {
5 | "start": "ts-node src/index.ts",
6 | "dev": "docker-compose up -d --no-recreate; nodemon -x ts-node src/index.ts",
7 | "db-migrate": "ts-node src/db/migrate.ts",
8 | "deploy": "heroku git:remote --remote heroku-updater -a focused-task"
9 | },
10 | "dependencies": {
11 | "@databases/pg": "5.0.0",
12 | "@types/express": "4.17.11",
13 | "@types/node": "14.14.41",
14 | "@types/node-fetch": "2.5.10",
15 | "express": "^4.17.1",
16 | "isbot": "3.0.26",
17 | "node-fetch": "^2.6.7",
18 | "nodemon": "2.0.7",
19 | "ts-node": "9.1.1",
20 | "typescript": "4.2.4"
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/updater/src/config.ts:
--------------------------------------------------------------------------------
1 | import createConnectionPool from '@databases/pg';
2 |
3 | export const db = process.env.DATABASE_URL
4 | ? createConnectionPool({
5 | onQueryError: (_query: any, { text }: any, err: any) => {
6 | console.log(
7 | `${new Date().toISOString()} ERROR QUERY ${text} - ${err.message}`,
8 | );
9 | },
10 | })
11 | : createConnectionPool(
12 | 'postgresql://focusedtask:focusedtask@127.0.0.1:5432/focusedtask',
13 | );
14 |
15 | export const port = process.env.PORT || 3000;
16 |
17 | export const releasesURL =
18 | 'https://focused-tasks.s3.eu-central-1.amazonaws.com/releases.json';
19 |
20 | export const siteURL = 'https://github.com/RStankov/FocusedTask';
21 |
--------------------------------------------------------------------------------
/updater/src/db/index.ts:
--------------------------------------------------------------------------------
1 | import { db } from '../config';
2 | import { sql } from '@databases/pg';
3 |
4 | export async function logDownload(version: string, ref: any) {
5 | await db.query(sql`
6 | INSERT INTO download_logs
7 | (version, requested_at, referer)
8 | VALUES
9 | (${version}, ${new Date()}, ${ref})
10 | `);
11 |
12 | db.query(sql`SELECT * FROM download_logs`).then((r) => console.log(r));
13 | }
14 |
15 | export async function logUpdate(version: string, installationId: string) {
16 | const time = new Date();
17 |
18 | const results = await db.query(
19 | sql`
20 | SELECT id
21 | FROM update_logs
22 | WHERE installation_id=${installationId}`,
23 | );
24 |
25 | const id = results[0] && results[0].id;
26 |
27 | if (id) {
28 | await db.query(
29 | sql`
30 | UPDATE update_logs
31 | SET version=${version},
32 | last_request_at=${time}
33 | WHERE id=${id}`,
34 | );
35 | } else {
36 | try {
37 | await db.query(sql`
38 | INSERT INTO update_logs
39 | (version, installation_id, created_at, last_request_at)
40 | VALUES
41 | (${version}, ${installationId}, ${time}, ${time})
42 | `);
43 | } catch (e) {
44 | // NOTE(rstankov): Handle race-condition
45 | if (
46 | !e.toString().includes('duplicate key value violates unique constraint')
47 | ) {
48 | throw e;
49 | }
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/updater/src/db/migrate.ts:
--------------------------------------------------------------------------------
1 | import { db } from '../config';
2 | import { sql } from '@databases/pg';
3 |
4 | const MIGRATIONS = [
5 | () =>
6 | db.query(sql`
7 | ALTER TABLE download_logs ADD COLUMN referer VARCHAR(255);
8 | `),
9 | () =>
10 | db.query(sql`
11 | ALTER TABLE download_logs ALTER COLUMN requested_at TYPE TIMESTAMP USING requested_at::timestamp;
12 | `),
13 | ];
14 |
15 | async function migrate() {
16 | await db.query(
17 | sql`
18 | CREATE TABLE IF NOT EXISTS update_logs (
19 | id SERIAL PRIMARY KEY,
20 | installation_id VARCHAR (255) UNIQUE NOT NULL,
21 | version VARCHAR (255) NOT NULL,
22 | created_at TIMESTAMP NOT NULL,
23 | last_request_at TIMESTAMP NOT NULL
24 | );
25 | `,
26 | );
27 |
28 | await db.query(
29 | sql`
30 | CREATE TABLE IF NOT EXISTS download_logs (
31 | id SERIAL PRIMARY KEY,
32 | version VARCHAR (255) NOT NULL,
33 | requested_at VARCHAR (255) UNIQUE NOT NULL
34 | );
35 | `,
36 | );
37 |
38 | await db.query(
39 | sql`
40 | CREATE TABLE IF NOT EXISTS database_migrations (
41 | id SERIAL PRIMARY KEY,
42 | version INT NOT NULL UNIQUE
43 | );
44 | `,
45 | );
46 |
47 | const result = await db.query(
48 | sql`SELECT MAX(version) FROM database_migrations`,
49 | );
50 |
51 | const currentVersion = result[0].max || 0;
52 |
53 | if (currentVersion !== MIGRATIONS.length) {
54 | for (let i = currentVersion; i < MIGRATIONS.length; i++) {
55 | await MIGRATIONS[i]();
56 | console.log(`🚧 Migrate: ${i + 1}/${MIGRATIONS.length}`);
57 | }
58 |
59 | if (currentVersion === 0) {
60 | await db.query(
61 | sql`INSERT INTO database_migrations(version) values(${MIGRATIONS.length})`,
62 | );
63 | } else {
64 | await db.query(
65 | sql`UPDATE database_migrations SET version=${MIGRATIONS.length}`,
66 | );
67 | }
68 | }
69 |
70 | console.log('✅ DB MIGRATE');
71 | }
72 |
73 | migrate()
74 | .then(() => process.exit(0))
75 | .catch((e) => {
76 | console.error(e);
77 | process.exit(1);
78 | });
79 |
--------------------------------------------------------------------------------
/updater/src/index.ts:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | import isBot from 'isbot';
3 | import { port, siteURL } from './config';
4 | import { fetchCurrentRelease, fetchAllReleases } from './releases';
5 | import { logDownload, logUpdate } from './db';
6 | import { db } from './config';
7 |
8 | const app = express();
9 |
10 | app.use('/status', (_req, res) => {
11 | res.send('running');
12 | });
13 |
14 | app.use('/download', async (req, res) => {
15 | const release = await fetchCurrentRelease();
16 | res.redirect(release.updateTo.url);
17 |
18 | if (!(req.headers['user-agent'] && isBot(req.headers['user-agent']))) {
19 | logDownload(release.version, req.query.ref || req.header('Referer'));
20 | }
21 | });
22 |
23 | app.use('/releases', async (req, res) => {
24 | const releases = await fetchAllReleases();
25 |
26 | const installationId = (req.headers['x-installation'] || 'unknown') as string;
27 |
28 | logUpdate(releases.currentRelease, installationId);
29 |
30 | res.send(releases);
31 | });
32 |
33 | app.use('/', (_req, res) => res.redirect(301, siteURL));
34 |
35 | app.listen(port, () => console.log(`Server listing on port ${port}`));
36 |
37 | process.once('SIGTERM', () => {
38 | db.dispose().catch((ex) => {
39 | console.error(ex);
40 | });
41 | });
42 |
--------------------------------------------------------------------------------
/updater/src/releases.ts:
--------------------------------------------------------------------------------
1 | import fetch from 'node-fetch';
2 | import { releasesURL } from './config';
3 |
4 | interface TRelease {
5 | version: string;
6 | updateTo: {
7 | version: string;
8 | name: string;
9 | pub_date: string;
10 | notes: string;
11 | url: string;
12 | };
13 | }
14 |
15 | interface TReleases {
16 | currentRelease: string;
17 | releases: TRelease[];
18 | }
19 |
20 | export async function fetchAllReleases(): Promise {
21 | const response = await fetch(releasesURL);
22 | const json = await response.json();
23 |
24 | return json;
25 | }
26 |
27 | export async function fetchCurrentRelease(): Promise {
28 | const releases = await fetchAllReleases();
29 | const currentRelease =
30 | releases.releases.find(r => r.version === releases.currentRelease) ||
31 | releases.releases[0];
32 | return currentRelease;
33 | }
34 |
--------------------------------------------------------------------------------
/updater/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": false,
3 | "compilerOptions": {
4 | "target": "esnext",
5 | "module": "commonjs",
6 | "resolveJsonModule": true,
7 | "jsx": "preserve",
8 | "allowJs": true,
9 | "moduleResolution": "node",
10 | "esModuleInterop": true,
11 | "allowSyntheticDefaultImports": true,
12 | "strict": true,
13 | "noEmit": true,
14 | "noImplicitAny": true,
15 | "noImplicitThis": true,
16 | "noUnusedLocals": true,
17 | "noUnusedParameters": true,
18 | "removeComments": false,
19 | "preserveConstEnums": true,
20 | "sourceMap": true,
21 | "skipLibCheck": true,
22 | "experimentalDecorators": true,
23 | "emitDecoratorMetadata": true,
24 | "baseUrl": "./src",
25 | "typeRoots": ["./node_modules/@types"],
26 | "lib": ["dom", "es2015", "es2016", "esnext.asynciterable"],
27 | "forceConsistentCasingInFileNames": true,
28 | "isolatedModules": true
29 | },
30 | "include": ["./src"],
31 | "exclude": ["node_modules/**/*"]
32 | }
33 |
--------------------------------------------------------------------------------